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() {
135 perms=($(stat -L --format='%a %g %G %u %U' $file))
136 local gperm=${perms[0]:1:1}
137 local wperm=${perms[0]:2:1}
138 local gid=${perms[1]}
139 local group=${perms[2]}
140 local owner=${perms[3]}
142 if [ "$owner" != 0 ]; then
143 echo "Configuration files must be owned by root! Dying on file $file"
144 fatal "Configuration files must be owned by root! Dying on file $file"
147 if [ $wperm -gt 0 ]; then
148 echo "Configuration files must not be world writable/readable! Dying on file $file"
149 fatal "Configuration files must not be world writable/readable! Dying on file $file"
152 if [ $gperm -gt 0 ]; then
153 case "$admingroup" in
157 if [ "$gid" != 0 ]; then
158 echo "Configuration files must not be writable/readable by group ${perms[2]}! Dying on file $file"
159 fatal "Configuration files must not be writable/readable by group ${perms[2]}! Dying on file $file"
166 # simple lowercase function
168 echo "$1" | tr [:upper:] [:lower:]
171 # simple to integer function
173 echo "$1" | tr -d [:alpha:]
177 # function isnow(): returns 1 if the time/day passed as $1 matches
178 # the current time/day.
180 # format is <day> at <time>:
186 # we grab the current time once, since processing
187 # all the configs might take more than an hour.
190 nowdayofweek=`date +%A`
191 nowdayofweek=`tolower "$nowdayofweek"`
196 whendayofweek=$1; at=$2; whentime=$3;
197 whenday=`toint "$whendayofweek"`
198 whendayofweek=`tolower "$whendayofweek"`
199 whentime=`echo "$whentime" | sed 's/:[0-9][0-9]$//' | sed -r 's/^([0-9])$/0\1/'`
201 if [ "$whendayofweek" == "everyday" -o "$whendayofweek" == "daily" ]; then
202 whendayofweek=$nowdayofweek
205 if [ "$whenday" == "" ]; then
206 if [ "$whendayofweek" != "$nowdayofweek" ]; then
207 whendayofweek=${whendayofweek%s}
208 if [ "$whendayofweek" != "$nowdayofweek" ]; then
212 elif [ "$whenday" != "$nowday" ]; then
216 [ "$at" == "at" ] || return 0
217 [ "$whentime" == "$nowtime" ] || return 0
225 This script allows you to coordinate system backup by dropping a few
226 simple configuration files into @CFGDIR@/backup.d/. Typically, this
227 script is run hourly from cron.
229 The following options are available:
230 -h, --help This usage message
231 -d, --debug Run in debug mode, where all log messages are
232 output to the current shell.
233 -f, --conffile FILE Use FILE for the main configuration instead
234 of @CFGDIR@/backupninja.conf
235 -t, --test Test run mode. This will test if the backup
236 could run, without actually preforming any
237 backups. For example, it will attempt to authenticate
238 or test that ssh keys are set correctly.
239 -n, --now Perform actions now, instead of when they might
240 be scheduled. No output will be created unless also
242 --run FILE Execute the specified action file and then exit.
243 Also puts backupninja in debug mode.
245 When in debug mode, output to the console will be colored:
248 debug "Debugging info (when run with -d)"
249 info "Informational messages (verbosity level 4)"
250 warning "Warnings (verbosity level 3 and up)"
251 error "Errors (verbosity level 2 and up)"
252 fatal "Fatal, halting errors (always shown)"
256 ## this function handles the running of a backup action
258 ## these globals are modified:
259 ## fatals, errors, warnings, actions_run, errormsg
262 function process_action() {
268 # skip over this config if "when" option
269 # is not set to the current time.
270 getconf when "$defaultwhen"
271 if [ "$processnow" == 1 ]; then
272 info ">>>> starting action $file (because of --now)"
274 elif [ "$when" == "hourly" ]; then
275 info ">>>> starting action $file (because 'when = hourly')"
284 if [ $ret == 0 ]; then
285 debug "skipping $file because it is not $w"
287 info ">>>> starting action $file (because it is $w)"
294 [ "$run" == "no" ] && return
296 let "actions_run += 1"
299 local bufferfile=`maketemp backupninja.buffer`
300 echo "" > $bufferfile
303 . $scriptdirectory/$suffix $file
306 echo $a >> $bufferfile
307 [ $debug ] && colorize "$a"
311 # ^^^^^^^^ we have a problem! we can't grab the return code "$?". grrr.
314 _warnings=`cat $bufferfile | grep "^Warning: " | wc -l`
315 _errors=`cat $bufferfile | grep "^Error: " | wc -l`
316 _fatals=`cat $bufferfile | grep "^Fatal: " | wc -l`
318 ret=`grep "\(^Warning: \|^Error: \|^Fatal: \)" $bufferfile`
320 if [ $_fatals != 0 ]; then
321 msg "*failed* -- $file"
322 errormsg="$errormsg\n== fatal errors from $file ==\n\n$ret\n"
323 passthru "Fatal: <<<< finished action $file: FAILED"
324 elif [ $_errors != 0 ]; then
325 msg "*error* -- $file"
326 errormsg="$errormsg\n== errors from $file ==\n\n$ret\n"
327 error "<<<< finished action $file: ERROR"
328 elif [ $_warnings != 0 ]; then
329 msg "*warning* -- $file"
330 errormsg="$errormsg\n== warnings from $file ==\n\n$ret\n"
331 warning "<<<< finished action $file: WARNING"
333 msg "success -- $file"
334 info "<<<< finished action $file: SUCCESS"
337 let "fatals += _fatals"
338 let "errors += _errors"
339 let "warnings += _warnings"
342 #####################################################
346 conffile="@CFGDIR@/backupninja.conf"
349 ## process command line options
351 while [ $# -ge 1 ]; do
354 -d|--debug) debug=1;;
355 -t|--test) test=1;debug=1;;
356 -n|--now) processnow=1;;
361 echo "-f|--conffile option must be followed by an existing filename"
362 fatal "-f|--conffile option must be followed by an existing filename"
365 # we shift here to avoid processing the file path
374 echo "--run option must be fallowed by a backupninja action file"
375 fatal "--run option must be fallowed by a backupninja action file"
382 echo "Unknown option $1"
383 fatal "Unknown option $1"
395 ## Load and confirm basic configuration values
398 if [ ! -r "$conffile" ]; then
399 echo "Configuration file $conffile not found."
400 fatal "Configuration file $conffile not found."
404 libdirectory=`grep '^libdirectory' $conffile | awk '{print $3}'`
405 if [ -z "$libdirectory" ]; then
406 if [ -d "@libdir@" ]; then
407 libdirectory="@libdir@"
409 echo "Could not find entry 'libdirectory' in $conffile."
410 fatal "Could not find entry 'libdirectory' in $conffile."
413 if [ ! -d "$libdirectory" ]; then
414 echo "Lib directory $libdirectory not found."
415 fatal "Lib directory $libdirectory not found."
419 # include shared functions
420 . $libdirectory/tools
421 . $libdirectory/vserver
425 # get global config options (second param is the default)
426 getconf configdirectory @CFGDIR@/backup.d
427 getconf scriptdirectory @datadir@
430 getconf reportsuccess yes
431 getconf reportwarning yes
433 getconf when "Everyday at 01:00"
435 getconf logfile @localstatedir@/log/backupninja.log
436 getconf usecolors "yes"
437 getconf SLAPCAT /usr/sbin/slapcat
438 getconf LDAPSEARCH /usr/bin/ldapsearch
439 getconf RDIFFBACKUP /usr/bin/rdiff-backup
440 getconf MYSQL /usr/bin/mysql
441 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
442 getconf MYSQLDUMP /usr/bin/mysqldump
443 getconf PGSQLDUMP /usr/bin/pg_dump
444 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
445 getconf GZIP /bin/gzip
446 getconf RSYNC /usr/bin/rsync
447 getconf admingroup root
449 # initialize vservers support
450 # (get config variables and check real vservers availability)
451 init_vservers nodialog
453 if [ ! -d "$configdirectory" ]; then
454 echo "Configuration directory '$configdirectory' not found."
455 fatal "Configuration directory '$configdirectory' not found."
458 [ -f "$logfile" ] || touch $logfile
460 if [ "$UID" != "0" ]; then
461 echo "`basename $0` can only be run as root"
465 ## Process each configuration file
467 # by default, don't make files which are world or group readable.
470 # these globals are set by process_action()
477 if [ "$singlerun" ]; then
480 files=`find $configdirectory -follow -mindepth 1 -maxdepth 1 -type f ! -name '.*.swp' | sort -n`
482 if [ -z "$files" ]; then
483 fatal "No backup actions configured in '$configdirectory', run ninjahelper!"
487 for file in $files; do
488 [ -f "$file" ] || continue
490 check_perms ${file%/*} # check containing dir
493 base=`basename $file`
494 if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
495 info "Skipping $file"
499 if [ -e "$scriptdirectory/$suffix" ]; then
500 process_action $file $suffix
502 error "Can't process file '$file': no handler script for suffix '$suffix'"
503 msg "*missing handler* -- $file"
507 ## mail the messages to the report address
509 if [ $actions_run == 0 ]; then doit=0
510 elif [ "$reportemail" == "" ]; then doit=0
511 elif [ $fatals != 0 ]; then doit=1
512 elif [ $errors != 0 ]; then doit=1
513 elif [ "$reportsuccess" == "yes" ]; then doit=1
514 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
518 if [ $doit == 1 ]; then
519 debug "send report to $reportemail"
521 [ $warnings == 0 ] || subject="WARNING"
522 [ $errors == 0 ] || subject="ERROR"
523 [ $fatals == 0 ] || subject="FAILED"
526 for ((i=0; i < ${#messages[@]} ; i++)); do
530 if [ "$reportspace" == "yes" ]; then
532 for i in $(ls "$configdirectory"); do
533 backuploc=$(grep ^directory "$configdirectory"/"$i" | awk '{print $3}')
534 if [ "$backuploc" != "$previous" ]; then
535 mountdev=$(mount | grep "$backuploc" | awk '{print $1}')
537 previous="$backuploc"
541 } | mail -s "backupninja: $hostname $subject" $reportemail
544 if [ $actions_run != 0 ]; then
545 info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."