Skip to content

Instantly share code, notes, and snippets.

@pkutzner
Last active November 13, 2018 17:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pkutzner/099a32e256fb2fa26016aa227e04a36f to your computer and use it in GitHub Desktop.
Save pkutzner/099a32e256fb2fa26016aa227e04a36f to your computer and use it in GitHub Desktop.
#!/bin/bash -
#===============================================================================
#
# FILE: csb_updated.sh
#
# USAGE: ./csb_updated.sh
#
# DESCRIPTION: Updated version of Client SQL Backup (csb) script.
#
# OPTIONS: ---
# REQUIREMENTS: gzip/bzip2, mysql, ssh, mysqldump, mail/mailx
# BUGS: ---
# NOTES: ---
# AUTHOR: Preston Kutzner (pmk), pkutzner at gmail.com
# Loosely based off of the logic in MySQL Backup Script
# http://sourceforge.net/projects/automysqlbackup
# Copyright (c) 2002-2003 wipe_out@lycos.co.uk
# COMPANY:
# CREATED: 05/28/2010 17:01:25 CDT
# REVISION: 3.3
# LICENSE: GNU GPLv2
#
#===============================================================================
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
set -o nounset # Treat unset variables as an error
#-------------------------------------------------------------------------------
# BEGIN User Selectable Options
#-------------------------------------------------------------------------------
CONFDIR="/data/mysql_auto_backup/config" # Default config directory
BACKUPDIR="/data/client_db_backups" # Default backup storage location
MAILADDR="user@example.com" # E-Mail reports to this addr.
LOGTYPE="quiet" # Default logging type, see -help
COMP="bzip2" # Default compression to use
DOWEEKLY=6 # Day of week to perform weekly
# backup. (1=Monday)
# Default options for compression programs
SZ_OPTS="a -an -txz -m0=lzma2 -mx=5 -md32m -mmt=2 -si -so" # 7z options
BZ2_OPTS="-fc" # bzip2 options
GZ_OPTS="-c" # gzip options
# The following options allow us to ssh to hosts without adding them to our known
# hosts file. This also tells SSH to fail with an error if it is unable to
# establish a port forward.
#SSH_OPTS="-oUserKnownHostsFile=/dev/null -oStrictHostKeyChecking=no -oLogLevel=Error"
#SSH_TUN_OPTS="-oExitOnForwardFailure=yes $SSH_OPTS"
SSH_OPTS=( '-oUserKnownHostsFile=/dev/null'
'-oStrictHostKeyChecking=no'
'-oLogLevel=Error'
'-oBatchMode=yes'
'-oConnectTimeout=5'
)
SSH_TUN_OPTS=( $SSH_OPTS[@] '-oExitOnForwardFailure=yes' )
# These are switches that get evaluated True/False. Bash/sh/zsh True=0 False=1
DEBUG=1
SEPDIR=1 # Use separate directories for
# each database
#-------------------------------------------------------------------------------
# END User Selectable Options
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
# BEGIN Constants
#-------------------------------------------------------------------------------
EXCLUDED=( 'information_schema' 'mysql.event' 'performance_schema' ) # Excluded databases
declare -a EXCLUDE=()
#-------------------------------------------------------------------------------
# END Constants
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
# BEGIN Function Definitions
#-------------------------------------------------------------------------------
#=== FUNCTION ================================================================
# NAME: usage
# DESCRIPTION: Display script usage to user
# PARAMETERS: None
# RETURNS: Nothing
#===============================================================================
usage ()
{
cat <<EOF
USAGE:
$(basename "$0") [options]
OPTIONS:
-h : Show this help
-v : Debug output (verbose)
-b <path> : Specify output path for backups.
-c <path> : Specify config file path.
-f <file> : Use only specified config file.
-a <address> : Email address to send logs to.
-d <db_name(s)> : Database name, or comma-separated list of databases.
to be backed up. (e.g. "db1,db2,etc.")
-D : Debug
-o <type> : Specify output (stdout|log|both|quiet)
screen - Output to screen only.
log - Output to file, mail when done.
both - log + stdout
quiet - Mail log(s) only if errors reported.
-z <type> : Specify compression type ( bzip2 | gzip | 7zip )
EOF
exit 0
} # ---------- end of function usage ----------
#=== FUNCTION ================================================================
# NAME: send
# DESCRIPTION: Email contents of log files to $MAILADDR
# PARAMETERS: Logfile as $1
# RETURNS: Success/Fail
#===============================================================================
send ()
{
mail -s "MySQL backup log for $JOBNO - $DATE" $MAILADDR <"$1" || return 1
return 0
} # ---------- end of function send ----------
# Test SSH connectivity
_testssh() {
$SSH_CMD -qq -oUserKnownHostsFile=/dev/null -oStrictHostKeyChecking=no -oBatchMode=yes -oConnectTimeout=5 -i "${KEYFILE}" -p ${SSH_PORT:-22} ${RUNAME}@${RHOST} "echo 2>&1" && return 0 || return 1
}
# Check if list contains value
# e.g. contains $MYLIST $MYVALUE
# If MYLIST is an array, it must be passed as string (${MYLIST[*]})
contains() {
[[ "$1" =~ (^|[[:space:]])"$2"($|[[:space:]]) ]] && return 0 || return 1
}
#=== FUNCTION ================================================================
# NAME: startLogging
# DESCRIPTION: Setup logging according to specified log type
# PARAMETERS: None
# RETURNS: Success/Fail
#===============================================================================
startLogging ()
{
# The "${0##*/}" variable reference convention used below gives us the same result
# as doing a `basename $0` but without the expense of having to do a nested subshell
# call for it.
if ! LOGFILE=$(mktemp -q "/tmp/${0##*/}-log.XXXXXX"); then
echo >&2 "[ERROR]: Unable to create temporary log file. (mktemp exited with $?)."
exit 1
elif ! ERRFILE=$(mktemp -q "/tmp/${0##*/}-err.XXXXXX"); then
echo >&2 "[ERROR]: Unable to create temporary error log. (mktemp exited with $?)."
exit 1
fi
exec 6>&1 # Attach stdout to FD #6
exec 7>&2 # Attach stderr to FD #7
case "$LOGTYPE" in
both)
# Send output both to file and screen using BASH process substitution and tee.
if ! exec > >(tee -a "$LOGFILE"); then # Pipe stdout to file and screen
exec 1>&6 6>&- # Restore stdout
exec 2>&7 7>&- # Restore stderr
echo >&2 "[ERROR]: Unable to redirect stdout."
return 1
elif ! exec 2> >(tee -a "$ERRFILE"); then # Pipe stderr to file and screen
exec 1>&6 6>&- # Restore stdout
exec 2>&7 7>&- # Restore stderr
echo >&2 "[ERROR]: Unable to redirect stderr."
return 1
fi
;;
log|quiet)
if ! exec > "$LOGFILE"; then # Redirect stout to file
exec 1>&6 6>&- # Restore stdout and release FD#6
exec 2>&7 7>&- # Restore stderr and release FD#7
echo >&2 "[ERROR]: Unable to redirect stdout."
return 1
elif ! exec 2> "$ERRFILE"; then # Redirect stderr to file
exec 1>&6 6>&- # Restore stdout and release FD#6
exec 2>&7 7>&- # Restore stderr and release FD#7
echo >&2 "[ERROR]: Unable to redirect stderr."
return 1
fi
;;
screen)
# Drop through.
;;
esac
} # ---------- end of function startLogging ----------
#=== FUNCTION ================================================================
# NAME: stopLogging
# DESCRIPTION: Stop logging, send logs if neccessary and clean-up.
# PARAMETERS: None
# RETURNS: Success/Fail
#===============================================================================
stopLogging ()
{
case "$LOGTYPE" in
log|both)
exec 1>&6 6>&- # Restore stdout
exec 2>&7 7>&- # Restore stderr
# If the log file is not empty, send it.
if [[ -s "${LOGFILE}" ]]; then
send "${LOGFILE}" || echo >&2 "${0##*/}: Unable to send ${LOGFILE}"
rm -f "${LOGFILE}"
else
rm -f "${LOGFILE}"
fi
# If the error log is non-empty, email it.
if [[ -s "${ERRFILE}" ]]; then
send "${ERRFILE}" || echo >&2 "${0##*/}: Unable to send ${ERRFILE}"
rm -f "${ERRFILE}"
else
rm -f "${ERRFILE}"
fi
;;
quiet)
exec 1>&6 6>&- # Restore stdout
exec 2>&7 7>&- # Restore stderr
# If the error log is non-empty, email both logs.
if [[ -s "${ERRFILE}" ]]; then
send "${LOGFILE}" || echo >&2 "${0##*/}: Unable to send ${LOGFILE}"
rm -f "${LOGFILE}"
send "${ERRFILE}" || echo >&2 "${0##*/}: Unable to send ${ERRFILE}"
rm -f "${ERRFILE}"
else
rm -f "${LOGFILE}" "${ERRFILE}"
fi
;;
screen)
# Drop through
;;
esac
} # ---------- end of function stopLogging ----------
#=== FUNCTION ================================================================
# NAME: getDatabases
# DESCRIPTION: Retrieve list of databases from $RHOST
# PARAMETERS: Nothing, uses globals.
# RETURNS: Array of database names.
#===============================================================================
getDatabases ()
{
if ! $SSH_CMD ${SSH_OPTS[*]} -p ${SSH_PORT:-22} -i "$KEYFILE" ${RUNAME}@${RHOST} "/usr/bin/mysql -u $MUNAME -p'$MPASSWD' -s -r -e \"SHOW DATABASES;\" | grep -Ev \"\(information_schema\|performance_schema\)\""; then
echo >&2 "[ERROR]: Unable to retrieve list of databases from $RHOST"
return 1
fi
return 0
} # ---------- end of function getDatabases ----------
#--- FUNCTION ----------------------------------------------------------------
# NAME: set_compression
# DESCRIPTION: Set compression command-lines and file suffix based on
# selected compression program.
# PARAMETERS: none
# RETURNS: success/fail
#-------------------------------------------------------------------------------
set_compression ()
{
local ssh_line="$SSH_CMD ${SSH_OPTS[*]} -p ${SSH_PORT:-22} -i \"$KEYFILE\" ${RUNAME}@${RHOST}"
echo "Compression chosen: ${COMP}"
case "$COMP" in
gzip)
if ! COMP_CMD=$(eval "$ssh_line" "which gzip"); then
echo "[INFO]: gzip specified, but unavailable! Attempting fallback to bzip2"
if ! COMP_CMD=$(eval "$ssh_line" "which bzip2"); then
echo "[INFO]: bzip2 specified, but unavailable! Attempting fallback to 7zip"
if ! COMP_CMD=$(eval "$ssh_line" "which 7z"); then
echo >&2 "[ERROR]: Unable to find suitable compresson program on $RHOST!"
return 1
else
COMP="7zip"
COMP_CMD="$COMP_CMD $SZ_OPTS"
COMP_TEST_CMD="$LOC_7ZIP t"
COMP_INFO_CMD="$LOC_7ZIP l"
SUFFIX=".xz"
fi
else
COMP="bzip2"
COMP_CMD="$COMP_CMD $BZ2_OPTS"
COMP_TEST_CMD="$LOC_BZIP2 -t"
SUFFIX=".bz2"
fi
else
COMP_CMD="$COMP_CMD $GZ_OPTS"
COMP_TEST_CMD="$LOC_GZIP -t"
COMP_INFO_CMD="$LOC_GZIP -l"
SUFFIX=".gz"
fi
;;
bzip2)
if ! COMP_CMD=$(eval "$ssh_line" "which bzip2"); then
echo "[INFO]: bzip2 specified but unavailable! Attemting fallback to gzip"
if ! COMP_CMD=$(eval "$ssh_line" "which gzip"); then
echo "[INFO]: gzip specified but unavailable! Attempting fallback to 7zip"
if ! COMP_CMD=$(eval "$ssh_line" "which 7z"); then
echo >&2 "[ERROR]: Unable to find suitable compresson program on $RHOST!"
return 1
else
COMP="7zip"
COMP_CMD="$COMP_CMD $SZ_OPTS"
COMP_TEST_CMD="$LOC_7ZIP $SZ_TOPTS"
COMP_INFO_CMD="$LOC_7ZIP l"
SUFFIX=".xz"
fi
else
COMP="gzip"
COMP_CMD="$COMP_CMD $GZ_OPTS"
COMP_TEST_CMD="$LOC_GZIP $GZ_TOPTS"
COMP_INFO_CMD="$LOC_GZIP -l"
SUFFIX=".gz"
fi
else
COMP_CMD="$COMP_CMD $BZ2_OPTS"
COMP_TEST_CMD="$LOC_BZIP2 $BZ2_TOPTS"
SUFFIX=".bz2"
fi
;;
7zip)
if ! COMP_CMD=$(eval "$ssh_line" "which 7z"); then
echo "[INFO]: 7zip specified but unavailable! Attempting fallback to bzip2"
if ! COMP_CMD=$(eval "$ssh_line" "which bzip2"); then
echo "[INFO]: bzip2 specified but unavailable! Attempting fallback to gzip"
if ! COMP_CMD=$(eval "$ssh_line" "which gzip"); then
echo >&2 "[ERROR]: Unable to find suitable compression program on $RHOST!"
return 1
else
COMP="gzip"
COMP_CMD="$COMP_CMD $GZ_OPTS"
COMP_TEST_CMD="$LOC_GZIP $GZ_TOPTS"
COMP_INFO_CMD="$LOC_GZIP -l"
SUFFIX=".gz"
fi
else
COMP="bzip2"
COMP_CMD="$COMP_CMD $BZ2_OPTS"
COMP_TEST_CMD="$LOC_BZIP2 $BZ2_TOPTS"
SUFFIX=".bz2"
fi
else
COMP_CMD="$COMP_CMD $SZ_OPTS"
COMP_TEST_CMD="$LOC_7ZIP $SZ_TOPTS"
COMP_INFO_CMD="$LOC_7ZIP l"
SUFFIX=".xz"
fi
;;
esac
#echo "Remote compression command: ${COMP_CMD}"
} # ---------- end of function set_compression ----------
#--- FUNCTION ----------------------------------------------------------------
# NAME: getSize
# DESCRIPTION: Get size of database to be dumped
# PARAMETERS: Database name
# RETURNS: Size of database to be dumped
#-------------------------------------------------------------------------------
getSize ()
{
$SSH_CMD ${SSH_OPTS[*]} -p ${SSH_PORT:-22} -i "$KEYFILE" ${RUNAME}@${RHOST} <<DATA
set -o pipefail
eval /usr/bin/mysql --user=$MUNAME --password="\"$MPASSWD\"" -s -r -e 'SELECT CONCAT\(sum\(ROUND\(\(\(DATA_LENGTH + INDEX_LENGTH - DATA_FREE\) / 1024 / 1024\),2\)\)," MB"\) FROM INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA like "$1";'
DATA
} # ---------- end of function getSize ----------
#=== FUNCTION ================================================================
# NAME: backup
# DESCRIPTION: Back up database to the given filename.
# PARAMETERS: Database to backup as $1, filename as $2
# RETURNS: Success/Fail
#===============================================================================
backup ()
{
local outzip="$2.sql${SUFFIX}"
local mdbopt=""
# We have to set these options to properly suppress warning messages from
# mysqldump regarding the mysql.event table, which doesn't exist. Unfortunately
# this is the "correct" way to do this, as there is no '--ignore-events' option.
# Yes, we're saying "also capture the events table, but don't really capture it."
[[ "$1" = "mysql" ]] && mdbopt=( '--events' '--ignore-table=mysql.event' )
# Backup existing files, if any, in case things go wrong...
[[ -e "$outzip" ]] && mv "${outzip}"{,.bak}
# IMO, this is ugly, but allows us to pull large database dumps compressed
# over the wire. 'set -o pipefail' is necessary here due to the fact that
# $COMP_CMD will return success even if mysqldump was unsuccessful. This
# convention allows us to detect a failure anywhere in the pipe and have
# ssh pass it back to us via $?, which we can then trap and make decisions
# based on its value.
#
# The use of the "here" file below is required for proper esacping of quotes
#echo "Remote dump command: $MYSQLDUMP --user=$MUNAME --password=\""${MPASSWD}"\" $MOPT "$1" | $COMP_CMD 2>/dev/null"
# $SSH_CMD $SSH_OPTS -i "$KEYFILE" ${RUNAME}@${RHOST} > $outzip <<DATA
#set -o pipefail
#eval $MYSQLDUMP --user=$MUNAME --password=\""${MPASSWD}"\" $MOPT "$1" | $COMP_CMD 2>/dev/null
#DATA
$SSH_CMD ${SSH_OPTS[*]} -p ${SSH_PORT:-22} -i "$KEYFILE" ${RUNAME}@${RHOST} >/dev/null <<DATA
set -o pipefail
eval $MYSQLDUMP --user=$MUNAME --password=\""${MPASSWD}"\" "${MOPT[*]}" "${mdbopt[*]}" "$1" | $COMP_CMD > /var/tmp/dump 2>/dev/null
DATA
# If dump failed, restore backups and return from function with a failure.
if [[ $? -ne 0 ]]; then
echo >&2 "[ERROR]: Failed to dump database $1 to file ${RHOST}:/var/tmp/dump"
# Since we'll get a file even if the dump fails, we need to clean up
# after ourselves. We do that w/the next 2 lines.
echo >&2 " Removing failed dump archive..."
$SSH_CMD ${SSH_OPTS[*]} -p ${SSH_PORT:-22} -i "$KEYFILE" ${RUNAME}@${RHOST} "set -o pipefail; [[ -e /var/tmp/dump ]] && rm -f /var/tmp/dump" && echo "done." || echo "failed."
#[[ -e "$outzip" ]] && rm -f "$outzip"
#[[ -e "$outzip.bak" ]] && mv ${outzip}{.bak,}
#echo >&2 " Backup restored."
echo >&2 " Dump archive removed."
return 1 # Return to calling function w/error
fi
if [[ -e "$outzip" ]]; then
echo -n "Backing up previous archive... "
if mv "${outzip}"{,.bak}; then
echo "done."
else
echo "failed."
echo >&2 "[ERROR]: Unable to backup original archive."
return 1
fi
fi
echo -n "Downloading archive from ${RHOST}... "
scp -qB ${SSH_OPTS[*]} -P ${SSH_PORT:-22} -i "$KEYFILE" ${RUNAME}@${RHOST}:/var/tmp/dump "${outzip}"
if [[ $? -ne 0 ]]; then
echo "failed."
echo >&2 "[ERROR]: Unable to copy dumped database from ${RHOST} to ${outzip}."
echo >&2 " Removing failed dump archive..."
[[ -e "$outzip" ]] && rm -f "$outzip"
[[ -e "$outzip.bak" ]] && mv "${outzip}"{.bak,}
echo >&2 " Backup restored."
return 1
else
echo "done."
fi
# Test the compressed file. If it fails, restore backup file and
# throw an error.
echo -n "Testing archive... "
if ! $COMP_TEST_CMD "$outzip" >/dev/null 2>&1; then
echo "failed."
echo >&2 "[ERROR]: Archive file failed verification!"
[[ -e "$outzip.bak" ]] && mv "${outzip}"{.bak,}
echo >&2 " Backup restored."
echo >&2
echo >&2 "Removing remote dump file..."
$SSH_CMD ${SSH_OPTS[*]} -p ${SSH_PORT:-22} -i "$KEYFILE" ${RUNAME}@${RHOST} "rm -f /var/tmp/dump"
if [[ $? -ne 0 ]]; then
echo >&2 "[WARNING]: Unable to remove remote dump file /var/tmp/dump"
echo >&2 " Please log in and remove it manually."
fi
return 1 # Return to calling function w/error
fi
echo "OK"
echo
echo -n "Removing remote dump file... "
$SSH_CMD ${SSH_OPTS[*]} -p ${SSH_PORT:-22} -i "$KEYFILE" ${RUNAME}@${RHOST} "rm -f /var/tmp/dump"
if [[ $? -ne 0 ]]; then
echo "failed!"
echo >&2 "[WARNING]: Unable to remove remote dump file /var/tmp/dump"
echo >&2 " Please log in and remove it manually."
else
echo "done."
fi
case $COMP in
7zip)
"$COMP_INFO_CMD" "$outzip" | tail -n5
;;
gzip)
"$COMP_INFO_CMD" "$outzip"
;;
bzip2)
echo "No archive info available for bzip2 files."
;;
*)
;;
esac
# If everything went well, remove backups.
[[ -e "$outzip.bak" ]] && rm -f "$outzip.bak"
echo
return 0
} # ---------- end of function doBackup ----------
#=== FUNCTION ================================================================
# NAME: doMonthly
# DESCRIPTION: Perform monthly backup of databases for $RHOST.
# PARAMETERS: None
# RETURNS: Success/Fail
#===============================================================================
doMonthly ()
{
local retcode=0
local database=''
for database in "${DBLIST[@]}"; do
# We almost never have permission to backup this database, so if it's
# in the list, skip it.
contains "${EXCLUDED[*]}" "$database" && continue
#if [[ "$database" = "information_schema" ]] || [[ "$database" = "mysql.event" ]] || [[ "$database" = "performance_schema" ]]; then
# continue
#fi
echo "Monthly bakcup of database: $database"
if [[ $SEPDIR ]]; then
local outdir="${BACKUPDIR}/${JOBNO}/monthly/$database"
else
local outdir="${BACKUPDIR}/${JOBNO}/monthly"
fi
local outfile="$outdir/${database}_month-$MONTH"
if [[ ! -e $outdir ]] && ! mkdir -p "$outdir"; then
echo >&2 "[ERROR]: Unable to create output directory $outdir"
retcode=1
continue
fi
if ! backup "$database" "$outfile"; then
echo >&2 "[ERROR]: Unable to backup database $database"
retcode=1
continue
fi
done
return $retcode
} # ---------- end of function doMonthly ----------
#=== FUNCTION ================================================================
# NAME: doWeekly
# DESCRIPTION: Perform a weekly backup of databases on $RHOST
# PARAMETERS: None
# RETURNS: Success/Fail
#===============================================================================
doWeekly ()
{
local retcode=0
local database=''
for database in "${DBLIST[@]}"; do
contains "${EXCLUDED[*]}" "$database" && continue
#if [[ "$database" = "information_schema" ]] || [[ "$database" = "mysql.event" ]] || [[ "$database" = "performance_schema" ]]; then
# continue
#fi
echo "Weekly backup of database: $database"
if [[ $SEPDIR ]]; then
local outdir="${BACKUPDIR}/${JOBNO}/weekly/$database"
else
local outdir="${BACKUPDIR}/${JOBNO}/weekly"
fi
if [[ ${WEEK} -le 5 ]]; then
local remw=$((48 + WEEK))
elif [[ ${WEEK} -lt 15 ]]; then
local remw=0$((WEEK - 5))
else
local remw=$((WEEK - 5))
fi
local outfile="${outdir}/${database}_week-$remw"
if [[ ! -e $outdir ]] && ! mkdir -p "$outdir"; then
echo >&2 "[ERROR]: Unable to create output directory $outdir"
retcode=1
continue
fi
if ! backup "$database" "$outfile"; then
echo >&2 "[ERROR]: Unable to backup database $database"
retcode=1
continue
fi
done
return $retcode
} # ---------- end of function doWeekly ----------
#=== FUNCTION ================================================================
# NAME: doDaily
# DESCRIPTION: Daily backup of databases on $RHOST
# PARAMETERS: None
# RETURNS: Success/Fail
#===============================================================================
doDaily ()
{
local retcode=0
local database=''
for database in "${DBLIST[@]}"; do
contains "${EXCLUDED[*]}" "$database" && continue
#if [[ "$database" = "information_schema" ]] || [[ "$database" = "mysql.event" ]] || [[ "$database" = "performance_schema" ]]; then
# continue
#fi
if [[ $SEPDIR ]]; then
local outdir="${BACKUPDIR}/${JOBNO}/daily/$database"
else
local outdir="${BACKUPDIR}/${JOBNO}/daily"
fi
local outfile="$outdir/${database}_day-$DOW"
if [[ ! -e $outdir ]] && ! mkdir -p "$outdir"; then
echo >&2 "[ERROR]: Unable to create output directory $outdir"
retcode=1
continue
fi
echo "Info for database $database: "
#echo "Database size (uncompressed): " $(getSize $database)
backup "$database" "$outfile"
# if ! backup "$database" "$outfile"; then
if [[ ! $? ]]; then
echo >&2 "[ERROR]: Unable to backup database $database"
retcode=1
echo
continue
fi
done
return $retcode
} # ---------- end of function doDaily ----------
#--- FUNCTION ----------------------------------------------------------------
# NAME: hms
# DESCRIPTION: Convert secons to h:m:s
# PARAMETERS: Seconds to convert
# RETURNS: String '##h:##m:##s'
#-------------------------------------------------------------------------------
hms ()
{
printf "%dh:%dm:%ds\n" $(($1/3600)) $(($1%3600/60)) $(($1%60))
} # ---------- end of function hms ----------
#=== FUNCTION ================================================================
# NAME: header
# DESCRIPTION: Output log header
# PARAMETERS: None
# RETURNS: None
#===============================================================================
header ()
{
cat <<EOF
$THICKBAR
Client Database Backup
Backup of database(s) for $JOBNO on server - $RHOST
$THICKBAR
$THINBAR
Backup start time: $(date -d "@$STARTTIME")
$THINBAR
EOF
} # ---------- end of function header ----------
#=== FUNCTION ================================================================
# NAME: footer
# DESCRIPTION: Output log footer
# PARAMETERS: None
# RETURNS: None
#===============================================================================
footer ()
{
cat <<EOF
$THINBAR
Backup end time: $(date -d "@$ENDTIME") (elapsed: $(hms $((ENDTIME - STARTTIME))))
$THINBAR
$THICKBAR
Size - Location
$(du -hs "${BACKUPDIR}/${JOBNO}" 2>/dev/null || echo "N/A ${BACKUPDIR}/${JOBNO}")
$(du -hs "$BACKUPDIR")
$THICKBAR
EOF
} # ---------- end of function footer ----------
#-------------------------------------------------------------------------------
# BEGIN Program Constants
#-------------------------------------------------------------------------------
# In 2 of the date commands below we store the number with the leading 0 stripped
# off. This is because, under certain circumstances, Bash will treat a number --
# specifically 08 -- as an octal number when doing number comparisons. This
# causes the comparisons to fail with a 'value too great for base (error token is
# "08")' error. Storing these numbers without a leading '0' mitigates this
# problem.
DATE=$(date +%Y-%m-%d_%Hh%Mm) # Today's date
DOW=$(date +%u) # Current day of week (0=Sun, 6=Sat)
DNOW=$(date +%y) # 2-digit year
DOM=$(date +%-d) # Day of the month, don't pad w/leading 0
MONTH=$(date +%B) # Current month
WEEK=$(date +%-V) # Current week number (don't pad w/leading 0)
#MOPT="-q --databases --compress --add-drop-database --add-drop-table" # Default opts for mysqldump
MOPT=('-q' '--databases' '--compress' '--add-drop-database' '--add-drop-table' '--skip-events' '--single-transaction' '--skip-lock-tables')
MYSQL_HOST=127.0.0.1 # Default mysql host (localhost)
CODE=0
THICKBAR="=============================================================================="
THINBAR="------------------------------------------------------------------------------"
SSH_CMD="$(which ssh) -q" # -q = quiet mode
MYSQL=$(which mysql)
LOC_GZIP=$(which gzip)
LOC_BZIP2=$(which bzip2)
LOC_7ZIP=$(which 7z)
TEE=$(which tee)
GZ_TOPTS="-t"
BZ2_TOPTS="-t"
SZ_TOPTS="t"
# Due to our '-o nounset' option in this script, we must declare all variables
# before we use them, otherwise bash will throw an error.
MYSQL_TCP_PORT=''
COMP_CMD=''
COMP_TEST_CMD=''
COMP_INFO_CMD=''
CONFIGS=''
LOGDIR=''
LOGFILE=''
ERRFILE=''
DATABASE=''
JOBNO=''
STARTTIME=''
ENDTIME=''
i=''
#-------------------------------------------------------------------------------
# END Program Constants
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
# MAIN LOOP
#-------------------------------------------------------------------------------
# Trap kill signals (CTRL+C, etc.) and clean-up before exiting.
# It should be obvious, but this will not catch SIGKILL (kill -9)
trap 'echo >&2 "Received kill request, exiting..."; stopLogging; $exit 0' SIGHUP SIGINT SIGTERM
# Get command-line arguments
while getopts "hva:b:c:d:f:o:z:" opt; do
case $opt in
h) usage;;
a) MAILADDR="$OPTARG";;
b) BACKUPDIR="$OPTARG";;
c) CONFDIR="$OPTARG";;
f) CONFIGS="${CONFDIR}/${OPTARG##*/}";;
d) DATABASE="$OPTARG";; # User-specified database
D) DEBUG=0;;
o) LOGTYPE="$OPTARG";;
v) set -x;; # Verbose (a.k.a. Debug)
z) COMP="${OPTARG}";; # Overload default setting
*) usage;;
esac
done
# Read our list of config files into an array we'll loop through.
# We use this particular construction to make sure we can read-in filenames
# with embedded white-space and newline characters. See 'man 1 find' for
# info on the -print0 switch and 'man 1 read' for info on the options used
# with the read statement.
if [[ -z $CONFIGS ]]; then
while IFS= read -r -d '' file; do
CONFIGS[i++]="$file"
done< <(find "$CONFDIR" -type f -name "*.cnf" -print0) # Use output of find statement to feed loop.
fi
for CONFIG in "${CONFIGS[@]}"; do # Loop through config files.
JOBNO=${CONFIG##*/} # Basename of config file
JOBNO=${JOBNO%%\.*} # Strip file extension to get Job No.
sleep 3
if ! startLogging; then
echo >&2 "[ERROR]: Unable to start logging. Output will be to screen."
fi
if [[ ! -e "${CONFIG}" ]] || [[ ! -r "${CONFIG}" ]]; then
echo >&2 "[ERROR]: ${CONFIG} does not exist or is not readable! Skipping..."
continue # Continue to next item in for loop.
else
# Include our config file. Bring in variables defined in the config file
# as local variables to this script.
. "$CONFIG"
fi
STARTTIME=$(date +%s)
header
# Check if we have a user-supplied database name/list"
if [[ -z $DATABASE ]]; then
# The convention below replaces ',' characters with newline '\n' characters
# but saves us the expense of a sub-shell to do an echo through 'tr'
# After the conversion has been done, each db name then becomes an array
# element.
DBLIST=(${DBLIST//,/$'\x0A'})
else
DBLIST=(${DATABASE//,/$'\x0A'}) # Allows for list of dbs @ cl.
fi
if [[ ! -e "$KEYFILE" || ! -r "$KEYFILE" ]]; then # Can we read our ssh keyfile?
echo >&2 "[ERROR]: Key file $KEYFILE missing or unreadable."
CODE=1
ENDTIME=$(date +%s)
footer
stopLogging
continue # Skip to next config file
fi
# Check connectivity to remote host.
if _testssh; then
: # noop
else
echo >&2 "[ERROR]: Connection to $RHOST failed. Skipping to next host."
CODE=1
ENDTIME=$(date +%s)
footer
stopLogging
continue
fi
#STATUS=$(eval $SSH_CMD ${SSH_OPTS[*]} -p ${SSH_PORT:-22} ${RUNAME}@${RHOST} -i \"${KEYFILE}\" echo ok 2>&1)
#[[ $DEBUG ]] && { echo "SSH STATUS: $STATUS"; }
#if [[ $STATUS == "Permission denied"* ]]; then
# echo >&2 "[ERROR]: Connection to $RHOST is not authorized"
# echo >&2 " Skipping to next host."
# CODE=1
# ENDTIME=$(date +%s)
# footer
# stopLogging
# continue
#elif [[ ! $STATUS == ok ]]; then
# echo >&2 "[ERROR]: Unable to connect to host $RHOST due to unknown error"
# echo >&2 " Skipping to next host."
# CODE=1
# ENDTIME=$(date +%s)
# footer
# stopLogging
# continue
#fi
#if [[ $DEBUG ]]; then
# echo "Checking host connectivity..."
# echo "Command: $SSH_CMD $SSH_OPTS -p ${SSH_PORT:-22} ${RUNAME}@${RHOST} -i \"$KEYFILE\" \"/bin/true\" >/dev/null"
#fi
#if ! eval $SSH_CMD $SSH_OPTS -p ${SSH_PORT:-22} ${RUNAME}@${RHOST} -i "$KEYFILE" "/bin/true" >/dev/null; then
# echo >&2 "[ERROR]: Unable to connect to host $RHOST"
# echo >&2 " Skipping to next host."
# CODE=1
# ENDTIME=$(date +%s)
# footer
# stopLogging
# continue # Skip to next config file
#fi
# This sleep needs to be here to prevent the script from trying to connect
# to the database before the establishment of the port forward has been
# completed.
#sleep 3
if [[ ${#DBLIST[@]} -eq 1 ]] && [[ "${DBLIST[0]}" = "all" ]]; then # Should we get a list of databases from server?
if ! DBLIST=($(getDatabases)); then # Did we successfully get a list?
echo >&2 "[ERROR]: Unable to retrieve list of databases from $RHOST"
echo >&2 " Skipping to next host."
CODE=1
ENDTIME=$(date +%s)
footer
stopLogging
continue # Skip to next config file
fi
fi
# Initialize some settings based on chosen compression program.
if ! set_compression; then
echo >&2 "[ERROR]: Setting compression type failed for $RHOST"
echo >&2 " Skipping to next host."
CODE=1
ENDTIME=$(date +%s)
footer
stopLogging
continue
fi
# Get path to mysqldump binary on remote host. Error and move on if unsuccessful.
if ! MYSQLDUMP=$($SSH_CMD ${SSH_OPTS[*]} -p ${SSH_PORT:-22} -i "$KEYFILE" ${RUNAME}@${RHOST} "which mysqldump"); then
echo >&2 "[ERROR]: Unable to find mysqldump on $RHOST"
echo >&2 " Skipping to next host."
CODE=1
ENDTIME=$(date +%s)
footer
stopLogging
continue
fi
# Add specified databases to list of excluded databases.
if [[ ${#EXCLUDE[@]} -gt 0 ]]; then
EXCLUDED+=( "${EXCLUDE[@]}" )
fi
if [[ $DOM -eq 1 ]]; then
if ! doMonthly; then
CODE=1
echo "Errors were reported during monthly backup."
fi
fi
if [[ $DOW -eq $DOWEEKLY ]]; then
if ! doWeekly; then
CODE=1
echo "Errors were reported during weekly backup."
fi
fi
if ! doDaily; then
CODE=1
echo "Errors were reported during daily backup."
fi
ENDTIME=$(date +%s)
footer
stopLogging
unset MUNAME MPASSWD DBLIST RUNAME RHOST RPORT KEYFILE SSH_PORT
done
exit $CODE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment