tagged 0.4.4
[matthijs/upstream/backupninja.git] / backupninja
index 236fb67b1aa0bee5d572e0c3fcb0d6b2a3cbb21e..b7d4a149e2e73683745e92f224faf899abd1c71f 100755 (executable)
 # GNU General Public License for more details.
 #
 
-#####################################################
-## DEFAULTS
-
-DEBUG=${DEBUG:=0}
-CONFFILE="/etc/backupninja.conf"
-USECOLOURS=1
-
 #####################################################
 ## FUNCTIONS
 
 function setupcolors() {
-       if [  "$USECOLOURS" == 1 ]
-       then
-               BLUE="\033[34;01m"
-               GREEN="\033[32;01m"
-               YELLOW="\033[33;01m"
-               PURPLE="\033[35;01m"
-               RED="\033[31;01m"
-               OFF="\033[0m"
-               CYAN="\033[36;01m"
-       fi
+       BLUE="\033[34;01m"
+       GREEN="\033[32;01m"
+       YELLOW="\033[33;01m"
+       PURPLE="\033[35;01m"
+       RED="\033[31;01m"
+       OFF="\033[0m"
+       CYAN="\033[36;01m"
+       COLORS=($BLUE $GREEN $YELLOW $RED $PURPLE)
 }
 
