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)
33 function colorize () {
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
127 # enforces very strict permissions on configuration file $file.
130 function check_perms() {
132 local perms=`ls -ld $file`
134 if [ "$perms" != "------" ]; then
135 echo "Configuration files must not be group or world writable/readable! Dying on file $file"
136 fatal "Configuration files must not be group or world writable/readable! Dying on file $file"
138 if [ `ls -ld $file | awk '{print $3}'` != "root" ]; then
139 echo "Configuration files must be owned by root! Dying on file $file"
140 fatal "Configuration files must be owned by root! Dying on file $file"
144 # simple lowercase function
146 echo "$1" | tr [:upper:] [:lower:]
149 # simple to integer function
151 echo "$1" | tr -d [:alpha:]
155 # function isnow(): returns 1 if the time/day passed as $1 matches
156 # the current time/day.
158 # format is <day> at <time>:
164 # we grab the current time once, since processing
165 # all the configs might take more than an hour.
168 nowdayofweek=`date +%A`
169 nowdayofweek=`tolower "$nowdayofweek"`
174 whendayofweek=$1; at=$2; whentime=$3;
175 whenday=`toint "$whendayofweek"`
176 whendayofweek=`tolower "$whendayofweek"`
177 whentime=`echo "$whentime" | sed 's/:[0-9][0-9]$//' | sed -r 's/^([0-9])$/0\1/'`
179 if [ "$whendayofweek" == "everyday" -o "$whendayofweek" == "daily" ]; then
180 whendayofweek=$nowdayofweek
183 if [ "$whenday" == "" ]; then
184 if [ "$whendayofweek" != "$nowdayofweek" ]; then
185 whendayofweek=${whendayofweek%s}
186 if [ "$whendayofweek" != "$nowdayofweek" ]; then
190 elif [ "$whenday" != "$nowday" ]; then
194 [ "$at" == "at" ] || return 0
195 [ "$whentime" == "$nowtime" ] || return 0
203 This script allows you to coordinate system backup by dropping a few
204 simple configuration files into @CFGDIR@/backup.d/. Typically, this
205 script is run hourly from cron.
207 The following options are available:
208 -h, --help This usage message
209 -d, --debug Run in debug mode, where all log messages are
210 output to the current shell.
211 -f, --conffile FILE Use FILE for the main configuration instead
212 of @CFGDIR@/backupninja.conf
213 -t, --test Test run mode. This will test if the backup
214 could run, without actually preforming any
215 backups. For example, it will attempt to authenticate
216 or test that ssh keys are set correctly.
217 -n, --now Perform actions now, instead of when they might
218 be scheduled. No output will be created unless also
220 --run FILE Execute the specified action file and then exit.
221 Also puts backupninja in debug mode.
223 When in debug mode, output to the console will be colored:
226 debug "Debugging info (when run with -d)"
227 info "Informational messages (verbosity level 4)"
228 warning "Warnings (verbosity level 3 and up)"
229 error "Errors (verbosity level 2 and up)"
230 fatal "Fatal, halting errors (always shown)"
234 ## this function handles the running of a backup action
236 ## these globals are modified:
237 ## fatals, errors, warnings, actions_run, errormsg
240 function process_action() {
246 # skip over this config if "when" option
247 # is not set to the current time.
248 getconf when "$defaultwhen"
249 if [ "$processnow" == 1 ]; then
250 info ">>>> starting action $file (because of --now)"
252 elif [ "$when" == "hourly" ]; then
253 info ">>>> starting action $file (because 'when = hourly')"
262 if [ $ret == 0 ]; then
263 debug "skipping $file because it is not $w"
265 info ">>>> starting action $file (because it is $w)"
272 [ "$run" == "no" ] && return
274 let "actions_run += 1"
277 local bufferfile=`maketemp backupninja.buffer`
278 echo "" > $bufferfile
281 . $scriptdirectory/$suffix $file
284 echo $a >> $bufferfile
285 [ $debug ] && colorize "$a"
289 # ^^^^^^^^ we have a problem! we can't grab the return code "$?". grrr.
292 _warnings=`cat $bufferfile | grep "^Warning: " | wc -l`
293 _errors=`cat $bufferfile | grep "^Error: " | wc -l`
294 _fatals=`cat $bufferfile | grep "^Fatal: " | wc -l`
296 ret=`grep "\(^Warning: \|^Error: \|^Fatal: \)" $bufferfile`
298 if [ $_fatals != 0 ]; then
299 msg "*failed* -- $file"
300 errormsg="$errormsg\n== fatal errors from $file ==\n\n$ret\n"
301 passthru "Fatal: <<<< finished action $file: FAILED"
302 elif [ $_errors != 0 ]; then
303 msg "*error* -- $file"
304 errormsg="$errormsg\n== errors from $file ==\n\n$ret\n"
305 error "<<<< finished action $file: ERROR"
306 elif [ $_warnings != 0 ]; then
307 msg "*warning* -- $file"
308 errormsg="$errormsg\n== warnings from $file ==\n\n$ret\n"
309 warning "<<<< finished action $file: WARNING"
311 msg "success -- $file"
312 info "<<<< finished action $file: SUCCESS"
315 let "fatals += _fatals"
316 let "errors += _errors"
317 let "warnings += _warnings"
320 #####################################################
324 conffile="@CFGDIR@/backupninja.conf"
327 ## process command line options
329 while [ $# -ge 1 ]; do
332 -d|--debug) debug=1;;
333 -t|--test) test=1;debug=1;;
334 -n|--now) processnow=1;;
339 echo "-f|--conffile option must be followed by an existing filename"
340 fatal "-f|--conffile option must be followed by an existing filename"
343 # we shift here to avoid processing the file path
352 echo "--run option must be fallowed by a backupninja action file"
353 fatal "--run option must be fallowed by a backupninja action file"
360 echo "Unknown option $1"
361 fatal "Unknown option $1"
373 ## Load and confirm basic configuration values
376 if [ ! -r "$conffile" ]; then
377 echo "Configuration file $conffile not found."
378 fatal "Configuration file $conffile not found."
382 libdirectory=`grep '^libdirectory' $conffile | awk '{print $3}'`
383 if [ -z "$libdirectory" ]; then
384 if [ -d "@libdir@" ]; then
385 libdirectory="@libdir@"
387 echo "Could not find entry 'libdirectory' in $conffile."
388 fatal "Could not find entry 'libdirectory' in $conffile."
391 if [ ! -d "$libdirectory" ]; then
392 echo "Lib directory $libdirectory not found."
393 fatal "Lib directory $libdirectory not found."
397 # include shared functions
398 . $libdirectory/tools
402 # get global config options (second param is the default)
403 getconf configdirectory @CFGDIR@/backup.d
404 getconf scriptdirectory @datadir@
406 getconf reportsuccess yes
407 getconf reportwarning yes
409 getconf when "Everyday at 01:00"
411 getconf logfile @localstatedir@/log/backupninja.log
412 getconf usecolors "yes"
413 getconf SLAPCAT /usr/sbin/slapcat
414 getconf LDAPSEARCH /usr/bin/ldapsearch
415 getconf RDIFFBACKUP /usr/bin/rdiff-backup
416 getconf MYSQL /usr/bin/mysql
417 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
418 getconf MYSQLDUMP /usr/bin/mysqldump
419 getconf PGSQLDUMP /usr/bin/pg_dump
420 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
421 getconf GZIP /bin/gzip
422 getconf RSYNC /usr/bin/rsync
424 getconf VSERVERINFO /usr/sbin/vserver-info
425 getconf VSERVER /usr/sbin/vserver
426 getconf VROOTDIR `if [ -f "$VSERVERINFO" ]; then $VSERVERINFO info SYSINFO |grep vserver-Rootdir | awk '{print $2}'; fi`
428 if [ ! -d "$configdirectory" ]; then
429 echo "Configuration directory '$configdirectory' not found."
430 fatal "Configuration directory '$configdirectory' not found."
433 [ -f "$logfile" ] || touch $logfile
435 if [ "$UID" != "0" ]; then
436 echo "`basename $0` can only be run as root"
440 if [ "$vservers" == "yes" -a ! -d "$VROOTDIR" ]; then
441 echo "vservers option set in config, but $VROOTDIR is not a directory!"
442 fatal "vservers option set in config, but $VROOTDIR is not a directory!"
445 ## Process each configuration file
447 # by default, don't make files which are world or group readable.
450 # these globals are set by process_action()
457 if [ "$singlerun" ]; then
460 files=`find $configdirectory -mindepth 1 ! -name '.*.swp' | sort -n`
463 for file in $files; do
464 [ -f "$file" ] || continue
468 base=`basename $file`
469 if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
470 info "Skipping $file"
474 if [ -e "$scriptdirectory/$suffix" ]; then
475 process_action $file $suffix
477 error "Can't process file '$file': no handler script for suffix '$suffix'"
478 msg "*missing handler* -- $file"
482 ## mail the messages to the report address
484 if [ $actions_run == 0 ]; then doit=0
485 elif [ "$reportemail" == "" ]; then doit=0
486 elif [ $fatals != 0 ]; then doit=1
487 elif [ $errors != 0 ]; then doit=1
488 elif [ "$reportsuccess" == "yes" ]; then doit=1
489 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
493 if [ $doit == 1 ]; then
494 debug "send report to $reportemail"
496 [ $warnings == 0 ] || subject="WARNING"
497 [ $errors == 0 ] || subject="ERROR"
498 [ $fatals == 0 ] || subject="FAILED"
501 for ((i=0; i < ${#messages[@]} ; i++)); do
505 } | mail $reportemail -s "backupninja: $hostname $subject"
508 if [ $actions_run != 0 ]; then
509 info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."