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."
428 scriptdir=`grep scriptdirectory $conffile | awk '{print $3}'`
429 if [ -z "$scriptdir" ]; then
430 if [ -d "@datadir@" ]; then
431 scriptdir="@datadir@"
433 echo "Could not find entry 'scriptdirectory' in $conffile"
434 fatal "Could not find entry 'scriptdirectory' in $conffile"
437 if [ ! -d "$scriptdir" ]; then
438 echo "Script directory $scriptdir not found."
439 fatal "Script directory $scriptdir not found."
444 libdir=`grep libdirectory $conffile | awk '{print $3}'`
445 if [ -z "$libdir" ]; then
446 if [ -d "@libdir@" ]; then
449 echo "Could not find entry 'libdirectory' in $conffile."
450 fatal "Could not find entry 'libdirectory' in $conffile."
453 if [ ! -d "$libdir" ]; then
454 echo "Lib directory $libdir not found."
455 fatal "Lib directory $libdir not found."
461 # get global config options (second param is the default)
462 getconf configdirectory @CFGDIR@/backup.d
464 getconf reportsuccess yes
465 getconf reportwarning yes
467 getconf when "Everyday at 01:00"
469 getconf logfile @localstatedir@/log/backupninja.log
470 getconf usecolors "yes"
471 getconf SLAPCAT /usr/sbin/slapcat
472 getconf LDAPSEARCH /usr/bin/ldapsearch
473 getconf RDIFFBACKUP /usr/bin/rdiff-backup
474 getconf MYSQL /usr/bin/mysql
475 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
476 getconf MYSQLDUMP /usr/bin/mysqldump
477 getconf PGSQLDUMP /usr/bin/pg_dump
478 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
479 getconf GZIP /bin/gzip
480 getconf RSYNC /usr/bin/rsync
482 getconf VSERVERINFO /usr/sbin/vserver-info
483 getconf VSERVER /usr/sbin/vserver
484 getconf VROOTDIR `if [ -f "$VSERVERINFO" ]; then $VSERVERINFO info SYSINFO |grep vserver-Rootdir | awk '{print $2}'; fi`
486 if [ ! -d "$configdirectory" ]; then
487 echo "Configuration directory '$configdirectory' not found."
488 fatal "Configuration directory '$configdirectory' not found."
491 [ -f "$logfile" ] || touch $logfile
493 if [ "$UID" != "0" ]; then
494 echo "$0 can only be run as root"
498 if [ "$vservers" == "yes" -a ! -d "$VROOTDIR" ]; then
499 echo "vservers option set in config, but $VROOTDIR is not a directory!"
500 fatal "vservers option set in config, but $VROOTDIR is not a directory!"
503 ## Process each configuration file
505 # by default, don't make files which are world or group readable.
508 # these globals are set by process_action()
515 if [ "$singlerun" ]; then
518 files=`find $configdirectory -mindepth 1 ! -name '.*.swp' | sort -n`
521 for file in $files; do
522 [ -f "$file" ] || continue
526 base=`basename $file`
527 if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
528 info "Skipping $file"
532 if [ -e "$scriptdir/$suffix" ]; then
533 process_action $file $suffix
535 error "Can't process file '$file': no handler script for suffix '$suffix'"
536 msg "*missing handler* -- $file"
540 ## mail the messages to the report address
542 if [ $actions_run == 0 ]; then doit=0
543 elif [ "$reportemail" == "" ]; then doit=0
544 elif [ $fatals != 0 ]; then doit=1
545 elif [ $errors != 0 ]; then doit=1
546 elif [ "$reportsuccess" == "yes" ]; then doit=1
547 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
551 if [ $doit == 1 ]; then
552 debug "send report to $reportemail"
554 [ $warnings == 0 ] || subject="WARNING"
555 [ $errors == 0 ] || subject="ERROR"
556 [ $fatals == 0 ] || subject="FAILED"
559 for ((i=0; i < ${#messages[@]} ; i++)); do
563 } | mail $reportemail -s "backupninja: $hostname $subject"
566 if [ $actions_run != 0 ]; then
567 info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."