-function run() {
-       RUNERROR=0
-       debug 0 "$@"
-       returnstring=`$@ 2>&1`
-       RUNERROR=$?
-       RUNERRORS=$[RUNERRORS+RUNERROR]
-       if [ "$RUNERROR" != 0 ]; then
-               debug 3 "Exitcode $RUNERROR returned when running: $@"
-               debug 3 "$returnstring"
+function colorize() {
+       if [ "$usecolors" == "yes" ]; then
+               local typestr=`echo "$@" | sed 's/\(^[^:]*\).*$/\1/'`
+               [ "$typestr" == "Debug" ] && type=0
+               [ "$typestr" == "Info" ] && type=1
+               [ "$typestr" == "Warning" ] && type=2
+               [ "$typestr" == "Error" ] && type=3
+               [ "$typestr" == "Fatal" ] && type=4
+               color=${COLORS[$type]}
+               endcolor=$OFF
+               echo -e "$color$@$endcolor"
        else
-               debug 0 "$returnstring"
+               echo -e "$@"
        fi
-       return $RUNERROR
 }
 
-# We have the following debug levels:
+# We have the following message levels:
 # 0 - debug - blue
 # 1 - normal messages - green
 # 2 - warnings - yellow
@@ -66,33 +58,58 @@ function run() {
 # used to capture output from handlers
 echo_debug_msg=0
 
-function debug() {
+usecolors=yes
 
+function printmsg() {
        [ ${#@} -gt 1 ] || return
-        
-       TYPES=(Debug Info Warning Error Fatal)
-       COLOURS=($BLUE $GREEN $YELLOW $RED $PURPLE)
+
        type=$1
-       colour=${COLOURS[$type]}
        shift
+       if [ $type == 100 ]; then
+               typestr=`echo "$@" | sed 's/\(^[^:]*\).*$/\1/'`
+               [ "$typestr" == "Debug" ] && type=0
+               [ "$typestr" == "Info" ] && type=1
+               [ "$typestr" == "Warning" ] && type=2
+               [ "$typestr" == "Error" ] && type=3
+               [ "$typestr" == "Fatal" ] && type=4
+               typestr=""
+       else
+               types=(Debug Info Warning Error Fatal)
+               typestr="${types[$type]}: "
+       fi
+       
        print=$[4-type]
-       if [ "$print" -lt "$loglevel" -o "$DEBUG" == 1 ]; then
-               if [ -z "$logfile" ]; then
-                       echo -e "${colour}${TYPES[$type]}: $@${OFF}" >&2
-               else
-                       if [ "$DEBUG" == 1 -o "$type" == 4 ]; then
-                               echo -e "${colour}${TYPES[$type]}: $@${OFF}" >&2
-                       fi
-                       echo -e "${colour}${TYPES[$type]}: $@${OFF}" >> $logfile
-               fi
+       
+       if [ $echo_debug_msg == 1 ]; then
+               echo -e "$typestr$@" >&2
+       elif [ $debug ]; then
+               colorize "$typestr$@" >&2
        fi
-       if [ "$echo_debug_msg" != "0" -a "$type" -gt "1" ]; then
-               echo -e "${TYPES[$type]}: $@"
+       
+       if [ $print -lt $loglevel ]; then
+               if [ -w "$logfile" ]; then
+                       colorize "$typestr$@" >> $logfile
+               fi
        fi
 }
 
+function passthru() {
+       printmsg 100 "$@"
+}
+function debug() {
+       printmsg 0 "$@"
+}
+function info() {
+       printmsg 1 "$@"
+}
+function warning() {
+       printmsg 2 "$@"
+}
+function error() {
+       printmsg 3 "$@" 
+}
 function fatal() {
-       debug 4 "$@"
+       printmsg 4 "$@"
        exit 2
 }
 
@@ -133,139 +150,330 @@ function getconf() {
        eval $1='$ret'
 }
 
+#
+# enforces very strict permissions on configuration file $file.
+#
 
-#####################################################
-## MAIN
+function check_perms() {
+       local file=$1
+       local perms=`ls -ld $file`
+       perms=${perms:4:6}
+       if [ "$perms" != "------" ]; then
+               fatal "Configuration files must not be group or world readable! Dying on file $file"
+       fi
+       if [ `ls -ld $file | awk '{print $3}'` != "root" ]; then
+               fatal "Configuration files must be owned by root! Dying on file $file"
+       fi
+}
 
-## process command line options
+# simple lowercase function
+function tolower() {
+       echo "$1" | tr [:upper:] [:lower:]
+}
 
-if [ "$1" == "--help" ]; then
-       HELP=1;DEBUG=1;loglevel=4
-else
-       while getopts h,f:,d,t option
-       do
-               case "$option" in
-                       h) HELP=1;DEBUG=1;loglevel=4;;
-                       d) DEBUG=1;loglevel=4;;
-                       f) CONFFILE="$OPTARG";;
-                       t) test=1;DEBUG=1;;
-               esac
-       done
-fi
-       
-setupcolors
+# simple to integer function
+function toint() {
+       echo "$1" | tr [:alpha:] -d 
+}
 
-## Print help
+#
+# function isnow(): returns 1 if the time/day passed as $1 matches
+# the current time/day.
+#
+# format is <day> at <time>:
+#   sunday at 16
+#   8th at 01
+#   everyday at 22
+#
+
+# we grab the current time once, since processing
+# all the configs might take more than an hour.
+nowtime=`date +%H`
+nowday=`date +%d`
+nowdayofweek=`date +%A`
+nowdayofweek=`tolower "$nowdayofweek"`
+
+function isnow() {
+       local when="$1"
+       set -- $when
+       whendayofweek=$1; at=$2; whentime=$3;
+       whenday=`toint "$whendayofweek"`
+       whendayofweek=`tolower "$whendayofweek"`
+       whentime=`echo "$whentime" | sed 's/:[0-9][0-9]$//'`
+
+       if [ "$whendayofweek" == "everyday" ]; then
+               whendayofweek=$nowdayofweek
+       fi
+
+       if [ "$whenday" == "" ]; then
+               if [ "$whendayofweek" != "$nowdayofweek" ]; then
+                       whendayofweek=${whendayofweek%s}
+                       if [ "$whendayofweek" != "$nowdayofweek" ]; then
+                               return 0
+                       fi
+               fi
+       elif [ "$whenday" != "$nowday" ]; then
+               return 0
+       fi
+
+       [ "$at" == "at" ] || return 0
+       [ "$whentime" == "$nowtime" ] || return 0
+
+       return 1
+}
 
-if [ "$HELP" == 1 ]; then
-cat << EOF
+function usage() {
+       cat << EOF
 $0 usage:
 This script allows you to coordinate system backup by dropping a few
-simple configuration files into /etc/backup.d/. In general, this script
-is run from a cron job late at night. 
+simple configuration files into /etc/backup.d/. Typically, this
+script is run hourly from cron.
 
 The following options are available:
--h         This help message
--d         Run in debug mode, where all log messages are output to the current shell.
--f <file>  Use <file> for the main configuration instead of /etc/backupninja.conf
--t         Run in test mode, no actions are actually taken.
-
+-h, --help           This usage message
+-d, --debug          Run in debug mode, where all log messages are
+                     output to the current shell.
+-f, --conffile FILE  Use FILE for the main configuration instead
+                     of /etc/backupninja.conf
+-t, --test           Run in test mode, no actions are actually taken.
+-n, --now            Perform actions now, instead of when they
+                     might be scheduled.
+    --run FILE       Execute the specified action file and then exit.    
 When using colored output, there are:
 EOF
-debug 0 "Debugging info (when run with -d)"
-debug 1 "Informational messages (verbosity level 4)"
-debug 2 "Warnings (verbosity level 3 and up)"
-debug 3 "Errors (verbosity level 2 and up)"
-debug 4 "Fatal, halting errors (always shown)"
-exit 0
-fi
+       debug=1
+       debug   "Debugging info (when run with -d)"
+       info    "Informational messages (verbosity level 4)"
+       warning "Warnings (verbosity level 3 and up)"
+       error   "Errors (verbosity level 2 and up)"
+       fatal   "Fatal, halting errors (always shown)"
+}
+
+##
+## this function handles the running of a backup action
+##
+## these globals are modified:
+## fatals, errors, warnings, actions_run, errormsg
+##
+
+function process_action() {
+       local file="$1"
+       local suffix="$2"
+
+       setfile $file
+
+       # skip over this config if "when" option
+       # is not set to the current time.
+       getconf when "$defaultwhen"
+       if [ "$processnow" == 1 ]; then
+               info "running $file because of --now"
+       else
+               IFS=$'\t\n'
+               for w in $when; do
+                       IFS=$' \t\n'
+                       isnow "$w"
+                       ret=$?
+                       IFS=$'\t\n'
+                       if [ $ret == 0 ]; then
+                               debug "skipping $file because it is not $w"
+                               return
+                       else
+                               info "running $file because it is $w"
+                       fi
+               done
+               IFS=$' \t\n'
+       fi
+       
+       let "actions_run += 1"
+
+       # call the handler:
+       local bufferfile="/tmp/backupninja.buffer.$$"
+       echo "" > $bufferfile
+       echo_debug_msg=1
+       (
+               . $scriptdir/$suffix $file
+       ) 2>&1 | (
+               while read a; do
+                       echo $a >> $bufferfile
+                       [ $debug ] && colorize "$a"
+               done
+       )
+       retcode=$?
+       # ^^^^^^^^ we have a problem! we can't grab the return code "$?". grrr.
+       echo_debug_msg=0
+
+       _warnings=`cat $bufferfile | grep "^Warning: " | wc -l`
+       _errors=`cat $bufferfile | grep "^Error: " | wc -l`
+       _fatals=`cat $bufferfile | grep "^Fatal: " | wc -l`
+       
+       ret=`grep "\(^Warning: \|^Error: \|^Fatal: \)" $bufferfile`
+       rm $bufferfile
+       if [ $_fatals != 0 ]; then
+               msg "*failed* -- $file"
+               errormsg="$errormsg\n== failures from $file ==\n\n$ret\n"
+       elif [ $_errors != 0 ]; then
+               msg "*error* -- $file"
+               errormsg="$errormsg\n== errors from $file ==\n\n$ret\n"
+       elif [ $_warnings != 0 ]; then
+               msg "*warning* -- $file"
+               errormsg="$errormsg\n== warnings from $file ==\n\n$ret\n"
+       else
+               msg "success -- $file"
+#      elif [ $retcode == 0 ]; then
+#              msg "success -- $file"
+#      else
+#              msg "unknown -- $file"
+       fi
+
+       let "fatals += _fatals"
+       let "errors += _errors"
+       let "warnings += _warnings"     
+}
+
+#####################################################
+## MAIN
+
+setupcolors
+conffile="/etc/backupninja.conf"
+loglevel=3
+
+## process command line options
+
+while [ $# -ge 1 ]; do
+       case $1 in
+               -h|--help) usage;;
+               -d|--debug) debug=1;;
+               -t|--test) test=1;debug=1;;
+               -n|--now) processnow=1;;
+               -f|--conffile)
+                       if [ -f $2 ]; then
+                               conffile=$2
+                       else
+                               fatal "-f|--conffile option must be followed by an existing filename"
+                               usage
+                       fi
+                       # we shift here to avoid processing the file path 
+                       shift
+                       ;;
+               --run)
+                       debug=1
+                       if [ -f $2 ]; then
+                               singlerun=$2
+                               processnow=1
+                       else
+                               fatal "--run option must be fallowed by a backupninja action file"
+                               usage
+                       fi
+                       shift
+                       ;;
+               *)
+                       debug=1
+                       fatal "Unknown option $1"
+                       usage
+                       exit
+                       ;;
+       esac
+       shift
+done                                                                                                                                                                                                           
+
+#if [ $debug ]; then
+#      usercolors=yes
+#fi
 
 ## Load and confirm basic configuration values
 
 # bootstrap
