#!/bin/bash # # egdbback, copyright 2005-2008 Egressive Limited, http://egressive.com # # this script runs periodic database backups... # #========================================== # The Variables - these must be define # before they're referenced #========================================== VERSION="0.3" EGDB_NAME=`basename $0` EGDB_DIR=/etc/egscripts/egdbback EGDB_CMD=$EGDB_DIR/egdbback EGDB_CONF_DIR=$EGDB_DIR/conf SITE_CONF="$EGDB_CONF_DIR/site.conf" # the appropriate mysql paths MYDIR=/var/lib/mysql # # this provides values for MACHINE_NAME and EMAIL . $SITE_CONF # LOGS=/var/log LOG=$LOGS/egdbback.log #========================================== # Defaults - these shouldn't need changing #========================================== PERIODS="hourly daily weekly monthly yearly" hourlyNUM="24" dailyNUM="7" weeklyNUM="4" monthlyNUM="12" yearlyNUM="7" # required programs MAIL=`which mail` GREP=`which grep` LS=`which ls` ID=`which id` DATE=`which date` DF=`which df` GZIP=`which gzip` GZIPSUF=gz RM=`which rm` LN=`which ln` CUT=`which cut` AWK=`which awk` # determine today's date TODAY=`$DATE '+%Y-%m-%d-%a'` # determine today's date NOW=`$DATE '+%H-%M-%S'` # a timestamp for logging purposes TIMESTAMP=`$DATE '+%Y-%m-%d %H:%M.%S'` # temporary holding point for email TMP_DIR=/tmp TMP_EMAIL=$TMP_DIR/$EGDB_NAME"_tmp_email_"$TODAY.$TIME # #========================================== # MySQL vars... MYTMP=$MYBKP/tmp-$DATE MYDUMP=`which mysqldump` MYSQL=`which mysql` MYSHOW=`which mysqlshow` # pattern for "ls" command to build list of # pruneable backup files... # -1t = 1 column, ordered by time of last mod PRUNEABLES_CMD="ls -1t" # #========================================== # The Functions - these have to be defined # before they're called! Note, refer # to them without the "()" and provide # up to one argument, referred to as "$@" # in the function... #========================================== # # function to direct a message... message() { # a timestamp for logging purposes timestamp if test -w $LOG ; then echo "$EGDB_NAME: $TIMESTAMP $@" >> $LOG fi if test -w $TMP_EMAIL ; then echo "$EGDB_NAME: $TIMESTAMP $@" >> $TMP_EMAIL fi verbose "$TIMESTAMP $@" } # # function to direct a message... verbose() { if test $VERBOSE ; then echo "$@" fi } # # insert a blank line into the log and on the console insert_blank() { echo "" >> $TMP_EMAIL verbose "" } # # update date and time info.. today() { # determine today's date TODAY=`$DATE '+%Y-%m-%d-%a'` } # # now() { # determine today's date NOW=`$DATE '+%H-%M-%S'` } # # timestamp() { # a timestamp for logging purposes TIMESTAMP=`$DATE '+%Y-%m-%d %H:%M.%S'` } # # roll_tests() { hourly_ROLL=0 daily_ROLL=0 weekly_ROLL=0 monthly_ROLL=0 # hour of the day 0-23 if test `$DATE '+%k'` -eq "0" ; then message "it's time to create a new daily backup!" hourly_ROLL=1 fi # day of the week 0-6 if test `$DATE '+%w'` -eq "0" ; then message "it's time to create a new weekly backup!" daily_ROLL=1 fi # day of month 0-31ish if test `$DATE '+%w'` -eq "00" ; then message "it's time to create a new monthly backup!" weekly_ROLL=1 fi # day of year 0-366ish if test `$DATE '+%j'` -eq "000" ; then message "it's time to create a new yearly backup!" yearly_ROLL=1 fi } # # create the temporary email file create_tmp_email() { if test -d $TMP_DIR ; then if test -w $TMP_DIR ; then touch $TMP_EMAIL 2>&1 else error "Email tmp directory $TMP_DIR is not writable" fi else error "Email tmp directory $TMP_DIR does not exist" fi if test -w $TMP_EMAIL ; then message "created temporary email $TMP_EMAIL" else error "Failed to create temporary email $TMP_EMAIL" fi } # # send the contents of the temporary file to the # designated report recipient send_email_report() { if test -f $TMP_EMAIL ; then message "sending email report to $EMAIL_TO" if test $ERROR_STATUS == 1 ; then EMAIL_SUBJ="[ERROR] $MACHINE_NAME $DEFAULT_EMAIL_SUBJ" else EMAIL_SUBJ="[SUCCESS] $MACHINE_NAME $DEFAULT_EMAIL_SUBJ" fi # check space again to see how close things are to full message "Printing disk space for reference purposes:" DISK_SPACE=`$DF` message "$DISK_SPACE" # send it to each email address in the list... for EMAIL_ADD in $EMAIL_TO do RES=`$MAIL -s "$EMAIL_SUBJ" $EMAIL_ADD < $TMP_EMAIL` if test -z $RES ; then if test $ERROR_STATUS == 1 ; then message "Error email report successfully sent to $EMAIL_ADD" else message "Email report successfully sent to $EMAIL_ADD" fi else if ! test $ERROR_STATUS == 1 ; then error "Email report send to $EMAIL_ADD failed with this message: $RES" fi fi done if test -w $TMP_EMAIL ; then $RM $TMP_EMAIL 2>&1 else if ! test $ERROR_STATUS == 1 ; then error "Failed to remove email message file, $TMP_EMAIL: permission denied." fi fi if test -f $TMP_EMAIL ; then error "Failed to remove email message file $TMP_EMAIL for an unknown reason" else message "successfully removed temporary email $TMP_EMAIL" fi else if ! test $ERROR_STATUS == 1 ; then error "Email message file, $TMP_EMAIL, does not exist." fi fi } # # function to direct an error... error() { # # recognise that the system experienced # an error and send out a special email, and halt # the program! timestamp ERROR_STATUS=1 # # get current user details USER_DETAILS=`$ID` if test -w $LOG ; then echo "$EGDB_NAME: **ERROR** $TIMESTAMP $@" >> $LOG echo "$EGDB_NAME: user details - $USER_DETAILS" >> $LOG fi if test -w $TMP_EMAIL ; then echo "$EGDB_NAME: **ERROR** $TIMESTAMP $@" >> $TMP_EMAIL echo " user details: $USER_DETAILS" >> $LOG fi verbose "$TIMESTAMP **ERROR** $@" verbose " user details: $USER_DETAILS" send_email_report exit 1 } # # build list of backup configuration files get_confs() { CONFS=`$LS -1 $EGDB_CONF_DIR/$CONF_ROOT*.conf 2>&1` TEST=`echo $CONFS | $GREP -c "No such file or directory" -` #TEST2=`echo $CONFS | $GREP -c "$EGDB_CONF_DIR" -` if test $TEST == 1 ; then error "No configuration files found with root $CONF_ROOT in $EGDB_CONF_DIR" fi #echo "CONFS = $CONFS" } # # convert last monthly into daily, daily->weekly, weekly->monthly, monthly->yearly roll_over() { for PERIOD in $PERIODS do # Clean out old backups verbose "sorting out $PERIOD" # this subsitutes the PERIOD, e.g. hourly, weekly, etc. to build the variable compounded with NUM BU_TO_KEEP=$[${PERIOD}NUM] message "keeping last $BU_TO_KEEP backups" PATTERN="$BU_DIR/$PERIOD-$DB.*" # build the list, with the suffix... PRUNEABLES=`$PRUNEABLES_CMD $PATTERN.$GZIPSUF` message "pruning older files based on $PATTERN.$GZIPSUF" message "pruneable files: $PRUNEABLES" if test "$?" -eq "0" ; then # # set counter NUM=0 # go through the list of files and remove those we don't want for PRUNEABLE in $PRUNEABLES do NUM=$(($NUM + 1)) if test $NUM -gt $BU_TO_KEEP ; then message "deleting $PRUNEABLE" if test $DRY_RUN == 1 ; then message "would delete $PRUNEABLE if this wasn't a dry run" else rm $PRUNEABLE 2>&1 > /dev/null fi else message "keeping $PRUNEABLE" fi done fi done } # delete old backups delete_old() { # if test -n $FROOT && test -n $BU_TO_KEEP ; then # pattern to search for to build the list... PATTERN="$BU_DIR/$FROOT.*" # build the list, with the suffix... PRUNEABLES=`$PRUNEABLES_CMD $PATTERN.$GZIPSUF` if test "$?" -eq "0" ; then message "pruning older files based on $PATTERN.$GZIPSUF" message "keeping last $BU_TO_KEEP backups" # # set counter NUM=0 # go through the list of files and remove those we don't want for PRUNEABLE in $PRUNEABLES do NUM=$(($NUM + 1)) if test $NUM -gt $BU_TO_KEEP ; then message "deleting $PRUNEABLE" if test $DRY_RUN == 1 ; then message "would delete $PRUNEABLE if this wasn't a dry run" else rm $PRUNEABLE 2>&1 > /dev/null fi else message "keeping $PRUNEABLE" fi done fi else message "keeping older backups, missing root_filename..." fi } # # check_space() { RES=`$DF -h` #message "$RES" TEST=0 PCENT="100%" TEST=`echo "$RES" | $GREP -c "$PCENT"` if test $TEST == 1 ; then PART_LINE=`echo "$RES" | $GREP "$PCENT"` # get the device name which is showing $PCENT TEST=`echo $PART_LINE | $CUT -f 1 -d ' '` # if it has a partition number in the name, it's probably a block # device rather than, say, a CDROM... TEST2=`echo $TEST | $GREP -c [0-9]` echo "Test2 = $TEST2" if test $TEST2 == 1 ; then error "Partition $TEST is full - detail: $PART_LINE" else message "false alarm - it's only the $TEST volume..." fi fi } # # this is where we do all the backup-related stuff - what ebu used to do... do_backup() { # first, find out what dbs there are DBLIST=`$MYSHOW -u $USER -p$PASSWORD | $AWK '{ if ($2!~/^$/&&$2!="Databases") { print $2;}}'` #message "List of databases: $DBLIST" # for each db in the list, create a compressed backup in the right place message "creating a backup of the MySQL databases on $MACHINE_NAME..." for DB in $DBLIST do FROOT=$CONF_ROOT-$DB now today FNAME=$FROOT.$TODAY.$NOW # --opt is same as --add-drop-table --add-locks --all --extended-insert --quick --lock-tables CMD="$MYDUMP -u $USER -p$PASSWORD --opt $DB" # create the new backup! if test $DRY_RUN == 1 ; then message "I would be doing this (if it was not a dry run):" message "$CMD > $BU_DIR/$FNAME" else message "backing up table $DB..." message "running $MYDUMP with user $USER, creating $BU_DIR/$FNAME" $CMD > $BU_DIR/$FNAME if test -f $BU_DIR/$FNAME ; then $GZIP $BU_DIR/$FNAME else error "backup sql file, $BU_DIR/$FNAME, doesn't exist!" fi fi # delete old backups, so that only BU_TO_KEEP of them are stored at once roll_over delete_old done } #======================================== # The Error Checking #======================================== # # first, check that all the necessary files and stuff are there and usable # # if the log doesn't exist, create it if ! test -f $LOG ; then message "creating non-existent log file $LOG" RET=`touch $LOG 2>&1 /dev/null` TEST=`echo $RET | $GREP -c "Permission denied" -` if test $TEST == 1 ; then error "Failed to create log file $LOG, user cannot write to that directory" fi fi # checking whether it's there an is writable if ! test -w $LOG ; then error "Log file $LOG not writable" fi #======================================== # The Functional Part of the Script #======================================== # set variable defaults ERROR_STATUS=0 # initially assume no errors VERBOSE=0 # initially assume quiet output CONF_ROOT="hourly" # set it to something we know so we can test if it's changed DRY_RUN=0 # assume this is not a dry run... CURRENT_LINK="none" MODE=help # process any arguments while test $# -ne 0 ; do # while there are arguments message "argument: $1" case $1 in --help|-h|-?) verbose "setting mode to HELP" MODE=help ;; --verbose|-v) verbose "setting verbostity to ON" VERBOSE=1 ;; --dryrun) verbose "setting dryrun to ON" DRY_RUN=1 ;; *) verbose "unknown option: $1" message "unknown option: $1" MODE=help ;; esac shift done if ! test $MODE == "help" ; then MODE=run fi # # set things in motion based on the script arguments case $MODE in run) create_tmp_email get_confs # run backup for each file for BU_CONF in $CONFS do message "do backup with conf: $BU_CONF" . $BU_CONF # grab the current variables for this conf file... do_backup # put a space in the email to separate tasks insert_blank done # # send the resulting email send_email_report ;; help) if test $CONF_ROOT; then echo "error: please specify a period for egbackup" fi echo "" echo "$EGDB_NAME, version $VERSION, copyright 2005-7 Egressive Ltd, http://egressive.com" echo "===================" echo "usage: $EGDB_NAME" echo "== options ==" echo "-v or --verbose - give extra feedback on progress to stdout" echo "-c or --configroot config_filename - root for configuration filenames" echo "--dryrun - do everything else, but *don't* delete anything or run the backup" echo "" ;; esac