backupninja.in 16.9 KB
Newer Older
micah's avatar
micah committed
1
#!@BASH@
2
# -*- mode: sh; sh-basic-offset: 3; indent-tabs-mode: nil; -*-
3
# vim: set filetype=sh sw=3 sts=3 expandtab autoindent:
4
#
micah's avatar
micah committed
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#                          |\_
# B A C K U P N I N J A   /()/
#                         `\|
#
# Copyright (C) 2004-05 riseup.net -- property is theft.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#

#####################################################
## FUNCTIONS

25
function setupcolors () {
26
27
28
29
30
31
32
33
   BLUE="\033[34;01m"
   GREEN="\033[32;01m"
   YELLOW="\033[33;01m"
   PURPLE="\033[35;01m"
   RED="\033[31;01m"
   OFF="\033[0m"
   CYAN="\033[36;01m"
   COLORS=($BLUE $GREEN $YELLOW $RED $PURPLE $CYAN)
micah's avatar
micah committed
34
35
}

36
function colorize () {
37
38
39
40
41
42
43
44
45
46
47
48
49
50
   if [ "$usecolors" == "yes" ]; then
      local typestr=`echo "$@" | @SED@ 's/\(^[^:]*\).*$/\1/'`
      [ "$typestr" == "Debug" ] && type=0
      [ "$typestr" == "Info" ] && type=1
      [ "$typestr" == "Warning" ] && type=2
      [ "$typestr" == "Error" ] && type=3
      [ "$typestr" == "Fatal" ] && type=4
      [ "$typestr" == "Halt" ] && type=5
      color=${COLORS[$type]}
      endcolor=$OFF
      echo -e "$color$@$endcolor"
   else
      echo -e "$@"
   fi
micah's avatar
micah committed
51
52
53
54
55
56
}

# We have the following message levels:
# 0 - debug - blue
# 1 - normal messages - green
# 2 - warnings - yellow
57
58
# 3 - errors - red
# 4 - fatal - purple
59
# 5 - halt - cyan
micah's avatar
micah committed
60
61
62
63
64
65
66
67
68
# First variable passed is the error level, all others are printed

# if 1, echo out all warnings, errors, or fatal
# used to capture output from handlers
echo_debug_msg=0

usecolors=yes

function printmsg() {
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
   [ ${#@} -gt 1 ] || return

   type=$1
   shift
   if [ $type == 100 ]; then
      typestr=`echo "$@" | @SED@ 's/\(^[^:]*\).*$/\1/'`
      [ "$typestr" == "Debug" ] && type=0
      [ "$typestr" == "Info" ] && type=1
      [ "$typestr" == "Warning" ] && type=2
      [ "$typestr" == "Error" ] && type=3
      [ "$typestr" == "Fatal" ] && type=4
      [ "$typestr" == "Halt" ] && type=5
      typestr=""
   else
      types=(Debug Info Warning Error Fatal Halt)
      typestr="${types[$type]}: "
   fi

   print=$[4-type]

   if [ $echo_debug_msg == 1 ]; then
      echo -e "$typestr$@" >&2
   elif [ $debug ]; then
      colorize "$typestr$@" >&2
   fi

   if [ $print -lt $loglevel ]; then
      logmsg "$typestr$@"
   fi
micah's avatar
micah committed
98
99
100
}

function logmsg() {
101
102
103
   if [ -w "$logfile" ]; then
      echo -e `LC_ALL=C date "+%h %d %H:%M:%S"` "$@" >> $logfile
   fi
micah's avatar
micah committed
104
105
106
}

function passthru() {
107
   printmsg 100 "$@"
micah's avatar
micah committed
108
109
}
function debug() {
110
   printmsg 0 "$@"
micah's avatar
micah committed
111
112
}
function info() {
113
   printmsg 1 "$@"
micah's avatar
micah committed
114
115
}
function warning() {
116
   printmsg 2 "$@"
micah's avatar
micah committed
117
118
}
function error() {
119
   printmsg 3 "$@"
micah's avatar
micah committed
120
121
}
function fatal() {
122
123
   printmsg 4 "$@"
   exit 2
micah's avatar
micah committed
124
}
125
function halt() {
126
127
   printmsg 5 "$@"
   exit 2
128
}
micah's avatar
micah committed
129
130
131

msgcount=0
function msg {
132
133
   messages[$msgcount]=$1
   let "msgcount += 1"
micah's avatar
micah committed
134
135
136
137
138
139
140
}

#
# enforces very strict permissions on configuration file $file.
#

function check_perms() {
141
   local file=$1
142
   debug "check_perms $file"
143
   local perms
144
145
   local owners

146
   perms=($(@STAT@ -L --format='%A' $file))
147
148
149
150
151
152
   debug "perms: $perms"
   local gperm=${perms:4:3}
   debug "gperm: $gperm"
   local wperm=${perms:7:3}
   debug "wperm: $wperm"

153
   owners=($(@STAT@ -L --format='%g %G %u %U' $file))
154
155
156
   local gid=${owners[0]}
   local group=${owners[1]}
   local owner=${owners[2]}
157
158
159
160
161

   if [ "$owner" != 0 ]; then
      echo "Configuration files must be owned by root! Dying on file $file"
      fatal "Configuration files must be owned by root! Dying on file $file"
   fi
162

163
   if [ "$wperm" != '---' ]; then
164
165
166
167
      echo "Configuration files must not be world writable/readable! Dying on file $file"
      fatal "Configuration files must not be world writable/readable! Dying on file $file"
   fi

168
   if [ "$gperm" != '---' ]; then
169
170
171
172
173
      case "$admingroup" in
         $gid|$group) :;;

         *)
           if [ "$gid" != 0 ]; then
174
175
              echo "Configuration files must not be writable/readable by group $group! Use the admingroup option in backupninja.conf. Dying on file $file"
              fatal "Configuration files must not be writable/readable by group $group! Use the admingroup option in backupninja.conf. Dying on file $file"
176
177
178
179
           fi
         ;;
         esac
   fi
micah's avatar
micah committed
180
181
182
183
}