-[ -r "$CONFFILE" ] || fatal "Configuration file $CONFFILE not found."
-scriptdir=`grep scriptdirectory $CONFFILE | awk '{print $3}'`
-[ -n "$scriptdir" ] || fatal "Cound not find entry 'scriptdirectory' in $CONFFILE."
+[ -r "$conffile" ] || fatal "Configuration file $conffile not found."
+scriptdir=`grep scriptdirectory $conffile | awk '{print $3}'`
+[ -n "$scriptdir" ] || fatal "Cound not find entry 'scriptdirectory' in $conffile"
 [ -d "$scriptdir" ] || fatal "Script directory $scriptdir not found."
-setfile $CONFFILE
+setfile $conffile
 
 # get global config options (second param is the default)
 getconf configdirectory /etc/backup.d
 getconf reportemail
+getconf reportsuccess yes
+getconf reportwarning yes
 getconf loglevel 3
+getconf when "Everyday at 01:00"
+defaultwhen=$when
 getconf logfile /var/log/backupninja.log
+getconf usecolors "yes"
 getconf SLAPCAT /usr/sbin/slapcat
 getconf RDIFFBACKUP /usr/bin/rdiff-backup
 getconf MYSQL /usr/bin/mysql
 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
 getconf MYSQLDUMP /usr/bin/mysqldump
 getconf GZIP /bin/gzip
