2 # -*- mode: sh; sh-basic-offset: 3; indent-tabs-mode: nil; -*-
3 # vim: set filetype=sh sw=3 sts=3 expandtab autoindent:
6 # B A C K U P N I N J A /()/
9 # Copyright (C) 2004-05 riseup.net -- property is theft.
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
22 #####################################################
25 function setupcolors () {
33 COLORS=($BLUE $GREEN $YELLOW $RED $PURPLE $CYAN)
36 function colorize () {
37 if [ "$usecolors" == "yes" ]; then
38 local typestr=`echo "$@" | @SED@ 's/\(^[^:]*\).*$/\1/'`
39 [ "$typestr" == "Debug" ] && type=0
40 [ "$typestr" == "Info" ] && type=1
41 [ "$typestr" == "Warning" ] && type=2
42 [ "$typestr" == "Error" ] && type=3
43 [ "$typestr" == "Fatal" ] && type=4
44 [ "$typestr" == "Halt" ] && type=5
45 color=${COLORS[$type]}
47 echo -e "$color$@$endcolor"
53 # We have the following message levels:
55 # 1 - normal messages - green
56 # 2 - warnings - yellow
60 # First variable passed is the error level, all others are printed
62 # if 1, echo out all warnings, errors, or fatal
63 # used to capture output from handlers
69 [ ${#@} -gt 1 ] || return
73 if [ $type == 100 ]; then
74 typestr=`echo "$@" | @SED@ 's/\(^[^:]*\).*$/\1/'`
75 [ "$typestr" == "Debug" ] && type=0
76 [ "$typestr" == "Info" ] && type=1
77 [ "$typestr" == "Warning" ] && type=2
78 [ "$typestr" == "Error" ] && type=3
79 [ "$typestr" == "Fatal" ] && type=4
80 [ "$typestr" == "Halt" ] && type=5
83 types=(Debug Info Warning Error Fatal Halt)
84 typestr="${types[$type]}: "
89 if [ $echo_debug_msg == 1 ]; then
90 echo -e "$typestr$@" >&2
92 colorize "$typestr$@" >&2
95 if [ $print -lt $loglevel ]; then
101 if [ -w "$logfile" ]; then
102 echo -e `LC_ALL=C date "+%h %d %H:%M:%S"` "$@" >> $logfile
106 function passthru() {
132 messages[$msgcount]=$1
137 # enforces very strict permissions on configuration file $file.
140 function check_perms() {
142 debug "check_perms $file"
146 perms=($(@STAT@ -L --format='%A' $file))
147 debug "perms: $perms"
148 local gperm=${perms:4:3}
149 debug "gperm: $gperm"
150 local wperm=${perms:7:3}
151 debug "wperm: $wperm"
153 owners=($(@STAT@ -L --format='%g %G %u %U' $file))
154 local gid=${owners[0]}
155 local group=${owners[1]}
156 local owner=${owners[2]}
158 if [ "$owner" != 0 ]; then
159 echo "Configuration files must be owned by root! Dying on file $file"
160 fatal "Configuration files must be owned by root! Dying on file $file"
163 if [ "$wperm" != '---' ]; then
164 echo "Configuration files must not be world writable/readable! Dying on file $file"
165 fatal "Configuration files must not be world writable/readable! Dying on file $file"
168 if [ "$gperm" != '---' ]; then
169 case "$admingroup" in
173 if [ "$gid" != 0 ]; then
174 echo "Configuration files must not be writable/readable by group $group! Use the admingroup option in backupninja.conf. Dying on file $file"
175 fatal "Configuration files must not be writable/readable by group $group! Use the admingroup option in backupninja.conf. Dying on file $file"
182 # simple lowercase function
184 echo "$1" | tr '[:upper:]' '[:lower:]'
187 # simple to integer function
189 echo "$1" | tr -d '[:alpha:]'
193 # function isnow(): returns 1 if the time/day passed as $1 matches
194 # the current time/day.
196 # format is <day> at <time>:
202 # we grab the current time once, since processing
203 # all the configs might take more than an hour.
204 nowtime=`LC_ALL=C date +%H`
205 nowday=`LC_ALL=C date +%d`
206 nowdayofweek=`LC_ALL=C date +%A`
207 nowdayofweek=`tolower "$nowdayofweek"`
213 [ "$when" == "manual" ] && return 0
215 whendayofweek=$1; at=$2; whentime=$3;
216 whenday=`toint "$whendayofweek"`
217 whendayofweek=`tolower "$whendayofweek"`
218 whentime=`echo "$whentime" | @SED@ 's/:[0-9][0-9]$//' | @SED@ -r 's/^([0-9])$/0\1/'`
220 if [ "$whendayofweek" == "everyday" -o "$whendayofweek" == "daily" ]; then
221 whendayofweek=$nowdayofweek
224 if [ "$whenday" == "" ]; then
225 if [ "$whendayofweek" != "$nowdayofweek" ]; then
226 whendayofweek=${whendayofweek%s}
227 if [ "$whendayofweek" != "$nowdayofweek" ]; then
231 elif [ "$whenday" != "$nowday" ]; then
235 [ "$at" == "at" ] || return 0
236 [ "$whentime" == "$nowtime" ] || return 0
244 This script allows you to coordinate system backup by dropping a few
245 simple configuration files into @CFGDIR@/backup.d/. Typically, this
246 script is run hourly from cron.
248 The following options are available:
249 -h, --help This usage message
250 -d, --debug Run in debug mode, where all log messages are
251 output to the current shell.
252 -f, --conffile FILE Use FILE for the main configuration instead
253 of @CFGDIR@/backupninja.conf
254 -t, --test Test run mode. This will test if the backup
255 could run, without actually preforming any
256 backups. For example, it will attempt to authenticate
257 or test that ssh keys are set correctly.
258 -n, --now Perform actions now, instead of when they might
259 be scheduled. No output will be created unless also
261 --run FILE Execute the specified action file and then exit.
262 Also puts backupninja in debug mode.
264 When in debug mode, output to the console will be colored:
267 colorize "Debug: Debugging info (when run with -d)"
268 colorize "Info: Informational messages (verbosity level 4)"
269 colorize "Warning: Warnings (verbosity level 3 and up)"
270 colorize "Error: Errors (verbosity level 2 and up)"
271 colorize "Fatal: Errors which halt a given backup action (always shown)"
272 colorize "Halt: Errors which halt the whole backupninja run (always shown)"
276 ## this function handles the running of a backup action
278 ## these globals are modified:
279 ## halts, fatals, errors, warnings, actions_run, errormsg
282 function process_action() {
288 # skip over this config if "when" option
289 # is not set to the current time.
290 getconf when "$defaultwhen"
291 if [ "$processnow" == 1 ]; then
292 info ">>>> starting action $file (because of --now)"
294 elif [ "$when" == "hourly" ]; then
295 info ">>>> starting action $file (because 'when = hourly')"
304 if [ $ret == 0 ]; then
305 debug "skipping $file because current time does not match $w"
307 info ">>>> starting action $file (because current time matches $w)"
314 [ "$run" == "no" ] && return
316 let "actions_run += 1"
319 local bufferfile=`maketemp backupninja.buffer`
320 echo "" > $bufferfile
323 . $scriptdirectory/$suffix $file
326 echo $a >> $bufferfile
327 [ $debug ] && colorize "$a"
331 # ^^^^^^^^ we have a problem! we can't grab the return code "$?". grrr.
334 _warnings=`cat $bufferfile | grep "^Warning: " | wc -l`
335 _errors=`cat $bufferfile | grep "^Error: " | wc -l`
336 _fatals=`cat $bufferfile | grep "^Fatal: " | wc -l`
337 _halts=`cat $bufferfile | grep "^Halt: " | wc -l`
338 _infos=`cat $bufferfile | grep "^Info: " | wc -l`
340 ret=`grep "\(^Info: \|^Warning: \|^Error: \|^Fatal: \|Halt: \)" $bufferfile`
342 if [ $_halts != 0 ]; then
343 msg "*halt* -- $file"
344 errormsg="$errormsg\n== halt request from $file==\n\n$ret\n"
345 passthru "Halt: <<<< finished action $file: FAILED"
346 elif [ $_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 if [ $_infos != 0 -a "$reportinfo" == "yes" ]; then
361 errormsg="$errormsg\n== infos from $file ==\n\n$ret\n"
363 info "<<<< finished action $file: SUCCESS"
366 let "halts += _halts"
367 let "fatals += _fatals"
368 let "errors += _errors"
369 let "warnings += _warnings"
372 #####################################################
376 conffile="@CFGDIR@/backupninja.conf"
379 ## process command line options
381 while [ $# -ge 1 ]; do
384 -d|--debug) debug=1; export BACKUPNINJA_DEBUG=yes;;
385 -t|--test) test=1;debug=1;;
386 -n|--now) processnow=1;;
391 echo "-f|--conffile option must be followed by an existing filename"
392 fatal "-f|--conffile option must be followed by an existing filename"
395 # we shift here to avoid processing the file path
404 echo "--run option must be followed by a backupninja action file"
405 fatal "--run option must be followed by a backupninja action file"
412 echo "Unknown option $1"
413 fatal "Unknown option $1"
425 ## Load and confirm basic configuration values
428 if [ ! -r "$conffile" ]; then
429 echo "Configuration file $conffile not found."
430 fatal "Configuration file $conffile not found."
434 libdirectory=`grep '^libdirectory' $conffile | @AWK@ '{print $3}'`
435 if [ -z "$libdirectory" ]; then
436 if [ -d "@libdir@" ]; then
437 libdirectory="@libdir@"
439 echo "Could not find entry 'libdirectory' in $conffile."
440 fatal "Could not find entry 'libdirectory' in $conffile."
443 if [ ! -d "$libdirectory" ]; then
444 echo "Lib directory $libdirectory not found."
445 fatal "Lib directory $libdirectory not found."
449 # include shared functions
450 . $libdirectory/tools
451 . $libdirectory/array
452 . $libdirectory/backend
453 . $libdirectory/vserver
457 # get global config options (second param is the default)
458 getconf configdirectory @CFGDIR@/backup.d
459 getconf scriptdirectory @datadir@
460 getconf reportdirectory
464 getconf reportsuccess yes
465 getconf reportinfo no
467 getconf reportwarning yes
469 getconf when "Everyday at 01:00"
471 getconf logfile @localstatedir@/log/backupninja.log
472 getconf usecolors "yes"
473 getconf default_backend
474 getconf SLAPCAT /usr/sbin/slapcat
475 getconf LDAPSEARCH /usr/bin/ldapsearch
476 getconf RDIFFBACKUP /usr/bin/rdiff-backup
477 getconf CSTREAM /usr/bin/cstream
478 getconf MYSQLADMIN /usr/bin/mysqladmin
479 getconf MYSQL /usr/bin/mysql
480 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
481 getconf MYSQLDUMP /usr/bin/mysqldump
482 getconf PGSQLDUMP /usr/bin/pg_dump
483 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
484 getconf PGSQLUSER postgres
485 getconf GZIP /bin/gzip
486 getconf RSYNC /usr/bin/rsync
487 getconf admingroup root
491 # initialize legacy vservers support
492 # (get config variables and check real vservers availability)
493 init_vservers nodialog
495 if [ ! -d "$configdirectory" ]; then
496 echo "Configuration directory '$configdirectory' not found."
497 fatal "Configuration directory '$configdirectory' not found."
500 [ -f "$logfile" ] || touch $logfile
502 if [ "$UID" != "0" ]; then
503 echo "`basename $0` can only be run as root"
507 ## Process each configuration file
509 # by default, don't make files which are world or group readable.
512 # these globals are set by process_action()
520 if [ "$singlerun" ]; then
523 files=`find $configdirectory -follow -mindepth 1 -maxdepth 1 -type f ! -name '.*.swp' | sort -n`
525 if [ -z "$files" ]; then
526 fatal "No backup actions configured in '$configdirectory', run ninjahelper!"
530 for file in $files; do
531 [ -f "$file" ] || continue
532 [ "$halts" = "0" ] || continue
534 check_perms ${file%/*} # check containing dir
537 base=`basename $file`
538 if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
539 info "Skipping $file"
543 if [ -e "$scriptdirectory/$suffix" ]; then
544 process_action $file $suffix
546 error "Can't process file '$file': no handler script for suffix '$suffix'"
547 msg "*missing handler* -- $file"
551 ## mail the messages to the report address
553 if [ $actions_run == 0 ]; then doit=0
554 elif [ "$reportemail" == "" ]; then doit=0
555 elif [ $fatals != 0 ]; then doit=1
556 elif [ $errors != 0 ]; then doit=1
557 elif [ "$reportsuccess" == "yes" ]; then doit=1
558 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
562 if [ $doit == 1 ]; then
563 debug "send report to $reportemail"
565 [ $warnings == 0 ] || subject="WARNING"
566 [ $errors == 0 ] || subject="ERROR"
567 [ $fatals == 0 ] || subject="FAILED"
570 for ((i=0; i < ${#messages[@]} ; i++)); do
574 if [ "$reportspace" == "yes" ]; then
576 for i in $(ls "$configdirectory"); do
577 backuploc=$(grep ^directory "$configdirectory"/"$i" | @AWK@ '{print $3}')
578 if [ "$backuploc" != "$previous" -a -n "$backuploc" -a -d "$backuploc" ]; then
580 previous="$backuploc"
584 } | mail -s "backupninja: $hostname $subject" $reportemail
587 if [ $actions_run != 0 ]; then
588 info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."
589 if [ "$halts" != "0" ]; then
590 info "Backup was halted prematurely. Some actions may not have run."
594 if [ -n "$reporthost" ]; then
595 debug "send $logfile to $reportuser@$reporthost:$reportdirectory"
596 rsync -qt $logfile $reportuser@$reporthost:$reportdirectory