2 # -*- mode: sh; sh-basic-offset: 3; indent-tabs-mode: nil; -*-
5 # B A C K U P N I N J A /()/
8 # Copyright (C) 2004-05 riseup.net -- property is theft.
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
21 #####################################################
24 function setupcolors () {
32 COLORS=($BLUE $GREEN $YELLOW $RED $PURPLE)
35 function colorize () {
36 if [ "$usecolors" == "yes" ]; then
37 local typestr=`echo "$@" | sed 's/\(^[^:]*\).*$/\1/'`
38 [ "$typestr" == "Debug" ] && type=0
39 [ "$typestr" == "Info" ] && type=1
40 [ "$typestr" == "Warning" ] && type=2
41 [ "$typestr" == "Error" ] && type=3
42 [ "$typestr" == "Fatal" ] && type=4
43 color=${COLORS[$type]}
45 echo -e "$color$@$endcolor"
51 # We have the following message levels:
53 # 1 - normal messages - green
54 # 2 - warnings - yellow
57 # First variable passed is the error level, all others are printed
59 # if 1, echo out all warnings, errors, or fatal
60 # used to capture output from handlers
66 [ ${#@} -gt 1 ] || return
70 if [ $type == 100 ]; then
71 typestr=`echo "$@" | sed 's/\(^[^:]*\).*$/\1/'`
72 [ "$typestr" == "Debug" ] && type=0
73 [ "$typestr" == "Info" ] && type=1
74 [ "$typestr" == "Warning" ] && type=2
75 [ "$typestr" == "Error" ] && type=3
76 [ "$typestr" == "Fatal" ] && type=4
79 types=(Debug Info Warning Error Fatal)
80 typestr="${types[$type]}: "
85 if [ $echo_debug_msg == 1 ]; then
86 echo -e "$typestr$@" >&2
88 colorize "$typestr$@" >&2
91 if [ $print -lt $loglevel ]; then
97 if [ -w "$logfile" ]; then
98 echo -e `date "+%h %d %H:%M:%S"` "$@" >> $logfile
102 function passthru() {
124 messages[$msgcount]=$1
129 # enforces very strict permissions on configuration file $file.
132 function check_perms() {
134 local perms=`ls -ld $file`
136 if [ "$perms" != "------" ]; then
137 echo "Configuration files must not be group or world writable/readable! Dying on file $file"
138 fatal "Configuration files must not be group or world writable/readable! Dying on file $file"
140 if [ `ls -ld $file | awk '{print $3}'` != "root" ]; then
141 echo "Configuration files must be owned by root! Dying on file $file"
142 fatal "Configuration files must be owned by root! Dying on file $file"
146 # simple lowercase function
148 echo "$1" | tr [:upper:] [:lower:]
151 # simple to integer function
153 echo "$1" | tr -d [:alpha:]
157 # function isnow(): returns 1 if the time/day passed as $1 matches
158 # the current time/day.
160 # format is <day> at <time>:
166 # we grab the current time once, since processing
167 # all the configs might take more than an hour.
170 nowdayofweek=`date +%A`
171 nowdayofweek=`tolower "$nowdayofweek"`
176 whendayofweek=$1; at=$2; whentime=$3;
177 whenday=`toint "$whendayofweek"`
178 whendayofweek=`tolower "$whendayofweek"`
179 whentime=`echo "$whentime" | sed 's/:[0-9][0-9]$//' | sed -r 's/^([0-9])$/0\1/'`
181 if [ "$whendayofweek" == "everyday" -o "$whendayofweek" == "daily" ]; then
182 whendayofweek=$nowdayofweek
185 if [ "$whenday" == "" ]; then
186 if [ "$whendayofweek" != "$nowdayofweek" ]; then
187 whendayofweek=${whendayofweek%s}
188 if [ "$whendayofweek" != "$nowdayofweek" ]; then
192 elif [ "$whenday" != "$nowday" ]; then
196 [ "$at" == "at" ] || return 0
197 [ "$whentime" == "$nowtime" ] || return 0
205 This script allows you to coordinate system backup by dropping a few
206 simple configuration files into @CFGDIR@/backup.d/. Typically, this
207 script is run hourly from cron.
209 The following options are available:
210 -h, --help This usage message
211 -d, --debug Run in debug mode, where all log messages are
212 output to the current shell.
213 -f, --conffile FILE Use FILE for the main configuration instead
214 of @CFGDIR@/backupninja.conf
215 -t, --test Test run mode. This will test if the backup
216 could run, without actually preforming any
217 backups. For example, it will attempt to authenticate
218 or test that ssh keys are set correctly.
219 -n, --now Perform actions now, instead of when they might
220 be scheduled. No output will be created unless also
222 --run FILE Execute the specified action file and then exit.
223 Also puts backupninja in debug mode.
225 When in debug mode, output to the console will be colored:
228 debug "Debugging info (when run with -d)"
229 info "Informational messages (verbosity level 4)"
230 warning "Warnings (verbosity level 3 and up)"
231 error "Errors (verbosity level 2 and up)"
232 fatal "Fatal, halting errors (always shown)"
236 ## this function handles the running of a backup action
238 ## these globals are modified:
239 ## fatals, errors, warnings, actions_run, errormsg
242 function process_action() {
248 # skip over this config if "when" option
249 # is not set to the current time.
250 getconf when "$defaultwhen"
251 if [ "$processnow" == 1 ]; then
252 info ">>>> starting action $file (because of --now)"
254 elif [ "$when" == "hourly" ]; then
255 info ">>>> starting action $file (because 'when = hourly')"
264 if [ $ret == 0 ]; then
265 debug "skipping $file because it is not $w"
267 info ">>>> starting action $file (because it is $w)"
274 [ "$run" == "no" ] && return
276 let "actions_run += 1"
279 local bufferfile=`maketemp backupninja.buffer`
280 echo "" > $bufferfile
283 . $scriptdirectory/$suffix $file
286 echo $a >> $bufferfile
287 [ $debug ] && colorize "$a"
291 # ^^^^^^^^ we have a problem! we can't grab the return code "$?". grrr.
294 _warnings=`cat $bufferfile | grep "^Warning: " | wc -l`
295 _errors=`cat $bufferfile | grep "^Error: " | wc -l`
296 _fatals=`cat $bufferfile | grep "^Fatal: " | wc -l`
298 ret=`grep "\(^Warning: \|^Error: \|^Fatal: \)" $bufferfile`
300 if [ $_fatals != 0 ]; then
301 msg "*failed* -- $file"
302 errormsg="$errormsg\n== fatal errors from $file ==\n\n$ret\n"
303 passthru "Fatal: <<<< finished action $file: FAILED"
304 elif [ $_errors != 0 ]; then
305 msg "*error* -- $file"
306 errormsg="$errormsg\n== errors from $file ==\n\n$ret\n"
307 error "<<<< finished action $file: ERROR"
308 elif [ $_warnings != 0 ]; then
309 msg "*warning* -- $file"
310 errormsg="$errormsg\n== warnings from $file ==\n\n$ret\n"
311 warning "<<<< finished action $file: WARNING"
313 msg "success -- $file"
314 info "<<<< finished action $file: SUCCESS"
317 let "fatals += _fatals"
318 let "errors += _errors"
319 let "warnings += _warnings"
322 #####################################################
326 conffile="@CFGDIR@/backupninja.conf"
329 ## process command line options
331 while [ $# -ge 1 ]; do
334 -d|--debug) debug=1;;
335 -t|--test) test=1;debug=1;;
336 -n|--now) processnow=1;;
341 echo "-f|--conffile option must be followed by an existing filename"
342 fatal "-f|--conffile option must be followed by an existing filename"
345 # we shift here to avoid processing the file path
354 echo "--run option must be fallowed by a backupninja action file"
355 fatal "--run option must be fallowed by a backupninja action file"
362 echo "Unknown option $1"
363 fatal "Unknown option $1"
375 ## Load and confirm basic configuration values
378 if [ ! -r "$conffile" ]; then
379 echo "Configuration file $conffile not found."
380 fatal "Configuration file $conffile not found."
384 libdirectory=`grep '^libdirectory' $conffile | awk '{print $3}'`
385 if [ -z "$libdirectory" ]; then
386 if [ -d "@libdir@" ]; then
387 libdirectory="@libdir@"
389 echo "Could not find entry 'libdirectory' in $conffile."
390 fatal "Could not find entry 'libdirectory' in $conffile."
393 if [ ! -d "$libdirectory" ]; then
394 echo "Lib directory $libdirectory not found."
395 fatal "Lib directory $libdirectory not found."
399 # include shared functions
400 . $libdirectory/tools
404 # get global config options (second param is the default)
405 getconf configdirectory @CFGDIR@/backup.d
406 getconf scriptdirectory @datadir@
408 getconf reportsuccess yes
409 getconf reportwarning yes
411 getconf when "Everyday at 01:00"
413 getconf logfile @localstatedir@/log/backupninja.log
414 getconf usecolors "yes"
415 getconf SLAPCAT /usr/sbin/slapcat
416 getconf LDAPSEARCH /usr/bin/ldapsearch
417 getconf RDIFFBACKUP /usr/bin/rdiff-backup
418 getconf MYSQL /usr/bin/mysql
419 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
420 getconf MYSQLDUMP /usr/bin/mysqldump
421 getconf PGSQLDUMP /usr/bin/pg_dump
422 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
423 getconf GZIP /bin/gzip
424 getconf RSYNC /usr/bin/rsync
426 getconf VSERVERINFO /usr/sbin/vserver-info
427 getconf VSERVER /usr/sbin/vserver
428 getconf VROOTDIR `if [ -f "$VSERVERINFO" ]; then $VSERVERINFO info SYSINFO |grep vserver-Rootdir | awk '{print $2}'; fi`
430 if [ ! -d "$configdirectory" ]; then
431 echo "Configuration directory '$configdirectory' not found."
432 fatal "Configuration directory '$configdirectory' not found."
435 [ -f "$logfile" ] || touch $logfile
437 if [ "$UID" != "0" ]; then
438 echo "`basename $0` can only be run as root"
442 if [ "$vservers" == "yes" -a ! -d "$VROOTDIR" ]; then
443 echo "vservers option set in config, but $VROOTDIR is not a directory!"
444 fatal "vservers option set in config, but $VROOTDIR is not a directory!"
447 ## Process each configuration file
449 # by default, don't make files which are world or group readable.
452 # these globals are set by process_action()
459 if [ "$singlerun" ]; then
462 files=`find $configdirectory -mindepth 1 ! -name '.*.swp' | sort -n`
465 for file in $files; do
466 [ -f "$file" ] || continue
470 base=`basename $file`
471 if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
472 info "Skipping $file"
476 if [ -e "$scriptdirectory/$suffix" ]; then
477 process_action $file $suffix
479 error "Can't process file '$file': no handler script for suffix '$suffix'"
480 msg "*missing handler* -- $file"
484 ## mail the messages to the report address
486 if [ $actions_run == 0 ]; then doit=0
487 elif [ "$reportemail" == "" ]; then doit=0
488 elif [ $fatals != 0 ]; then doit=1
489 elif [ $errors != 0 ]; then doit=1
490 elif [ "$reportsuccess" == "yes" ]; then doit=1
491 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
495 if [ $doit == 1 ]; then
496 debug "send report to $reportemail"
498 [ $warnings == 0 ] || subject="WARNING"
499 [ $errors == 0 ] || subject="ERROR"
500 [ $fatals == 0 ] || subject="FAILED"
503 for ((i=0; i < ${#messages[@]} ; i++)); do
507 } | mail $reportemail -s "backupninja: $hostname $subject"
510 if [ $actions_run != 0 ]; then
511 info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."