# simple lowercase function
function tolower() {
184
   echo "$1" | tr '[:upper:]' '[:lower:]'
micah's avatar
micah committed
185
186
187
188
}

# simple to integer function
function toint() {
189
   echo "$1" | tr -d '[:alpha:]'
micah's avatar
micah committed
190
191
192
193
194
195
196
197
198
199
200
201
202
203
}

#
# function isnow(): returns 1 if the time/day passed as $1 matches
# the current time/day.
#
# format is <day> at <time>:
#   sunday at 16
#   8th at 01
#   everyday at 22
#

# we grab the current time once, since processing
# all the configs might take more than an hour.
204
205
206
nowtime=`LC_ALL=C date +%H`
nowday=`LC_ALL=C date +%d`
nowdayofweek=`LC_ALL=C date +%A`
micah's avatar
micah committed
207
208
209
nowdayofweek=`tolower "$nowdayofweek"`

function isnow() {
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
   local when="$1"
   set -- $when

   [ "$when" == "manual" ] && return 0

   whendayofweek=$1; at=$2; whentime=$3;
   whenday=`toint "$whendayofweek"`
   whendayofweek=`tolower "$whendayofweek"`
   whentime=`echo "$whentime" | @SED@ 's/:[0-9][0-9]$//' | @SED@ -r 's/^([0-9])$/0\1/'`

   if [ "$whendayofweek" == "everyday" -o "$whendayofweek" == "daily" ]; then
      whendayofweek=$nowdayofweek
   fi

   if [ "$whenday" == "" ]; then
      if [ "$whendayofweek" != "$nowdayofweek" ]; then
         whendayofweek=${whendayofweek%s}
         if [ "$whendayofweek" != "$nowdayofweek" ]; then
            return 0
         fi
      fi
   elif [ "$whenday" != "$nowday" ]; then
      return 0
   fi

   [ "$at" == "at" ] || return 0
   [ "$whentime" == "$nowtime" ] || return 0

   return 1
micah's avatar
micah committed
239
240
241
}

function usage() {
242
   cat << EOF
micah's avatar
micah committed
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
$0 usage:
This script allows you to coordinate system backup by dropping a few
simple configuration files into @CFGDIR@/backup.d/. Typically, this
script is run hourly from cron.

The following options are available:
-h, --help           This usage message
-d, --debug          Run in debug mode, where all log messages are
                     output to the current shell.
-f, --conffile FILE  Use FILE for the main configuration instead
                     of @CFGDIR@/backupninja.conf
-t, --test           Test run mode. This will test if the backup
                     could run, without actually preforming any
                     backups. For example, it will attempt to authenticate
                     or test that ssh keys are set correctly.
-n, --now            Perform actions now, instead of when they might
                     be scheduled. No output will be created unless also
                     run with -d.
261
    --run FILE       Execute the specified action file and then exit.
micah's avatar
micah committed
262
                     Also puts backupninja in debug mode.
263

micah's avatar
micah committed
264
265
When in debug mode, output to the console will be colored:
EOF
266
267
268
269
270
271
272
   usecolors=yes
   colorize "Debug: Debugging info (when run with -d)"
   colorize "Info: Informational messages (verbosity level 4)"
   colorize "Warning: Warnings (verbosity level 3 and up)"
   colorize "Error: Errors (verbosity level 2 and up)"
   colorize "Fatal: Errors which halt a given backup action (always shown)"
   colorize "Halt: Errors which halt the whole backupninja run (always shown)"
micah's avatar
micah committed
273
274
275
276
277
278
}

