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 --printf='%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 writable/readable by group ${perms[2]}! Dying on file $file"
159 fatal "Configuration files must 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@
429 getconf reportsuccess yes
430 getconf reportwarning yes
432 getconf when "Everyday at 01:00"
434 getconf logfile @localstatedir@/log/backupninja.log
435 getconf usecolors "yes"
436 getconf SLAPCAT /usr/sbin/slapcat
437 getconf LDAPSEARCH /usr/bin/ldapsearch
438 getconf RDIFFBACKUP /usr/bin/rdiff-backup
439 getconf MYSQL /usr/bin/mysql
440 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
441 getconf MYSQLDUMP /usr/bin/mysqldump
442 getconf PGSQLDUMP /usr/bin/pg_dump
443 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
444 getconf GZIP /bin/gzip
445 getconf RSYNC /usr/bin/rsync
446 getconf admingroup root
448 # initialize vservers support
449 # (get config variables and check real vservers availability)
450 init_vservers nodialog
452 if [ ! -d "$configdirectory" ]; then
453 echo "Configuration directory '$configdirectory' not found."
454 fatal "Configuration directory '$configdirectory' not found."
457 [ -f "$logfile" ] || touch $logfile
459 if [ "$UID" != "0" ]; then
460 echo "`basename $0` can only be run as root"
464 ## Process each configuration file
466 # by default, don't make files which are world or group readable.
469 # these globals are set by process_action()
476 if [ "$singlerun" ]; then
479 files=`find -L $configdirectory -mindepth 1 -maxdepth 1 -type f ! -name '.*.swp' | sort -n`
481 if [ -z "$files" ]; then
482 fatal "No backup actions configured in '$configdirectory', run ninjahelper!"
486 for file in $files; do
487 [ -f "$file" ] || continue
489 check_perms ${file%/*} # check containing dir
492 base=`basename $file`
493 if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
494 info "Skipping $file"
498 if [ -e "$scriptdirectory/$suffix" ]; then
499 process_action $file $suffix
501 error "Can't process file '$file': no handler script for suffix '$suffix'"
502 msg "*missing handler* -- $file"
506 ## mail the messages to the report address
508 if [ $actions_run == 0 ]; then doit=0
509 elif [ "$reportemail" == "" ]; then doit=0
510 elif [ $fatals != 0 ]; then doit=1
511 elif [ $errors != 0 ]; then doit=1
512 elif [ "$reportsuccess" == "yes" ]; then doit=1
513 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
517 if [ $doit == 1 ]; then
518 debug "send report to $reportemail"
520 [ $warnings == 0 ] || subject="WARNING"
521 [ $errors == 0 ] || subject="ERROR"
522 [ $fatals == 0 ] || subject="FAILED"
525 for ((i=0; i < ${#messages[@]} ; i++)); do
529 } | mail $reportemail -s "backupninja: $hostname $subject"
532 if [ $actions_run != 0 ]; then
533 info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."