mirror of
https://0xacab.org/liberate/backupninja.git
synced 2024-11-23 11:13:18 +01:00
995e34e166
and maildir handler for creating safe temp files.
550 lines
13 KiB
Bash
Executable File
550 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
|
# |\_
|
|
# 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
|
|
|
|
function setupcolors() {
|
|
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)
|
|
}
|
|
|
|
function colorize() {
|
|
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
|
|
color=${COLORS[$type]}
|
|
endcolor=$OFF
|
|
echo -e "$color$@$endcolor"
|
|
else
|
|
echo -e "$@"
|
|
fi
|
|
}
|
|
|
|
# We have the following message levels:
|
|
# 0 - debug - blue
|
|
# 1 - normal messages - green
|
|
# 2 - warnings - yellow
|
|
# 3 - errors - orange
|
|
# 4 - fatal - red
|
|
# 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() {
|
|
[ ${#@} -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=""
|
|
else
|
|
types=(Debug Info Warning Error Fatal)
|
|
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
|
|
}
|
|
|
|
function logmsg() {
|
|
if [ -w "$logfile" ]; then
|
|
echo -e `date "+%h %d %H:%M:%S"` "$@" >> $logfile
|
|
fi
|
|
}
|
|
|
|
function passthru() {
|
|
printmsg 100 "$@"
|
|
}
|
|
function debug() {
|
|
printmsg 0 "$@"
|
|
}
|
|
function info() {
|
|
printmsg 1 "$@"
|
|
}
|
|
function warning() {
|
|
printmsg 2 "$@"
|
|
}
|
|
function error() {
|
|
printmsg 3 "$@"
|
|
}
|
|
function fatal() {
|
|
printmsg 4 "$@"
|
|
exit 2
|
|
}
|
|
|
|
msgcount=0
|
|
function msg {
|
|
messages[$msgcount]=$1
|
|
let "msgcount += 1"
|
|
}
|
|
|
|
function setfile() {
|
|
CURRENT_CONF_FILE=$1
|
|
}
|
|
|
|
function setsection() {
|
|
CURRENT_SECTION=$1
|
|
}
|
|
|
|
|
|
#
|
|
# create a temporary file in a secure way.
|
|
#
|
|
function maketemp() {
|
|
if [ -x /bin/mktemp ]
|
|
then
|
|
local tempfile=`mktemp /tmp/$1.XXXXXXXX`
|
|
else
|
|
DATE=`date`
|
|
sectmp=`echo $DATE | /usr/bin/md5sum | cut -d- -f1`
|
|
local tempfile=/tmp/$1.$sectmp
|
|
fi
|
|
echo $tempfile
|
|
}
|
|
|
|
|
|
#
|
|
# sets a global var with name equal to $1
|
|
# to the value of the configuration parameter $1
|
|
# $2 is the default.
|
|
#
|
|
|
|
function getconf() {
|
|
CURRENT_PARAM=$1
|
|
ret=`awk -f $scriptdir/parseini S=$CURRENT_SECTION P=$CURRENT_PARAM $CURRENT_CONF_FILE`
|
|
# if nothing is returned, set the default
|
|
if [ "$ret" == "" -a "$2" != "" ]; then
|
|
ret="$2"
|
|
fi
|
|
|
|
# replace * with %, so that it is not globbed.
|
|
ret="${ret//\\*/__star__}"
|
|
|
|
# this is weird, but single quotes are needed to
|
|
# allow for returned values with spaces. $ret is still expanded
|
|
# because it is in an 'eval' statement.
|
|
eval $1='$ret'
|
|
}
|
|
|
|
#
|
|
# enforces very strict permissions on configuration file $file.
|
|
#
|
|
|
|
function check_perms() {
|
|
local file=$1
|
|
local perms=`ls -ld $file`
|
|
perms=${perms:4:6}
|
|
if [ "$perms" != "------" ]; then
|
|
echo "Configuration files must not be group or world writable/readable! Dying on file $file"
|
|
fatal "Configuration files must not be group or world writable/readable! Dying on file $file"
|
|
fi
|
|
if [ `ls -ld $file | awk '{print $3}'` != "root" ]; 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
|
|
}
|
|
|
|
# simple lowercase function
|
|
function tolower() {
|
|
echo "$1" | tr [:upper:] [:lower:]
|
|
}
|
|
|
|
# simple to integer function
|
|
function toint() {
|
|
echo "$1" | tr [:alpha:] -d
|
|
}
|
|
|
|
#
|
|
# 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.
|
|
nowtime=`date +%H`
|
|
nowday=`date +%d`
|
|
nowdayofweek=`date +%A`
|
|
nowdayofweek=`tolower "$nowdayofweek"`
|
|
|
|
function isnow() {
|
|
local when="$1"
|
|
set -- $when
|
|
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
|
|
}
|
|
|
|
function usage() {
|
|
cat << EOF
|
|
$0 usage:
|
|
This script allows you to coordinate system backup by dropping a few
|
|
simple configuration files into /etc/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 /etc/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.
|
|
--run FILE Execute the specified action file and then exit.
|
|
Also puts backupninja in debug mode.
|
|
|
|
When in debug mode, output to the console will be colored:
|
|
EOF
|
|
debug=1
|
|
debug "Debugging info (when run with -d)"
|
|
info "Informational messages (verbosity level 4)"
|
|
warning "Warnings (verbosity level 3 and up)"
|
|
error "Errors (verbosity level 2 and up)"
|
|
fatal "Fatal, halting errors (always shown)"
|
|
}
|
|
|
|
##
|
|
## this function handles the running of a backup action
|
|
##
|
|
## these globals are modified:
|
|
## fatals, errors, warnings, actions_run, errormsg
|
|
##
|
|
|
|
function process_action() {
|
|
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 it is not $w"
|
|
else
|
|
info ">>>> starting action $file (because it is $w)"
|
|
run="yes"
|
|
fi
|
|
done
|
|
IFS=$' \t\n'
|
|
fi
|
|
debug $run
|
|
[ "$run" == "no" ] && return
|
|
|
|
let "actions_run += 1"
|
|
|
|
# call the handler:
|
|
local bufferfile=`maketemp backupninja.buffer`
|
|
echo "" > $bufferfile
|
|
echo_debug_msg=1
|
|
(
|
|
. $scriptdir/$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
|
|
|
|
_warnings=`cat $bufferfile | grep "^Warning: " | wc -l`
|
|
_errors=`cat $bufferfile | grep "^Error: " | wc -l`
|
|
_fatals=`cat $bufferfile | grep "^Fatal: " | wc -l`
|
|
|
|
ret=`grep "\(^Warning: \|^Error: \|^Fatal: \)" $bufferfile`
|
|
rm $bufferfile
|
|
if [ $_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"
|
|
info "<<<< finished action $file: SUCCESS"
|
|
fi
|
|
|
|
let "fatals += _fatals"
|
|
let "errors += _errors"
|
|
let "warnings += _warnings"
|
|
}
|
|
|
|
#####################################################
|
|
## MAIN
|
|
|
|
setupcolors
|
|
conffile="/etc/backupninja.conf"
|
|
loglevel=3
|
|
|
|
## process command line options
|
|
|
|
while [ $# -ge 1 ]; do
|
|
case $1 in
|
|
-h|--help) usage;;
|
|
-d|--debug) debug=1;;
|
|
-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 fallowed by a backupninja action file"
|
|
fatal "--run option must be fallowed by a backupninja action file"
|
|
usage
|
|
fi
|
|
shift
|
|
;;
|
|
*)
|
|
debug=1
|
|
echo "Unknown option $1"
|
|
fatal "Unknown option $1"
|
|
usage
|
|
exit
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
#if [ $debug ]; then
|
|
# usercolors=yes
|
|
#fi
|
|
|
|
## Load and confirm basic configuration values
|
|
|
|
# bootstrap
|
|
if [ ! -r "$conffile" ]; then
|
|
echo "Configuration file $conffile not found."
|
|
fatal "Configuration file $conffile not found."
|
|
fi
|
|
|
|
scriptdir=`grep scriptdirectory $conffile | awk '{print $3}'`
|
|
if [ ! -n "$scriptdir" ]; then
|
|
echo "Cound not find entry 'scriptdirectory' in $conffile"
|
|
fatal "Cound not find entry 'scriptdirectory' in $conffile"
|
|
fi
|
|
|
|
if [ ! -d "$scriptdir" ]; then
|
|
echo "Script directory $scriptdir not found."
|
|
fatal "Script directory $scriptdir not found."
|
|
fi
|
|
|
|
setfile $conffile
|
|
|
|
# get global config options (second param is the default)
|
|
getconf configdirectory /etc/backup.d
|
|
getconf reportemail
|
|
getconf reportsuccess yes
|
|
getconf reportwarning yes
|
|
getconf loglevel 3
|
|
getconf when "Everyday at 01:00"
|
|
defaultwhen=$when
|
|
getconf logfile /var/log/backupninja.log
|
|
getconf usecolors "yes"
|
|
getconf SLAPCAT /usr/sbin/slapcat
|
|
getconf LDAPSEARCH /usr/bin/ldapsearch
|
|
getconf RDIFFBACKUP /usr/bin/rdiff-backup
|
|
getconf MYSQL /usr/bin/mysql
|
|
getconf MYSQLHOTCOPY /usr/bin/mysqlhotcopy
|
|
getconf MYSQLDUMP /usr/bin/mysqldump
|
|
getconf PGSQLDUMP /usr/bin/pg_dump
|
|
getconf PGSQLDUMPALL /usr/bin/pg_dumpall
|
|
getconf GZIP /bin/gzip
|
|
getconf RSYNC /usr/bin/rsync
|
|
getconf vservers no
|
|
getconf VSERVERINFO /usr/sbin/vserver-info
|
|
getconf VSERVER /usr/sbin/vserver
|
|
getconf VROOTDIR `if [ -f "$VSERVERINFO" ]; then $VSERVERINFO info SYSINFO |grep vserver-Rootdir | awk '{print $2}'; fi`
|
|
|
|
if [ ! -d "$configdirectory" ]; then
|
|
echo "Configuration directory '$configdirectory' not found."
|
|
fatal "Configuration directory '$configdirectory' not found."
|
|
fi
|
|
|
|
[ -f "$logfile" ] || touch $logfile
|
|
|
|
if [ "$UID" != "0" ]; then
|
|
echo "$0 can only be run as root"
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$vservers" == "yes" -a ! -d "$VROOTDIR" ]; then
|
|
echo "vservers option set in config, but $VROOTDIR is not a directory!"
|
|
fatal "vservers option set in config, but $VROOTDIR is not a directory!"
|
|
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()
|
|
fatals=0
|
|
errors=0
|
|
warnings=0
|
|
actions_run=0
|
|
errormsg=""
|
|
|
|
if [ "$singlerun" ]; then
|
|
files=$singlerun
|
|
else
|
|
files=`find $configdirectory -mindepth 1 ! -name '.*.swp' | sort -n`
|
|
fi
|
|
|
|
for file in $files; do
|
|
[ -f "$file" ] || continue
|
|
|
|
check_perms $file
|
|
suffix="${file##*.}"
|
|
base=`basename $file`
|
|
if [ "${base:0:1}" == "0" -o "$suffix" == "disabled" ]; then
|
|
info "Skipping $file"
|
|
continue
|
|
fi
|
|
|
|
if [ -e "$scriptdir/$suffix" ]; then
|
|
process_action $file $suffix
|
|
else
|
|
error "Can't process file '$file': no handler script for suffix '$suffix'"
|
|
msg "*missing handler* -- $file"
|
|
fi
|
|
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
|
|
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"
|
|
} | mail $reportemail -s "backupninja: $hostname $subject"
|
|
fi
|
|
|
|
if [ $actions_run != 0 ]; then
|
|
info "FINISHED: $actions_run actions run. $fatals fatal. $errors error. $warnings warning."
|
|
fi
|