actually include the example file
[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 reportdirectory
437 getconf reportemail
438 getconf reporthost
439 getconf reportspace
440 getconf reportsuccess yes
441 getconf reportuser
442 getconf reportwarning yes
443 getconf loglevel 3
444 getconf when "Everyday at 01:00"
445 defaultwhen=$when
446 getconf logfile @localstatedir@/log/backupninja.log
447 getconf usecolors "yes"
448 getconf SLAPCAT /usr/sbin/slapcat
449 getconf LDAPSEARCH /usr/bin/ldapsearch
450 getconf RDIFFBACKUP /usr/bin/rdiff-backup
451 getconf CSTREAM=/usr/bin/cstream
452 getconf MYSQLADMIN /usr/bin/mysqladmin
453 getconf MYSQL /usr/bin/mysql
454 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
455 getconf MYSQLDUMP /usr/bin/mysqldump
456 getconf PGSQLDUMP /usr/bin/pg_dump
457 getconf PGSQLDUMPALL /usr/bin/pg_dumpall
458 getconf PGSQLUSER postgres
459 getconf GZIP /bin/gzip
460 getconf RSYNC /usr/bin/rsync
461 getconf admingroup root
462
463 # initialize vservers support
464 # (get config variables and check real vservers availability)
465 init_vservers nodialog
466
467 if [ ! -d "$configdirectory" ]; then
468         echo "Configuration directory '$configdirectory' not found."
469         fatal "Configuration directory '$configdirectory' not found."
470 fi
471
472 [ -f "$logfile" ] || touch $logfile
473
474 if [ "$UID" != "0" ]; then
475         echo "`basename $0` can only be run as root"
476         exit 1
477 fi
478
479 ## Process each configuration file
480
481 # by default, don't make files which are world or group readable.
482 umask 077
483
484 # these globals are set by process_action()
485 fatals=0
486 errors=0
487 warnings=0
488 actions_run=0
489 errormsg=""
490
491 if [ "$singlerun" ]; then
492         files=$singlerun
493 else
494         files=`find $configdirectory -follow -mindepth 1 -maxdepth 1 -type f ! -name '.*.swp' | sort -n`
495
496         if [ -z "$files" ]; then
497                 fatal "No backup actions configured in '$configdirectory', run ninjahelper!"
498         fi
499 fi
500
501 for file in $files; do
502         [ -f "$file" ] || continue
503
504         check_perms ${file%/*} # check containing dir
505         check_perms $file
506         suffix="${file##*.}"
507         base=`basename $file`
508         if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
509                 info "Skipping $file"
510                 continue
511         fi
512
513         if [ -e "$scriptdirectory/$suffix" ]; then
514                 process_action $file $suffix
515         else
516                 error "Can't process file '$file': no handler script for suffix '$suffix'"
517                 msg "*missing handler* -- $file"
518         fi
519 done
520
521 ## mail the messages to the report address
522
523 if [ $actions_run == 0 ]; then doit=0
524 elif [ "$reportemail" == "" ]; then doit=0
525 elif [ $fatals != 0 ]; then doit=1
526 elif [ $errors != 0 ]; then doit=1
527 elif [ "$reportsuccess" == "yes" ]; then doit=1
528 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
529 else doit=0
530 fi
531
532 if [ $doit == 1 ]; then
533         debug "send report to $reportemail"
534         hostname=`hostname`
535         [ $warnings == 0 ] || subject="WARNING"
536         [ $errors == 0 ] || subject="ERROR"
537         [ $fatals == 0 ] || subject="FAILED"
538         
539         {
540                 for ((i=0; i < ${#messages[@]} ; i++)); do
541                         echo ${messages[$i]}
542                 done
543                 echo -e "$errormsg"
544                 if [ "$reportspace" == "yes" ]; then
545                         previous=""
546                         for i in $(ls "$configdirectory"); do
547                         backuploc=$(grep ^directory "$configdirectory"/"$i" | @AWK@ '{print $3}')
548                         if [ "$backuploc" != "$previous" ]; then
549                                 df -h "$backuploc"
550                                 previous="$backuploc"
551                         fi
552                         done
553                 fi
554         } | mail -s "backupninja: $hostname $subject" $reportemail
555 fi
556
557 if [ $actions_run != 0 ]; then
558         info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."
559 fi
560
561 if [ -n "$reporthost" ]; then
562         debug "send $logfile to $reportuser@$reporthost:$reportdirectory"
563         rsync -qt $logfile $reportuser@$reporthost:$reportdirectory
564 fi