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