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 #####################################################
22 function setupcolors() {
30 COLORS=($BLUE $GREEN $YELLOW $RED $PURPLE)
34 if [ "$usecolors" == "yes" ]; then
35 local typestr=`echo "$@" | sed 's/\(^[^:]*\).*$/\1/'`
36 [ "$typestr" == "Debug" ] && type=0
37 [ "$typestr" == "Info" ] && type=1
38 [ "$typestr" == "Warning" ] && type=2
39 [ "$typestr" == "Error" ] && type=3
40 [ "$typestr" == "Fatal" ] && type=4
41 color=${COLORS[$type]}
43 echo -e "$color$@$endcolor"
49 # We have the following message levels:
51 # 1 - normal messages - green
52 # 2 - warnings - yellow
55 # First variable passed is the error level, all others are printed
57 # if 1, echo out all warnings, errors, or fatal
58 # used to capture output from handlers
64 [ ${#@} -gt 1 ] || return
68 if [ $type == 100 ]; then
69 typestr=`echo "$@" | sed 's/\(^[^:]*\).*$/\1/'`
70 [ "$typestr" == "Debug" ] && type=0
71 [ "$typestr" == "Info" ] && type=1
72 [ "$typestr" == "Warning" ] && type=2
73 [ "$typestr" == "Error" ] && type=3
74 [ "$typestr" == "Fatal" ] && type=4
77 types=(Debug Info Warning Error Fatal)
78 typestr="${types[$type]}: "
83 if [ $echo_debug_msg == 1 ]; then
84 echo -e "$typestr$@" >&2
86 colorize "$typestr$@" >&2
89 if [ $print -lt $loglevel ]; then
95 if [ -w "$logfile" ]; then
96 echo -e `date "+%h %d %H:%M:%S"` "$@" >> $logfile
100 function passthru() {
122 messages[$msgcount]=$1
130 function setsection() {
135 # sets a global var with name equal to $1
136 # to the value of the configuration parameter $1
142 ret=`awk -f $scriptdir/parseini S=$CURRENT_SECTION P=$CURRENT_PARAM $CURRENT_CONF_FILE`
143 # if nothing is returned, set the default
144 if [ "$ret" == "" -a "$2" != "" ]; then
148 # replace * with %, so that it is not globbed.
149 ret="${ret//\\*/__star__}"
151 # this is weird, but single quotes are needed to
152 # allow for returned values with spaces. $ret is still expanded
153 # because it is in an 'eval' statement.
158 # enforces very strict permissions on configuration file $file.
161 function check_perms() {
163 local perms=`ls -ld $file`
165 if [ "$perms" != "------" ]; then
166 echo "Configuration files must not be group or world writable/readable! Dying on file $file"
167 fatal "Configuration files must not be group or world writable/readable! Dying on file $file"
169 if [ `ls -ld $file | awk '{print $3}'` != "root" ]; then
170 echo "Configuration files must be owned by root! Dying on file $file"
171 fatal "Configuration files must be owned by root! Dying on file $file"
175 # simple lowercase function
177 echo "$1" | tr [:upper:] [:lower:]
180 # simple to integer function
182 echo "$1" | tr [:alpha:] -d
186 # function isnow(): returns 1 if the time/day passed as $1 matches
187 # the current time/day.
189 # format is <day> at <time>:
195 # we grab the current time once, since processing
196 # all the configs might take more than an hour.
199 nowdayofweek=`date +%A`
200 nowdayofweek=`tolower "$nowdayofweek"`
205 whendayofweek=$1; at=$2; whentime=$3;
206 whenday=`toint "$whendayofweek"`
207 whendayofweek=`tolower "$whendayofweek"`
208 whentime=`echo "$whentime" | sed 's/:[0-9][0-9]$//' | sed -r 's/^([0-9])$/0\1/'`
210 if [ "$whendayofweek" == "everyday" -o "$whendayofweek" == "daily" ]; then
211 whendayofweek=$nowdayofweek
214 if [ "$whenday" == "" ]; then
215 if [ "$whendayofweek" != "$nowdayofweek" ]; then
216 whendayofweek=${whendayofweek%s}
217 if [ "$whendayofweek" != "$nowdayofweek" ]; then
221 elif [ "$whenday" != "$nowday" ]; then
225 [ "$at" == "at" ] || return 0
226 [ "$whentime" == "$nowtime" ] || return 0
234 This script allows you to coordinate system backup by dropping a few
235 simple configuration files into /etc/backup.d/. Typically, this
236 script is run hourly from cron.
238 The following options are available:
239 -h, --help This usage message
240 -d, --debug Run in debug mode, where all log messages are
241 output to the current shell.
242 -f, --conffile FILE Use FILE for the main configuration instead
243 of /etc/backupninja.conf
244 -t, --test Test run mode. This will test if the backup
245 could run, without actually preforming any
246 backups. For example, it will attempt to authenticate
247 or test that ssh keys are set correctly.
248 -n, --now Perform actions now, instead of when they might
249 be scheduled. No output will be created unless also
251 --run FILE Execute the specified action file and then exit.
252 Also puts backupninja in debug mode.
254 When in debug mode, output to the console will be colored:
257 debug "Debugging info (when run with -d)"
258 info "Informational messages (verbosity level 4)"
259 warning "Warnings (verbosity level 3 and up)"
260 error "Errors (verbosity level 2 and up)"
261 fatal "Fatal, halting errors (always shown)"
265 ## this function handles the running of a backup action
267 ## these globals are modified:
268 ## fatals, errors, warnings, actions_run, errormsg
271 function process_action() {
277 # skip over this config if "when" option
278 # is not set to the current time.
279 getconf when "$defaultwhen"
280 if [ "$processnow" == 1 ]; then
281 info ">>>> starting action $file (because of --now)"
283 elif [ "$when" == "hourly" ]; then
284 info ">>>> starting action $file (because 'when = hourly')"
293 if [ $ret == 0 ]; then
294 debug "skipping $file because it is not $w"
296 info ">>>> starting action $file (because it is $w)"
303 [ "$run" == "no" ] && return
305 let "actions_run += 1"
308 if [ -x /bin/mktemp ]
310 local bufferfile=`mktemp /tmp/backupninja.buffer.XXXXXXXX`
313 sectmp=`echo $DATE | /usr/bin/md5sum | cut -d- -f1`
314 local bufferfile=/tmp/backupninja.buffer.$sectmp
316 echo "" > $bufferfile
319 . $scriptdir/$suffix $file
322 echo $a >> $bufferfile
323 [ $debug ] && colorize "$a"
327 # ^^^^^^^^ we have a problem! we can't grab the return code "$?". grrr.
330 _warnings=`cat $bufferfile | grep "^Warning: " | wc -l`
331 _errors=`cat $bufferfile | grep "^Error: " | wc -l`
332 _fatals=`cat $bufferfile | grep "^Fatal: " | wc -l`
334 ret=`grep "\(^Warning: \|^Error: \|^Fatal: \)" $bufferfile`
336 if [ $_fatals != 0 ]; then
337 msg "*failed* -- $file"
338 errormsg="$errormsg\n== fatal errors from $file ==\n\n$ret\n"
339 passthru "Fatal: <<<< finished action $file: FAILED"
340 elif [ $_errors != 0 ]; then
341 msg "*error* -- $file"
342 errormsg="$errormsg\n== errors from $file ==\n\n$ret\n"
343 error "<<<< finished action $file: ERROR"
344 elif [ $_warnings != 0 ]; then
345 msg "*warning* -- $file"
346 errormsg="$errormsg\n== warnings from $file ==\n\n$ret\n"
347 warning "<<<< finished action $file: WARNING"
349 msg "success -- $file"
350 info "<<<< finished action $file: SUCCESS"
353 let "fatals += _fatals"
354 let "errors += _errors"
355 let "warnings += _warnings"
358 #####################################################
362 conffile="/etc/backupninja.conf"
365 ## process command line options
367 while [ $# -ge 1 ]; do
370 -d|--debug) debug=1;;
371 -t|--test) test=1;debug=1;;
372 -n|--now) processnow=1;;
377 echo "-f|--conffile option must be followed by an existing filename"
378 fatal "-f|--conffile option must be followed by an existing filename"
381 # we shift here to avoid processing the file path
390 echo "--run option must be fallowed by a backupninja action file"
391 fatal "--run option must be fallowed by a backupninja action file"
398 echo "Unknown option $1"
399 fatal "Unknown option $1"
411 ## Load and confirm basic configuration values
414 if [ ! -r "$conffile" ]; then
415 echo "Configuration file $conffile not found."
416 fatal "Configuration file $conffile not found."
419 scriptdir=`grep scriptdirectory $conffile | awk '{print $3}'`
420 if [ ! -n "$scriptdir" ]; then
421 echo "Cound not find entry 'scriptdirectory' in $conffile"
422 fatal "Cound not find entry 'scriptdirectory' in $conffile"
425 if [ ! -d "$scriptdir" ]; then
426 echo "Script directory $scriptdir not found."
427 fatal "Script directory $scriptdir not found."
432 # get global config options (second param is the default)
433 getconf configdirectory /etc/backup.d
435 getconf reportsuccess yes
436 getconf reportwarning yes
438 getconf when "Everyday at 01:00"
440 getconf logfile /var/log/backupninja.log
441 getconf usecolors "yes"
442 getconf SLAPCAT /usr/sbin/slapcat
443 getconf LDAPSEARCH /usr/bin/ldapsearch
444 getconf RDIFFBACKUP /usr/bin/rdiff-backup
445 getconf MYSQL /usr/bin/mysql
446 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
447 getconf MYSQLDUMP /usr/bin/mysqldump
448 getconf PGSQLDUMP /usr/bin/pg_dump
449 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
450 getconf GZIP /bin/gzip
451 getconf RSYNC /usr/bin/rsync
453 getconf VSERVERINFO /usr/sbin/vserver-info
454 getconf VSERVER /usr/sbin/vserver
455 getconf VROOTDIR `if [ -f "$VSERVERINFO" ]; then $VSERVERINFO info SYSINFO |grep vserver-Rootdir | awk '{print $2}'; fi`
457 if [ ! -d "$configdirectory" ]; then
458 echo "Configuration directory '$configdirectory' not found."
459 fatal "Configuration directory '$configdirectory' not found."
462 [ -f "$logfile" ] || touch $logfile
464 if [ "$UID" != "0" ]; then
465 echo "$0 can only be run as root"
469 if [ "$vservers" == "yes" -a ! -d "$VROOTDIR" ]; then
470 echo "vservers option set in config, but $VROOTDIR is not a directory!"
471 fatal "vservers option set in config, but $VROOTDIR is not a directory!"
474 ## Process each configuration file
476 # by default, don't make files which are world or group readable.
479 # these globals are set by process_action()
486 if [ "$singlerun" ]; then
489 files=`find $configdirectory ! -name '.*.swp' -mindepth 1 | sort -n`
492 for file in $files; do
493 [ -f "$file" ] || continue
497 base=`basename $file`
498 if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
499 info "Skipping $file"
503 if [ -e "$scriptdir/$suffix" ]; then
504 process_action $file $suffix
506 error "Can't process file '$file': no handler script for suffix '$suffix'"
507 msg "*missing handler* -- $file"
511 ## mail the messages to the report address
513 if [ $actions_run == 0 ]; then doit=0
514 elif [ "$reportemail" == "" ]; then doit=0
515 elif [ $fatals != 0 ]; then doit=1
516 elif [ $errors != 0 ]; then doit=1
517 elif [ "$reportsuccess" == "yes" ]; then doit=1
518 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
522 if [ $doit == 1 ]; then
523 debug "send report to $reportemail"
525 [ $warnings == 0 ] || subject="WARNING"
526 [ $errors == 0 ] || subject="ERROR"
527 [ $fatals == 0 ] || subject="FAILED"
530 for ((i=0; i < ${#messages[@]} ; i++)); do
534 } | mail $reportemail -s "backupninja: $hostname $subject"
537 if [ $actions_run != 0 ]; then
538 info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."