#!/bin/sh
#
# snapshot-tar-backup.sh - FreeBSD tar backup of filesystem(s) using snapshots
#
# Copyright (c) 2010 EPIPE Communications <http://epipe.com/>
#
# Permission to use, copy, modify, and/or distribute this software
# for any purpose with or without fee is hereby granted, provided
# that the above copyright notice and this permission notice appear
# in all copies.
# 
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
#
#
# Version: 1.0
#
# Version history:
#
#   - 1.0: initial release (2010-08-31)
#
# Download location:
#
#   http://dist.epipe.com/freebsd/
#
# Description:
#
#   Takes a tar(1) backup of filesystem(s) by using ufs file system 
#   snapshots. The script takes consistent backups of live file systems. 
#   For example live database files can be backed up this way ("hot backup"). 
#   Normally this is possible only with dump(8) with -L option but not 
#   with tar(1). Easy to use for both local and remote backups with a 
#   simple front-end script or from command line. See below for examples.
#
# Depedencies:
#
#   /usr/ports/sysutils/freebsd-snapshot
#
#   See http://people.freebsd.org/~rse/snapshot/ for additional information
#   about this useful tool for managing FreeBSD file system snapshots.
#
#   freebsd-snapshot can be used to make scheduled snapshots from cron(8) 
#   and to provide user access to them through amd(8), but these features
#   do not need to be enabled to use this script. Only the script
#   /usr/local/sbin/snapshot is required.
#
# Usage examples:
#
#   Make a snapshot of / /usr /var and /home partitions and save a
#   tar backup of that in /my/backups/backup.tar:
#
#	snapshot-tar-backup.sh / /usr /var /home > /my/backups/backup.tar
#
#   Make a snapshot of /home and /data on remote system host2.example.org
#   and take a gzipped tar backup of that to a local file 
#   /some/dir/host2-home-data.tar.gz:
#
#	ssh host2.example.org -l root \
#	    "/usr/local/etc/snapshot-tar-backup.sh /home /data" \
#	    | gzip > /some/dir/host2-home-data.tar.gz
#
#   The above assumes that this script has been installed as 
#   /usr/local/etc/snapshot-tar-backup.sh in the remote host.
#   You could also alter the command to run the compression 
#   process at the other end.
#
# Caveats:
#
#   - The backup source locations must be ufs2 file system roots, not 
#     arbitrary directories within a file system.
#
#   - If several filesystems are specified, they must be listed in
#     order so that the higher level mount points are listed first.
#
#     The following is correct:
#	snapshot-tar-backup.sh / /usr /var /var/db/mysql
#	snapshot-tar-backup.sh /var /var/db/mysql
#
#     The following is INCORRECT:
#	snapshot-tar-backup.sh /usr /
#       snapshot-tar-backup.sh /var/db/mysql /var
#
#   - Does not work with ZFS because the script is looking for ".snap" 
#     directories at the filesystem roots and expects the snapshots
#     to be created there. This should be trivial to change.
#
#   - If the system crashes or the script is otherwise interrupted in 
#     the middle, snapshots are not removed until the script is run for 
#     the next time.
#
#   - When this script is used to take remote backups, the script must
#     be installed on the remote host(s).
#
#   - Must be run as root.
#
# Author contact information:
#
#   Janne Snabb <snabb@epipe.com>
#   http://epipe.com/
#

# check that we have some arguments
if [ $# -lt 1 ] ; then
	echo 1>&2 "usage: $0 /fs /fs2 .."
	exit 2
fi

# list of file systems to back up from the arguments
fss="$*"

# check that sysutils/freebsd-snapshot port is installed
if [ ! -x /usr/local/sbin/snapshot ] ; then
	echo 1>&2 "$0: required port sysutils/freebsd-snapshot is not installed"
	exit 2
fi

# check that we are running as root (effecive uid)
if [ `/usr/bin/id -u` != "0" ] ; then
	echo 1>&2 "$0: must be run as root"
	exit 2
fi

# check that the arguments point to file systems with .snap directory
for fs in ${fss} ; do
	if [ -z "${fs}" -o ! -d ${fs}/.snap ] ; then
		echo 1>&2 "$0: ${fs}/.snap missing, is ${fs} an ufs2 root?"
		exit 2
	fi
done

# function which returns its arguments in reverse order by using recursion
reverse() {
	if [ $# -gt 0 ] ; then
		local arg="$1"
		shift
		reverse "$@"
		echo "$arg "
	fi
}

# make a temporary mount point for snapshots (you might want to exclude
# this locataion from updatedb by putting it in /etc/locate.rc)
mountpoint=`/usr/bin/mktemp -d /mnt-backup-XXXXXX`

if [ $? -ne 0 ] ; then
	echo 1>&2 "$0: unable to create temporary mount point"
	exit 1
fi

# make snapshots of all filesystems
for fs in ${fss} ; do
	/usr/local/sbin/snapshot make ${fs}:backup
done

# mount the snapshots at our mount point
for fs in ${fss} ; do
	/usr/local/sbin/snapshot mount -o ro ${fs}:backup ${mountpoint}/${fs}
done

# output a tar archive of our snapshot tree to STDOUT
/usr/bin/tar -c -f - -C ${mountpoint} .

# capture tar return value
tarstatus=$?

if [ $? -ne 0 ] ; then
	echo 1>&2 "$0: tar returned failure: $?"
	# continuing anyway to destroy the snapshots
fi

# umount the snapshots (in reverse order to avoid problems)
for fs in `reverse ${fss}` ; do
	/usr/local/sbin/snapshot umount ${mountpoint}/${fs}
done

# remove the snapshots as they are no longer needed
for fs in ${fss} ; do
	/bin/rm -f ${fs}/.snap/backup.*
done

# remove the mount point directory
/bin/rm -rf ${mountpoint}

# exit with tar status
exit $tarstatus

# eof
