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