+getconf RSYNC /usr/bin/rsync
 
 [ -d "$configdirectory" ] || fatal "Configuration directory '$configdirectory' not found."
-[ `id -u` == "0" ] || fatal "Can only be run as root"
 
-## Process each configuration file
+if [ "$UID" != "0" ]; then
+       echo "$0 can only be run as root"
+       exit 1
+fi
 
-debug 1 "====== starting at "`date`" ======"
+## Process each configuration file
 
 # by default, don't make files which are world or group readable.
 umask 077
 
-for file in $configdirectory/*; do     
-       perms=`ls -ld $file`
-       perms=${perms:4:6}
-       if [ "$perms" != "------" ]; then
-               fatal "Configuration files must not be group or world readable! Dying on file $file"
-       fi
-       if [ `ls -ld $file | awk '{print $3}'` != "root" ]; then
-               fatal "Configuration files must be owned by root! Dying on file $file"
-       fi
+# these globals are set by process_action()
+fatals=0
+errors=0
+warnings=0
+actions_run=0
+errormsg=""
+
+if [ "$singlerun" ]; then
+       files=$singlerun
+else
+       files=`find $configdirectory -mindepth 1 `
+fi
+
+for file in $files; do
+       [ -f "$file" ] || continue
+
+       check_perms $file
        suffix="${file##*.}"
        base=`basename $file`
        if [ "${base:0:1}" == "0" ]; then
-               debug 1 "Skipping $file"
+               info "Skipping $file"
                continue
-       else
-               debug 1 "Processing $file"
        fi
 
        if [ -e "$scriptdir/$suffix" ]; then
-               setfile $file
-               echo_debug_msg=1
-               ret=`( . $scriptdir/$suffix $file )`
-               retcode="$?"
-               warnings=`echo $ret | grep -e "^Warning: " | wc -l`
-               errors=`echo $ret | grep -e "^Error: \|^Fatal: " | wc -l`
-               if [ $errors != 0 ]; then
-                       msg "*failed* -- $file"
-                       error="$error\n== errors from $file ==\n\n$ret\n"
-               elif [ $warnings != 0 ]; then
-                       msg "*warning* -- $file"
-                       error="$error\n== warnings from $file ==\n\n$ret\n"
-               elif [ $retcode == 0 ]; then
-                       msg "success -- $file"
-               else
-                       msg "unknown -- $file"
-               fi
-               echo_debug_msg=0
+               process_action $file $suffix
        else
-               debug 3 "Can't process file '$file': no handler script for suffix '$suffix'"
+               error "Can't process file '$file': no handler script for suffix '$suffix'"
                msg "*missing handler* -- $file"
        fi
 done
 
 ## mail the messages to the report address
 
-if [ "$reportemail" != "" ]; then
+if [ $actions_run == 0 ]; then doit=0
+elif [ "$reportemail" == "" ]; then doit=0
+elif [ $fatals != 0 ]; then doit=1
+elif [ $errors != 0 ]; then doit=1
+elif [ "$reportsuccess" == "yes" ]; then doit=1
+elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
+else doit=0
+fi
+
+if [ $doit == 1 ]; then
+       debug "send report to $reportemail"
        hostname=`hostname`
+       [ $warnings == 0 ] || subject="WARNING"
+       [ $errors == 0 ] || subject="ERROR"
+       [ $fatals == 0 ] || subject="FAILED"
+       
        {
                for ((i=0; i < ${#messages[@]} ; i++)); do
                        echo ${messages[$i]}
                done
-               echo -e "$error"
-       } | mail $reportemail -s "backupninja: $hostname"
+               echo -e "$errormsg"
+       } | mail $reportemail -s "backupninja: $hostname $subject"
 fi
 
-debug 1 "====== finished at "`date`" ======"
-
-############################################################