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