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 $CYAN)
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 [ "$typestr" == "Halt" ] && type=5
44 color=${COLORS[$type]}
46 echo -e "$color$@$endcolor"
52 # We have the following message levels:
54 # 1 - normal messages - green
55 # 2 - warnings - yellow
59 # First variable passed is the error level, all others are printed
61 # if 1, echo out all warnings, errors, or fatal
62 # used to capture output from handlers
68 [ ${#@} -gt 1 ] || return
72 if [ $type == 100 ]; then
73 typestr=`echo "$@" | @SED@ 's/\(^[^:]*\).*$/\1/'`
74 [ "$typestr" == "Debug" ] && type=0
75 [ "$typestr" == "Info" ] && type=1
76 [ "$typestr" == "Warning" ] && type=2
77 [ "$typestr" == "Error" ] && type=3
78 [ "$typestr" == "Fatal" ] && type=4
79 [ "$typestr" == "Halt" ] && type=5
82 types=(Debug Info Warning Error Fatal Halt)
83 typestr="${types[$type]}: "
88 if [ $echo_debug_msg == 1 ]; then
89 echo -e "$typestr$@" >&2
91 colorize "$typestr$@" >&2
94 if [ $print -lt $loglevel ]; then
100 if [ -w "$logfile" ]; then
101 echo -e `LC_ALL=C date "+%h %d %H:%M:%S"` "$@" >> $logfile
105 function passthru() {
131 messages[$msgcount]=$1
136 # enforces very strict permissions on configuration file $file.
139 function check_perms() {
141 debug "check_perms $file"
145 perms=($(stat -L --format='%A' $file))
146 debug "perms: $perms"
147 local gperm=${perms:4:3}
148 debug "gperm: $gperm"
149 local wperm=${perms:7:3}
150 debug "wperm: $wperm"
152 owners=($(stat -L --format='%g %G %u %U' $file))
153 local gid=${owners[0]}
154 local group=${owners[1]}
155 local owner=${owners[2]}
157 if [ "$owner" != 0 ]; then
158 echo "Configuration files must be owned by root! Dying on file $file"
159 fatal "Configuration files must be owned by root! Dying on file $file"
162 if [ "$wperm" != '---' ]; then
163 echo "Configuration files must not be world writable/readable! Dying on file $file"
164 fatal "Configuration files must not be world writable/readable! Dying on file $file"
167 if [ "$gperm" != '---' ]; then
168 case "$admingroup" in
172 if [ "$gid" != 0 ]; then
173 echo "Configuration files must not be writable/readable by group $group! Use the admingroup option in backupninja.conf. Dying on file $file"
174 fatal "Configuration files must not be writable/readable by group $group! Use the admingroup option in backupninja.conf. Dying on file $file"
181 # simple lowercase function
183 echo "$1" | tr '[:upper:]' '[:lower:]'
186 # simple to integer function
188 echo "$1" | tr -d '[:alpha:]'
192 # function isnow(): returns 1 if the time/day passed as $1 matches
193 # the current time/day.
195 # format is <day> at <time>:
201 # we grab the current time once, since processing
202 # all the configs might take more than an hour.
203 nowtime=`LC_ALL=C date +%H`
204 nowday=`LC_ALL=C date +%d`
205 nowdayofweek=`LC_ALL=C date +%A`
206 nowdayofweek=`tolower "$nowdayofweek"`
211 whendayofweek=$1; at=$2; whentime=$3;
212 whenday=`toint "$whendayofweek"`
213 whendayofweek=`tolower "$whendayofweek"`
214 whentime=`echo "$whentime" | @SED@ 's/:[0-9][0-9]$//' | @SED@ -r 's/^([0-9])$/0\1/'`
216 if [ "$whendayofweek" == "everyday" -o "$whendayofweek" == "daily" ]; then
217 whendayofweek=$nowdayofweek
220 if [ "$whenday" == "" ]; then
221 if [ "$whendayofweek" != "$nowdayofweek" ]; then
222 whendayofweek=${whendayofweek%s}
223 if [ "$whendayofweek" != "$nowdayofweek" ]; then
227 elif [ "$whenday" != "$nowday" ]; then
231 [ "$at" == "at" ] || return 0
232 [ "$whentime" == "$nowtime" ] || return 0
240 This script allows you to coordinate system backup by dropping a few
241 simple configuration files into @CFGDIR@/backup.d/. Typically, this
242 script is run hourly from cron.
244 The following options are available:
245 -h, --help This usage message
246 -d, --debug Run in debug mode, where all log messages are
247 output to the current shell.
248 -f, --conffile FILE Use FILE for the main configuration instead
249 of @CFGDIR@/backupninja.conf
250 -t, --test Test run mode. This will test if the backup
251 could run, without actually preforming any
252 backups. For example, it will attempt to authenticate
253 or test that ssh keys are set correctly.
254 -n, --now Perform actions now, instead of when they might
255 be scheduled. No output will be created unless also
257 --run FILE Execute the specified action file and then exit.
258 Also puts backupninja in debug mode.
260 When in debug mode, output to the console will be colored:
263 colorize "Debug: Debugging info (when run with -d)"
264 colorize "Info: Informational messages (verbosity level 4)"
265 colorize "Warning: Warnings (verbosity level 3 and up)"
266 colorize "Error: Errors (verbosity level 2 and up)"
267 colorize "Fatal: Errors which halt a given backup action (always shown)"
268 colorize "Halt: Errors which halt the whole backupninja run (always shown)"
272 ## this function handles the running of a backup action
274 ## these globals are modified:
275 ## halts, fatals, errors, warnings, actions_run, errormsg
278 function process_action() {
284 # skip over this config if "when" option
285 # is not set to the current time.
286 getconf when "$defaultwhen"
287 if [ "$processnow" == 1 ]; then
288 info ">>>> starting action $file (because of --now)"
290 elif [ "$when" == "hourly" ]; then
291 info ">>>> starting action $file (because 'when = hourly')"
300 if [ $ret == 0 ]; then
301 debug "skipping $file because it is not $w"
303 info ">>>> starting action $file (because it is $w)"
310 [ "$run" == "no" ] && return
312 let "actions_run += 1"
315 local bufferfile=`maketemp backupninja.buffer`
316 echo "" > $bufferfile
319 . $scriptdirectory/$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`
333 _halts=`cat $bufferfile | grep "^Halt: " | wc -l`
335 ret=`grep "\(^Warning: \|^Error: \|^Fatal: \|Halt: \)" $bufferfile`
337 if [ $_halts != 0 ]; then
338 msg "*halt* -- $file"
339 errormsg="$errormsg\n== halt request from $file==\n\n$ret\n"
340 passthru "Halt: <<<< finished action $file: FAILED"
341 elif [ $_fatals != 0 ]; then
342 msg "*failed* -- $file"
343 errormsg="$errormsg\n== fatal errors from $file ==\n\n$ret\n"
344 passthru "Fatal: <<<< finished action $file: FAILED"
345 elif [ $_errors != 0 ]; then
346 msg "*error* -- $file"
347 errormsg="$errormsg\n== errors from $file ==\n\n$ret\n"
348 error "<<<< finished action $file: ERROR"
349 elif [ $_warnings != 0 ]; then
350 msg "*warning* -- $file"
351 errormsg="$errormsg\n== warnings from $file ==\n\n$ret\n"
352 warning "<<<< finished action $file: WARNING"
354 msg "success -- $file"
355 info "<<<< finished action $file: SUCCESS"
358 let "halts += _halts"
359 let "fatals += _fatals"
360 let "errors += _errors"
361 let "warnings += _warnings"
364 #####################################################
368 conffile="@CFGDIR@/backupninja.conf"
371 ## process command line options
373 while [ $# -ge 1 ]; do
376 -d|--debug) debug=1;;
377 -t|--test) test=1;debug=1;;
378 -n|--now) processnow=1;;
383 echo "-f|--conffile option must be followed by an existing filename"
384 fatal "-f|--conffile option must be followed by an existing filename"
387 # we shift here to avoid processing the file path
396 echo "--run option must be followed by a backupninja action file"
397 fatal "--run option must be followed by a backupninja action file"
404 echo "Unknown option $1"
405 fatal "Unknown option $1"
417 ## Load and confirm basic configuration values
420 if [ ! -r "$conffile" ]; then
421 echo "Configuration file $conffile not found."
422 fatal "Configuration file $conffile not found."
426 libdirectory=`grep '^libdirectory' $conffile | @AWK@ '{print $3}'`
427 if [ -z "$libdirectory" ]; then
428 if [ -d "@libdir@" ]; then
429 libdirectory="@libdir@"
431 echo "Could not find entry 'libdirectory' in $conffile."
432 fatal "Could not find entry 'libdirectory' in $conffile."
435 if [ ! -d "$libdirectory" ]; then
436 echo "Lib directory $libdirectory not found."
437 fatal "Lib directory $libdirectory not found."
441 # include shared functions
442 . $libdirectory/tools
443 . $libdirectory/vserver
447 # get global config options (second param is the default)
448 getconf configdirectory @CFGDIR@/backup.d
449 getconf scriptdirectory @datadir@
450 getconf reportdirectory
454 getconf reportsuccess yes
456 getconf reportwarning yes
458 getconf when "Everyday at 01:00"
460 getconf logfile @localstatedir@/log/backupninja.log
461 getconf usecolors "yes"
462 getconf SLAPCAT /usr/sbin/slapcat
463 getconf LDAPSEARCH /usr/bin/ldapsearch
464 getconf RDIFFBACKUP /usr/bin/rdiff-backup
465 getconf CSTREAM /usr/bin/cstream
466 getconf MYSQLADMIN /usr/bin/mysqladmin
467 getconf MYSQL /usr/bin/mysql
468 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
469 getconf MYSQLDUMP /usr/bin/mysqldump
470 getconf PGSQLDUMP /usr/bin/pg_dump
471 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
472 getconf PGSQLUSER postgres
473 getconf GZIP /bin/gzip
474 getconf RSYNC /usr/bin/rsync
475 getconf admingroup root
477 # initialize vservers support
478 # (get config variables and check real vservers availability)
479 init_vservers nodialog
481 if [ ! -d "$configdirectory" ]; then
482 echo "Configuration directory '$configdirectory' not found."
483 fatal "Configuration directory '$configdirectory' not found."
486 [ -f "$logfile" ] || touch $logfile
488 if [ "$UID" != "0" ]; then
489 echo "`basename $0` can only be run as root"
493 ## Process each configuration file
495 # by default, don't make files which are world or group readable.
498 # these globals are set by process_action()
506 if [ "$singlerun" ]; then
509 files=`find $configdirectory -follow -mindepth 1 -maxdepth 1 -type f ! -name '.*.swp' | sort -n`
511 if [ -z "$files" ]; then
512 fatal "No backup actions configured in '$configdirectory', run ninjahelper!"
516 for file in $files; do
517 [ -f "$file" ] || continue
518 [ "$halts" = "0" ] || continue
520 check_perms ${file%/*} # check containing dir
523 base=`basename $file`
524 if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
525 info "Skipping $file"
529 if [ -e "$scriptdirectory/$suffix" ]; then
530 process_action $file $suffix
532 error "Can't process file '$file': no handler script for suffix '$suffix'"
533 msg "*missing handler* -- $file"
537 ## mail the messages to the report address
539 if [ $actions_run == 0 ]; then doit=0
540 elif [ "$reportemail" == "" ]; then doit=0
541 elif [ $fatals != 0 ]; then doit=1
542 elif [ $errors != 0 ]; then doit=1
543 elif [ "$reportsuccess" == "yes" ]; then doit=1
544 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
548 if [ $doit == 1 ]; then
549 debug "send report to $reportemail"
551 [ $warnings == 0 ] || subject="WARNING"
552 [ $errors == 0 ] || subject="ERROR"
553 [ $fatals == 0 ] || subject="FAILED"
556 for ((i=0; i < ${#messages[@]} ; i++)); do
560 if [ "$reportspace" == "yes" ]; then
562 for i in $(ls "$configdirectory"); do
563 backuploc=$(grep ^directory "$configdirectory"/"$i" | @AWK@ '{print $3}')
564 if [ "$backuploc" != "$previous" -a -n "$backuploc" ]; then
566 previous="$backuploc"
570 } | mail -s "backupninja: $hostname $subject" $reportemail
573 if [ $actions_run != 0 ]; then
574 info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."
575 if [ "$halts" != "0" ]; then
576 info "Backup was halted prematurely. Some actions may not have run."
580 if [ -n "$reporthost" ]; then
581 debug "send $logfile to $reportuser@$reporthost:$reportdirectory"
582 rsync -qt $logfile $reportuser@$reporthost:$reportdirectory