2a1b76ebae4c1d512a6a3f800a92a7e9573cc326
[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