commit d9f75447a64a16ad7a83c725957f7a6535799a3d Author: Emmanuel Benoît Date: Sat Jul 28 16:27:29 2012 +0200 Backup system Imported both the server- and client-side backup scripts. diff --git a/README b/README new file mode 100644 index 0000000..de9ad5d --- /dev/null +++ b/README @@ -0,0 +1,5 @@ +This repo contains various scripts which I figure could be useful to someone. + + backup/ A set of backup scripts + +License: WTFPL (http://sam.zoy.org/wtfpl/) diff --git a/backup/README b/backup/README new file mode 100644 index 0000000..c81839a --- /dev/null +++ b/backup/README @@ -0,0 +1,8 @@ +This set of scripts can be used to implement a rather primitive backup system. + + server/ + The scripts and example configuration files for the backup + server. + + ssh-client/ + Scripts that can be used on remote systems. diff --git a/backup/server/README b/backup/server/README new file mode 100644 index 0000000..ee43374 --- /dev/null +++ b/backup/server/README @@ -0,0 +1,107 @@ +Backup server scripts +====================== + +The scripts in this directory implement a backup "server". While quite +primitive, it supports a rather flexible configuration and can be customised in +various ways (e.g. support for new types of data fetching). + + +Installation +------------- + +1/ Copy the backup script to /usr/local/sbin +2/ Copy the share/ directory to /usr/local/share/backup (omit the + "postprocess" script if you don't need it - see below for more info) +3/ Copy backup.conf and backup.conf.d/ to /etc +4/ Configure the server +5/ Add cron jobs to execute backups (see crontab.example) + + +Configuration +-------------- + +The main configuration file, backup.conf, defines a few variables. You need to +modify some of these values for the system to function properly (e.g. title +for backup reports, main archive storage location, and possibly the log +directory). It also allows you to modify the location of the data fetching and +postprocessing scripts, as well as the location of the rest of the +configuration, should you need / want to do that. + +The main configuration files (backup.conf.d/*.conf) include some documentation. +They are mostly self-explanatory, with the exception of the "fetch modes" thing. + + +Fetch modes +------------ + +A "fetch mode" associates a data acquisition script (share/fetch-*) and a +specific configuration for this script. Fetch modes must be listed in the +backup.conf.d/fetch-modes.conf file. Hosts are then associated to a fetch mode. + +When the backup archives from a host need to be generated, the mode's +configuration will be read from backup.conf.d/fetch/.conf if +it exists (otherwise defaults will be assumed). After that, the backup script +also attempts to load backup.conf.d//.conf +if it exists. + +Two fetch scripts are provided: + + fetch-local Fetch data from a mounted filesystem + BASE Base directory for all "hosts" + Default: / + ROOT Root directory of a host relative to the base + Default: / + + fetch-ssh Fetch data through SSH (see ../ssh-client/) + SSH_HOST Host to connect to + SSH_KEY Private key to use + SSH_PORT Port to connect to + Default: 22 + SSH_USER User to log in as + Default: user running the server script + + +Default postprocessing script +------------------------------ + +The default postprocessing script will encrypt all archives using a fixed key, +and send them to some remote server using FTP. + +To enable the script, simply make sure the "share/postprocess" script is +present and executable. + +The script's configuration includes the server's name or address, the +credentials needed to log in, and the amount of remote rotations; in addition, +a second file contains the encryption key. + + +Customisation - Fetch scripts +------------------------------ + +Fetch scripts must be written in Bash and must define a function named "FETCH". +The function will output the archive's data on its standard output; any error +should be written to the standard error stream. + +When the function is called, all variables loaded from the fetch mode or host +configuration files will be present, as well as the following variables: + + backup_directory The directory to backup, from types.conf + backup_exclude An array of directories to exclude from the + resulting archive, as defined in + exclude.conf + + +Customisation - Postprocessing +------------------------------- + +The postprocessing script is launched by the main script when it starts. It +will be passed the name of a temporary directory in which its data resides. + +The first thing a postprocessing script ought to do is create a file named "pid" +in that directory, writing its ... PID ... into it (thank you, Captain Obvious!) + +The main script will then write a host name and backup type identifier whenever +it finishes fetching an archive. + +Anything written to the standard output or standard error stream will end up in +the main report. diff --git a/backup/server/backup b/backup/server/backup new file mode 100755 index 0000000..1928d18 --- /dev/null +++ b/backup/server/backup @@ -0,0 +1,441 @@ +#!/bin/bash + + +MODE="$1" +[ "x$MODE" = "x" ] && exit 1 + +source /etc/backup.conf +if ! [ -d "$BACKUP_TARGET" ]; then + echo "missing target directory $BACKUP_TARGET" + exit 1 +fi + + +function checkMode +{ + local tocheck="$1" + local mode= + for mode in `grep -v '^#' "${BACKUP_CONFS}/modes.conf" | awk '{ print $1 }'`; do + [ "x$mode" = "x$tocheck" ] && return + done + exit 1 +} + + +function checkType +{ + local tocheck="$1" + local btype= + for btype in `grep -v '^#' "${BACKUP_CONFS}/types.conf" | awk '{ print $1 }'`; do + [ "x$btype" = "x$tocheck" ] && return 0 + done + return 1 +} + + +function getHosts +{ + grep -v '^#' "${BACKUP_CONFS}/hosts.conf" | awk '{ print $1 }' +} + + +function printHostHeader +{ + local host="$1" + local id= + local name= + + grep '^'"$host" "${BACKUP_CONFS}/hosts.conf" | while read id name; do + if [ "x$id" = "x$host" ]; then + echo "$name (identifier: $id)"; + break; + fi + done +} + + +function getBackupTypes +{ + local mode="$1" + local host="$2" + local cmode= + local chost= + local ctypes= + local ctype= + grep -v '^#' "${BACKUP_CONFS}/modes.conf" | while read cmode chost ctypes; do + if [ "x$cmode" != "x$mode" ] && [ "x$cmode" != "x*" ]; then + continue; + fi + + if [ "x$chost" != "x$host" ] && [ "x$chost" != "x*" ]; then + continue; + fi + + echo "$ctypes" | sed -e 's/,/ /g' + done +} + + +function getTypeName +{ + local btype="$1" + local ctype= + local crot= + local cdir= + local cdesc= + + grep '^'"$btype" "${BACKUP_CONFS}/types.conf" | while read ctype crot cdir cdesc; do + if [ "x$ctype" != "x$btype" ]; then + continue; + fi + echo "$cdesc" + break + done +} + + +function getTypeRotation +{ + local btype="$1" + local ctype= + local crot= + local cdir= + local cdesc= + + grep '^'"$btype" "${BACKUP_CONFS}/types.conf" | while read ctype crot cdir cdesc; do + if [ "x$ctype" != "x$btype" ]; then + continue; + fi + echo "$crot" + break + done +} + + +function getTypeDirectory +{ + local btype="$1" + local ctype= + local cdir= + local crot= + local cdesc= + + grep '^'"$btype" "${BACKUP_CONFS}/types.conf" | while read ctype crot cdir cdesc; do + if [ "x$ctype" != "x$btype" ]; then + continue; + fi + echo "$cdir" + break + done +} + + +function getTypeExcludes +{ + local btype="$1" + local host="$2" + local etype= + local ehost= + local edir= + + grep -v '^#' "${BACKUP_CONFS}/exclude.conf" | while read etype ehost edir; do + if [ "x$etype" != "x$btype" ]; then + continue; + fi + + if [ "x$ehost" != "x$host" ] && [ "x$ehost" != "x*" ]; then + continue; + fi + + echo "$edir" + done +} + + +function getFetchMode +{ + local host="$1" + local fhost= + local fmode= + local fparams= + + grep '^'"$host" "${BACKUP_CONFS}/fetch-hosts.conf" | while read fhost fmode fparams; do + if [ "x$fhost" = "x$host" ]; then + echo $fmode; + break; + fi + done +} + + +function getFetchScript +{ + local mode="$1" + local fmode= + local fscript= + local fparams= + + grep '^'"$mode" "${BACKUP_CONFS}/fetch-modes.conf" | while read fmode fscript fparams; do + if [ "x$mode" = "x$fmode" ]; then + echo "$fscript"; + break; + fi + done +} + + +function fetchData +{ + local fetchmode="$1" + local fetchscript="$2" + local host="$3" + local btype="$4" + local fetchconf="$5" + + if ! [ -d "$BACKUP_TARGET/$host" ]; then + mkdir "$BACKUP_TARGET/$host" + fi + + local logfile="`mktemp`" + chmod 600 "$logfile" + + local tempfile="`mktemp`" + chmod 600 "$tempfile" + + ( + if [ -f "${BACKUP_CONFS}/fetch/$fetchmode.conf" ]; then + source "${BACKUP_CONFS}/fetch/$fetchmode.conf" + fi + + if [ -f "${BACKUP_CONFS}/fetch/$fetchmode/$host.conf" ]; then + source "${BACKUP_CONFS}/fetch/$fetchmode/$host.conf" + fi + + source "$fetchconf" + source "${BACKUP_SCRIPTS}/fetch-$fetchscript" + FETCH || exit 1 + ) 2>$logfile | gzip -5 > "$tempfile" + + cat "$logfile" + if grep -q 'ERROR' $logfile; then + echo -e "\t\t\tBackup files will not be rotated" + echo "$host" >>"$ERROR_FILE" + rm -f "$tempfile" + else + if [ $btrot -gt 1 ]; then + echo -e "\t\t\tRotating files ..." + for index in $( seq $btrot -1 2 ); do + local previous=$(( $index - 1 )) + if ! [ -f "$BACKUP_TARGET/$host/$btype-$previous.tar.gz" ]; then + continue; + fi + /bin/mv -f "$BACKUP_TARGET/$host/$btype-$previous.tar.gz" \ + "$BACKUP_TARGET/$host/$btype-$index.tar.gz" + done + fi + /bin/mv -f "$tempfile" "$BACKUP_TARGET/$host/$btype-1.tar.gz" + echo -e "\t\t\tBackup completed" + echo "$host $btype" >&3 + fi + rm -f "$logfile" +} + + +function executeBackupType +{ + local btype="$1" + local host="$2" + + if ! checkType "$btype"; then + echo -e "\tCONFIGURATION ERROR: unknown type '$btype'"; + echo "$host" >>"$ERROR_FILE" + return 1 + fi + + local btname="`getTypeName "$btype"`" + local btrot="`getTypeRotation "$btype"`" + echo -e "\t$btname ($btype)" + + local btdir="`getTypeDirectory "$btype"`" + local btexclude=( `getTypeExcludes "$btype" "$host"` ) + local fetchmode="`getFetchMode "$host"`" + local fetchscript="`getFetchScript "$fetchmode"`" + local index= + + echo -e "\t\tDirectory:\t\t$btdir" + for index in $( seq 0 $(( ${#btexclude[@]} - 1 )) ); do + echo -e "\t\tExcluded directory:\t${btexclude[$index]}" + done + echo -e "\t\tFetching:\t\tmode $fetchmode, script $fetchscript" + + if [ -f "${BACKUP_SCRIPTS}/fetch-$fetchscript" ]; then + echo -e "\t\tStarting backup..." + local fetchconf="`mktemp`" + chmod 600 "$fetchconf" + { + echo "backup_directory=\"$btdir\"" + echo 'backup_exclude=()' + for index in $( seq 0 $(( ${#btexclude[@]} - 1 )) ); do + echo 'backup_exclude['$index']="'"${btexclude[$index]}"'"' + done + } > "$fetchconf" + fetchData "$fetchmode" "$fetchscript" "$host" "$btype" "$fetchconf" + rm -f "$fetchconf" + else + echo -e "\t\tCONFIGURATION ERROR: unknown fetch script '$fetchscript'" + echo + echo "$host" >>"$ERROR_FILE" + return 1 + fi + + echo +} + + +function backupHost +{ + local mode="$1" + local host="$2" + + local types=( `getBackupTypes "$mode" "$host"` ) + if [ ${#types[*]} -eq 0 ]; then + return + fi + + echo "======================================================" + echo + printHostHeader "$host" + echo + + local index= + for index in $( seq 0 $(( ${#types[@]} - 1 )) ); do + local btype="${types[$index]}" + executeBackupType "$btype" "$host" + done + echo + echo +} + + +function backupHosts +{ + local mode="$1" + local hosts=( `getHosts` ) + local index= + + for index in $( seq 0 $((${#hosts[@]} - 1))); + do + local host="${hosts[$index]}" + backupHost "$mode" "$host" + done +} + + +function computeTime +{ + local total=$(( $2 - $1 )) + date -u -d "@$total" +'%d %H %M %S' | sed -e 's/ 0/ /g' | ( + read day hour minutes seconds; + echo $(( $day - 1 ))' day(s), '$hour' hour(s), '$minutes' minute(s) and '$seconds' second(s)' + ) +} + + +function getNextLogFile +{ + local base="$BACKUP_LOG/`date +'%Y-%m-%d'`" + local counter=1 + while [ -f "$base-$counter.log" ]; do + counter=$(( $counter + 1 )) + done + echo "$base-$counter.log" +} + + +checkMode "$MODE" + +if [ -f "${BACKUP_SCRIPTS}/postprocess" ]; then + PP_DIR=`mktemp -d` + chmod 700 $PP_DIR + mkfifo -m 600 "$PP_DIR/pp_fifo" + bash "${BACKUP_SCRIPTS}/postprocess" "$PP_DIR" <"$PP_DIR/pp_fifo" >"$PP_DIR/log" 2>&1 & + exec 3>"$PP_DIR/pp_fifo" +else + exec 3>/dev/null +fi + + +ERROR_FILE="`mktemp`" +chmod 600 "$ERROR_FILE" +LOG_FILE="`mktemp`" +chmod 600 "$LOG_FILE" +START_FULL="`date +'%Y-%m-%d %H:%M:%S'`" +START_TS="`date +'%s'`" + +backupHosts "$MODE" >$LOG_FILE 2>&1 +END_FULL="`date +'%Y-%m-%d %H:%M:%S'`" +END_TS="`date +'%s'`" + +if [ -f "${BACKUP_SCRIPTS}/postprocess" ] && [ -f "$PP_DIR/pid" ]; then + PP_PID=`cat $PP_DIR/pid` + exec 3>&- + while [ -e "/proc/$PP_PID" ]; do + sleep 1 + done + PP_FULL="`date +'%Y-%m-%d %H:%M:%S'`" + PP_TS="`date +'%s'`" +fi + +FINAL_LOG=`getNextLogFile` +exec 7>&1 >$FINAL_LOG +if [ -z "`cat $ERROR_FILE`" ]; then + echo "Successful backup." + TAG="BACKUP" +else + echo "BACKUP FAILURE!" + echo + echo "The following hosts encountered errors:" + for host in `cat "$ERROR_FILE" | sort | uniq`; do + echo -e "\t* $host" + done + TAG="BACKUP-FAILURE" +fi + +if [ -f "${BACKUP_SCRIPTS}/postprocess" ] && [ -f "$PP_DIR/log" ] && grep -q ERROR "$PP_DIR/log"; then + echo "There were errors during post-processing." + if [ "$TAG" = "BACKUP" ]; then + TAG="BACKUP-WARNING" + fi +fi + +spent="`computeTime "$START_TS" "$END_TS"`" +totsize="`du -sh ${BACKUP_TARGET} | awk '{ print $1 }'`" +latestsize="`du -sch ${BACKUP_TARGET}/*/*-1.tar.gz | tail -n 1 | awk '{ print $1 }'`" +freesize="`df -Ph ${BACKUP_TARGET} | tail -n 1 | awk '{ print $4 }'`" +echo "Full log below." +echo +echo "======================================================" +echo "STATISTICS" +echo -e "\tStarted:\t\t$START_FULL" +echo -e "\tEnded:\t\t\t$END_FULL" +if [ -f "${BACKUP_SCRIPTS}/postprocess" ] && [ -f "$PP_DIR/log" ]; then + echo -e "\tBackup time:\t\t$spent" + echo -e "\tPost-processing ended:\t$PP_FULL" + spent="`computeTime "$START_TS" "$PP_TS"`" +fi +echo -e "\tTotal time:\t\t$spent" +echo -e "\tTotal size:\t\t$totsize" +echo -e "\tLatest:\t\t\t$latestsize" +echo -e "\tFree space:\t\t$freesize" +echo +cat "$LOG_FILE" +if [ -f "${BACKUP_SCRIPTS}/postprocess" ] && [ -f "$PP_DIR/log" ]; then + echo + cat "$PP_DIR/log" + rm -rf "$PP_DIR" +fi +exec 1>&7 7>&- + +cat "$FINAL_LOG" | mail -s "[$TAG] - $BACKUP_NAME - `date +'%Y-%m-%d'` - $MODE mode" $BACKUP_EMAIL + +rm -f "$ERROR_FILE" "$LOG_FILE" diff --git a/backup/server/backup.conf b/backup/server/backup.conf new file mode 100644 index 0000000..dd74feb --- /dev/null +++ b/backup/server/backup.conf @@ -0,0 +1,19 @@ +# Main backup server configuration + +# "Title" for the backup, used in e-mail reports +BACKUP_NAME="Change me" + +# Directory that contains the various configuration files +BACKUP_CONFS=/etc/backup.conf.d + +# Directory that contains the various scripts (fetching, postprocessing) +BACKUP_SCRIPTS=/usr/local/share/backup + +# Target directory for the archives +BACKUP_TARGET=/change/this + +# Logs directory +BACKUP_LOG=/var/log/backup + +# E-mail address to send reports to +BACKUP_MAIL=root diff --git a/backup/server/backup.conf.d/exclude.conf b/backup/server/backup.conf.d/exclude.conf new file mode 100644 index 0000000..c3bb555 --- /dev/null +++ b/backup/server/backup.conf.d/exclude.conf @@ -0,0 +1,7 @@ +# Locations to exclude from backups, based on backup types and optionally host +# names. If a location is to be excluded for all hosts, "*" may be used. +# +# Backup type Host name Directory to exclude +full * /var/tmp +full * /var/cache +full * /tmp diff --git a/backup/server/backup.conf.d/fetch-hosts.conf b/backup/server/backup.conf.d/fetch-hosts.conf new file mode 100644 index 0000000..dd3a19e --- /dev/null +++ b/backup/server/backup.conf.d/fetch-hosts.conf @@ -0,0 +1,7 @@ +# For each host to backup, define the fetch mode to use. Fetch modes are +# defined in fetch-modes.conf +# +# Host Mode +backup-server self +ve-host ve +remote-system remote diff --git a/backup/server/backup.conf.d/fetch-modes.conf b/backup/server/backup.conf.d/fetch-modes.conf new file mode 100644 index 0000000..c0b032b --- /dev/null +++ b/backup/server/backup.conf.d/fetch-modes.conf @@ -0,0 +1,7 @@ +# List each "fetch mode", associating an (arbitrary) name with the name of the +# script to use. +# +# Mode Fetch script +self local +ve local +remote ssh diff --git a/backup/server/backup.conf.d/fetch/remote.conf b/backup/server/backup.conf.d/fetch/remote.conf new file mode 100644 index 0000000..8c52e9c --- /dev/null +++ b/backup/server/backup.conf.d/fetch/remote.conf @@ -0,0 +1,12 @@ +# Configuration for remote fetches + +# SSH port to connect to +SSH_PORT="22" +# Backup user on the remote system +SSH_USER="rbackup" + +# SSH private key +SSH_KEY="/path/to/private.key" + +# Host to connect to. Do not change this unless you know what you're doing. +SSH_HOST="$host" diff --git a/backup/server/backup.conf.d/fetch/ve.conf b/backup/server/backup.conf.d/fetch/ve.conf new file mode 100644 index 0000000..ce7ceae --- /dev/null +++ b/backup/server/backup.conf.d/fetch/ve.conf @@ -0,0 +1,7 @@ +# This is an example for "local" fetching on a set of OpenVZ VE's whose roots +# are mounted under /mnt/virtual-envs + +BASE=/mnt/virtual-envs + +# Then, for each host, there's a file in the ve/ subdirectory which defines the +# root. diff --git a/backup/server/backup.conf.d/fetch/ve/ve-host.conf b/backup/server/backup.conf.d/fetch/ve/ve-host.conf new file mode 100644 index 0000000..01645a4 --- /dev/null +++ b/backup/server/backup.conf.d/fetch/ve/ve-host.conf @@ -0,0 +1,2 @@ +# Define the root for a VE host +ROOT=1000 diff --git a/backup/server/backup.conf.d/hosts.conf b/backup/server/backup.conf.d/hosts.conf new file mode 100644 index 0000000..ad31d18 --- /dev/null +++ b/backup/server/backup.conf.d/hosts.conf @@ -0,0 +1,6 @@ +# List all hosts and the title they will be given in the report. +# +# Host Title +backup-server Backup server +ve-host Some OpenVZ virtual environment +remote-system A remote system diff --git a/backup/server/backup.conf.d/modes.conf b/backup/server/backup.conf.d/modes.conf new file mode 100644 index 0000000..3b2edf7 --- /dev/null +++ b/backup/server/backup.conf.d/modes.conf @@ -0,0 +1,11 @@ +# Define backup modes. A backup mode corresponds to a set of locations to +# backup; these locations (defined in types.conf and exclude.conf) can be +# listed for all hosts (using "*" as the host name) or for some specific hosts. +# +# When the main backup script is called, its first (and only) parameter should +# be the name of the mode. +# +# Mode Host name Backup types +daily * config,logs,varlib +daily remote-system home +weekly * full diff --git a/backup/server/backup.conf.d/post/crypto.key b/backup/server/backup.conf.d/post/crypto.key new file mode 100644 index 0000000..2952ef0 --- /dev/null +++ b/backup/server/backup.conf.d/post/crypto.key @@ -0,0 +1 @@ +key to use when encrypting files during post-processing goes here diff --git a/backup/server/backup.conf.d/post/ftp-access.conf b/backup/server/backup.conf.d/post/ftp-access.conf new file mode 100644 index 0000000..4ddd660 --- /dev/null +++ b/backup/server/backup.conf.d/post/ftp-access.conf @@ -0,0 +1,13 @@ +# Configuration file for the example post-processing script + +# The FTP server +ftp_host="remote-ftp.example.org" + +# User name on the FTP server +ftp_user="backup" + +# Password on the FTP server +ftp_pass="password" + +# Amount of rotations on the server +ftp_rotate=4 diff --git a/backup/server/backup.conf.d/types.conf b/backup/server/backup.conf.d/types.conf new file mode 100644 index 0000000..a81ff66 --- /dev/null +++ b/backup/server/backup.conf.d/types.conf @@ -0,0 +1,12 @@ +# Backup "types" correspond to locations to backup, along with a description +# used in the report, and the amount of copies to keep. +# +# The exclude.conf file is used in conjunction with this file to determine +# locations to exclude from each specific backup type. +# +# Backup type Rotation Directory Description +config 7 /etc System configuration +logs 7 /var/log Logs +varlib 7 /var/lib Various runtime information +home 7 /home Home directory +full 2 / Full backup diff --git a/backup/server/crontab.example b/backup/server/crontab.example new file mode 100644 index 0000000..69f70c2 --- /dev/null +++ b/backup/server/crontab.example @@ -0,0 +1,6 @@ +SHELL=/bin/sh +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin + +# m h dom mon dow user command +12 1 * * 0 root /usr/local/sbin/backup weekly +12 1 * * 1-6 root /usr/local/sbin/backup daily diff --git a/backup/server/share/fetch-local b/backup/server/share/fetch-local new file mode 100644 index 0000000..4dec4c1 --- /dev/null +++ b/backup/server/share/fetch-local @@ -0,0 +1,44 @@ +#!/bin/bash + +function FETCH +{ + local fetchroot="/" + + if [ "x$BASE" != "x" ]; then + fetchroot="$fetchroot/$BASE" + fi + + if [ "x$ROOT" != "x" ]; then + fetchroot="$fetchroot/$ROOT" + fi + + fetchroot="`echo "$fetchroot" | sed -e 's/\/\+/\//g'`" + if ! [ -d "$fetchroot" ]; then + echo -e "\t\t\tCONFIGURATION ERROR: missing root directory '$fetchroot'" >&2 + exit 1 + fi + echo -e "\t\t\tRoot directory:\t$fetchroot" >&2 + + local tarerrors="`mktemp`" + chmod 600 "$tarerrors" + + local command='tar --numeric-owner --one-file-system --ignore-failed-read --warning=none -c' + local index= + for index in $( seq 0 $(( ${#backup_exclude[@]} - 1 )) ); do + command="$command"' "--exclude='"`echo "./${backup_exclude[$index]}" | sed -e 's/\/\+/\//g' -e 's/\/$//'`"'"' + done + command="$command"' ".'"$backup_directory"'"' + + cd "$fetchroot" + eval $command 2>"$tarerrors" + if ! [ -z "`cat $tarerrors`" ]; then + echo -e "\t\t\tFETCH ERROR: something went wrong while creating the archive:" >&2 + echo -e "-----------------------------------------------------" >&2 + cat "$tarerrors" >&2 + echo -e "-----------------------------------------------------" >&2 + rm -f "$tarerrors" + return 1 + fi + rm -f "$tarerrors" + return 0 +} diff --git a/backup/server/share/fetch-ssh b/backup/server/share/fetch-ssh new file mode 100644 index 0000000..ee20c10 --- /dev/null +++ b/backup/server/share/fetch-ssh @@ -0,0 +1,55 @@ +#!/bin/bash + +function FETCH +{ + if [ -z "$SSH_KEY" ]; then + echo -e "\t\t\tCONFIGURATION ERROR: no SSH key" >&2 + exit 1 + elif [ -z "$SSH_HOST" ]; then + echo -e "\t\t\tCONFIGURATION ERROR: no destination SSH host" >&2 + exit 1 + fi + + local command="ssh -T" + + if [ "x$SSH_USER" != "x" ]; then + command="$command -l $SSH_USER" + fi + + if [ "x$SSH_PORT" != "x" ]; then + command="$command -p $SSH_PORT" + fi + + command="$command -i $SSH_KEY $SSH_HOST echo" + + local errorfile="`mktemp`" + chmod 600 "$errorfile" + + { + echo "$backup_directory" + local index= + for index in $( seq 0 $(( ${#backup_exclude[@]} - 1 )) ); do + echo "${backup_exclude[$index]}" + done + } | eval $command 2>"$errorfile" + + local nerrfile=`mktemp` + echo 0 > $nerrfile + cat $errorfile | while read line; do + if [[ "$line" =~ ^\.\.\.SRC\.\.\..*$ ]]; then + local text="`echo "$line" | sed -e 's/^.........//'`" + if [[ "$text" =~ ERROR ]]; then + echo 1 > $nerrfile; + fi + printf "\t\t\t%s\n" "$text" >&2 + else + echo -e "\t\t\tCONNECTION ERROR: SSH or the remote script caused errors:" >&2 + printf "\t\t\t\t%s\n" "$line" >&2 + echo 1 > $nerrfile + fi + done + local rv="`cat $nerrfile`" + rm -f "$nerrfile" "$errorfile" + return $rv +} + diff --git a/backup/server/share/postprocess b/backup/server/share/postprocess new file mode 100755 index 0000000..e239b1f --- /dev/null +++ b/backup/server/share/postprocess @@ -0,0 +1,179 @@ +#!/bin/bash + +# +# An example post-processing script +# +# This script runs in parallel to the main backup script (once the actual +# data fetching is completed, the main script will wait for the post-processing +# script to complete). +# +# It uses openssl to encrypt backup archives, then sends them to a remote FTP +# server using kermit; backups of type "full" (i.e. root filesystems) will not +# be processed. +# + +[ -z "$1" ] && exit 1 +echo $$ >"$1/pid" + +source /etc/backup.conf +source "${BACKUP_CONFS}/post/ftp-access.conf" + +if ! [ -f "${BACKUP_CONFS}/post/crypto.key" ]; then + echo "ERROR: no cryptographic key" + exit 1 +fi + + +function crypt +{ + openssl enc -kfile "${BACKUP_CONFS}/post/crypto.key" -aes-256-cbc -e +} + + +function makeFTPScript +{ + local script=`mktemp` + chmod 600 $script + { + echo "set ftp passive-mode off" + echo "ftp open $ftp_host /user:$ftp_user /password:$ftp_pass" + echo "if fail exit 1 Connection failed" + echo 'if not \v(ftp_loggedin) exit 1 Login failed' + for cmd in "$@"; do + if [[ "$cmd" =~ ^lcd\ ]]; then + echo "$cmd" + else + echo "ftp $cmd" + echo "if fail exit 1 ftp $cmd: \\v(ftp_message)" + fi + done + echo "ftp bye" + echo "exit 0" + } > $script + echo $script +} + + +function executeKermitScript +{ + local script="$1" + local dest="$2" + + wermit + < $script > $dest 2>/dev/null + local result=$? + rm -f "$script" + return $result +} + + +function fileExists +{ + local file="$1" + local script=`makeFTPScript "check $file"` + local output=`mktemp` + if ! executeKermitScript "$script" $output; then + if grep -q '^ftp check ' $output; then + echo "no" + else + echo "error" + fi + else + echo "yes" + fi + rm -f "$output" +} + + +function rotateRemoteFilesFor +{ + local host="$1" + local btype="$2" + + local commands=() + local fnum= + for fnum in $( seq $ftp_rotate -1 1 ); do + local fname="/encrypted-${host}-${btype}-$fnum.tar.gz" + local fe=`fileExists "$fname"` + if [ "x$fe" = "xerror" ]; then + echo "FTP check error for $fname" + exit 1; + elif [ "x$fe" = "xyes" ]; then + local ncommand= + if [ $fnum -eq $ftp_rotate ]; then + ncommand="delete $fname" + else + ncommand="rename $fname /encrypted-${host}-${btype}-$(( $fnum + 1 )).tar.gz" + fi + commands=( "${commands[@]}" "$ncommand" ) + fi + done + [ ${#commands[@]} -eq 0 ] && return 0 + + local temp=`mktemp` + executeKermitScript `makeFTPScript "${commands[@]}"` "$temp" + local rv=$? + rm -f "$temp" + return $rv +} + + +function putRemoteFileFor +{ + local host="$1" + local btype="$2" + local tempdir="$3" + + if ! rotateRemoteFilesFor "$host" "$btype"; then + return 1; + fi + + local temp=`mktemp` + executeKermitScript `makeFTPScript "lcd $tempdir" "put encrypted-${host}-${btype}-1.tar.gz"` $temp + local rv=$? + rm -f "$temp" + + return $rv +} + + + +function handleFile +{ + local host="$1" + local btype="$2" + + if [ "x$btype" = "xfull" ]; then + return + fi + + echo -e "\tCopying data for $host / $btype to FTP server" >&2 + + local tempdir="`mktemp -d`" + local src="${BACKUP_TARGET}/${host}/${btype}-1.tar.gz" + local dest="$tempdir/encrypted-${host}-${btype}-1.tar.gz" + cat "$src" | crypt > $dest + + putRemoteFileFor $host $btype $tempdir + + rm -rf "$tempdir" +} + + +function initPost +{ + echo "======================================================" + echo "POST-PROCESSING BACKUPS" + echo +} + +function finishPost +{ + echo +} + + +initPost +while read host btype; do + handleFile $host $btype 2>&1 +done +finishPost diff --git a/backup/ssh-client/README b/backup/ssh-client/README new file mode 100644 index 0000000..eba7694 --- /dev/null +++ b/backup/ssh-client/README @@ -0,0 +1,31 @@ +Client-side scripts for SSH backup +=================================== + +The scripts in this directory are meant to be used with the backup server's SSH +fetch script. + + +Installation +------------- + +1/ Create an user that uses the backup-user-shell as its shell and + /var/lib/rbackup as its home directory. +2/ Authorize the server's SSH key (limiting the key to the backup server's + address is a good idea) to log in as that specific user +3/ Authorize the backup user to run the main script as root (see sudo.example) + +If you want the archive sent to the backup server to be encrypted locally, +write the encryption key in the /etc/rbackup-encryption-key file (mode 0600 for +root). Otherwise, make sure the file does not exist. + + +Notes +------ + +1/ If the backup server is compromised, then so is the system being backed up. + +2/ If you use local encryption (which would mitigate the problem described + above), make sure you have a copy of the key somewhere. + +3/ If you want to use something other than /var/lib/rbackup as the user's home + directory, you'll have to change the backup-user-shell script. diff --git a/backup/ssh-client/backup-client b/backup/ssh-client/backup-client new file mode 100755 index 0000000..be6a15d --- /dev/null +++ b/backup/ssh-client/backup-client @@ -0,0 +1,56 @@ +#!/bin/bash + +function printToServer +{ + echo "...SRC...$*" >&2 +} + +function catToServer +{ + sed -e 's/^/...SRC.../' < "$1" >&2 +} + +read backup_directory + +if [ -z "$backup_directory" ]; then + printToServer "ERROR: no directory to backup" + exit 1 +else + backup_directory="`echo "/$backup_directory" | sed -e 's/\/\+/\//g' -e 's/\/$//'`" + if ! [ -d "$backup_directory" ]; then + printToServer "ERROR: missing directory $backup_directory" + exit 1 + fi +fi + +backup_exclude=( ) +while read backup_edir; do + backup_exclude=( ${backup_exclude[@]} $backup_edir ) +done + + +command='ionice -c2 -n7 tar --numeric-owner --one-file-system --ignore-failed-read --warning=none -c' +index= +for index in $( seq 0 $(( ${#backup_exclude[@]} - 1 )) ); do + command="$command"' "--exclude='"`echo "./${backup_exclude[$index]}" | sed -e 's/\/\+/\//g' -e 's/\/$//'`"'"' +done +command="$command"' ".'"$backup_directory"'"' +if [ -f "/etc/rbackup-encryption-key" ]; then + command="$command | nice -n20 openssl enc -kfile /etc/rbackup-encryption-key -aes-256-cbc -e" +fi +printToServer "Remote host ready" + +tarerrors="`mktemp`" +chmod 600 "$tarerrors" +cd / + +eval $command 2>"$tarerrors" +if ! [ -z "`cat $tarerrors`" ]; then + printToServer "FETCH ERROR: something went wrong while creating the archive:" >&2 + printToServer "-----------------------------------------------------" >&2 + catToServer "$tarerrors" + printToServer "-----------------------------------------------------" >&2 + rm -f "$tarerrors" + exit 1 +fi +rm -f "$tarerrors" diff --git a/backup/ssh-client/backup-user-shell b/backup/ssh-client/backup-user-shell new file mode 100755 index 0000000..c57ce14 --- /dev/null +++ b/backup/ssh-client/backup-user-shell @@ -0,0 +1,3 @@ +#!/bin/sh + +exec sudo /bin/bash /var/lib/rbackup/backup-client diff --git a/backup/ssh-client/sudo.example b/backup/ssh-client/sudo.example new file mode 100644 index 0000000..7a25139 --- /dev/null +++ b/backup/ssh-client/sudo.example @@ -0,0 +1,4 @@ +# Remote backup system, assuming the remote backup user is "rbackup" and +# the script has been installed in /var/lib/rbackup +# +rbackup ALL= (root) NOPASSWD: /bin/bash /var/lib/rbackup/backup-client