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