Recursively ignore subdirs in /etc/backup.d (Closes: #361102)
[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 . $libdirectory/vserver
402
403 setfile $conffile
404
405 # get global config options (second param is the default)
406 getconf configdirectory @CFGDIR@/backup.d
407 getconf scriptdirectory @datadir@
408 getconf reportemail
409 getconf reportsuccess yes
410 getconf reportwarning yes
411 getconf loglevel 3
412 getconf when "Everyday at 01:00"
413 defaultwhen=$when
414 getconf logfile @localstatedir@/log/backupninja.log
415 getconf usecolors "yes"
416 getconf SLAPCAT /usr/sbin/slapcat
417 getconf LDAPSEARCH /usr/bin/ldapsearch
418 getconf RDIFFBACKUP /usr/bin/rdiff-backup
419 getconf MYSQL /usr/bin/mysql
420 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
421 getconf MYSQLDUMP /usr/bin/mysqldump
422 getconf PGSQLDUMP /usr/bin/pg_dump
423 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
424 getconf GZIP /bin/gzip
425 getconf RSYNC /usr/bin/rsync
426
427 # initialize vservers support
428 # (get config variables and check real vservers availability)
429 init_vservers nodialog
430
431 if [ ! -d "$configdirectory" ]; then
432         echo "Configuration directory '$configdirectory' not found."
433         fatal "Configuration directory '$configdirectory' not found."
434 fi
435
436 [ -f "$logfile" ] || touch $logfile
437
438 if [ "$UID" != "0" ]; then
439         echo "`basename $0` can only be run as root"
440         exit 1
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 -maxdepth 1 -type f ! -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