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"`
212 [ "$when" == "manual" ] && return 0
214 whendayofweek=$1; at=$2; whentime=$3;
215 whenday=`toint "$whendayofweek"`
216 whendayofweek=`tolower "$whendayofweek"`
217 whentime=`echo "$whentime" | @SED@ 's/:[0-9][0-9]$//' | @SED@ -r 's/^([0-9])$/0\1/'`
219 if [ "$whendayofweek" == "everyday" -o "$whendayofweek" == "daily" ]; then
220 whendayofweek=$nowdayofweek
223 if [ "$whenday" == "" ]; then
224 if [ "$whendayofweek" != "$nowdayofweek" ]; then
225 whendayofweek=${whendayofweek%s}
226 if [ "$whendayofweek" != "$nowdayofweek" ]; then
230 elif [ "$whenday" != "$nowday" ]; then
234 [ "$at" == "at" ] || return 0
235 [ "$whentime" == "$nowtime" ] || return 0
243 This script allows you to coordinate system backup by dropping a few
244 simple configuration files into @CFGDIR@/backup.d/. Typically, this
245 script is run hourly from cron.
247 The following options are available:
248 -h, --help This usage message
249 -d, --debug Run in debug mode, where all log messages are
250 output to the current shell.
251 -f, --conffile FILE Use FILE for the main configuration instead
252 of @CFGDIR@/backupninja.conf
253 -t, --test Test run mode. This will test if the backup
254 could run, without actually preforming any
255 backups. For example, it will attempt to authenticate
256 or test that ssh keys are set correctly.
257 -n, --now Perform actions now, instead of when they might
258 be scheduled. No output will be created unless also
260 --run FILE Execute the specified action file and then exit.
261 Also puts backupninja in debug mode.
263 When in debug mode, output to the console will be colored:
266 colorize "Debug: Debugging info (when run with -d)"
267 colorize "Info: Informational messages (verbosity level 4)"
268 colorize "Warning: Warnings (verbosity level 3 and up)"
269 colorize "Error: Errors (verbosity level 2 and up)"
270 colorize "Fatal: Errors which halt a given backup action (always shown)"
271 colorize "Halt: Errors which halt the whole backupninja run (always shown)"
275 ## this function handles the running of a backup action
277 ## these globals are modified:
278 ## halts, fatals, errors, warnings, actions_run, errormsg
281 function process_action() {
287 # skip over this config if "when" option
288 # is not set to the current time.
289 getconf when "$defaultwhen"
290 if [ "$processnow" == 1 ]; then
291 info ">>>> starting action $file (because of --now)"
293 elif [ "$when" == "hourly" ]; then
294 info ">>>> starting action $file (because 'when = hourly')"
303 if [ $ret == 0 ]; then
304 debug "skipping $file because current time does not match $w"
306 info ">>>> starting action $file (because current time matches $w)"
313 [ "$run" == "no" ] && return
315 let "actions_run += 1"
318 local bufferfile=`maketemp backupninja.buffer`
319 echo "" > $bufferfile
322 . $scriptdirectory/$suffix $file
325 echo $a >> $bufferfile
326 [ $debug ] && colorize "$a"
330 # ^^^^^^^^ we have a problem! we can't grab the return code "$?". grrr.
333 _warnings=`cat $bufferfile | grep "^Warning: " | wc -l`
334 _errors=`cat $bufferfile | grep "^Error: " | wc -l`
335 _fatals=`cat $bufferfile | grep "^Fatal: " | wc -l`
336 _halts=`cat $bufferfile | grep "^Halt: " | wc -l`
338 ret=`grep "\(^Warning: \|^Error: \|^Fatal: \|Halt: \)" $bufferfile`
340 if [ $_halts != 0 ]; then
341 msg "*halt* -- $file"
342 errormsg="$errormsg\n== halt request from $file==\n\n$ret\n"
343 passthru "Halt: <<<< finished action $file: FAILED"
344 elif [ $_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 "halts += _halts"
362 let "fatals += _fatals"
363 let "errors += _errors"
364 let "warnings += _warnings"
367 #####################################################
371 conffile="@CFGDIR@/backupninja.conf"
374 ## process command line options
376 while [ $# -ge 1 ]; do
379 -d|--debug) debug=1;;
380 -t|--test) test=1;debug=1;;
381 -n|--now) processnow=1;;
386 echo "-f|--conffile option must be followed by an existing filename"
387 fatal "-f|--conffile option must be followed by an existing filename"
390 # we shift here to avoid processing the file path
399 echo "--run option must be followed by a backupninja action file"
400 fatal "--run option must be followed by a backupninja action file"
407 echo "Unknown option $1"
408 fatal "Unknown option $1"
420 ## Load and confirm basic configuration values
423 if [ ! -r "$conffile" ]; then
424 echo "Configuration file $conffile not found."
425 fatal "Configuration file $conffile not found."
429 libdirectory=`grep '^libdirectory' $conffile | @AWK@ '{print $3}'`
430 if [ -z "$libdirectory" ]; then
431 if [ -d "@libdir@" ]; then
432 libdirectory="@libdir@"
434 echo "Could not find entry 'libdirectory' in $conffile."
435 fatal "Could not find entry 'libdirectory' in $conffile."
438 if [ ! -d "$libdirectory" ]; then
439 echo "Lib directory $libdirectory not found."
440 fatal "Lib directory $libdirectory not found."
444 # include shared functions
445 . $libdirectory/tools
446 . $libdirectory/vserver
450 # get global config options (second param is the default)
451 getconf configdirectory @CFGDIR@/backup.d
452 getconf scriptdirectory @datadir@
453 getconf reportdirectory
457 getconf reportsuccess yes
459 getconf reportwarning yes
461 getconf when "Everyday at 01:00"
463 getconf logfile @localstatedir@/log/backupninja.log
464 getconf usecolors "yes"
465 getconf SLAPCAT /usr/sbin/slapcat
466 getconf LDAPSEARCH /usr/bin/ldapsearch
467 getconf RDIFFBACKUP /usr/bin/rdiff-backup
468 getconf CSTREAM /usr/bin/cstream
469 getconf MYSQLADMIN /usr/bin/mysqladmin
470 getconf MYSQL /usr/bin/mysql
471 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
472 getconf MYSQLDUMP /usr/bin/mysqldump
473 getconf PGSQLDUMP /usr/bin/pg_dump
474 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
475 getconf PGSQLUSER postgres
476 getconf GZIP /bin/gzip
477 getconf RSYNC /usr/bin/rsync
478 getconf admingroup root
480 # initialize vservers support
481 # (get config variables and check real vservers availability)
482 init_vservers nodialog
484 if [ ! -d "$configdirectory" ]; then
485 echo "Configuration directory '$configdirectory' not found."
486 fatal "Configuration directory '$configdirectory' not found."
489 [ -f "$logfile" ] || touch $logfile
491 if [ "$UID" != "0" ]; then
492 echo "`basename $0` can only be run as root"
496 ## Process each configuration file
498 # by default, don't make files which are world or group readable.
501 # these globals are set by process_action()
509 if [ "$singlerun" ]; then
512 files=`find $configdirectory -follow -mindepth 1 -maxdepth 1 -type f ! -name '.*.swp' | sort -n`
514 if [ -z "$files" ]; then
515 fatal "No backup actions configured in '$configdirectory', run ninjahelper!"
519 for file in $files; do
520 [ -f "$file" ] || continue
521 [ "$halts" = "0" ] || continue
523 check_perms ${file%/*} # check containing dir
526 base=`basename $file`
527 if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
528 info "Skipping $file"
532 if [ -e "$scriptdirectory/$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 if [ "$reportspace" == "yes" ]; then
565 for i in $(ls "$configdirectory"); do
566 backuploc=$(grep ^directory "$configdirectory"/"$i" | @AWK@ '{print $3}')
567 if [ "$backuploc" != "$previous" -a -n "$backuploc" ]; then
569 previous="$backuploc"
573 } | mail -s "backupninja: $hostname $subject" $reportemail
576 if [ $actions_run != 0 ]; then
577 info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."
578 if [ "$halts" != "0" ]; then
579 info "Backup was halted prematurely. Some actions may not have run."
583 if [ -n "$reporthost" ]; then
584 debug "send $logfile to $reportuser@$reporthost:$reportdirectory"
585 rsync -qt $logfile $reportuser@$reporthost:$reportdirectory