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() {
134 debug "check_perms $file"
138 perms=($(stat -L --format='%A' $file))
139 debug "perms: $perms"
140 local gperm=${perms:4:3}
141 debug "gperm: $gperm"
142 local wperm=${perms:7:3}
143 debug "wperm: $wperm"
145 owners=($(stat -L --format='%g %G %u %U' $file))
146 local gid=${owners[0]}
147 local group=${owners[1]}
148 local owner=${owners[2]}
150 if [ "$owner" != 0 ]; then
151 echo "Configuration files must be owned by root! Dying on file $file"
152 fatal "Configuration files must be owned by root! Dying on file $file"
155 if [ "$wperm" != '---' ]; then
156 echo "Configuration files must not be world writable/readable! Dying on file $file"
157 fatal "Configuration files must not be world writable/readable! Dying on file $file"
160 if [ "$gperm" != '---' ]; then
161 case "$admingroup" in
165 if [ "$gid" != 0 ]; then
166 echo "Configuration files must not be writable/readable by group $group! Use the admingroup option in backupninja.conf. Dying on file $file"
167 fatal "Configuration files must not be writable/readable by group $group! Use the admingroup option in backupninja.conf. Dying on file $file"
174 # simple lowercase function
176 echo "$1" | tr [:upper:] [:lower:]
179 # simple to integer function
181 echo "$1" | tr -d [:alpha:]
185 # function isnow(): returns 1 if the time/day passed as $1 matches
186 # the current time/day.
188 # format is <day> at <time>:
194 # we grab the current time once, since processing
195 # all the configs might take more than an hour.
198 nowdayofweek=`date +%A`
199 nowdayofweek=`tolower "$nowdayofweek"`
204 whendayofweek=$1; at=$2; whentime=$3;
205 whenday=`toint "$whendayofweek"`
206 whendayofweek=`tolower "$whendayofweek"`
207 whentime=`echo "$whentime" | sed 's/:[0-9][0-9]$//' | sed -r 's/^([0-9])$/0\1/'`
209 if [ "$whendayofweek" == "everyday" -o "$whendayofweek" == "daily" ]; then
210 whendayofweek=$nowdayofweek
213 if [ "$whenday" == "" ]; then
214 if [ "$whendayofweek" != "$nowdayofweek" ]; then
215 whendayofweek=${whendayofweek%s}
216 if [ "$whendayofweek" != "$nowdayofweek" ]; then
220 elif [ "$whenday" != "$nowday" ]; then
224 [ "$at" == "at" ] || return 0
225 [ "$whentime" == "$nowtime" ] || return 0
233 This script allows you to coordinate system backup by dropping a few
234 simple configuration files into @CFGDIR@/backup.d/. Typically, this
235 script is run hourly from cron.
237 The following options are available:
238 -h, --help This usage message
239 -d, --debug Run in debug mode, where all log messages are
240 output to the current shell.
241 -f, --conffile FILE Use FILE for the main configuration instead
242 of @CFGDIR@/backupninja.conf
243 -t, --test Test run mode. This will test if the backup
244 could run, without actually preforming any
245 backups. For example, it will attempt to authenticate
246 or test that ssh keys are set correctly.
247 -n, --now Perform actions now, instead of when they might
248 be scheduled. No output will be created unless also
250 --run FILE Execute the specified action file and then exit.
251 Also puts backupninja in debug mode.
253 When in debug mode, output to the console will be colored:
256 debug "Debugging info (when run with -d)"
257 info "Informational messages (verbosity level 4)"
258 warning "Warnings (verbosity level 3 and up)"
259 error "Errors (verbosity level 2 and up)"
260 fatal "Fatal, halting errors (always shown)"
264 ## this function handles the running of a backup action
266 ## these globals are modified:
267 ## fatals, errors, warnings, actions_run, errormsg
270 function process_action() {
276 # skip over this config if "when" option
277 # is not set to the current time.
278 getconf when "$defaultwhen"
279 if [ "$processnow" == 1 ]; then
280 info ">>>> starting action $file (because of --now)"
282 elif [ "$when" == "hourly" ]; then
283 info ">>>> starting action $file (because 'when = hourly')"
292 if [ $ret == 0 ]; then
293 debug "skipping $file because it is not $w"
295 info ">>>> starting action $file (because it is $w)"
302 [ "$run" == "no" ] && return
304 let "actions_run += 1"
307 local bufferfile=`maketemp backupninja.buffer`
308 echo "" > $bufferfile
311 . $scriptdirectory/$suffix $file
314 echo $a >> $bufferfile
315 [ $debug ] && colorize "$a"
319 # ^^^^^^^^ we have a problem! we can't grab the return code "$?". grrr.
322 _warnings=`cat $bufferfile | grep "^Warning: " | wc -l`
323 _errors=`cat $bufferfile | grep "^Error: " | wc -l`
324 _fatals=`cat $bufferfile | grep "^Fatal: " | wc -l`
326 ret=`grep "\(^Warning: \|^Error: \|^Fatal: \)" $bufferfile`
328 if [ $_fatals != 0 ]; then
329 msg "*failed* -- $file"
330 errormsg="$errormsg\n== fatal errors from $file ==\n\n$ret\n"
331 passthru "Fatal: <<<< finished action $file: FAILED"
332 elif [ $_errors != 0 ]; then
333 msg "*error* -- $file"
334 errormsg="$errormsg\n== errors from $file ==\n\n$ret\n"
335 error "<<<< finished action $file: ERROR"
336 elif [ $_warnings != 0 ]; then
337 msg "*warning* -- $file"
338 errormsg="$errormsg\n== warnings from $file ==\n\n$ret\n"
339 warning "<<<< finished action $file: WARNING"
341 msg "success -- $file"
342 info "<<<< finished action $file: SUCCESS"
345 let "fatals += _fatals"
346 let "errors += _errors"
347 let "warnings += _warnings"
350 #####################################################
354 conffile="@CFGDIR@/backupninja.conf"
357 ## process command line options
359 while [ $# -ge 1 ]; do
362 -d|--debug) debug=1;;
363 -t|--test) test=1;debug=1;;
364 -n|--now) processnow=1;;
369 echo "-f|--conffile option must be followed by an existing filename"
370 fatal "-f|--conffile option must be followed by an existing filename"
373 # we shift here to avoid processing the file path
382 echo "--run option must be fallowed by a backupninja action file"
383 fatal "--run option must be fallowed by a backupninja action file"
390 echo "Unknown option $1"
391 fatal "Unknown option $1"
403 ## Load and confirm basic configuration values
406 if [ ! -r "$conffile" ]; then
407 echo "Configuration file $conffile not found."
408 fatal "Configuration file $conffile not found."
412 libdirectory=`grep '^libdirectory' $conffile | awk '{print $3}'`
413 if [ -z "$libdirectory" ]; then
414 if [ -d "@libdir@" ]; then
415 libdirectory="@libdir@"
417 echo "Could not find entry 'libdirectory' in $conffile."
418 fatal "Could not find entry 'libdirectory' in $conffile."
421 if [ ! -d "$libdirectory" ]; then
422 echo "Lib directory $libdirectory not found."
423 fatal "Lib directory $libdirectory not found."
427 # include shared functions
428 . $libdirectory/tools
429 . $libdirectory/vserver
433 # get global config options (second param is the default)
434 getconf configdirectory @CFGDIR@/backup.d
435 getconf scriptdirectory @datadir@
438 getconf reportsuccess yes
439 getconf reportwarning yes
441 getconf when "Everyday at 01:00"
443 getconf logfile @localstatedir@/log/backupninja.log
444 getconf usecolors "yes"
445 getconf SLAPCAT /usr/sbin/slapcat
446 getconf LDAPSEARCH /usr/bin/ldapsearch
447 getconf RDIFFBACKUP /usr/bin/rdiff-backup
448 getconf MYSQL /usr/bin/mysql
449 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
450 getconf MYSQLDUMP /usr/bin/mysqldump
451 getconf PGSQLDUMP /usr/bin/pg_dump
452 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
453 getconf PGSQLUSER postgres
454 getconf GZIP /bin/gzip
455 getconf RSYNC /usr/bin/rsync
456 getconf admingroup root
458 # initialize vservers support
459 # (get config variables and check real vservers availability)
460 init_vservers nodialog
462 if [ ! -d "$configdirectory" ]; then
463 echo "Configuration directory '$configdirectory' not found."
464 fatal "Configuration directory '$configdirectory' not found."
467 [ -f "$logfile" ] || touch $logfile
469 if [ "$UID" != "0" ]; then
470 echo "`basename $0` can only be run as root"
474 ## Process each configuration file
476 # by default, don't make files which are world or group readable.
479 # these globals are set by process_action()
486 if [ "$singlerun" ]; then
489 files=`find $configdirectory -follow -mindepth 1 -maxdepth 1 -type f ! -name '.*.swp' | sort -n`
491 if [ -z "$files" ]; then
492 fatal "No backup actions configured in '$configdirectory', run ninjahelper!"
496 for file in $files; do
497 [ -f "$file" ] || continue
499 check_perms ${file%/*} # check containing dir
502 base=`basename $file`
503 if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
504 info "Skipping $file"
508 if [ -e "$scriptdirectory/$suffix" ]; then
509 process_action $file $suffix
511 error "Can't process file '$file': no handler script for suffix '$suffix'"
512 msg "*missing handler* -- $file"
516 ## mail the messages to the report address
518 if [ $actions_run == 0 ]; then doit=0
519 elif [ "$reportemail" == "" ]; then doit=0
520 elif [ $fatals != 0 ]; then doit=1
521 elif [ $errors != 0 ]; then doit=1
522 elif [ "$reportsuccess" == "yes" ]; then doit=1
523 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
527 if [ $doit == 1 ]; then
528 debug "send report to $reportemail"
530 [ $warnings == 0 ] || subject="WARNING"
531 [ $errors == 0 ] || subject="ERROR"
532 [ $fatals == 0 ] || subject="FAILED"
535 for ((i=0; i < ${#messages[@]} ; i++)); do
539 if [ "$reportspace" == "yes" ]; then
541 for i in $(ls "$configdirectory"); do
542 backuploc=$(grep ^directory "$configdirectory"/"$i" | awk '{print $3}')
543 if [ "$backuploc" != "$previous" ]; then
544 mountdev=$(mount | grep "$backuploc" | awk '{print $1}')
546 previous="$backuploc"
550 } | mail -s "backupninja: $hostname $subject" $reportemail
553 if [ $actions_run != 0 ]; then
554 info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."