-#!/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