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@
436 getconf reportdirectory
440 getconf reportsuccess yes
442 getconf reportwarning yes
444 getconf when "Everyday at 01:00"
446 getconf logfile @localstatedir@/log/backupninja.log
447 getconf usecolors "yes"
448 getconf SLAPCAT /usr/sbin/slapcat
449 getconf LDAPSEARCH /usr/bin/ldapsearch
450 getconf RDIFFBACKUP /usr/bin/rdiff-backup
451 getconf CSTREAM=/usr/bin/cstream
452 getconf MYSQLADMIN /usr/bin/mysqladmin
453 getconf MYSQL /usr/bin/mysql
454 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
455 getconf MYSQLDUMP /usr/bin/mysqldump
456 getconf PGSQLDUMP /usr/bin/pg_dump
457 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
458 getconf PGSQLUSER postgres
459 getconf GZIP /bin/gzip
460 getconf RSYNC /usr/bin/rsync
461 getconf admingroup root
463 # initialize vservers support
464 # (get config variables and check real vservers availability)
465 init_vservers nodialog
467 if [ ! -d "$configdirectory" ]; then
468 echo "Configuration directory '$configdirectory' not found."
469 fatal "Configuration directory '$configdirectory' not found."
472 [ -f "$logfile" ] || touch $logfile
474 if [ "$UID" != "0" ]; then
475 echo "`basename $0` can only be run as root"
479 ## Process each configuration file
481 # by default, don't make files which are world or group readable.
484 # these globals are set by process_action()
491 if [ "$singlerun" ]; then
494 files=`find $configdirectory -follow -mindepth 1 -maxdepth 1 -type f ! -name '.*.swp' | sort -n`
496 if [ -z "$files" ]; then
497 fatal "No backup actions configured in '$configdirectory', run ninjahelper!"
501 for file in $files; do
502 [ -f "$file" ] || continue
504 check_perms ${file%/*} # check containing dir
507 base=`basename $file`
508 if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
509 info "Skipping $file"
513 if [ -e "$scriptdirectory/$suffix" ]; then
514 process_action $file $suffix
516 error "Can't process file '$file': no handler script for suffix '$suffix'"
517 msg "*missing handler* -- $file"
521 ## mail the messages to the report address
523 if [ $actions_run == 0 ]; then doit=0
524 elif [ "$reportemail" == "" ]; then doit=0
525 elif [ $fatals != 0 ]; then doit=1
526 elif [ $errors != 0 ]; then doit=1
527 elif [ "$reportsuccess" == "yes" ]; then doit=1
528 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
532 if [ $doit == 1 ]; then
533 debug "send report to $reportemail"
535 [ $warnings == 0 ] || subject="WARNING"
536 [ $errors == 0 ] || subject="ERROR"
537 [ $fatals == 0 ] || subject="FAILED"
540 for ((i=0; i < ${#messages[@]} ; i++)); do
544 if [ "$reportspace" == "yes" ]; then
546 for i in $(ls "$configdirectory"); do
547 backuploc=$(grep ^directory "$configdirectory"/"$i" | @AWK@ '{print $3}')
548 if [ "$backuploc" != "$previous" ]; then
550 previous="$backuploc"
554 } | mail -s "backupninja: $hostname $subject" $reportemail
557 if [ $actions_run != 0 ]; then
558 info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."
561 if [ -n "$reporthost" ]; then
562 debug "send $logfile to $reportuser@$reporthost:$reportdirectory"
563 rsync -qt $logfile $reportuser@$reporthost:$reportdirectory