e5c55aea505aef9912feafa5cb91ddc95fe42465
[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         local bufferfile="/tmp/backupninja.buffer.$$"
309         echo "" > $bufferfile
310         echo_debug_msg=1
311         (
312                 . $scriptdir/$suffix $file
313         ) 2>&1 | (
314                 while read a; do
315                         echo $a >> $bufferfile
316                         [ $debug ] && colorize "$a"
317                 done
318         )
319         retcode=$?
320         # ^^^^^^^^ we have a problem! we can't grab the return code "$?". grrr.
321         echo_debug_msg=0
322
323         _warnings=`cat $bufferfile | grep "^Warning: " | wc -l`
324         _errors=`cat $bufferfile | grep "^Error: " | wc -l`
325         _fatals=`cat $bufferfile | grep "^Fatal: " | wc -l`
326         
327         ret=`grep "\(^Warning: \|^Error: \|^Fatal: \)" $bufferfile`
328         rm $bufferfile
329         if [ $_fatals != 0 ]; then
330                 msg "*failed* -- $file"
331                 errormsg="$errormsg\n== fatal errors from $file ==\n\n$ret\n"
332                 passthru "Fatal: <<<< finished action $file: FAILED"
333         elif [ $_errors != 0 ]; then
334                 msg "*error* -- $file"
335                 errormsg="$errormsg\n== errors from $file ==\n\n$ret\n"
336                 error "<<<< finished action $file: ERROR"
337         elif [ $_warnings != 0 ]; then
338                 msg "*warning* -- $file"
339                 errormsg="$errormsg\n== warnings from $file ==\n\n$ret\n"
340                 warning "<<<< finished action $file: WARNING"
341         else
342                 msg "success -- $file"
343                 info "<<<< finished action $file: SUCCESS"
344         fi
345
346         let "fatals += _fatals"
347         let "errors += _errors"
348         let "warnings += _warnings"     
349 }
350
351 #####################################################
352 ## MAIN
353
354 setupcolors
355 conffile="/etc/backupninja.conf"
356 loglevel=3
357
358 ## process command line options
359
360 while [ $# -ge 1 ]; do
361         case $1 in
362                 -h|--help) usage;;
363                 -d|--debug) debug=1;;
364                 -t|--test) test=1;debug=1;;
365                 -n|--now) processnow=1;;
366                 -f|--conffile)
367                         if [ -f $2 ]; then
368                                 conffile=$2
369                         else
370                                 echo "-f|--conffile option must be followed by an existing filename"
371                                 fatal "-f|--conffile option must be followed by an existing filename"
372                                 usage
373                         fi
374                         # we shift here to avoid processing the file path 
375                         shift
376                         ;;
377                 --run)
378                         debug=1
379                         if [ -f $2 ]; then
380                                 singlerun=$2
381                                 processnow=1
382                         else
383                                 echo "--run option must be fallowed by a backupninja action file"
384                                 fatal "--run option must be fallowed by a backupninja action file"
385                                 usage
386                         fi
387                         shift
388                         ;;
389                 *)
390                         debug=1
391                         echo "Unknown option $1"
392                         fatal "Unknown option $1"
393                         usage
394                         exit
395                         ;;
396         esac
397         shift
398 done                                                                                                                                                                                                            
399
400 #if [ $debug ]; then
401 #       usercolors=yes
402 #fi
403
404 ## Load and confirm basic configuration values
405
406 # bootstrap
407 if [ ! -r "$conffile" ]; then
408         echo "Configuration file $conffile not found." 
409         fatal "Configuration file $conffile not found."
410 fi
411
412 scriptdir=`grep scriptdirectory $conffile | awk '{print $3}'`
413 if [ ! -n "$scriptdir" ]; then
414         echo "Cound not find entry 'scriptdirectory' in $conffile" 
415         fatal "Cound not find entry 'scriptdirectory' in $conffile"
416 fi
417
418 if [ ! -d "$scriptdir" ]; then
419         echo "Script directory $scriptdir not found." 
420         fatal "Script directory $scriptdir not found."
421 fi
422
423 setfile $conffile
424
425 # get global config options (second param is the default)
426 getconf configdirectory /etc/backup.d
427 getconf reportemail
428 getconf reportsuccess yes
429 getconf reportwarning yes
430 getconf loglevel 3
431 getconf when "Everyday at 01:00"
432 defaultwhen=$when
433 getconf logfile /var/log/backupninja.log
434 getconf usecolors "yes"
435 getconf SLAPCAT /usr/sbin/slapcat
436 getconf LDAPSEARCH /usr/bin/ldapsearch
437 getconf RDIFFBACKUP /usr/bin/rdiff-backup
438 getconf MYSQL /usr/bin/mysql
439 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
440 getconf MYSQLDUMP /usr/bin/mysqldump
441 getconf PGSQLDUMP /usr/bin/pg_dump
442 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
443 getconf GZIP /bin/gzip
444 getconf RSYNC /usr/bin/rsync
445 getconf vservers no
446 getconf VSERVERINFO /usr/sbin/vserver-info
447 getconf VSERVER /usr/sbin/vserver
448 getconf VROOTDIR `if [ -f "$VSERVERINFO" ]; then $VSERVERINFO info SYSINFO |grep vserver-Rootdir | awk '{print $2}'; fi`
449
450 if [ ! -d "$configdirectory" ]; then
451         echo "Configuration directory '$configdirectory' not found."
452         fatal "Configuration directory '$configdirectory' not found."
453 fi
454
455 [ -f "$logfile" ] || touch $logfile
456
457 if [ "$UID" != "0" ]; then
458         echo "$0 can only be run as root"
459         exit 1
460 fi
461
462 if [ "$vservers" == "yes" -a ! -d "$VROOTDIR" ]; then
463         echo "vservers option set in config, but $VROOTDIR is not a directory!"
464         fatal "vservers option set in config, but $VROOTDIR is not a directory!"
465 fi
466
467 ## Process each configuration file
468
469 # by default, don't make files which are world or group readable.
470 umask 077
471
472 # these globals are set by process_action()
473 fatals=0
474 errors=0
475 warnings=0
476 actions_run=0
477 errormsg=""
478
479 if [ "$singlerun" ]; then
480         files=$singlerun
481 else
482         files=`find $configdirectory ! -name '.*.swp' -mindepth 1 | sort -n`
483 fi
484
485 for file in $files; do
486         [ -f "$file" ] || continue
487
488         check_perms $file
489         suffix="${file##*.}"
490         base=`basename $file`
491         if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
492                 info "Skipping $file"
493                 continue
494         fi
495
496         if [ -e "$scriptdir/$suffix" ]; then
497                 process_action $file $suffix
498         else
499                 error "Can't process file '$file': no handler script for suffix '$suffix'"
500                 msg "*missing handler* -- $file"
501         fi
502 done
503
504 ## mail the messages to the report address
505
506 if [ $actions_run == 0 ]; then doit=0
507 elif [ "$reportemail" == "" ]; then doit=0
508 elif [ $fatals != 0 ]; then doit=1
509 elif [ $errors != 0 ]; then doit=1
510 elif [ "$reportsuccess" == "yes" ]; then doit=1
511 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
512 else doit=0
513 fi
514
515 if [ $doit == 1 ]; then
516         debug "send report to $reportemail"
517         hostname=`hostname`
518         [ $warnings == 0 ] || subject="WARNING"
519         [ $errors == 0 ] || subject="ERROR"
520         [ $fatals == 0 ] || subject="FAILED"
521         
522         {
523                 for ((i=0; i < ${#messages[@]} ; i++)); do
524                         echo ${messages[$i]}
525                 done
526                 echo -e "$errormsg"
527         } | mail $reportemail -s "backupninja: $hostname $subject"
528 fi
529
530 if [ $actions_run != 0 ]; then
531         info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."
532 fi