The following script was developed and tested under Debian Lenny.
#!/bin/bash ################################################################### # # This shell script creates remote incremental backups # from all of your OpenVZ virtual environments using # duplicity and LVM snapshots. This uses far less space than # the vzdump utility but is not as configurable. # # The script is meant to be run from cron.daily. It sends # its output to stdout and syslog. Modify log_real() to override. # # GPLv3 2009 by Erno Rigo <erno_AT_rigo_DOT_info> # # also see: http://wwww.openvz.org/ # http://duplicity.nongnu.org/ # ################################################################### # script configuration section # backup destination host DEST_HOST="my.backup.host.example.com" # username on destination (you should setup a key-based passwordless # ssh account for this purpose - see man ssh-keygen) DEST_USER="root" # absolute path on backup destination - should start but not end with / DEST_PATH="/BACKUPS" # prefix to use everywhere - this should be an unique backup ID PREFIX="vzbackup" # OpenVZ configuration directory location - usually /etc/vz/conf VZCONF=/etc/vz/conf # extended regular expression to filter VE config file list with # eg.: set this to '(100|230).conf$' in order to just backup VE ID 100 and 230 # all VE-s will be backed up if you leave this empty EXCLUDE_VE_REGEX="" # LVM snapshot volume size - you should have at least 10-20% of # your LV size available within the same VG to have snapshotting work reliably SNAPSIZE=600M # additional options for duplicity # should at least specify --full-if-older-than # you should setup encryption here if you need it DUPLICITY_OPTS="--no-encryption --volsize 100 --full-if-older-than 1W" # number of full backups to keep # minimum backup window size = full-if-older-than * KEEP KEEP=2 ################################################################### # do not touch bellow this line unless # you know what you're doing # destination URL for duplicity # note: since duplicity cannot autmatically create # its destination directory, only ssh is supported for now DEST="ssh://$DEST_USER@$DEST_HOST/$DEST_PATH" # this is the main log function # it sends output to stdout and the logger log_real() { if [ $# -eq 0 ]; then parm=`cat` else parm="$@" fi echo -e "$parm" | while read line; do echo "+ $line" | logger -s -t "$0" done } # normal log message log() { log_real "$@" } # fatal log message fatal() { log_real "FATAL: $@" echo "-1" > $EXITCODEFILE exit -1 } # initialization do_init() { log $0 initializing... trap "do_cleanup 2>&1 | log" exit TMPDIR=`mktemp -t -d $PREFIX.XXXXXXXXXX || fatal unable to mktemp` EXITCODEFILE=`mktemp -p "$TMPDIR" || fatal unable to mktemp` echo 0 > $EXITCODEFILE } # cleanup function (called from the "exit" bash trap) do_cleanup() { echo "cleaning up $TMPDIR (errors bellow this line can be ignored safely)" umount -f "$TMPDIR/snapshot" [ -f "$TMPDIR/snapdev" ] && snapdev=`cat "$TMPDIR/snapdev"` && lvs "$snapdev" && do_cmd lvremove -f "$snapdev" rm -rf --one-file-system "$TMPDIR" echo "cleanup done" } # run generic shell command. logs and exits if command fails do_cmd_real() { type="$1" shift 1 #id=$RANDOM #echo "executing command id#$id: $@" >&2 out=`mktemp -p "$TMPDIR" || fatal unable to mktemp` >&2 case "$type" in echo) "$@" 2>&1 | tee "$out" >&2 ;; noecho) "$@" 2>&1 > "$out" ;; esac ecode=$? [ $ecode -ne 0 ] && fatal "exit code $ecode for command: $@" cat "$out" rm -f "$out" } # normal command execution do_cmd() { do_cmd_real noecho "$@" } # command execution with output logging do_cmd_log() { do_cmd_real echo "$@" } # dump a specific VE (identified by VEID and config file path) do_dumpve() { veid="$1" conf="$2" # discover environment private=`( VEID=$veid ; . "$conf" ; readlink -f $VE_PRIVATE )` dev=`do_cmd df -P -T "$private" | tail -n+2 | awk '{print $1}'` fstype=`do_cmd df -P -T "$private" | tail -n+2 | awk '{print $2}'` mountpoint=`do_cmd df -P -T "$private" | tail -n+2 | awk '{print $7}'` mountpoint=`readlink -f $mountpoint` relprivate=`echo "$private" | cut -b$(echo "$mountpoint" | wc -c)- ` absprivate="$TMPDIR/snapshot/$relprivate" lv=`do_cmd lvs -o lv_name --rows --separator ":" $dev | cut -f2 -d':'` vg=`do_cmd lvs -o vg_name --rows --separator ":" $dev | cut -f2 -d':'` lvdev="/dev/$vg/$lv" # amuse user # echo "VE $veid config:$conf private:$private device:$dev lvdev:$lvdev mountpoint:$mountpoint relprivate:$relprivate" echo "VE $veid lvdev:$lvdev mountpoint:$mountpoint relprivate:$relprivate fstype:$fstype" # prepare and mount snapshot do_cmd lvcreate --size $SNAPSIZE --snapshot --name "$PREFIX-snapshot" $lvdev echo "/dev/$vg/$PREFIX-snapshot" > "$TMPDIR/snapdev" do_cmd mkdir -p "$TMPDIR/snapshot" do_cmd mount -v -t "$fstype" "/dev/$vg/$PREFIX-snapshot" "$TMPDIR/snapshot" # backup VE using duplicity pushd "$absprivate" >/dev/null mkdir -p "$TMPDIR/dirs/$PREFIX-$veid" do_cmd scp -r "$TMPDIR/dirs/$PREFIX-$veid" "$DEST_USER@$DEST_HOST:/$DEST_PATH/" do_cmd duplicity cleanup $DUPLICITY_OPTS "$DEST/$PREFIX-$veid/" do_cmd duplicity $DUPLICITY_OPTS ./ "$DEST/$PREFIX-$veid/" do_cmd duplicity remove-all-but-n-full $KEEP $DUPLICITY_OPTS "$DEST/$PREFIX-$veid/" popd >/dev/null # unmount and remove snapshot do_cmd umount -v "$TMPDIR/snapshot" do_cmd lvremove -f "/dev/$vg/$PREFIX-snapshot" } # find VEs to backup - call do_dumpve() for each do_work() { echo "running" do_cmd find "$VZCONF" -type f | do_cmd egrep "^$VZCONF/?[0-9]{2,}.conf\$" | do_cmd egrep "$EXCLUDE_VE_REGEX" | sort -n | uniq | while read conf; do veid=`echo "$conf" | rev | cut -f2 -d'.' | egrep -o "^[0-9]+" | rev` echo "backing up VE $veid" do_dumpve "$veid" "$conf" echo "done backing up VE $veid" done } # script entry point - send everything to the log() function { do_init || fatal "unable to init" ;} && { do_work 2>&1 | log ;} # exit with the defined exit code (defaults to 0) ecode=`cat "$EXITCODEFILE"` log "done, exiting with code:$ecode" exit $ecode ################################################################### # end of file ###################################################################