diff options
Diffstat (limited to 'src/backupninja.in')
| -rwxr-xr-x | src/backupninja.in | 549 | 
1 files changed, 549 insertions, 0 deletions
| diff --git a/src/backupninja.in b/src/backupninja.in new file mode 100755 index 0000000..55a3d00 --- /dev/null +++ b/src/backupninja.in @@ -0,0 +1,549 @@ +#!@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 @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 +	( +		. $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="@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 + +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 @CFGDIR@/backup.d +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 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 | 
