added scheduling (!) see readme.
[matthijs/upstream/backupninja.git] / backupninja
1 #!/bin/bash
2 #                          |\_
3 # B A C K U P N I N J A   /()/
4 #                         `\|
5 #
6 # Copyright (C) 2004 riseup.net -- property is theft.
7 #
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18
19 #####################################################
20 ## DEFAULTS
21
22 DEBUG=${DEBUG:=0}
23 CONFFILE="/etc/backupninja.conf"
24 USECOLOURS=1
25
26 #####################################################
27 ## FUNCTIONS
28
29 function setupcolors() {
30         if [  "$USECOLOURS" == 1 ]
31         then
32                 BLUE="\033[34;01m"
33                 GREEN="\033[32;01m"
34                 YELLOW="\033[33;01m"
35                 PURPLE="\033[35;01m"
36                 RED="\033[31;01m"
37                 OFF="\033[0m"
38                 CYAN="\033[36;01m"
39         fi
40 }
41
42 function run() {
43         RUNERROR=0
44         debug 0 "$@"
45         returnstring=`$@ 2>&1`
46         RUNERROR=$?
47         RUNERRORS=$[RUNERRORS+RUNERROR]
48         if [ "$RUNERROR" != 0 ]; then
49                 debug 3 "Exitcode $RUNERROR returned when running: $@"
50                 debug 3 "$returnstring"
51         else
52                 debug 0 "$returnstring"
53         fi
54         return $RUNERROR
55 }
56
57 # We have the following debug levels:
58 # 0 - debug - blue
59 # 1 - normal messages - green
60 # 2 - warnings - yellow
61 # 3 - errors - orange
62 # 4 - fatal - red
63 # First variable passed is the error level, all others are printed
64
65 # if 1, echo out all warnings, errors, or fatal
66 # used to capture output from handlers
67 echo_debug_msg=0
68
69 function debug() {
70
71         [ ${#@} -gt 1 ] || return
72          
73         TYPES=(Debug Info Warning Error Fatal)
74         COLOURS=($BLUE $GREEN $YELLOW $RED $PURPLE)
75         type=$1
76         colour=${COLOURS[$type]}
77         shift
78         print=$[4-type]
79         if [ "$print" -lt "$loglevel" -o "$DEBUG" == 1 ]; then
80                 if [ -z "$logfile" ]; then
81                         echo -e "${colour}${TYPES[$type]}: $@${OFF}" >&2
82                 else
83                         if [ "$DEBUG" == 1 -o "$type" == 4 ]; then
84                                 echo -e "${colour}${TYPES[$type]}: $@${OFF}" >&2
85                         fi
86                         echo -e "${colour}${TYPES[$type]}: $@${OFF}" >> $logfile
87                 fi
88         fi
89         if [ "$echo_debug_msg" != "0" -a "$type" -gt "1" ]; then
90                 echo -e "${TYPES[$type]}: $@"
91         fi
92 }
93
94 function fatal() {
95         debug 4 "$@"
96         exit 2
97 }
98
99 msgcount=0
100 function msg {
101         messages[$msgcount]=$1
102         let "msgcount += 1"
103 }
104
105 function setfile() {
106         CURRENT_CONF_FILE=$1
107 }
108
109 function setsection() {
110         CURRENT_SECTION=$1
111 }
112
113 #
114 # sets a global var with name equal to $1
115 # to the value of the configuration parameter $1
116 # $2 is the default.
117
118
119 function getconf() {
120         CURRENT_PARAM=$1
121         ret=`awk -f $scriptdir/parseini S=$CURRENT_SECTION P=$CURRENT_PARAM $CURRENT_CONF_FILE`
122         # if nothing is returned, set the default
123         if [ "$ret" == "" -a "$2" != "" ]; then
124                 ret="$2"
125         fi
126
127         # replace * with %, so that it is not globbed.
128         ret="${ret//\\*/__star__}"
129
130         # this is weird, but single quotes are needed to 
131         # allow for returned values with spaces. $ret is still expanded
132         # because it is in an 'eval' statement.
133         eval $1='$ret'
134 }
135
136 #
137 # enforces very strict permissions on configuration file $file.
138 #
139
140 function check_perms() {
141         local file=$1
142         local perms=`ls -ld $file`
143         perms=${perms:4:6}
144         if [ "$perms" != "------" ]; then
145                 fatal "Configuration files must not be group or world readable! Dying on file $file"
146         fi
147         if [ `ls -ld $file | awk '{print $3}'` != "root" ]; then
148                 fatal "Configuration files must be owned by root! Dying on file $file"
149         fi
150 }
151
152 # simple lowercase function
153 function tolower() {
154         echo "$1" | tr [:upper:] [:lower:]
155 }
156
157 # simple to integer function
158 function toint() {
159         echo "$1" | tr [:alpha:] -d 
160 }
161
162 #
163 # function isnow(): returns 1 if the time/day passed as $1 matches
164 # the current time/day.
165 #
166 # format is <day> at <time>:
167 #   sunday at 16
168 #   8th at 01
169 #   everyday at 22
170 #
171
172 # we grab the current time once, since processing
173 # all the configs might take more than an hour.
174 nowtime=`date +%H`
175 nowday=`date +%d`
176 nowdayofweek=`date +%A`
177 nowdayofweek=`tolower "$nowdayofweek"`
178
179 function isnow() {
180         local when="$1"
181         set -- $when
182         whendayofweek=$1; at=$2; whentime=$3;
183         whenday=`toint "$whendayofweek"`
184         whendayofweek=`tolower "$whendayofweek"`
185         whentime=`echo "$whentime" | sed 's/:[0-9][0-9]$//'`
186
187         if [ "$whendayofweek" == "everyday" ]; then
188                 whendayofweek=$nowdayofweek
189         fi
190
191         if [ "$whenday" == "" ]; then
192                 if [ "$whendayofweek" != "$nowdayofweek" ]; then
193                         whendayofweek=${whendayofweek%s}
194                         if [ "$whendayofweek" != "$nowdayofweek" ]; then
195                                 return 0
196                         fi
197                 fi
198         elif [ "$whenday" != "$nowday" ]; then
199                 return 0
200         fi
201
202         [ "$at" == "at" ] || return 0
203         [ "$whentime" == "$nowtime" ] || return 0
204
205         return 1
206 }
207
208 #####################################################
209 ## MAIN
210
211 ## process command line options
212
213 if [ "$1" == "--help" ]; then
214         HELP=1;DEBUG=1;loglevel=4
215 else
216         while getopts h,f:,d,t option
217         do
218                 case "$option" in
219                         h) HELP=1;DEBUG=1;loglevel=4;;
220                         d) DEBUG=1;loglevel=4;;
221                         f) CONFFILE="$OPTARG";;
222                         t) test=1;DEBUG=1;;
223                 esac
224         done
225 fi
226         
227 setupcolors
228
229 ## Print help
230
231 if [ "$HELP" == 1 ]; then
232 cat << EOF
233 $0 usage:
234 This script allows you to coordinate system backup by dropping a few
235 simple configuration files into /etc/backup.d/. In general, this script
236 is run from a cron job late at night. 
237
238 The following options are available:
239 -h         This help message
240 -d         Run in debug mode, where all log messages are output to the current shell.
241 -f <file>  Use <file> for the main configuration instead of /etc/backupninja.conf
242 -t         Run in test mode, no actions are actually taken.
243
244 When using colored output, there are:
245 EOF
246 debug 0 "Debugging info (when run with -d)"
247 debug 1 "Informational messages (verbosity level 4)"
248 debug 2 "Warnings (verbosity level 3 and up)"
249 debug 3 "Errors (verbosity level 2 and up)"
250 debug 4 "Fatal, halting errors (always shown)"
251 exit 0
252 fi
253
254 ## Load and confirm basic configuration values
255
256 # bootstrap
257 [ -r "$CONFFILE" ] || fatal "Configuration file $CONFFILE not found."
258 scriptdir=`grep scriptdirectory $CONFFILE | awk '{print $3}'`
259 [ -n "$scriptdir" ] || fatal "Cound not find entry 'scriptdirectory' in $CONFFILE."
260 [ -d "$scriptdir" ] || fatal "Script directory $scriptdir not found."
261 setfile $CONFFILE
262
263 # get global config options (second param is the default)
264 getconf configdirectory /etc/backup.d
265 getconf reportemail
266 getconf reportsuccess yes
267 getconf reportwarning yes
268 getconf loglevel 3
269 getconf when "Everyday at 01:00"
270 defaultwhen=$when
271 getconf logfile /var/log/backupninja.log
272 getconf SLAPCAT /usr/sbin/slapcat
273 getconf RDIFFBACKUP /usr/bin/rdiff-backup
274 getconf MYSQL /usr/bin/mysql
275 getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
276 getconf MYSQLDUMP /usr/bin/mysqldump
277 getconf GZIP /bin/gzip
278
279 [ -d "$configdirectory" ] || fatal "Configuration directory '$configdirectory' not found."
280 [ `id -u` == "0" ] || fatal "Can only be run as root"
281
282 ## Process each configuration file
283
284 debug 1 "====== starting at "`date`" ======"
285
286 # by default, don't make files which are world or group readable.
287 umask 077
288
289 errors=0
290
291 for file in $configdirectory/*; do
292         [ -f $file ] || continue;
293
294         check_perms $file
295         suffix="${file##*.}"
296         base=`basename $file`
297         if [ "${base:0:1}" == "0" ]; then
298                 debug 1 "Skipping $file"
299                 continue
300         else
301                 debug 1 "Processing $file"
302         fi
303
304         if [ -e "$scriptdir/$suffix" ]; then
305                 setfile $file
306
307                 # skip over this config if "when" option
308                 # is not set to the current time.
309                 getconf when "$defaultwhen"
310                 IFS=$'\t\n'
311                 for w in $when; do
312                         IFS=$' \t\n'
313                         isnow "$w"
314                         ret=$?
315                         IFS=$'\t\n'
316                         if [ $ret == 0 ]; then
317                                 debug 0 "skipping $file because it is not $w"
318                                 continue
319                         else
320                                 debug 0 "running $file because it is $w"
321                                 continue
322                         fi              
323                 done
324                 IFS=$' \t\n'
325
326                 echo_debug_msg=1
327                 # call the handler:
328                 ret=`( . $scriptdir/$suffix $file )`
329                 retcode="$?"
330                 warnings=`echo $ret | grep -e "^Warning: " | wc -l`
331                 errors=`echo $ret | grep -e "^Error: \|^Fatal: " | wc -l`
332                 if [ $errors != 0 ]; then
333                         msg "*failed* -- $file"
334                         errormsg="$error\n== errors from $file ==\n\n$ret\n"
335                 elif [ $warnings != 0 ]; then
336                         msg "*warning* -- $file"
337                         errormsg="$error\n== warnings from $file ==\n\n$ret\n"
338                 elif [ $retcode == 0 ]; then
339                         msg "success -- $file"
340                 else
341                         msg "unknown -- $file"
342                 fi
343                 echo_debug_msg=0
344         else
345                 debug 3 "Can't process file '$file': no handler script for suffix '$suffix'"
346                 msg "*missing handler* -- $file"
347         fi
348 done
349
350 ## mail the messages to the report address
351
352 if [ "$reportemail" == "" ]; then doit=0
353 elif [ $errors != 0 ]; then doit=1
354 elif [ "$reportsuccess" == "yes" ]; then doit=1
355 elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
356 else doit=0
357 fi
358
359 if [ $doit == 1 ]; then
360         hostname=`hostname`
361         {
362                 for ((i=0; i < ${#messages[@]} ; i++)); do
363                         echo ${messages[$i]}
364                 done
365                 echo -e "$errormsg"
366         } | mail $reportemail -s "backupninja: $hostname"
367 fi
368
369 debug 1 "====== finished at "`date`" ======"
370
371 ############################################################