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 # sets a global var with name equal to $1
135 # to the value of the configuration parameter $1
141 ret=`awk -f $scriptdir/parseini S=$CURRENT_SECTION P=$CURRENT_PARAM $CURRENT_CONF_FILE`
142 # if nothing is returned, set the default
143 if [ "$ret" == "" -a "$2" != "" ]; then
147 # replace * with %, so that it is not globbed.
148 ret="${ret//\\*/__star__}"
150 # this is weird, but single quotes are needed to
151 # allow for returned values with spaces. $ret is still expanded
152 # because it is in an 'eval' statement.
157 # enforces very strict permissions on configuration file $file.
160 function check_perms() {
162 local perms=`ls -ld $file`
164 if [ "$perms" != "------" ]; then
165 echo "Configuration files must not be group or world writable/readable! Dying on file $file"
166 fatal "Configuration files must not be group or world writable/readable! Dying on file $file"
168 if [ `ls -ld $file | awk '{print $3}'` != "root" ]; then
169 echo "Configuration files must be owned by root! Dying on file $file"
170 fatal "Configuration files must be owned by root! Dying on file $file"
174 # simple lowercase function
176 echo "$1" | tr [:upper:] [:lower:]
179 # simple to integer function
181 echo "$1" | tr [:alpha:] -d
185 # function isnow(): returns 1 if the time/day passed as $1 matches
186 # the current time/day.
188 # format is <day> at <time>:
194 # we grab the current time once, since processing
195 # all the configs might take more than an hour.
198 nowdayofweek=`date +%A`
199 nowdayofweek=`tolower "$nowdayofweek"`
204 whendayofweek=$1; at=$2; whentime=$3;
205 whenday=`toint "$whendayofweek"`
206 whendayofweek=`tolower "$whendayofweek"`
207 whentime=`echo "$whentime" | sed 's/:[0-9][0-9]$//' | sed -r 's/^([0-9])$/0\1/'`
209 if [ "$whendayofweek" == "everyday" -o "$whendayofweek" == "daily" ]; then
210 whendayofweek=$nowdayofweek
213 if [ "$whenday" == "" ]; then
214 if [ "$whendayofweek" != "$nowdayofweek" ]; then
215 whendayofweek=${whendayofweek%s}
216 if [ "$whendayofweek" != "$nowdayofweek" ]; then
220 elif [ "$whenday" != "$nowday" ]; then
224 [ "$at" == "at" ] || return 0
225 [ "$whentime" == "$nowtime" ] || return 0
233 This script allows you to coordinate system backup by dropping a few
234 simple configuration files into @CFGDIR@/backup.d/. Typically, this
235 script is run hourly from cron.
237 The following options are available:
238 -h, --help This usage message
239 -d, --debug Run in debug mode, where all log messages are
240 output to the current shell.
241 -f, --conffile FILE Use FILE for the main configuration instead
242 of @CFGDIR@/backupninja.conf
243 -t, --test Test run mode. This will test if the backup
244 could run, without actually preforming any
245 backups. For example, it will attempt to authenticate
246 or test that ssh keys are set correctly.
247 -n, --now Perform actions now, instead of when they might
248 be scheduled. No output will be created unless also
250 --run FILE Execute the specified action file and then exit.
251 Also puts backupninja in debug mode.
253 When in debug mode, output to the console will be colored:
256 debug "Debugging info (when run with -d)"
257 info "Informational messages (verbosity level 4)"
258 warning "Warnings (verbosity level 3 and up)"
259 error "Errors (verbosity level 2 and up)"
260 fatal "Fatal, halting errors (always shown)"
264 ## this function handles the running of a backup action
266 ## these globals are modified:
267 ## fatals, errors, warnings, actions_run, errormsg
270 function process_action() {
276 # skip over this config if "when" option
277 # is not set to the current time.
278 getconf when "$defaultwhen"
279 if [ "$processnow" == 1 ]; then
280 info ">>>> starting action $file (because of --now)"
282 elif [ "$when" == "hourly" ]; then
283 info ">>>> starting action $file (because 'when = hourly')"
292 if [ $ret == 0 ]; then
293 debug "skipping $file because it is not $w"
295 info ">>>> starting action $file (because it is $w)"
302 [ "$run" == "no" ] && return
304 let "actions_run += 1"
307 local bufferfile=`maketemp backupninja.buffer`
308 echo "" > $bufferfile
311 . $scriptdir/$suffix $file
314 echo $a >> $bufferfile
315 [ $debug ] && colorize "$a"
319 # ^^^^^^^^ we have a problem! we can't grab the return code "$?". grrr.
322 _warnings=`cat $bufferfile | grep "^Warning: " | wc -l`
323 _errors=`cat $bufferfile | grep "^Error: " | wc -l`
324 _fatals=`cat $bufferfile | grep "^Fatal: " | wc -l`
326 ret=`grep "\(^Warning: \|^Error: \|^Fatal: \)" $bufferfile`
328 if [ $_fatals != 0 ]; then
329 msg "*failed* -- $file"
330 errormsg="$errormsg\n== fatal errors from $file ==\n\n$ret\n"
331 passthru "Fatal: <<<< finished action $file: FAILED"
332 elif [ $_errors != 0 ]; then
333 msg "*error* -- $file"
334 errormsg="$errormsg\n== errors from $file ==\n\n$ret\n"
335 error "<<<< finished action $file: ERROR"
336 elif [ $_warnings != 0 ]; then
337 msg "*warning* -- $file"
338 errormsg="$errormsg\n== warnings from $file ==\n\n$ret\n"
339 warning "<<<< finished action $file: WARNING"
341 msg "success -- $file"
342 info "<<<< finished action $file: SUCCESS"
345 let "fatals += _fatals"
346 let "errors += _errors"
347 let "warnings += _warnings"
350 #####################################################
354 conffile="@CFGDIR@/backupninja.conf"
357 ## process command line options
359 while [ $# -ge 1 ]; do
362 -d|--debug) debug=1;;
363 -t|--test) test=1;debug=1;;
364 -n|--now) processnow=1;;
369 echo "-f|--conffile option must be followed by an existing filename"
370 fatal "-f|--conffile option must be followed by an existing filename"
373 # we shift here to avoid processing the file path
382 echo "--run option must be fallowed by a backupninja action file"
383 fatal "--run option must be fallowed by a backupninja action file"
390 echo "Unknown option $1"
391 fatal "Unknown option $1"
403 ## Load and confirm basic configuration values
406 if [ ! -r "$conffile" ]; then
407 echo "Configuration file $conffile not found."
408 fatal "Configuration file $conffile not found."
412 scriptdir=`grep scriptdirectory $conffile | awk '{print $3}'`
413 if [ -z "$scriptdir" ]; then
414 if [ -d "@datadir@" ]; then
415 scriptdir="@datadir@"
417 echo "Could not find entry 'scriptdirectory' in $conffile"
418 fatal "Could not find entry 'scriptdirectory' in $conffile"
421 if [ ! -d "$scriptdir" ]; then
422 echo "Script directory $scriptdir not found."
423 fatal "Script directory $scriptdir not found."
428 libdir=`grep libdirectory $conffile | awk '{print $3}'`
429 if [ -z "$libdir" ]; then
430 if [ -d "@libdir@" ]; then
433 echo "Could not find entry 'libdirectory' in $conffile."
434 fatal "Could not find entry 'libdirectory' in $conffile."
437 if [ ! -d "$libdir" ]; then
438 echo "Lib directory $libdir not found."
439 fatal "Lib directory $libdir not found."
445 # get global config options (second param is the default)
446 getconf configdirectory @CFGDIR@/backup.d
448 getconf reportsuccess yes
449 getconf reportwarning yes
451 getconf when "Everyday at 01:00"
453 getconf logfile @localstatedir@/log/backupninja.log
454 getconf usecolors "yes"
455 getconf SLAPCAT /usr/sbin/slapcat
456 getconf LDAPSEARCH /usr/bin/ldapsearch
457 getconf RDIFFBACKUP /usr/bin/rdiff-backup
458 getconf MYSQL /usr/bin/mysql
459 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
460 getconf MYSQLDUMP /usr/bin/mysqldump
461 getconf PGSQLDUMP /usr/bin/pg_dump
462 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
463 getconf GZIP /bin/gzip
464 getconf RSYNC /usr/bin/rsync
466 getconf VSERVERINFO /usr/sbin/vserver-info
467 getconf VSERVER /usr/sbin/vserver
468 getconf VROOTDIR `if [ -f "$VSERVERINFO" ]; then $VSERVERINFO info SYSINFO |grep vserver-Rootdir | awk '{print $2}'; fi`
470 if [ ! -d "$configdirectory" ]; then
471 echo "Configuration directory '$configdirectory' not found."
472 fatal "Configuration directory '$configdirectory' not found."
475 # include shared functions
478 [ -f "$logfile" ] || touch $logfile
480 if [ "$UID" != "0" ]; then
481 echo "$0 can only be run as root"
485 if [ "$vservers" == "yes" -a ! -d "$VROOTDIR" ]; then
486 echo "vservers option set in config, but $VROOTDIR is not a directory!"
487 fatal "vservers option set in config, but $VROOTDIR is not a directory!"
490 ## Process each configuration file
492 # by default, don't make files which are world or group readable.
495 # these globals are set by process_action()
502 if [ "$singlerun" ]; then
505 files=`find $configdirectory -mindepth 1 ! -name '.*.swp' | sort -n`
508 for file in $files; do
509 [ -f "$file" ] || continue
513 base=`basename $file`
514 if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
515 info "Skipping $file"
519 if [ -e "$scriptdir/$suffix" ]; then
520 process_action $file $suffix
522 error "Can't process file '$file': no handler script for suffix '$suffix'"
523 msg "*missing handler* -- $file"
527 ## mail the messages to the report address
529 if [ $actions_run == 0 ]; then doit=0
530 elif [ "$reportemail" == "" ]; then doit=0
531 elif [ $fatals != 0 ]; then doit=1
532 elif [ $errors != 0 ]; then doit=1
533 elif [ "$reportsuccess" == "yes" ]; then doit=1
534 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
538 if [ $doit == 1 ]; then
539 debug "send report to $reportemail"
541 [ $warnings == 0 ] || subject="WARNING"
542 [ $errors == 0 ] || subject="ERROR"
543 [ $fatals == 0 ] || subject="FAILED"
546 for ((i=0; i < ${#messages[@]} ; i++)); do
550 } | mail $reportemail -s "backupninja: $hostname $subject"
553 if [ $actions_run != 0 ]; then
554 info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."