3 # B A C K U P N I N J A /()/
6 # Copyright (C) 2004-05 riseup.net -- property is theft.
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
19 #####################################################
33 if [ "$usecolors" == "yes" ]; then
34 local typestr=`echo "$@" | sed 's/\(^[^:]*\).*$/\1/'`
35 [ "$typestr" == "Debug" ] && COLOR=$BLUE
36 [ "$typestr" == "Info" ] && COLOR=$GREEN
37 [ "$typestr" == "Warning" ] && COLOR=$YELLOW
38 [ "$typestr" == "Error" ] && COLOR=$RED
39 [ "$typestr" == "Fatal" ] && COLOR=$PURPLE
41 echo -e "$COLOR$@$endcolor"
47 # We have the following message levels:
49 # 1 - normal messages - green
50 # 2 - warnings - yellow
53 # First variable passed is the error level, all others are printed
55 # if 1, echo out all warnings, errors, or fatal
56 # used to capture output from handlers
62 [ ${#@} -gt 1 ] || return
66 if [ $type == 100 ]; then
67 typestr=`echo "$@" | sed 's/\(^[^:]*\).*$/\1/'`
68 [ "$typestr" == "Debug" ] && type=0
69 [ "$typestr" == "Info" ] && type=1
70 [ "$typestr" == "Warning" ] && type=2
71 [ "$typestr" == "Error" ] && type=3
72 [ "$typestr" == "Fatal" ] && type=4
75 types=(Debug Info Warning Error Fatal)
76 typestr="${types[$type]}: "
81 if [ $echo_debug_msg == 1 ]; then
82 echo -e "$typestr$@" >&2
84 colorize "$typestr$@" >&2
87 if [ $print -lt $loglevel ]; then
93 if [ -w "$logfile" ]; then
94 echo -e `date "+%h %d %H:%M:%S"` "$@" >> $logfile
120 messages[$msgcount]=$1
128 function setsection() {
134 # create a temporary file in a secure way.
136 function maketemp() {
137 if [ -x /bin/mktemp ]
139 local tempfile=`mktemp /tmp/$1.XXXXXXXX`
142 sectmp=`echo $DATE | /usr/bin/md5sum | cut -d- -f1`
143 local tempfile=/tmp/$1.$sectmp
150 # sets a global var with name equal to $1
151 # to the value of the configuration parameter $1
157 ret=`awk -f $scriptdir/parseini S=$CURRENT_SECTION P=$CURRENT_PARAM $CURRENT_CONF_FILE`
158 # if nothing is returned, set the default
159 if [ "$ret" == "" -a "$2" != "" ]; then
163 # replace * with %, so that it is not globbed.
164 ret="${ret//\\*/__star__}"
166 # this is weird, but single quotes are needed to
167 # allow for returned values with spaces. $ret is still expanded
168 # because it is in an 'eval' statement.
173 # enforces very strict permissions on configuration file $file.
176 function check_perms() {
178 local perms=`ls -ld $file`
180 if [ "$perms" != "------" ]; then
181 echo "Configuration files must not be group or world writable/readable! Dying on file $file"
182 fatal "Configuration files must not be group or world writable/readable! Dying on file $file"
184 if [ `ls -ld $file | awk '{print $3}'` != "root" ]; then
185 echo "Configuration files must be owned by root! Dying on file $file"
186 fatal "Configuration files must be owned by root! Dying on file $file"
190 # simple lowercase function
192 echo "$1" | tr [:upper:] [:lower:]
195 # simple to integer function
197 echo "$1" | tr [:alpha:] -d
201 # function isnow(): returns 1 if the time/day passed as $1 matches
202 # the current time/day.
204 # format is <day> at <time>:
210 # we grab the current time once, since processing
211 # all the configs might take more than an hour.
214 nowdayofweek=`date +%A`
215 nowdayofweek=`tolower "$nowdayofweek"`
220 whendayofweek=$1; at=$2; whentime=$3;
221 whenday=`toint "$whendayofweek"`
222 whendayofweek=`tolower "$whendayofweek"`
223 whentime=`echo "$whentime" | sed 's/:[0-9][0-9]$//' | sed -r 's/^([0-9])$/0\1/'`
225 if [ "$whendayofweek" == "everyday" -o "$whendayofweek" == "daily" ]; then
226 whendayofweek=$nowdayofweek
229 if [ "$whenday" == "" ]; then
230 if [ "$whendayofweek" != "$nowdayofweek" ]; then
231 whendayofweek=${whendayofweek%s}
232 if [ "$whendayofweek" != "$nowdayofweek" ]; then
236 elif [ "$whenday" != "$nowday" ]; then
240 [ "$at" == "at" ] || return 0
241 [ "$whentime" == "$nowtime" ] || return 0
249 This script allows you to coordinate system backup by dropping a few
250 simple configuration files into @CFGDIR@/backup.d/. Typically, this
251 script is run hourly from cron.
253 The following options are available:
254 -h, --help This usage message
255 -d, --debug Run in debug mode, where all log messages are
256 output to the current shell.
257 -f, --conffile FILE Use FILE for the main configuration instead
258 of @CFGDIR@/backupninja.conf
259 -t, --test Test run mode. This will test if the backup
260 could run, without actually preforming any
261 backups. For example, it will attempt to authenticate
262 or test that ssh keys are set correctly.
263 -n, --now Perform actions now, instead of when they might
264 be scheduled. No output will be created unless also
266 --run FILE Execute the specified action file and then exit.
267 Also puts backupninja in debug mode.
269 When in debug mode, output to the console will be colored:
272 debug "Debugging info (when run with -d)"
273 info "Informational messages (verbosity level 4)"
274 warning "Warnings (verbosity level 3 and up)"
275 error "Errors (verbosity level 2 and up)"
276 fatal "Fatal, halting errors (always shown)"
280 ## this function handles the running of a backup action
282 ## these globals are modified:
283 ## fatals, errors, warnings, actions_run, errormsg
286 function process_action() {
292 # skip over this config if "when" option
293 # is not set to the current time.
294 getconf when "$defaultwhen"
295 if [ "$processnow" == 1 ]; then
296 info ">>>> starting action $file (because of --now)"
298 elif [ "$when" == "hourly" ]; then
299 info ">>>> starting action $file (because 'when = hourly')"
308 if [ $ret == 0 ]; then
309 debug "skipping $file because it is not $w"
311 info ">>>> starting action $file (because it is $w)"
318 [ "$run" == "no" ] && return
320 let "actions_run += 1"
323 local bufferfile=`maketemp backupninja.buffer`
324 echo "" > $bufferfile
327 . $scriptdir/$suffix $file
330 echo $a >> $bufferfile
331 [ $debug ] && colorize "$a"
335 # ^^^^^^^^ we have a problem! we can't grab the return code "$?". grrr.
338 _warnings=`cat $bufferfile | grep "^Warning: " | wc -l`
339 _errors=`cat $bufferfile | grep "^Error: " | wc -l`
340 _fatals=`cat $bufferfile | grep "^Fatal: " | wc -l`
342 ret=`grep "\(^Warning: \|^Error: \|^Fatal: \)" $bufferfile`
344 if [ $_fatals != 0 ]; then
345 msg "*failed* -- $file"
346 errormsg="$errormsg\n== fatal errors from $file ==\n\n$ret\n"
347 passthru "Fatal: <<<< finished action $file: FAILED"
348 elif [ $_errors != 0 ]; then
349 msg "*error* -- $file"
350 errormsg="$errormsg\n== errors from $file ==\n\n$ret\n"
351 error "<<<< finished action $file: ERROR"
352 elif [ $_warnings != 0 ]; then
353 msg "*warning* -- $file"
354 errormsg="$errormsg\n== warnings from $file ==\n\n$ret\n"
355 warning "<<<< finished action $file: WARNING"
357 msg "success -- $file"
358 info "<<<< finished action $file: SUCCESS"
361 let "fatals += _fatals"
362 let "errors += _errors"
363 let "warnings += _warnings"
366 #####################################################
370 conffile="@CFGDIR@/backupninja.conf"
373 ## process command line options
375 while [ $# -ge 1 ]; do
378 -d|--debug) debug=1;;
379 -t|--test) test=1;debug=1;;
380 -n|--now) processnow=1;;
385 echo "-f|--conffile option must be followed by an existing filename"
386 fatal "-f|--conffile option must be followed by an existing filename"
389 # we shift here to avoid processing the file path
398 echo "--run option must be fallowed by a backupninja action file"
399 fatal "--run option must be fallowed by a backupninja action file"
406 echo "Unknown option $1"
407 fatal "Unknown option $1"
419 ## Load and confirm basic configuration values
422 if [ ! -r "$conffile" ]; then
423 echo "Configuration file $conffile not found."
424 fatal "Configuration file $conffile not found."
427 scriptdir=`grep scriptdirectory $conffile | awk '{print $3}'`
428 if [ ! -n "$scriptdir" ]; then
429 echo "Cound not find entry 'scriptdirectory' in $conffile"
430 fatal "Cound not find entry 'scriptdirectory' in $conffile"
433 if [ ! -d "$scriptdir" ]; then
434 echo "Script directory $scriptdir not found."
435 fatal "Script directory $scriptdir not found."
440 # get global config options (second param is the default)
441 getconf configdirectory @CFGDIR@/backup.d
443 getconf reportsuccess yes
444 getconf reportwarning yes
446 getconf when "Everyday at 01:00"
448 getconf logfile @localstatedir@/log/backupninja.log
449 getconf usecolors "yes"
450 getconf SLAPCAT /usr/sbin/slapcat
451 getconf LDAPSEARCH /usr/bin/ldapsearch
452 getconf RDIFFBACKUP /usr/bin/rdiff-backup
453 getconf MYSQL /usr/bin/mysql
454 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
455 getconf MYSQLDUMP /usr/bin/mysqldump
456 getconf PGSQLDUMP /usr/bin/pg_dump
457 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
458 getconf GZIP /bin/gzip
459 getconf RSYNC /usr/bin/rsync
461 getconf VSERVERINFO /usr/sbin/vserver-info
462 getconf VSERVER /usr/sbin/vserver
463 getconf VROOTDIR `if [ -f "$VSERVERINFO" ]; then $VSERVERINFO info SYSINFO |grep vserver-Rootdir | awk '{print $2}'; fi`
465 if [ ! -d "$configdirectory" ]; then
466 echo "Configuration directory '$configdirectory' not found."
467 fatal "Configuration directory '$configdirectory' not found."
470 [ -f "$logfile" ] || touch $logfile
472 if [ "$UID" != "0" ]; then
473 echo "$0 can only be run as root"
477 if [ "$vservers" == "yes" -a ! -d "$VROOTDIR" ]; then
478 echo "vservers option set in config, but $VROOTDIR is not a directory!"
479 fatal "vservers option set in config, but $VROOTDIR is not a directory!"
482 ## Process each configuration file
484 # by default, don't make files which are world or group readable.
487 # these globals are set by process_action()
494 if [ "$singlerun" ]; then
497 files=`find $configdirectory -mindepth 1 ! -name '.*.swp' | sort -n`
500 for file in $files; do
501 [ -f "$file" ] || continue
505 base=`basename $file`
506 if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
507 info "Skipping $file"
511 if [ -e "$scriptdir/$suffix" ]; then
512 process_action $file $suffix
514 error "Can't process file '$file': no handler script for suffix '$suffix'"
515 msg "*missing handler* -- $file"
519 ## mail the messages to the report address
521 if [ $actions_run == 0 ]; then doit=0
522 elif [ "$reportemail" == "" ]; then doit=0
523 elif [ $fatals != 0 ]; then doit=1
524 elif [ $errors != 0 ]; then doit=1
525 elif [ "$reportsuccess" == "yes" ]; then doit=1
526 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
530 if [ $doit == 1 ]; then
531 debug "send report to $reportemail"
533 [ $warnings == 0 ] || subject="WARNING"
534 [ $errors == 0 ] || subject="ERROR"
535 [ $fatals == 0 ] || subject="FAILED"
538 for ((i=0; i < ${#messages[@]} ; i++)); do
542 } | mail $reportemail -s "backupninja: $hostname $subject"
545 if [ $actions_run != 0 ]; then
546 info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."