Converted some bashisms to POSIX -- no concept of arrays in posix, bleh
[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 scriptdir=`grep scriptdirectory $conffile | awk '{print $3}'`
428 if [ ! -n "$scriptdir" ]; then
429         echo "Cound not find entry 'scriptdirectory' in $conffile" 
430         fatal "Cound not find entry 'scriptdirectory' in $conffile"
431 fi
432
433 if [ ! -d "$scriptdir" ]; then
434         echo "Script directory $scriptdir not found." 
435         fatal "Script directory $scriptdir not found."
436 fi
437
438 setfile $conffile
439
440 # get global config options (second param is the default)
441 getconf configdirectory @CFGDIR@/backup.d
442 getconf reportemail
443 getconf reportsuccess yes
444 getconf reportwarning yes
445 getconf loglevel 3
446 getconf when "Everyday at 01:00"
447 defaultwhen=$when
448 getconf logfile @localstatedir@/log/backupninja.log
449 getconf usecolors "yes"
450 getconf SLAPCAT /usr/sbin/slapcat
451 getconf LDAPSEARCH /usr/bin/ldapsearch
452 getconf RDIFFBACKUP /usr/bin/rdiff-backup
453 getconf MYSQL /usr/bin/mysql
454 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
455 getconf MYSQLDUMP /usr/bin/mysqldump
456 getconf PGSQLDUMP /usr/bin/pg_dump
457 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
458 getconf GZIP /bin/gzip
459 getconf RSYNC /usr/bin/rsync
460 getconf vservers no
461 getconf VSERVERINFO /usr/sbin/vserver-info
462 getconf VSERVER /usr/sbin/vserver
463 getconf VROOTDIR `if [ -f "$VSERVERINFO" ]; then $VSERVERINFO info SYSINFO |grep vserver-Rootdir | awk '{print $2}'; fi`
464
465 if [ ! -d "$configdirectory" ]; then
466         echo "Configuration directory '$configdirectory' not found."
467         fatal "Configuration directory '$configdirectory' not found."
468 fi
469
470 [ -f "$logfile" ] || touch $logfile
471
472 if [ "$UID" != "0" ]; then
473         echo "$0 can only be run as root"
474         exit 1
475 fi
476
477 if [ "$vservers" == "yes" -a ! -d "$VROOTDIR" ]; then
478         echo "vservers option set in config, but $VROOTDIR is not a directory!"
479         fatal "vservers option set in config, but $VROOTDIR is not a directory!"
480 fi
481
482 ## Process each configuration file
483
484 # by default, don't make files which are world or group readable.
485 umask 077
486
487 # these globals are set by process_action()
488 fatals=0
489 errors=0
490 warnings=0
491 actions_run=0
492 errormsg=""
493
494 if [ "$singlerun" ]; then
495         files=$singlerun
496 else
497         files=`find $configdirectory -mindepth 1 ! -name '.*.swp' | sort -n`
498 fi
499
500 for file in $files; do
501         [ -f "$file" ] || continue
502
503         check_perms $file
504         suffix="${file##*.}"
505         base=`basename $file`
506         if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
507                 info "Skipping $file"
508                 continue
509         fi
510
511         if [ -e "$scriptdir/$suffix" ]; then
512                 process_action $file $suffix
513         else
514                 error "Can't process file '$file': no handler script for suffix '$suffix'"
515                 msg "*missing handler* -- $file"
516         fi
517 done
518
519 ## mail the messages to the report address
520
521 if [ $actions_run == 0 ]; then doit=0
522 elif [ "$reportemail" == "" ]; then doit=0
523 elif [ $fatals != 0 ]; then doit=1
524 elif [ $errors != 0 ]; then doit=1
525 elif [ "$reportsuccess" == "yes" ]; then doit=1
526 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
527 else doit=0
528 fi
529
530 if [ $doit == 1 ]; then
531         debug "send report to $reportemail"
532         hostname=`hostname`
533         [ $warnings == 0 ] || subject="WARNING"
534         [ $errors == 0 ] || subject="ERROR"
535         [ $fatals == 0 ] || subject="FAILED"
536         
537         {
538                 for ((i=0; i < ${#messages[@]} ; i++)); do
539                         echo ${messages[$i]}
540                 done
541                 echo -e "$errormsg"
542         } | mail $reportemail -s "backupninja: $hostname $subject"
543 fi
544
545 if [ $actions_run != 0 ]; then
546         info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."
547 fi