##
## this function handles the running of a backup action
##
## these globals are modified:
279
## halts, fatals, errors, warnings, actions_run, errormsg
micah's avatar
micah committed
280
281
282
##

function process_action() {
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
   local file="$1"
   local suffix="$2"
   local run="no"
   setfile $file

   # skip over this config if "when" option
   # is not set to the current time.
   getconf when "$defaultwhen"
   if [ "$processnow" == 1 ]; then
      info ">>>> starting action $file (because of --now)"
      run="yes"
   elif [ "$when" == "hourly" ]; then
      info ">>>> starting action $file (because 'when = hourly')"
      run="yes"
   else
      IFS=$'\t\n'
      for w in $when; do
         IFS=$' \t\n'
         isnow "$w"
         ret=$?
         IFS=$'\t\n'
         if [ $ret == 0 ]; then
            debug "skipping $file because current time does not match $w"
         else
            info ">>>> starting action $file (because current time matches $w)"
            run="yes"
         fi
      done
      IFS=$' \t\n'
   fi
   debug $run
   [ "$run" == "no" ] && return

Olivier Berger's avatar
Olivier Berger committed
316
317
318
319
320
321
   # Prepare for lock creation
   if [ ! -d /var/lock/backupninja ]; then
      mkdir /var/lock/backupninja
   fi
   lockfile=`echo $file | @SED@ 's,/,_,g'`
   lockfile=/var/lock/backupninja/$lockfile
322
323
324

   local bufferfile=`maketemp backupninja.buffer`
   echo "" > $bufferfile
Olivier Berger's avatar
Olivier Berger committed
325
326
327

   # start locked section : avoid concurrent execution of the same backup
   # uses a construct specific to shell scripts with flock. See man flock for details
328
   {
Olivier Berger's avatar
Olivier Berger committed
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
       debug "executing handler in locked section controlled by $lockfile"
       flock -x -w 5 200
       # if all is good, we acquired the lock
       if [ $? -eq 0 ]; then

	   let "actions_run += 1"

           # call the handler:
	   echo_debug_msg=1
	   (
	       . $scriptdirectory/$suffix $file
	   ) 2>&1 | (
	       while read a; do
		   echo $a >> $bufferfile
		   [ $debug ] && colorize "$a"
	       done
	   )
	   retcode=$?
           # ^^^^^^^^ we have a problem! we can't grab the return code "$?". grrr.
	   echo_debug_msg=0

       else
	   # a backup is probably ongoing already, so display an error message
352
	   debug "failed to acquire lock $lockfile"
Olivier Berger's avatar
Olivier Berger committed
353
354
	   echo "Fatal: Could not acquire lock $lockfile. A backup is probably already running for $file." >>$bufferfile
       fi
355
   } 200> $lockfile
Olivier Berger's avatar
Olivier Berger committed
356
   # end of locked section
357
358
359
360
361

   _warnings=`cat $bufferfile | grep "^Warning: " | wc -l`
   _errors=`cat $bufferfile | grep "^Error: " | wc -l`
   _fatals=`cat $bufferfile | grep "^Fatal: " | wc -l`
   _halts=`cat $bufferfile | grep "^Halt: " | wc -l`
362
   _infos=`cat $bufferfile | grep "^Info: " | wc -l`
363

364
   ret=`grep "\(^Info: \|^Warning: \|^Error: \|^Fatal: \|Halt: \)" $bufferfile`
365
   rm $bufferfile
Olivier Berger's avatar
Olivier Berger committed
366

367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
   if [ $_halts != 0 ]; then
      msg "*halt* -- $file"
      errormsg="$errormsg\n== halt request from $file==\n\n$ret\n"
      passthru "Halt: <<<< finished action $file: FAILED"
   elif [ $_fatals != 0 ]; then
      msg "*failed* -- $file"
      errormsg="$errormsg\n== fatal errors from $file ==\n\n$ret\n"
      passthru "Fatal: <<<< finished action $file: FAILED"
   elif [ $_errors != 0 ]; then
      msg "*error* -- $file"
      errormsg="$errormsg\n== errors from $file ==\n\n$ret\n"
      error "<<<< finished action $file: ERROR"
   elif [ $_warnings != 0 ]; then
      msg "*warning* -- $file"
      errormsg="$errormsg\n== warnings from $file ==\n\n$ret\n"
      warning "<<<< finished action $file: WARNING"
   else
      msg "success -- $file"
385
386
387
      if [ $_infos != 0 -a "$reportinfo" == "yes" ]; then
         errormsg="$errormsg\n== infos from $file ==\n\n$ret\n"
      fi
388
389
390
391
392
393
394
      info "<<<< finished action $file: SUCCESS"
   fi

   let "halts += _halts"
   let "fatals += _fatals"
   let "errors += _errors"
   let "warnings += _warnings"
micah's avatar
micah committed
395
396
397
398
399
400
401
402
403
404
405
406
}

