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() {
136 # create a temporary file in a secure way.
138 function maketemp() {
139 if [ -x /bin/mktemp ]
141 local tempfile=`mktemp /tmp/$1.XXXXXXXX`
144 sectmp=`echo $DATE | /usr/bin/md5sum | cut -d- -f1`
145 local tempfile=/tmp/$1.$sectmp
152 # sets a global var with name equal to $1
153 # to the value of the configuration parameter $1
159 ret=`awk -f $scriptdir/parseini S=$CURRENT_SECTION P=$CURRENT_PARAM $CURRENT_CONF_FILE`
160 # if nothing is returned, set the default
161 if [ "$ret" == "" -a "$2" != "" ]; then
165 # replace * with %, so that it is not globbed.
166 ret="${ret//\\*/__star__}"
168 # this is weird, but single quotes are needed to
169 # allow for returned values with spaces. $ret is still expanded
170 # because it is in an 'eval' statement.
175 # enforces very strict permissions on configuration file $file.
178 function check_perms() {
180 local perms=`ls -ld $file`
182 if [ "$perms" != "------" ]; then
183 echo "Configuration files must not be group or world writable/readable! Dying on file $file"
184 fatal "Configuration files must not be group or world writable/readable! Dying on file $file"
186 if [ `ls -ld $file | awk '{print $3}'` != "root" ]; then
187 echo "Configuration files must be owned by root! Dying on file $file"
188 fatal "Configuration files must be owned by root! Dying on file $file"
192 # simple lowercase function
194 echo "$1" | tr [:upper:] [:lower:]
197 # simple to integer function
199 echo "$1" | tr [:alpha:] -d
203 # function isnow(): returns 1 if the time/day passed as $1 matches
204 # the current time/day.
206 # format is <day> at <time>:
212 # we grab the current time once, since processing
213 # all the configs might take more than an hour.
216 nowdayofweek=`date +%A`
217 nowdayofweek=`tolower "$nowdayofweek"`
222 whendayofweek=$1; at=$2; whentime=$3;
223 whenday=`toint "$whendayofweek"`
224 whendayofweek=`tolower "$whendayofweek"`
225 whentime=`echo "$whentime" | sed 's/:[0-9][0-9]$//' | sed -r 's/^([0-9])$/0\1/'`
227 if [ "$whendayofweek" == "everyday" -o "$whendayofweek" == "daily" ]; then
228 whendayofweek=$nowdayofweek
231 if [ "$whenday" == "" ]; then
232 if [ "$whendayofweek" != "$nowdayofweek" ]; then
233 whendayofweek=${whendayofweek%s}
234 if [ "$whendayofweek" != "$nowdayofweek" ]; then
238 elif [ "$whenday" != "$nowday" ]; then
242 [ "$at" == "at" ] || return 0
243 [ "$whentime" == "$nowtime" ] || return 0
251 This script allows you to coordinate system backup by dropping a few
252 simple configuration files into /etc/backup.d/. Typically, this
253 script is run hourly from cron.
255 The following options are available:
256 -h, --help This usage message
257 -d, --debug Run in debug mode, where all log messages are
258 output to the current shell.
259 -f, --conffile FILE Use FILE for the main configuration instead
260 of /etc/backupninja.conf
261 -t, --test Test run mode. This will test if the backup
262 could run, without actually preforming any
263 backups. For example, it will attempt to authenticate
264 or test that ssh keys are set correctly.
265 -n, --now Perform actions now, instead of when they might
266 be scheduled. No output will be created unless also
268 --run FILE Execute the specified action file and then exit.
269 Also puts backupninja in debug mode.
271 When in debug mode, output to the console will be colored:
274 debug "Debugging info (when run with -d)"
275 info "Informational messages (verbosity level 4)"
276 warning "Warnings (verbosity level 3 and up)"
277 error "Errors (verbosity level 2 and up)"
278 fatal "Fatal, halting errors (always shown)"
282 ## this function handles the running of a backup action
284 ## these globals are modified:
285 ## fatals, errors, warnings, actions_run, errormsg
288 function process_action() {
294 # skip over this config if "when" option
295 # is not set to the current time.
296 getconf when "$defaultwhen"
297 if [ "$processnow" == 1 ]; then
298 info ">>>> starting action $file (because of --now)"
300 elif [ "$when" == "hourly" ]; then
301 info ">>>> starting action $file (because 'when = hourly')"
310 if [ $ret == 0 ]; then
311 debug "skipping $file because it is not $w"
313 info ">>>> starting action $file (because it is $w)"
320 [ "$run" == "no" ] && return
322 let "actions_run += 1"
325 local bufferfile=`maketemp backupninja.buffer`
326 echo "" > $bufferfile
329 . $scriptdir/$suffix $file
332 echo $a >> $bufferfile
333 [ $debug ] && colorize "$a"
337 # ^^^^^^^^ we have a problem! we can't grab the return code "$?". grrr.
340 _warnings=`cat $bufferfile | grep "^Warning: " | wc -l`
341 _errors=`cat $bufferfile | grep "^Error: " | wc -l`
342 _fatals=`cat $bufferfile | grep "^Fatal: " | wc -l`
344 ret=`grep "\(^Warning: \|^Error: \|^Fatal: \)" $bufferfile`
346 if [ $_fatals != 0 ]; then
347 msg "*failed* -- $file"
348 errormsg="$errormsg\n== fatal errors from $file ==\n\n$ret\n"
349 passthru "Fatal: <<<< finished action $file: FAILED"
350 elif [ $_errors != 0 ]; then
351 msg "*error* -- $file"
352 errormsg="$errormsg\n== errors from $file ==\n\n$ret\n"
353 error "<<<< finished action $file: ERROR"
354 elif [ $_warnings != 0 ]; then
355 msg "*warning* -- $file"
356 errormsg="$errormsg\n== warnings from $file ==\n\n$ret\n"
357 warning "<<<< finished action $file: WARNING"
359 msg "success -- $file"
360 info "<<<< finished action $file: SUCCESS"
363 let "fatals += _fatals"
364 let "errors += _errors"
365 let "warnings += _warnings"
368 #####################################################
372 conffile="/etc/backupninja.conf"
375 ## process command line options
377 while [ $# -ge 1 ]; do
380 -d|--debug) debug=1;;
381 -t|--test) test=1;debug=1;;
382 -n|--now) processnow=1;;
387 echo "-f|--conffile option must be followed by an existing filename"
388 fatal "-f|--conffile option must be followed by an existing filename"
391 # we shift here to avoid processing the file path
400 echo "--run option must be fallowed by a backupninja action file"
401 fatal "--run option must be fallowed by a backupninja action file"
408 echo "Unknown option $1"
409 fatal "Unknown option $1"
421 ## Load and confirm basic configuration values
424 if [ ! -r "$conffile" ]; then
425 echo "Configuration file $conffile not found."
426 fatal "Configuration file $conffile not found."
429 scriptdir=`grep scriptdirectory $conffile | awk '{print $3}'`
430 if [ ! -n "$scriptdir" ]; then
431 echo "Cound not find entry 'scriptdirectory' in $conffile"
432 fatal "Cound not find entry 'scriptdirectory' in $conffile"
435 if [ ! -d "$scriptdir" ]; then
436 echo "Script directory $scriptdir not found."
437 fatal "Script directory $scriptdir not found."
442 # get global config options (second param is the default)
443 getconf configdirectory /etc/backup.d
445 getconf reportsuccess yes
446 getconf reportwarning yes
448 getconf when "Everyday at 01:00"
450 getconf logfile /var/log/backupninja.log
451 getconf usecolors "yes"
452 getconf SLAPCAT /usr/sbin/slapcat
453 getconf LDAPSEARCH /usr/bin/ldapsearch
454 getconf RDIFFBACKUP /usr/bin/rdiff-backup
455 getconf MYSQL /usr/bin/mysql
456 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
457 getconf MYSQLDUMP /usr/bin/mysqldump
458 getconf PGSQLDUMP /usr/bin/pg_dump
459 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
460 getconf GZIP /bin/gzip
461 getconf RSYNC /usr/bin/rsync
463 getconf VSERVERINFO /usr/sbin/vserver-info
464 getconf VSERVER /usr/sbin/vserver
465 getconf VROOTDIR `if [ -f "$VSERVERINFO" ]; then $VSERVERINFO info SYSINFO |grep vserver-Rootdir | awk '{print $2}'; fi`
467 if [ ! -d "$configdirectory" ]; then
468 echo "Configuration directory '$configdirectory' not found."
469 fatal "Configuration directory '$configdirectory' not found."
472 [ -f "$logfile" ] || touch $logfile
474 if [ "$UID" != "0" ]; then
475 echo "$0 can only be run as root"
479 if [ "$vservers" == "yes" -a ! -d "$VROOTDIR" ]; then
480 echo "vservers option set in config, but $VROOTDIR is not a directory!"
481 fatal "vservers option set in config, but $VROOTDIR is not a directory!"
484 ## Process each configuration file
486 # by default, don't make files which are world or group readable.
489 # these globals are set by process_action()
496 if [ "$singlerun" ]; then
499 files=`find $configdirectory -mindepth 1 ! -name '.*.swp' | sort -n`
502 for file in $files; do
503 [ -f "$file" ] || continue
507 base=`basename $file`
508 if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
509 info "Skipping $file"
513 if [ -e "$scriptdir/$suffix" ]; then
514 process_action $file $suffix
516 error "Can't process file '$file': no handler script for suffix '$suffix'"
517 msg "*missing handler* -- $file"
521 ## mail the messages to the report address
523 if [ $actions_run == 0 ]; then doit=0
524 elif [ "$reportemail" == "" ]; then doit=0
525 elif [ $fatals != 0 ]; then doit=1
526 elif [ $errors != 0 ]; then doit=1
527 elif [ "$reportsuccess" == "yes" ]; then doit=1
528 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
532 if [ $doit == 1 ]; then
533 debug "send report to $reportemail"
535 [ $warnings == 0 ] || subject="WARNING"
536 [ $errors == 0 ] || subject="ERROR"
537 [ $fatals == 0 ] || subject="FAILED"
540 for ((i=0; i < ${#messages[@]} ; i++)); do
544 } | mail $reportemail -s "backupninja: $hostname $subject"
547 if [ $actions_run != 0 ]; then
548 info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."