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