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