r3559@krups: intrigeri | 2005-11-15 15:07:17 +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 # create a temporary file in a secure way.
135 #
136 function maketemp() {
137         if [ -x /bin/mktemp ]
138         then
139                 local tempfile=`mktemp /tmp/$1.XXXXXXXX`
140         else
141                 DATE=`date`
142                 sectmp=`echo $DATE | /usr/bin/md5sum | cut -d- -f1`
143                 local tempfile=/tmp/$1.$sectmp
144         fi
145         echo $tempfile
146 }
147
148
149 #
150 # sets a global var with name equal to $1
151 # to the value of the configuration parameter $1
152 # $2 is the default.
153
154
155 function getconf() {
156         CURRENT_PARAM=$1
157         ret=`awk -f $scriptdir/parseini S=$CURRENT_SECTION P=$CURRENT_PARAM $CURRENT_CONF_FILE`
158         # if nothing is returned, set the default
159         if [ "$ret" == "" -a "$2" != "" ]; then
160                 ret="$2"
161         fi
162
163         # replace * with %, so that it is not globbed.
164         ret="${ret//\\*/__star__}"
165
166         # this is weird, but single quotes are needed to 
167         # allow for returned values with spaces. $ret is still expanded
168         # because it is in an 'eval' statement.
169         eval $1='$ret'
170 }
171
172 #
173 # enforces very strict permissions on configuration file $file.
174 #
175
176 function check_perms() {
177         local file=$1
178         local perms=`ls -ld $file`
179         perms=${perms:4:6}
180         if [ "$perms" != "------" ]; then
181                 echo "Configuration files must not be group or world writable/readable! Dying on file $file"
182                 fatal "Configuration files must not be group or world writable/readable! Dying on file $file"
183         fi
184         if [ `ls -ld $file | awk '{print $3}'` != "root" ]; then
185                 echo "Configuration files must be owned by root! Dying on file $file"
186                 fatal "Configuration files must be owned by root! Dying on file $file"
187         fi
188 }
189
190 # simple lowercase function
191 function tolower() {
192         echo "$1" | tr [:upper:] [:lower:]
193 }
194
195 # simple to integer function
196 function toint() {
197         echo "$1" | tr [:alpha:] -d 
198 }
199
200 #
201 # function isnow(): returns 1 if the time/day passed as $1 matches
202 # the current time/day.
203 #
204 # format is <day> at <time>:
205 #   sunday at 16
206 #   8th at 01
207 #   everyday at 22
208 #
209
210 # we grab the current time once, since processing
211 # all the configs might take more than an hour.
212 nowtime=`date +%H`
213 nowday=`date +%d`
214 nowdayofweek=`date +%A`
215 nowdayofweek=`tolower "$nowdayofweek"`
216
217 function isnow() {
218         local when="$1"
219         set -- $when
220         whendayofweek=$1; at=$2; whentime=$3;
221         whenday=`toint "$whendayofweek"`
222         whendayofweek=`tolower "$whendayofweek"`
223         whentime=`echo "$whentime" | sed 's/:[0-9][0-9]$//' | sed -r 's/^([0-9])$/0\1/'`
224
225         if [ "$whendayofweek" == "everyday" -o "$whendayofweek" == "daily" ]; then
226                 whendayofweek=$nowdayofweek
227         fi
228
229         if [ "$whenday" == "" ]; then
230                 if [ "$whendayofweek" != "$nowdayofweek" ]; then
231                         whendayofweek=${whendayofweek%s}
232                         if [ "$whendayofweek" != "$nowdayofweek" ]; then
233                                 return 0
234                         fi
235                 fi
236         elif [ "$whenday" != "$nowday" ]; then
237                 return 0
238         fi
239
240         [ "$at" == "at" ] || return 0
241         [ "$whentime" == "$nowtime" ] || return 0
242
243         return 1
244 }
245
246 function usage() {
247         cat << EOF
248 $0 usage:
249 This script allows you to coordinate system backup by dropping a few
250 simple configuration files into @CFGDIR@/backup.d/. Typically, this
251 script is run hourly from cron.
252
253 The following options are available:
254 -h, --help           This usage message
255 -d, --debug          Run in debug mode, where all log messages are
256                      output to the current shell.
257 -f, --conffile FILE  Use FILE for the main configuration instead
258                      of @CFGDIR@/backupninja.conf
259 -t, --test           Test run mode. This will test if the backup
260                      could run, without actually preforming any
261                      backups. For example, it will attempt to authenticate
262                      or test that ssh keys are set correctly.
263 -n, --now            Perform actions now, instead of when they might
264                      be scheduled. No output will be created unless also
265                      run with -d.
266     --run FILE       Execute the specified action file and then exit.    
267                      Also puts backupninja in debug mode.
268                      
269 When in debug mode, output to the console will be colored:
270 EOF
271         debug=1
272         debug   "Debugging info (when run with -d)"
273         info    "Informational messages (verbosity level 4)"
274         warning "Warnings (verbosity level 3 and up)"
275         error   "Errors (verbosity level 2 and up)"
276         fatal   "Fatal, halting errors (always shown)"
277 }
278
279 ##
280 ## this function handles the running of a backup action
281 ##
282 ## these globals are modified:
283 ## fatals, errors, warnings, actions_run, errormsg
284 ##
285
286 function process_action() {
287         local file="$1"
288         local suffix="$2"
289         local run="no"
290         setfile $file
291
292         # skip over this config if "when" option
293         # is not set to the current time.
294         getconf when "$defaultwhen"
295         if [ "$processnow" == 1 ]; then
296                 info ">>>> starting action $file (because of --now)"
297                 run="yes"
298         elif [ "$when" == "hourly" ]; then
299                 info ">>>> starting action $file (because 'when = hourly')"
300                 run="yes"
301         else
302                 IFS=$'\t\n'
303                 for w in $when; do
304                         IFS=$' \t\n'
305                         isnow "$w"
306                         ret=$?
307                         IFS=$'\t\n'
308                         if [ $ret == 0 ]; then
309                                 debug "skipping $file because it is not $w"
310                         else
311                                 info ">>>> starting action $file (because it is $w)"
312                                 run="yes"
313                         fi
314                 done
315                 IFS=$' \t\n'
316         fi
317         debug $run
318         [ "$run" == "no" ] && return
319         
320         let "actions_run += 1"
321
322         # call the handler:
323         local bufferfile=`maketemp backupninja.buffer`
324         echo "" > $bufferfile
325         echo_debug_msg=1
326         (
327                 . $scriptdir/$suffix $file
328         ) 2>&1 | (
329                 while read a; do
330                         echo $a >> $bufferfile
331                         [ $debug ] && colorize "$a"
332                 done
333         )
334         retcode=$?
335         # ^^^^^^^^ we have a problem! we can't grab the return code "$?". grrr.
336         echo_debug_msg=0
337
338         _warnings=`cat $bufferfile | grep "^Warning: " | wc -l`
339         _errors=`cat $bufferfile | grep "^Error: " | wc -l`
340         _fatals=`cat $bufferfile | grep "^Fatal: " | wc -l`
341         
342         ret=`grep "\(^Warning: \|^Error: \|^Fatal: \)" $bufferfile`
343         rm $bufferfile
344         if [ $_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 "fatals += _fatals"
362         let "errors += _errors"
363         let "warnings += _warnings"     
364 }
365
366 #####################################################
367 ## MAIN
368
369 setupcolors
370 conffile="@CFGDIR@/backupninja.conf"
371 loglevel=3
372
373 ## process command line options
374
375 while [ $# -ge 1 ]; do
376         case $1 in
377                 -h|--help) usage;;
378                 -d|--debug) debug=1;;
379                 -t|--test) test=1;debug=1;;
380                 -n|--now) processnow=1;;
381                 -f|--conffile)
382                         if [ -f $2 ]; then
383                                 conffile=$2
384                         else
385                                 echo "-f|--conffile option must be followed by an existing filename"
386                                 fatal "-f|--conffile option must be followed by an existing filename"
387                                 usage
388                         fi
389                         # we shift here to avoid processing the file path 
390                         shift
391                         ;;
392                 --run)
393                         debug=1
394                         if [ -f $2 ]; then
395                                 singlerun=$2
396                                 processnow=1
397                         else
398                                 echo "--run option must be fallowed by a backupninja action file"
399                                 fatal "--run option must be fallowed by a backupninja action file"
400                                 usage
401                         fi
402                         shift
403                         ;;
404                 *)
405                         debug=1
406                         echo "Unknown option $1"
407                         fatal "Unknown option $1"
408                         usage
409                         exit
410                         ;;
411         esac
412         shift
413 done                                                                                                                                                                                                            
414
415 #if [ $debug ]; then
416 #       usercolors=yes
417 #fi
418
419 ## Load and confirm basic configuration values
420
421 # bootstrap
422 if [ ! -r "$conffile" ]; then
423         echo "Configuration file $conffile not found." 
424         fatal "Configuration file $conffile not found."
425 fi
426
427 # find $scriptdir
428 scriptdir=`grep scriptdirectory $conffile | awk '{print $3}'`
429 if [ -z "$scriptdir" ]; then
430         if [ -d "@datadir@" ]; then
431            scriptdir="@datadir@"
432         else
433            echo "Could not find entry 'scriptdirectory' in $conffile" 
434            fatal "Could not find entry 'scriptdirectory' in $conffile" 
435         fi
436 else
437         if [ ! -d "$scriptdir" ]; then
438            echo "Script directory $scriptdir not found."
439            fatal "Script directory $scriptdir not found."
440         fi         
441 fi
442
443 # find $libdir
444 libdir=`grep libdirectory $conffile | awk '{print $3}'`
445 if [ -z "$libdir" ]; then
446         if [ -d "@libdir@" ]; then
447            libdir="@libdir@"
448         else
449            echo "Could not find entry 'libdirectory' in $conffile." 
450            fatal "Could not find entry 'libdirectory' in $conffile." 
451         fi
452 else
453         if [ ! -d "$libdir" ]; then
454            echo "Lib directory $libdir not found." 
455            fatal "Lib directory $libdir not found." 
456         fi
457 fi
458
459 setfile $conffile
460
461 # get global config options (second param is the default)
462 getconf configdirectory @CFGDIR@/backup.d
463 getconf reportemail
464 getconf reportsuccess yes
465 getconf reportwarning yes
466 getconf loglevel 3
467 getconf when "Everyday at 01:00"
468 defaultwhen=$when
469 getconf logfile @localstatedir@/log/backupninja.log
470 getconf usecolors "yes"
471 getconf SLAPCAT /usr/sbin/slapcat
472 getconf LDAPSEARCH /usr/bin/ldapsearch
473 getconf RDIFFBACKUP /usr/bin/rdiff-backup
474 getconf MYSQL /usr/bin/mysql
475 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
476 getconf MYSQLDUMP /usr/bin/mysqldump
477 getconf PGSQLDUMP /usr/bin/pg_dump
478 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
479 getconf GZIP /bin/gzip
480 getconf RSYNC /usr/bin/rsync
481 getconf vservers no
482 getconf VSERVERINFO /usr/sbin/vserver-info
483 getconf VSERVER /usr/sbin/vserver
484 getconf VROOTDIR `if [ -f "$VSERVERINFO" ]; then $VSERVERINFO info SYSINFO |grep vserver-Rootdir | awk '{print $2}'; fi`
485
486 if [ ! -d "$configdirectory" ]; then
487         echo "Configuration directory '$configdirectory' not found."
488         fatal "Configuration directory '$configdirectory' not found."
489 fi
490
491 [ -f "$logfile" ] || touch $logfile
492
493 if [ "$UID" != "0" ]; then
494         echo "$0 can only be run as root"
495         exit 1
496 fi
497
498 if [ "$vservers" == "yes" -a ! -d "$VROOTDIR" ]; then
499         echo "vservers option set in config, but $VROOTDIR is not a directory!"
500         fatal "vservers option set in config, but $VROOTDIR is not a directory!"
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 fatals=0
510 errors=0
511 warnings=0
512 actions_run=0
513 errormsg=""
514
515 if [ "$singlerun" ]; then
516         files=$singlerun
517 else
518         files=`find $configdirectory -mindepth 1 ! -name '.*.swp' | sort -n`
519 fi
520
521 for file in $files; do
522         [ -f "$file" ] || continue
523
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 "$scriptdir/$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         } | mail $reportemail -s "backupninja: $hostname $subject"
564 fi
565
566 if [ $actions_run != 0 ]; then
567         info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."
568 fi