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