aboutsummaryrefslogtreecommitdiff
path: root/src/backupninja.in
diff options
context:
space:
mode:
Diffstat (limited to 'src/backupninja.in')
-rwxr-xr-xsrc/backupninja.in583
1 files changed, 583 insertions, 0 deletions
diff --git a/src/backupninja.in b/src/backupninja.in
new file mode 100755
index 0000000..afb9556
--- /dev/null
+++ b/src/backupninja.in
@@ -0,0 +1,583 @@
+#!@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 $CYAN)
+}
+
+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
+ [ "$typestr" == "Halt" ] && type=5
+ 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
+# 5 - halt - cyan
+# 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" == "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
+}
+
+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
+}
+function halt() {
+ printmsg 5 "$@"
+ 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
+ debug "check_perms $file"
+ local perms
+ local owners
+
+ perms=($(stat -L --format='%A' $file))
+ debug "perms: $perms"
+ local gperm=${perms:4:3}
+ debug "gperm: $gperm"
+ local wperm=${perms:7:3}
+ debug "wperm: $wperm"
+
+ owners=($(stat -L --format='%g %G %u %U' $file))
+ local gid=${owners[0]}
+ local group=${owners[1]}
+ local owner=${owners[2]}
+
+ 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" != '---' ]; 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" != '---' ]; then
+ case "$admingroup" in
+ $gid|$group) :;;
+
+ *)
+ if [ "$gid" != 0 ]; then
+ 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"
+ 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
+ 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)"
+}
+
+##
+## this function handles the running of a backup action
+##
+## these globals are modified:
+## halts, 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`
+ _halts=`cat $bufferfile | grep "^Halt: " | wc -l`
+
+ ret=`grep "\(^Warning: \|^Error: \|^Fatal: \|Halt: \)" $bufferfile`
+ rm $bufferfile
+ 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"
+ info "<<<< finished action $file: SUCCESS"
+ fi
+
+ let "halts += _halts"
+ 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 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
+
+#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 reportdirectory
+getconf reportemail
+getconf reporthost
+getconf reportspace
+getconf reportsuccess yes
+getconf reportuser
+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 CSTREAM /usr/bin/cstream
+getconf MYSQLADMIN /usr/bin/mysqladmin
+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 PGSQLUSER postgres
+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()
+halts=0
+fatals=0
+errors=0
+warnings=0
+actions_run=0
+errormsg=""
+
+if [ "$singlerun" ]; then
+ files=$singlerun
+else
+ files=`find $configdirectory -follow -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
+ [ "$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
+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"
+ if [ "$reportspace" == "yes" ]; then
+ previous=""
+ for i in $(ls "$configdirectory"); do
+ backuploc=$(grep ^directory "$configdirectory"/"$i" | @AWK@ '{print $3}')
+ if [ "$backuploc" != "$previous" -a -n "$backuploc" ]; then
+ df -h "$backuploc"
+ previous="$backuploc"
+ fi
+ done
+ fi
+ } | mail -s "backupninja: $hostname $subject" $reportemail
+fi
+
+if [ $actions_run != 0 ]; then
+ 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
+fi
+
+if [ -n "$reporthost" ]; then
+ debug "send $logfile to $reportuser@$reporthost:$reportdirectory"
+ rsync -qt $logfile $reportuser@$reporthost:$reportdirectory
+fi