#####################################################
## MAIN

setupcolors
conffile="@CFGDIR@/backupninja.conf"
loglevel=3

## process command line options

while [ $# -ge 1 ]; do
407
408
   case $1 in
      -h|--help) usage;;
409
      -d|--debug) debug=1; export BACKUPNINJA_DEBUG=yes;;
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
      -t|--test) test=1;debug=1;;
      -n|--now) processnow=1;;
      -f|--conffile)
         if [ -f $2 ]; then
            conffile=$2
         else
            echo "-f|--conffile option must be followed by an existing filename"
            fatal "-f|--conffile option must be followed by an existing filename"
            usage
         fi
         # we shift here to avoid processing the file path
         shift
         ;;
      --run)
         debug=1
         if [ -f $2 ]; then
            singlerun=$2
            processnow=1
         else
            echo "--run option must be followed by a backupninja action file"
            fatal "--run option must be followed by a backupninja action file"
            usage
         fi
         shift
         ;;
      *)
         debug=1
         echo "Unknown option $1"
         fatal "Unknown option $1"
         usage
         exit
         ;;
   esac
   shift
done
micah's avatar
micah committed
445
446

#if [ $debug ]; then
447
#   usercolors=yes
micah's avatar
micah committed
448
449
450
451
452
453
#fi

## Load and confirm basic configuration values

# bootstrap
if [ ! -r "$conffile" ]; then
454
455
   echo "Configuration file $conffile not found."
   fatal "Configuration file $conffile not found."
micah's avatar
micah committed
456
457
fi

458
# find $libdirectory
459
libdirectory=`grep '^libdirectory' $conffile | @AWK@ '{print $3}'`
460
if [ -z "$libdirectory" ]; then
461
462
463
464
465
466
   if [ -d "@libdir@" ]; then
      libdirectory="@libdir@"
   else
      echo "Could not find entry 'libdirectory' in $conffile."
      fatal "Could not find entry 'libdirectory' in $conffile."
   fi
467
else
468
469
470
471
   if [ ! -d "$libdirectory" ]; then
      echo "Lib directory $libdirectory not found."
      fatal "Lib directory $libdirectory not found."
   fi
micah's avatar
micah committed
472
473
fi

474
475
# include shared functions
. $libdirectory/tools
476
. $libdirectory/vserver
477

micah's avatar
micah committed
478
479
480
481
setfile $conffile

# get global config options (second param is the default)
getconf configdirectory @CFGDIR@/backup.d
482
getconf scriptdirectory @datadir@
micah's avatar
micah committed
483
getconf reportdirectory
micah's avatar
micah committed
484
getconf reportemail
micah's avatar
micah committed
485
getconf reporthost
486
getconf reportspace
micah's avatar
micah committed
487
getconf reportsuccess yes
488
getconf reportinfo no
micah's avatar
micah committed
489
getconf reportuser
micah's avatar
micah committed
490
491
492
493
494
495
496
497
498
getconf reportwarning yes
getconf loglevel 3
getconf when "Everyday at 01:00"
defaultwhen=$when
getconf logfile @localstatedir@/log/backupninja.log
getconf usecolors "yes"
getconf SLAPCAT /usr/sbin/slapcat
getconf LDAPSEARCH /usr/bin/ldapsearch
getconf RDIFFBACKUP /usr/bin/rdiff-backup
micah's avatar
micah committed
499
getconf CSTREAM /usr/bin/cstream
500
getconf MYSQLADMIN /usr/bin/mysqladmin
micah's avatar
micah committed
501
502
503
getconf MYSQL /usr/bin/mysql
getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
getconf MYSQLDUMP /usr/bin/mysqldump
Jacob Anawalt's avatar
Jacob Anawalt committed
504
getconf PSQL /usr/bin/psql
micah's avatar
micah committed
505
506
getconf PGSQLDUMP /usr/bin/pg_dump
getconf PGSQLDUMPALL /usr/bin/pg_dumpall
507
getconf PGSQLUSER postgres
micah's avatar
micah committed
508
getconf GZIP /bin/gzip
509
getconf GZIP_OPTS --rsyncable
micah's avatar
micah committed
510
getconf RSYNC /usr/bin/rsync
511
getconf admingroup root
512
513
514
515

