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