2
# -*- mode: sh; sh-basic-offset: 3; indent-tabs-mode: nil; -*-
5
# B A C K U P N I N J A /()/
8
# Copyright (C) 2004-05 riseup.net -- property is theft.
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.
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.
21
#####################################################
24
function setupcolors () {
32
COLORS=($BLUE $GREEN $YELLOW $RED $PURPLE)
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]}
45
echo -e "$color$@$endcolor"
51
# We have the following message levels:
53
# 1 - normal messages - green
54
# 2 - warnings - yellow
57
# First variable passed is the error level, all others are printed
59
# if 1, echo out all warnings, errors, or fatal
60
# used to capture output from handlers
66
[ ${#@} -gt 1 ] || return
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
79
types=(Debug Info Warning Error Fatal)
80
typestr="${types[$type]}: "
85
if [ $echo_debug_msg == 1 ]; then
86
echo -e "$typestr$@" >&2
88
colorize "$typestr$@" >&2
91
if [ $print -lt $loglevel ]; then
97
if [ -w "$logfile" ]; then
98
echo -e `date "+%h %d %H:%M:%S"` "$@" >> $logfile
102
function passthru() {
124
messages[$msgcount]=$1
129
# enforces very strict permissions on configuration file $file.
132
function check_perms() {
134
debug "check_perms $file"
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"
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]}
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"
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"
160
if [ "$gperm" != '---' ]; then
161
case "$admingroup" in
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"
174
# simple lowercase function
176
echo "$1" | tr '[:upper:]' '[:lower:]'
179
# simple to integer function
181
echo "$1" | tr -d '[:alpha:]'
185
# function isnow(): returns 1 if the time/day passed as $1 matches
186
# the current time/day.
188
# format is <day> at <time>:
194
# we grab the current time once, since processing
195
# all the configs might take more than an hour.
198
nowdayofweek=`date +%A`
199
nowdayofweek=`tolower "$nowdayofweek"`
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/'`
209
if [ "$whendayofweek" == "everyday" -o "$whendayofweek" == "daily" ]; then
210
whendayofweek=$nowdayofweek
213
if [ "$whenday" == "" ]; then
214
if [ "$whendayofweek" != "$nowdayofweek" ]; then
215
whendayofweek=${whendayofweek%s}
216
if [ "$whendayofweek" != "$nowdayofweek" ]; then
220
elif [ "$whenday" != "$nowday" ]; then
224
[ "$at" == "at" ] || return 0
225
[ "$whentime" == "$nowtime" ] || return 0
233
This script allows you to coordinate system backup by dropping a few
234
simple configuration files into /etc/backup.d/. Typically, this
235
script is run hourly from cron.
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 /etc/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
250
--run FILE Execute the specified action file and then exit.
251
Also puts backupninja in debug mode.
253
When in debug mode, output to the console will be colored:
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)"
264
## this function handles the running of a backup action
266
## these globals are modified:
267
## fatals, errors, warnings, actions_run, errormsg
270
function process_action() {
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)"
282
elif [ "$when" == "hourly" ]; then
283
info ">>>> starting action $file (because 'when = hourly')"
292
if [ $ret == 0 ]; then
293
debug "skipping $file because it is not $w"
295
info ">>>> starting action $file (because it is $w)"
302
[ "$run" == "no" ] && return
304
let "actions_run += 1"
307
local bufferfile=`maketemp backupninja.buffer`
308
echo "" > $bufferfile
311
. $scriptdirectory/$suffix $file
314
echo $a >> $bufferfile
315
[ $debug ] && colorize "$a"
319
# ^^^^^^^^ we have a problem! we can't grab the return code "$?". grrr.
322
_warnings=`cat $bufferfile | grep "^Warning: " | wc -l`
323
_errors=`cat $bufferfile | grep "^Error: " | wc -l`
324
_fatals=`cat $bufferfile | grep "^Fatal: " | wc -l`
326
ret=`grep "\(^Warning: \|^Error: \|^Fatal: \)" $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"
341
msg "success -- $file"
342
info "<<<< finished action $file: SUCCESS"
345
let "fatals += _fatals"
346
let "errors += _errors"
347
let "warnings += _warnings"
350
#####################################################
354
conffile="/etc/backupninja.conf"
357
## process command line options
359
while [ $# -ge 1 ]; do
362
-d|--debug) debug=1;;
363
-t|--test) test=1;debug=1;;
364
-n|--now) processnow=1;;
369
echo "-f|--conffile option must be followed by an existing filename"
370
fatal "-f|--conffile option must be followed by an existing filename"
373
# we shift here to avoid processing the file path
382
echo "--run option must be fallowed by a backupninja action file"
383
fatal "--run option must be fallowed by a backupninja action file"
390
echo "Unknown option $1"
391
fatal "Unknown option $1"
403
## Load and confirm basic configuration values
406
if [ ! -r "$conffile" ]; then
407
echo "Configuration file $conffile not found."
408
fatal "Configuration file $conffile not found."
412
libdirectory=`grep '^libdirectory' $conffile | awk '{print $3}'`
413
if [ -z "$libdirectory" ]; then
414
if [ -d "/usr/lib/backupninja" ]; then
415
libdirectory="/usr/lib/backupninja"
417
echo "Could not find entry 'libdirectory' in $conffile."
418
fatal "Could not find entry 'libdirectory' in $conffile."
421
if [ ! -d "$libdirectory" ]; then
422
echo "Lib directory $libdirectory not found."
423
fatal "Lib directory $libdirectory not found."
427
# include shared functions
428
. $libdirectory/tools
429
. $libdirectory/vserver
433
# get global config options (second param is the default)
434
getconf configdirectory /etc/backup.d
435
getconf scriptdirectory /usr/share/backupninja
438
getconf reportsuccess yes
439
getconf reportwarning yes
441
getconf when "Everyday at 01:00"
443
getconf logfile /var/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 MYSQLADMIN /usr/bin/mysqladmin
449
getconf MYSQL /usr/bin/mysql
450
getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
451
getconf MYSQLDUMP /usr/bin/mysqldump
452
getconf PGSQLDUMP /usr/bin/pg_dump
453
getconf PGSQLDUMPALL /usr/bin/pg_dumpall
454
getconf PGSQLUSER postgres
455
getconf GZIP /bin/gzip
456
getconf RSYNC /usr/bin/rsync
457
getconf admingroup root
459
# initialize vservers support
460
# (get config variables and check real vservers availability)
461
init_vservers nodialog
463
if [ ! -d "$configdirectory" ]; then
464
echo "Configuration directory '$configdirectory' not found."
465
fatal "Configuration directory '$configdirectory' not found."
468
[ -f "$logfile" ] || touch $logfile
470
if [ "$UID" != "0" ]; then
471
echo "`basename $0` can only be run as root"
475
## Process each configuration file
477
# by default, don't make files which are world or group readable.
480
# these globals are set by process_action()
487
if [ "$singlerun" ]; then
490
files=`find $configdirectory -follow -mindepth 1 -maxdepth 1 -type f ! -name '.*.swp' | sort -n`
492
if [ -z "$files" ]; then
493
fatal "No backup actions configured in '$configdirectory', run ninjahelper!"
497
for file in $files; do
498
[ -f "$file" ] || continue
500
check_perms ${file%/*} # check containing dir
503
base=`basename $file`
504
if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
505
info "Skipping $file"
509
if [ -e "$scriptdirectory/$suffix" ]; then
510
process_action $file $suffix
512
error "Can't process file '$file': no handler script for suffix '$suffix'"
513
msg "*missing handler* -- $file"
517
## mail the messages to the report address
519
if [ $actions_run == 0 ]; then doit=0
520
elif [ "$reportemail" == "" ]; then doit=0
521
elif [ $fatals != 0 ]; then doit=1
522
elif [ $errors != 0 ]; then doit=1
523
elif [ "$reportsuccess" == "yes" ]; then doit=1
524
elif [ "$reportwarning" == "yes" -a $warnings != 0 ]; then doit=1
528
if [ $doit == 1 ]; then
529
debug "send report to $reportemail"
531
[ $warnings == 0 ] || subject="WARNING"
532
[ $errors == 0 ] || subject="ERROR"
533
[ $fatals == 0 ] || subject="FAILED"
536
for ((i=0; i < ${#messages[@]} ; i++)); do
540
if [ "$reportspace" == "yes" ]; then
542
for i in $(ls "$configdirectory"); do
543
backuploc=$(grep ^directory "$configdirectory"/"$i" | awk '{print $3}')
544
if [ "$backuploc" != "$previous" ]; then
545
mountdev=$(mount | grep "$backuploc" | awk '{print $1}')
547
previous="$backuploc"
551
} | mail -s "backupninja: $hostname $subject" $reportemail
554
if [ $actions_run != 0 ]; then
555
info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."