Add a vim modeline with indentation settings.
[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
339    ret=`grep "\(^Warning: \|^Error: \|^Fatal: \|Halt: \)" $bufferfile`
340    rm $bufferfile
341    if [ $_halts != 0 ]; then
342       msg "*halt* -- $file"
343       errormsg="$errormsg\n== halt request from $file==\n\n$ret\n"
344       passthru "Halt: <<<< finished action $file: FAILED"
345    elif [ $_fatals != 0 ]; then
346       msg "*failed* -- $file"
347       errormsg="$errormsg\n== fatal errors from $file ==\n\n$ret\n"
348       passthru "Fatal: <<<< finished action $file: FAILED"
349    elif [ $_errors != 0 ]; then
350       msg "*error* -- $file"
351       errormsg="$errormsg\n== errors from $file ==\n\n$ret\n"
352       error "<<<< finished action $file: ERROR"
353    elif [ $_warnings != 0 ]; then
354       msg "*warning* -- $file"
355       errormsg="$errormsg\n== warnings from $file ==\n\n$ret\n"
356       warning "<<<< finished action $file: WARNING"
357    else
358       msg "success -- $file"
359       info "<<<< finished action $file: SUCCESS"
360    fi
361
362    let "halts += _halts"
363    let "fatals += _fatals"
364    let "errors += _errors"
365    let "warnings += _warnings"
366 }
367
368 #####################################################
369 ## MAIN
370
371 setupcolors
372 conffile="@CFGDIR@/backupninja.conf"
373 loglevel=3
374
375 ## process command line options
376
377 while [ $# -ge 1 ]; do
378    case $1 in
379       -h|--help) usage;;
380       -d|--debug) debug=1;;
381       -t|--test) test=1;debug=1;;
382       -n|--now) processnow=1;;
383       -f|--conffile)
384          if [ -f $2 ]; then
385             conffile=$2
386          else
387             echo "-f|--conffile option must be followed by an existing filename"
388             fatal "-f|--conffile option must be followed by an existing filename"
389             usage
390          fi
391          # we shift here to avoid processing the file path
392          shift
393          ;;
394       --run)
395          debug=1
396          if [ -f $2 ]; then
397             singlerun=$2
398             processnow=1
399          else
400             echo "--run option must be followed by a backupninja action file"
401             fatal "--run option must be followed by a backupninja action file"
402             usage
403          fi
404          shift
405          ;;
406       *)
407          debug=1
408          echo "Unknown option $1"
409          fatal "Unknown option $1"
410          usage
411          exit
412          ;;
413    esac
414    shift
415 done
416
417 #if [ $debug ]; then
418 #   usercolors=yes
419 #fi
420
421 ## Load and confirm basic configuration values
422
423 # bootstrap
424 if [ ! -r "$conffile" ]; then
425    echo "Configuration file $conffile not found."
426    fatal "Configuration file $conffile not found."
427 fi
428
429 # find $libdirectory
430 libdirectory=`grep '^libdirectory' $conffile | @AWK@ '{print $3}'`
431 if [ -z "$libdirectory" ]; then
432    if [ -d "@libdir@" ]; then
433       libdirectory="@libdir@"
434    else
435       echo "Could not find entry 'libdirectory' in $conffile."
436       fatal "Could not find entry 'libdirectory' in $conffile."
437    fi
438 else
439    if [ ! -d "$libdirectory" ]; then
440       echo "Lib directory $libdirectory not found."
441       fatal "Lib directory $libdirectory not found."
442    fi
443 fi
444
445 # include shared functions
446 . $libdirectory/tools
447 . $libdirectory/vserver
448
449 setfile $conffile
450
451 # get global config options (second param is the default)
452 getconf configdirectory @CFGDIR@/backup.d
453 getconf scriptdirectory @datadir@
454 getconf reportdirectory
455 getconf reportemail
456 getconf reporthost
457 getconf reportspace
458 getconf reportsuccess yes
459 getconf reportuser
460 getconf reportwarning yes
461 getconf loglevel 3
462 getconf when "Everyday at 01:00"
463 defaultwhen=$when
464 getconf logfile @localstatedir@/log/backupninja.log
465 getconf usecolors "yes"
466 getconf SLAPCAT /usr/sbin/slapcat
467 getconf LDAPSEARCH /usr/bin/ldapsearch
468 getconf RDIFFBACKUP /usr/bin/rdiff-backup
469 getconf CSTREAM /usr/bin/cstream
470 getconf MYSQLADMIN /usr/bin/mysqladmin
471 getconf MYSQL /usr/bin/mysql
472 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
473 getconf MYSQLDUMP /usr/bin/mysqldump
474 getconf PGSQLDUMP /usr/bin/pg_dump
475 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
476 getconf PGSQLUSER postgres
477 getconf GZIP /bin/gzip
478 getconf RSYNC /usr/bin/rsync
479 getconf admingroup root
480
481 # initialize vservers support
482 # (get config variables and check real vservers availability)
483 init_vservers nodialog
484
485 if [ ! -d "$configdirectory" ]; then
486    echo "Configuration directory '$configdirectory' not found."
487    fatal "Configuration directory '$configdirectory' not found."
488 fi
489
490 [ -f "$logfile" ] || touch $logfile
491
492 if [ "$UID" != "0" ]; then
493    echo "`basename $0` can only be run as root"
494    exit 1
495 fi
496
497 ## Process each configuration file
498
499 # by default, don't make files which are world or group readable.
500 umask 077
501
502 # these globals are set by process_action()
503 halts=0
504 fatals=0
505 errors=0
506 warnings=0
507 actions_run=0
508 errormsg=""
509
510 if [ "$singlerun" ]; then
511    files=$singlerun
512 else
513    files=`find $configdirectory -follow -mindepth 1 -maxdepth 1 -type f ! -name '.*.swp' | sort -n`
514
515    if [ -z "$files" ]; then
516       fatal "No backup actions configured in '$configdirectory', run ninjahelper!"
517    fi
518 fi
519
520 for file in $files; do
521    [ -f "$file" ] || continue
522    [ "$halts" = "0" ] || continue
523
524    check_perms ${file%/*} # check containing dir
525    check_perms $file
526    suffix="${file##*.}"
527    base=`basename $file`
528    if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
529       info "Skipping $file"
530       continue
531    fi
532
533    if [ -e "$scriptdirectory/$suffix" ]; then
534       process_action $file $suffix
535    else
536       error "Can't process file '$file': no handler script for suffix '$suffix'"
537       msg "*missing handler* -- $file"
538    fi
539 done
540
541 ## mail the messages to the report address
542
543 if [ $actions_run == 0 ]; then doit=0
544 elif [ "$reportemail" == "" ]; then doit=0
545 elif [ $fatals != 0 ]; then doit=1
546 elif [ $errors != 0 ]; then doit=1
547 elif [ "$reportsuccess" == "yes" ]; then doit=1
548 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
549 else doit=0
550 fi
551
552 if [ $doit == 1 ]; then
553    debug "send report to $reportemail"
554    hostname=`hostname`
555    [ $warnings == 0 ] || subject="WARNING"
556    [ $errors == 0 ] || subject="ERROR"
557    [ $fatals == 0 ] || subject="FAILED"
558
559    {
560       for ((i=0; i < ${#messages[@]} ; i++)); do
561           echo ${messages[$i]}
562       done
563       echo -e "$errormsg"
564       if [ "$reportspace" == "yes" ]; then
565          previous=""
566          for i in $(ls "$configdirectory"); do
567          backuploc=$(grep ^directory "$configdirectory"/"$i" | @AWK@ '{print $3}')
568          if [ "$backuploc" != "$previous" -a -n "$backuploc" ]; then
569             df -h "$backuploc"
570             previous="$backuploc"
571          fi
572          done
573       fi
574    } | mail -s "backupninja: $hostname $subject" $reportemail
575 fi
576
577 if [ $actions_run != 0 ]; then
578    info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."
579    if [ "$halts" != "0" ]; then
580       info "Backup was halted prematurely.  Some actions may not have run."
581    fi
582 fi
583
584 if [ -n "$reporthost" ]; then
585    debug "send $logfile to $reportuser@$reporthost:$reportdirectory"
586    rsync -qt $logfile $reportuser@$reporthost:$reportdirectory
587 fi