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