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