#!@BASH@ # -*- mode: sh; sh-basic-offset: 3; indent-tabs-mode: nil; -*- # # |\_ # 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 - red # 4 - fatal - purple # 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" } # # enforces very strict permissions on configuration file $file. # function check_perms() { local file=$1 local perms perms=($(stat -L --printf='%a %g %G %u %U' $file)) local gperm=${perms[0]:1:1} local wperm=${perms[0]:2:1} local gid=${perms[1]} local group=${perms[2]} local owner=${perms[3]} 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 if [ $wperm -gt 0 ]; then 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 if [ $gperm -gt 0 ]; then case "$admingroup" in $gid|$group) :;; *) if [ "$gid" != 0 ]; then echo "Configuration files must writable/readable by group ${perms[2]}! Dying on file $file" fatal "Configuration files must writable/readable by group ${perms[2]}! Dying on file $file" fi ;; esac fi } # simple lowercase function function tolower() { echo "$1" | tr [:upper:] [:lower:] } # simple to integer function function toint() { echo "$1" | tr -d [:alpha:] } # # 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 @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. --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 ( . $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 _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="@CFGDIR@/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 # find $libdirectory libdirectory=`grep '^libdirectory' $conffile | awk '{print $3}'` if [ -z "$libdirectory" ]; then if [ -d "@libdir@" ]; then libdirectory="@libdir@" else echo "Could not find entry 'libdirectory' in $conffile." fatal "Could not find entry 'libdirectory' in $conffile." fi else if [ ! -d "$libdirectory" ]; then echo "Lib directory $libdirectory not found." fatal "Lib directory $libdirectory not found." fi fi # include shared functions . $libdirectory/tools . $libdirectory/vserver setfile $conffile # get global config options (second param is the default) getconf configdirectory @CFGDIR@/backup.d getconf scriptdirectory @datadir@ getconf reportemail getconf reportsuccess yes 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 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 admingroup root # initialize vservers support # (get config variables and check real vservers availability) init_vservers nodialog 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 "`basename $0` can only be run as root" exit 1 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 -L $configdirectory -mindepth 1 -maxdepth 1 -type f ! -name '.*.swp' | sort -n` if [ -z "$files" ]; then fatal "No backup actions configured in '$configdirectory', run ninjahelper!" fi fi for file in $files; do [ -f "$file" ] || 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 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