Make all indentation consistent.
[matthijs/upstream/backupninja.git] / src / backupninja.in
1 #!@BASH@
2 # -*- mode: sh; sh-basic-offset: 3; indent-tabs-mode: nil; -*-
3 #
4 #                          |\_
5 # B A C K U P N I N J A   /()/
6 #                         `\|
7 #
8 # Copyright (C) 2004-05 riseup.net -- property is theft.
9 #
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.
14 #
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.
19 #
20
21 #####################################################
22 ## FUNCTIONS
23
24 function setupcolors () {
25    BLUE="\033[34;01m"
26    GREEN="\033[32;01m"
27    YELLOW="\033[33;01m"
28    PURPLE="\033[35;01m"
29    RED="\033[31;01m"
30    OFF="\033[0m"
31    CYAN="\033[36;01m"
32    COLORS=($BLUE $GREEN $YELLOW $RED $PURPLE $CYAN)
33 }
34
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       [ "$typestr" == "Halt" ] && type=5
44       color=${COLORS[$type]}
45       endcolor=$OFF
46       echo -e "$color$@$endcolor"
47    else
48       echo -e "$@"
49    fi
50 }
51
52 # We have the following message levels:
53 # 0 - debug - blue
54 # 1 - normal messages - green
55 # 2 - warnings - yellow
56 # 3 - errors - red
57 # 4 - fatal - purple
58 # 5 - halt - cyan
59 # First variable passed is the error level, all others are printed
60
61 # if 1, echo out all warnings, errors, or fatal
62 # used to capture output from handlers
63 echo_debug_msg=0
64
65 usecolors=yes
66
67 function printmsg() {
68    [ ${#@} -gt 1 ] || return
69
70    type=$1
71    shift
72    if [ $type == 100 ]; then
73       typestr=`echo "$@" | @SED@ 's/\(^[^:]*\).*$/\1/'`
74       [ "$typestr" == "Debug" ] && type=0
75       [ "$typestr" == "Info" ] && type=1
76       [ "$typestr" == "Warning" ] && type=2
77       [ "$typestr" == "Error" ] && type=3
78       [ "$typestr" == "Fatal" ] && type=4
79       [ "$typestr" == "Halt" ] && type=5
80       typestr=""
81    else
82       types=(Debug Info Warning Error Fatal Halt)
83       typestr="${types[$type]}: "
84    fi
85
86    print=$[4-type]
87
88    if [ $echo_debug_msg == 1 ]; then
89       echo -e "$typestr$@" >&2
90    elif [ $debug ]; then
91       colorize "$typestr$@" >&2
92    fi
93
94    if [ $print -lt $loglevel ]; then
95       logmsg "$typestr$@"
96    fi
97 }
98
99 function logmsg() {
100    if [ -w "$logfile" ]; then
101       echo -e `LC_ALL=C date "+%h %d %H:%M:%S"` "$@" >> $logfile
102    fi
103 }
104
105 function passthru() {
106    printmsg 100 "$@"
107 }
108 function debug() {
109    printmsg 0 "$@"
110 }
111 function info() {
112    printmsg 1 "$@"
113 }
114 function warning() {
115    printmsg 2 "$@"
116 }
117 function error() {
118    printmsg 3 "$@"
119 }
120 function fatal() {
121    printmsg 4 "$@"
122    exit 2
123 }
124 function halt() {
125    printmsg 5 "$@"
126    exit 2
127 }
128
129 msgcount=0
130 function msg {
131    messages[$msgcount]=$1
132    let "msgcount += 1"
133 }
134
135 #
136 # enforces very strict permissions on configuration file $file.
137 #
138
139 function check_perms() {
140    local file=$1
141    debug "check_perms $file"
142    local perms
143    local owners
144
145    perms=($(stat -L --format='%A' $file))
146    debug "perms: $perms"
147    local gperm=${perms:4:3}
148    debug "gperm: $gperm"
149    local wperm=${perms:7:3}
150    debug "wperm: $wperm"
151
152    owners=($(stat -L --format='%g %G %u %U' $file))
153    local gid=${owners[0]}
154    local group=${owners[1]}
155    local owner=${owners[2]}
156
157    if [ "$owner" != 0 ]; then
158       echo "Configuration files must be owned by root! Dying on file $file"
159       fatal "Configuration files must be owned by root! Dying on file $file"
160    fi
161
162    if [ "$wperm" != '---' ]; then
163       echo "Configuration files must not be world writable/readable! Dying on file $file"
164       fatal "Configuration files must not be world writable/readable! Dying on file $file"
165    fi
166
167    if [ "$gperm" != '---' ]; then
168       case "$admingroup" in
169          $gid|$group) :;;
170
171          *)
172            if [ "$gid" != 0 ]; then
173               echo "Configuration files must not be writable/readable by group $group! Use the admingroup option in backupninja.conf. Dying on file $file"
174               fatal "Configuration files must not be writable/readable by group $group! Use the admingroup option in backupninja.conf. Dying on file $file"
175            fi
176          ;;
177          esac
178    fi
179 }
180
181 # simple lowercase function
182 function tolower() {
183    echo "$1" | tr '[:upper:]' '[:lower:]'
184 }
185
186 # simple to integer function
187 function toint() {
188    echo "$1" | tr -d '[:alpha:]'
189 }
190
191 #
192 # function isnow(): returns 1 if the time/day passed as $1 matches
193 # the current time/day.
194 #
195 # format is <day> at <time>:
196 #   sunday at 16
197 #   8th at 01
198 #   everyday at 22
199 #
200
201 # we grab the current time once, since processing
202 # all the configs might take more than an hour.
203 nowtime=`LC_ALL=C date +%H`
204 nowday=`LC_ALL=C date +%d`
205 nowdayofweek=`LC_ALL=C date +%A`
206 nowdayofweek=`tolower "$nowdayofweek"`
207
208 function isnow() {
209    local when="$1"
210    set -- $when
211
212    [ "$when" == "manual" ] && return 0
213
214    whendayofweek=$1; at=$2; whentime=$3;
215    whenday=`toint "$whendayofweek"`
216    whendayofweek=`tolower "$whendayofweek"`
217    whentime=`echo "$whentime" | @SED@ 's/:[0-9][0-9]$//' | @SED@ -r 's/^([0-9])$/0\1/'`
218
219    if [ "$whendayofweek" == "everyday" -o "$whendayofweek" == "daily" ]; then
220       whendayofweek=$nowdayofweek
221    fi
222
223    if [ "$whenday" == "" ]; then
224       if [ "$whendayofweek" != "$nowdayofweek" ]; then
225          whendayofweek=${whendayofweek%s}
226          if [ "$whendayofweek" != "$nowdayofweek" ]; then
227             return 0
228          fi
229       fi
230    elif [ "$whenday" != "$nowday" ]; then
231       return 0
232    fi
233
234    [ "$at" == "at" ] || return 0
235    [ "$whentime" == "$nowtime" ] || return 0
236
237    return 1
238 }
239
240 function usage() {
241    cat << EOF
242 $0 usage:
243 This script allows you to coordinate system backup by dropping a few
244 simple configuration files into @CFGDIR@/backup.d/. Typically, this
245 script is run hourly from cron.
246
247 The following options are available:
248 -h, --help           This usage message
249 -d, --debug          Run in debug mode, where all log messages are
250                      output to the current shell.
251 -f, --conffile FILE  Use FILE for the main configuration instead
252                      of @CFGDIR@/backupninja.conf
253 -t, --test           Test run mode. This will test if the backup
254                      could run, without actually preforming any
255                      backups. For example, it will attempt to authenticate
256                      or test that ssh keys are set correctly.
257 -n, --now            Perform actions now, instead of when they might
258                      be scheduled. No output will be created unless also
259                      run with -d.
260     --run FILE       Execute the specified action file and then exit.
261                      Also puts backupninja in debug mode.
262
263 When in debug mode, output to the console will be colored:
264 EOF
265    usecolors=yes
266    colorize "Debug: Debugging info (when run with -d)"
267    colorize "Info: Informational messages (verbosity level 4)"
268    colorize "Warning: Warnings (verbosity level 3 and up)"
269    colorize "Error: Errors (verbosity level 2 and up)"
270    colorize "Fatal: Errors which halt a given backup action (always shown)"
271    colorize "Halt: Errors which halt the whole backupninja run (always shown)"
272 }
273
274 ##
275 ## this function handles the running of a backup action
276 ##
277 ## these globals are modified:
278 ## halts, fatals, errors, warnings, actions_run, errormsg
279 ##
280
281 function process_action() {
282    local file="$1"
283    local suffix="$2"
284    local run="no"
285    setfile $file
286
287    # skip over this config if "when" option
288    # is not set to the current time.
289    getconf when "$defaultwhen"
290    if [ "$processnow" == 1 ]; then
291       info ">>>> starting action $file (because of --now)"
292       run="yes"
293    elif [ "$when" == "hourly" ]; then
294       info ">>>> starting action $file (because 'when = hourly')"
295       run="yes"
296    else
297       IFS=$'\t\n'
298       for w in $when; do
299          IFS=$' \t\n'
300          isnow "$w"
301          ret=$?
302          IFS=$'\t\n'
303          if [ $ret == 0 ]; then
304             debug "skipping $file because current time does not match $w"
305          else
306             info ">>>> starting action $file (because current time matches $w)"
307             run="yes"
308          fi
309       done
310       IFS=$' \t\n'
311    fi
312    debug $run
313    [ "$run" == "no" ] && return
314
315    let "actions_run += 1"
316
317    # call the handler:
318    local bufferfile=`maketemp backupninja.buffer`
319    echo "" > $bufferfile
320    echo_debug_msg=1
321    (
322       . $scriptdirectory/$suffix $file
323    ) 2>&1 | (
324       while read a; do
325          echo $a >> $bufferfile
326          [ $debug ] && colorize "$a"
327       done
328    )
329    retcode=$?
330    # ^^^^^^^^ we have a problem! we can't grab the return code "$?". grrr.
331    echo_debug_msg=0
332
333    _warnings=`cat $bufferfile | grep "^Warning: " | wc -l`
334    _errors=`cat $bufferfile | grep "^Error: " | wc -l`
335    _fatals=`cat $bufferfile | grep "^Fatal: " | wc -l`
336    _halts=`cat $bufferfile | grep "^Halt: " | wc -l`
337
338    ret=`grep "\(^Warning: \|^Error: \|^Fatal: \|Halt: \)" $bufferfile`
339    rm $bufferfile
340    if [ $_halts != 0 ]; then
341       msg "*halt* -- $file"
342       errormsg="$errormsg\n== halt request from $file==\n\n$ret\n"
343       passthru "Halt: <<<< finished action $file: FAILED"
344    elif [ $_fatals != 0 ]; then
345       msg "*failed* -- $file"
346       errormsg="$errormsg\n== fatal errors from $file ==\n\n$ret\n"
347       passthru "Fatal: <<<< finished action $file: FAILED"
348    elif [ $_errors != 0 ]; then
349       msg "*error* -- $file"
350       errormsg="$errormsg\n== errors from $file ==\n\n$ret\n"
351       error "<<<< finished action $file: ERROR"
352    elif [ $_warnings != 0 ]; then
353       msg "*warning* -- $file"
354       errormsg="$errormsg\n== warnings from $file ==\n\n$ret\n"
355       warning "<<<< finished action $file: WARNING"
356    else
357       msg "success -- $file"
358       info "<<<< finished action $file: SUCCESS"
359    fi
360
361    let "halts += _halts"
362    let "fatals += _fatals"
363    let "errors += _errors"
364    let "warnings += _warnings"
365 }
366
367 #####################################################
368 ## MAIN
369
370 setupcolors
371 conffile="@CFGDIR@/backupninja.conf"
372 loglevel=3
373
374 ## process command line options
375
376 while [ $# -ge 1 ]; do
377    case $1 in
378       -h|--help) usage;;
379       -d|--debug) debug=1;;
380       -t|--test) test=1;debug=1;;
381       -n|--now) processnow=1;;
382       -f|--conffile)
383          if [ -f $2 ]; then
384             conffile=$2
385          else
386             echo "-f|--conffile option must be followed by an existing filename"
387             fatal "-f|--conffile option must be followed by an existing filename"
388             usage
389          fi
390          # we shift here to avoid processing the file path
391          shift
392          ;;
393       --run)
394          debug=1
395          if [ -f $2 ]; then
396             singlerun=$2
397             processnow=1
398          else
399             echo "--run option must be followed by a backupninja action file"
400             fatal "--run option must be followed by a backupninja action file"
401             usage
402          fi
403          shift
404          ;;
405       *)
406          debug=1
407          echo "Unknown option $1"
408          fatal "Unknown option $1"
409          usage
410          exit
411          ;;
412    esac
413    shift
414 done
415
416 #if [ $debug ]; then
417 #   usercolors=yes
418 #fi
419
420 ## Load and confirm basic configuration values
421
422 # bootstrap
423 if [ ! -r "$conffile" ]; then
424    echo "Configuration file $conffile not found."
425    fatal "Configuration file $conffile not found."
426 fi
427
428 # find $libdirectory
429 libdirectory=`grep '^libdirectory' $conffile | @AWK@ '{print $3}'`
430 if [ -z "$libdirectory" ]; then
431    if [ -d "@libdir@" ]; then
432       libdirectory="@libdir@"
433    else
434       echo "Could not find entry 'libdirectory' in $conffile."
435       fatal "Could not find entry 'libdirectory' in $conffile."
436    fi
437 else
438    if [ ! -d "$libdirectory" ]; then
439       echo "Lib directory $libdirectory not found."
440       fatal "Lib directory $libdirectory not found."
441    fi
442 fi
443
444 # include shared functions
445 . $libdirectory/tools
446 . $libdirectory/vserver
447
448 setfile $conffile
449
450 # get global config options (second param is the default)
451 getconf configdirectory @CFGDIR@/backup.d
452 getconf scriptdirectory @datadir@
453 getconf reportdirectory
454 getconf reportemail
455 getconf reporthost
456 getconf reportspace
457 getconf reportsuccess yes
458 getconf reportuser
459 getconf reportwarning yes
460 getconf loglevel 3
461 getconf when "Everyday at 01:00"
462 defaultwhen=$when
463 getconf logfile @localstatedir@/log/backupninja.log
464 getconf usecolors "yes"
465 getconf SLAPCAT /usr/sbin/slapcat
466 getconf LDAPSEARCH /usr/bin/ldapsearch
467 getconf RDIFFBACKUP /usr/bin/rdiff-backup
468 getconf CSTREAM /usr/bin/cstream
469 getconf MYSQLADMIN /usr/bin/mysqladmin
470 getconf MYSQL /usr/bin/mysql
471 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
472 getconf MYSQLDUMP /usr/bin/mysqldump
473 getconf PGSQLDUMP /usr/bin/pg_dump
474 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
475 getconf PGSQLUSER postgres
476 getconf GZIP /bin/gzip
477 getconf RSYNC /usr/bin/rsync
478 getconf admingroup root
479
480 # initialize vservers support
481 # (get config variables and check real vservers availability)
482 init_vservers nodialog
483
484 if [ ! -d "$configdirectory" ]; then
485    echo "Configuration directory '$configdirectory' not found."
486    fatal "Configuration directory '$configdirectory' not found."
487 fi
488
489 [ -f "$logfile" ] || touch $logfile
490
491 if [ "$UID" != "0" ]; then
492    echo "`basename $0` can only be run as root"
493    exit 1
494 fi
495
496 ## Process each configuration file
497
498 # by default, don't make files which are world or group readable.
499 umask 077
500
501 # these globals are set by process_action()
502 halts=0
503 fatals=0
504 errors=0
505 warnings=0
506 actions_run=0
507 errormsg=""
508
509 if [ "$singlerun" ]; then
510    files=$singlerun
511 else
512    files=`find $configdirectory -follow -mindepth 1 -maxdepth 1 -type f ! -name '.*.swp' | sort -n`
513
514    if [ -z "$files" ]; then
515       fatal "No backup actions configured in '$configdirectory', run ninjahelper!"
516    fi
517 fi
518
519 for file in $files; do
520    [ -f "$file" ] || continue
521    [ "$halts" = "0" ] || continue
522
523    check_perms ${file%/*} # check containing dir
524    check_perms $file
525    suffix="${file##*.}"
526    base=`basename $file`
527    if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
528       info "Skipping $file"
529       continue
530    fi
531
532    if [ -e "$scriptdirectory/$suffix" ]; then
533       process_action $file $suffix
534    else
535       error "Can't process file '$file': no handler script for suffix '$suffix'"
536       msg "*missing handler* -- $file"
537    fi
538 done
539
540 ## mail the messages to the report address
541
542 if [ $actions_run == 0 ]; then doit=0
543 elif [ "$reportemail" == "" ]; then doit=0
544 elif [ $fatals != 0 ]; then doit=1
545 elif [ $errors != 0 ]; then doit=1
546 elif [ "$reportsuccess" == "yes" ]; then doit=1
547 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
548 else doit=0
549 fi
550
551 if [ $doit == 1 ]; then
552    debug "send report to $reportemail"
553    hostname=`hostname`
554    [ $warnings == 0 ] || subject="WARNING"
555    [ $errors == 0 ] || subject="ERROR"
556    [ $fatals == 0 ] || subject="FAILED"
557
558    {
559       for ((i=0; i < ${#messages[@]} ; i++)); do
560           echo ${messages[$i]}
561       done
562       echo -e "$errormsg"
563       if [ "$reportspace" == "yes" ]; then
564          previous=""
565          for i in $(ls "$configdirectory"); do
566          backuploc=$(grep ^directory "$configdirectory"/"$i" | @AWK@ '{print $3}')
567          if [ "$backuploc" != "$previous" -a -n "$backuploc" ]; then
568             df -h "$backuploc"
569             previous="$backuploc"
570          fi
571          done
572       fi
573    } | mail -s "backupninja: $hostname $subject" $reportemail
574 fi
575
576 if [ $actions_run != 0 ]; then
577    info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."
578    if [ "$halts" != "0" ]; then
579       info "Backup was halted prematurely.  Some actions may not have run."
580    fi
581 fi
582
583 if [ -n "$reporthost" ]; then
584    debug "send $logfile to $reportuser@$reporthost:$reportdirectory"
585    rsync -qt $logfile $reportuser@$reporthost:$reportdirectory
586 fi