# initialize vservers support
# (get config variables and check real vservers availability)
init_vservers nodialog
micah's avatar
micah committed
516
517

if [ ! -d "$configdirectory" ]; then
518
519
   echo "Configuration directory '$configdirectory' not found."
   fatal "Configuration directory '$configdirectory' not found."
micah's avatar
micah committed
520
521
522
523
524
fi

[ -f "$logfile" ] || touch $logfile

if [ "$UID" != "0" ]; then
525
526
   echo "`basename $0` can only be run as root"
   exit 1
micah's avatar
micah committed
527
528
529
530
531
532
533
534
fi

## Process each configuration file

# by default, don't make files which are world or group readable.
umask 077

# these globals are set by process_action()
535
halts=0
micah's avatar
micah committed
536
537
538
539
540
541
542
fatals=0
errors=0
warnings=0
actions_run=0
errormsg=""

if [ "$singlerun" ]; then
543
   files=$singlerun
micah's avatar
micah committed
544
else
545
   files=`find $configdirectory -follow -mindepth 1 -maxdepth 1 -type f ! -name '.*.swp' | sort -n`
546

547
   if [ -z "$files" ]; then
548
      info "No backup actions configured in '$configdirectory', run ninjahelper!"
549
   fi
micah's avatar
micah committed
550
551
552
fi

for file in $files; do
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
   [ -f "$file" ] || continue
   [ "$halts" = "0" ] || continue

   check_perms ${file%/*} # check containing dir
   check_perms $file
   suffix="${file##*.}"
   base=`basename $file`
   if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
      info "Skipping $file"
      continue
   fi

   if [ -e "$scriptdirectory/$suffix" ]; then
      process_action $file $suffix
   else
      error "Can't process file '$file': no handler script for suffix '$suffix'"
      msg "*missing handler* -- $file"
   fi
micah's avatar
micah committed
571
572
573
574
575
576
577
578
579
580
581
582
583
584
done

## mail the messages to the report address

if [ $actions_run == 0 ]; then doit=0
elif [ "$reportemail" == "" ]; then doit=0
elif [ $fatals != 0 ]; then doit=1
elif [ $errors != 0 ]; then doit=1
elif [ "$reportsuccess" == "yes" ]; then doit=1
elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
else doit=0
fi

if [ $doit == 1 ]; then
585
586
587
588
589
590
591
592
593
594
595
596
597
598
   debug "send report to $reportemail"
   hostname=`hostname`
   [ $warnings == 0 ] || subject="WARNING"
   [ $errors == 0 ] || subject="ERROR"
   [ $fatals == 0 ] || subject="FAILED"

   {
      for ((i=0; i < ${#messages[@]} ; i++)); do
          echo ${messages[$i]}
      done
      echo -e "$errormsg"
      if [ "$reportspace" == "yes" ]; then
         previous=""
         for i in $(ls "$configdirectory"); do
intrigeri's avatar
intrigeri committed
599
            backuploc=$(grep ^directory "$configdirectory"/"$i" | @AWK@ '{print $3}')
600
            if [ "$backuploc" != "$previous" -a -n "$backuploc" -a -d "$backuploc" ]; then
intrigeri's avatar
intrigeri committed
601
602
603
               df -h "$backuploc"
               previous="$backuploc"
            fi
604
605
606
         done
      fi
   } | mail -s "backupninja: $hostname $subject" $reportemail
micah's avatar
micah committed
607
608
609
fi

if [ $actions_run != 0 ]; then
610
611
612
613
   info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."
   if [ "$halts" != "0" ]; then
      info "Backup was halted prematurely.  Some actions may not have run."
   fi
micah's avatar
micah committed
614
fi
micah's avatar
micah committed
615
616

if [ -n "$reporthost" ]; then
617
618
   debug "send $logfile to $reportuser@$reporthost:$reportdirectory"
   rsync -qt $logfile $reportuser@$reporthost:$reportdirectory
micah's avatar
micah committed
619
fi