#!/bin/sh -
#
# $Id: cpiobackup,v 1.14 2002/06/11 14:22:24 dgregor Exp $
#
# Address correspondence to <dj@gregor.com>
#
# Copyright (c) 1998 Daniel J. Gregor, Jr., All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. All advertising materials mentioning features or use of this software
#    must display the following acknowledgement:
# 	This product includes software developed by Daniel J. Gregor, Jr.
# 4. The name of Daniel J. Gregor, Jr. may not be used to endorse or promote
#    products derived from this software without specific prior written
#    permission.
# 
# THIS SOFTWARE IS PROVIDED BY DANIEL J. GREGOR, JR. ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL DANIEL J. GREGOR, JR. BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# 
# This is a script to backup local filesystems to a local tapedrive or to
# a remote tapedrive over ssh or rsh.
#
# This script is customized for Solaris.  Look for comments that begin
# with "# OSD:" (OS-dependant) to see what you have to change.
#

BACKUPTYPE=gtar

if [ x"${BASH}" != x"" ]; then
	ECHO="echo -e"
else
	ECHO="echo"
fi

# Note: the following sed code is split across lines, otherwise it will look
# like a revision tag that RCS thinks it should update.
REVISION="`${ECHO} '$Revision: 1.14 $' | sed -e 's/^\$Revision: //' \
	-e 's/ \$$//'`"

TEMPDIR=/tmp/cpiobackup.$$

# OSD: Get a list of filesystem info and stuff it into temporary files for
# later access.  This way, we won't have to keep on making a large number
# of possibly expensive connections (eg, ssh to a slow workstation) to
# remote hosts.
getfsinfo() {
	$ONBACKUPHOST df -k > $TEMPDIR/df
}

# OSD: how to get a list of filesystems	and mounted on locations
getfs() {
	cat $TEMPDIR/df | \
		sed '1d' | \
		awk '{ print $1, $6 }' | \
		egrep "${PARTITIONREGEXP}"
}

# OSD: get the sizes of a filesystem
fssizes() {
	cat $TEMPDIR/df | \
		sed '1d' | \
		grep "$1" | \
		awk '{ print "kbytes " $2, "used " $3, "avail " $4, "capacity " $5 }'
}

USAGE="Usage:\n\
\tcpiobackup [<options>] <backup_level> <tape_specification>\n\
\t\t[<partitions to backup>]\
"

VERBOSEUSAGE="$USAGE\n\
\n\
<backup_level> can be \"-0\" for a full backup or \"-1\" thorugh \"-9\" for\n\
an incremental backup.  In order to do incremental backups, a full backup\n\
must first be done with the \"-t\" option to create a timestamps directory.\n\
The \"-t\" must also be specified when doing an incremental backup\n\
\n\
<tape_specification> can specify either a local or remote tape drive:\n\
\tEg: /dev/rmt/0n or somehost:/dev/rmt/0n\n\
\n\
Options:\n\
\t-r\t\t\tRewind the tape when finished.\n\
\t-v\t\t\tVerbose.\n\
\t-n\t\t\tDry run mode (don't actually backup anything).\n\
\t-h\t\t\tHelp -- print this help screen.\n\
\t-b <remote host>\tPerform a remote backup on <remote host>.\n\
\t\t\t\trsh(1) will be used to connect to the remote\n\
\t\t\t\thost, unless the CPIOBACKUP_RSH environment\n\
\t\t\t\tvariable is set to use another program that\n\
\t\t\t\tsupports rsh semantics (Eg: ssh).\n\
\t-t <timestampsdir>\tTimestamps directory -- required when doing\n\
\t\t\t\tincremental backups.\n\
\t-V\t\t\tPrint version number and exit.\
"

BASENAME=`basename $0`
die(){
	EXITVAL=1

	case "$1" in
		-e*)
			if [ $# -ge 2 ]
			then
				shift
				EXITVAL=$1
				shift
			fi
		;;

		--)
			shift
		;;
	esac

	${ECHO} "${BASENAME}: $@" >&2
	exit ${EXITVAL}
}

#
# Function:     writetotape
# Description:  Write data from STDIN to a tape drive.  The drive can
#		either be local or remote.
# Return Value:	0 on success, return value from failed command on error.
# Required Variables:
#	TAPEHOST
#		The name of the remote host where backups will be sent.
#		If empty, backups will be sent to a local tape drive.
#	TAPEDEVICE
#		The device where backups will be sent.
#	CPIOBACKUP_RSH
#		Command to connect to a remote host.  Uses rsh(1) semantics.
#
writetotape(){
	if ${ECHO} "${TAPEDEVICE}" | grep '%p' > /dev/null
	then
		OURTAPEDEVICE="`${ECHO} ${TAPEDEVICE} | sed \"s!%p!${PARTITION}!g\"`"
	else
		OURTAPEDEVICE="${TAPEDEVICE}"
	fi
	
	if [ "x${TAPEHOST}" = "x" ]
	then
		# local backup
		command="quietdd obs=32k of=${OURTAPEDEVICE}"
	else
		# remote backup
		command="${CPIOBACKUP_RSH} ${TAPEHOST} quietdd obs=32k of=${OURTAPEDEVICE}"
	fi

	${ECHO} "Sending data to tape with \"${command}\"" >&4

	if [ x"$DRYRUN" = x"yes" ]
	then
		${ECHO} "${command}" >&4
	else
		${command} || return $?
	fi
}

#
# Function:     rewindtape
# Description:  Rewind tape in a tape drive.  The drive can
#		either be local or remote.
# Return Value:	0 on success, return value from failed command on error.
# Required Variables:
#	TAPEHOST
#		The name of the remote host where the tape will be rewound.
#		If empty, a local tape drive will be rewound.
#	TAPEDEVICE
#		The tape drive that will be rewound.
#	CPIOBACKUP_RSH
#		Command to connect to a remote host.  Uses rsh(1) semantics.
#
rewindtape(){
	if [ "x${TAPEHOST}" = "x" ]
	then
		# local backup
		command="mt -f ${TAPEDEVICE} rewind"
	else
		# local backup
		command="${CPIOBACKUP_RSH} ${TAPEHOST} mt -f ${TAPEDEVICE} rewind"
	fi

	${ECHO} "Rewinding tape with \"${command}\"" >&3

	if [ x"$DRYRUN" = x"yes" ]
	then
		${ECHO} "${command}" >&4
	else
		${command} || return $?
	fi
}

# Some sick Bourne shell file descriptor redirection to get rid of
# the status information from 'dd'.  From an article by Tom Christiansen
# titled "CSH Programming Considered Harmful".
quietdd() {
	dd_noise='^[0-9]+\+[0-9]+ records (in|out)$'
	exec 5>&1
	status=`((dd "$@" 2>&1 1>&5 5>&- 6>&-; ${ECHO} $? >&6) |
		egrep -v "$dd_noise" 1>&2 5>&- 6>&-) 6>&1`
	return $status;
}
 
quietcpio() {
	cpio_noise='^[0-9]+ blocks$'
	exec 7>&1
	status=`(("$@" 2>&1 1>&7 7>&- 8>&-; ${ECHO} $? >&8) |
		egrep -v "$cpio_noise" 1>&2 7>&- 8>&-) 8>&1`
	return $status;
}


if [ "x${CPIOBACKUP_RSH}" = "x" ]
then
	CPIOBACKUP_RSH=rsh
fi

set -- `getopt nrhvb:0123456789t:V "$@"`
if [ $? != 0 ]
then
	die "Invalid options\n$USAGE\nRun \"$BASENAME -h\" for detailed help information"
fi

VERBOSE=0
REWIND=""
BACKUPHOST="`uname -n`"
ONBACKUPHOST="eval"
DRYRUN=""

for OPTION in "$@"
do
	case $OPTION in
		-r)
			REWIND=yes
			shift
		;;

		-v)
			VERBOSE=`expr $VERBOSE + 1`
			shift
		;;

		-n)
			DRYRUN=yes
			shift
		;;

		-h)
			${ECHO} $VERBOSEUSAGE
			exit 1
		;;

		-b)
			shift
			BACKUPHOST="$1"
			ONBACKUPHOST="$CPIOBACKUP_RSH $BACKUPHOST"
			shift
		;;

		-t)
			shift
			TIMESTAMPDIR="$1"
			shift
		;;

		-0|-1|-2|-3|-4|-5|-6|-7|-8|-9)
			LEVEL=`${ECHO} $OPTION | sed 's/^-//'`
			shift
		;;

		-V)
			${ECHO} "$BASENAME: version $REVISION"
			exit 0
		;;

		--)
			shift
			break
		;;
	esac
done

if [ x"$LEVEL" = x"" ]; then
	die "Backup level not specified -- you must specify one of -0 through -9"
fi

if [ x"$LEVEL" != x"0" -a x"$TIMESTAMPDIR" = x"" ]; then
	die "non-level 0 backups require the specification of a timestamp directory (-t)"
fi

# CPIOOPTIONS
# OSD: options to give to CPIO (in addition to just '-o')
 
# PARTITIONREGEXP
# OSD: what regular expressions MUST disk devices match
 
OS="`$ONBACKUPHOST ${ECHO} \\\`uname -s\\\`-\\\`uname -r\\\``"
test $? -eq 0 || die "Could not run uname to determine operating system type"
case $OS in
	OpenBSD-*)
		PARTITIONREGEXP="^/dev/(wd|sd)"
		CPIOOPTIONS="-c -C 32256"
	;;      

	SunOS-5.*)
		PARTITIONREGEXP="^/dev[^ \t]*dsk/"
		CPIOOPTIONS="-c -C 65536"
	;;      

	OSF1-V4.0)
		PARTITIONREGEXP='^(/dev/rz|.*_domain#)'
		CPIOOPTIONS="-ce -C 65536"
	;;      

	Linux-*)
		PARTITIONREGEXP='^/dev/[hs]d' # XXX this isn't perfect
		CPIOOPTIONS="-c -C 65536"
	;;      

	*)
		die "Unknown operating system: $OS"
	;;
esac

case $VERBOSE in
	1)
		exec 3>&1 4>/dev/null
	;;

	2)
		CPIOOPTIONS="${CPIOOPTIONS} -v"
		exec 3>&1
		exec 4>&1
	;;


	*)
		exec 3>/dev/null 4>/dev/null
esac

if [ $# -lt 1 ]
then
	die "Invalid usage: not enough arguments\nRun \"${BASENAME} -h\" for help" >&2
fi

mkdir $TEMPDIR || die "Could not make temporary directory: $TEMPDIR"
trap "cd / ; rm -r $TEMPDIR ; exit" 0

if [ x"$TIMESTAMPDIR" != x"" ]
then
	$ONBACKUPHOST test -w $TIMESTAMPDIR || \
		die "$TIMESTAMPDIR is not writable"
	$ONBACKUPHOST /bin/ls -1tr $TIMESTAMPDIR > $TEMPDIR/timestamps \
		|| die "could not get listing of $TIMESTAMPDIR"
fi

# This is the "backup destination".  It can either be just a tape device,
# like "/dev/rmt/0n", or a host:device, like "backupserver:/dev/nrst0".
BACKUPTO="$1"; shift

# Figure out where the backups will go
if ${ECHO} "${BACKUPTO}" | grep ':' > /dev/null
then
	TAPEHOST=`${ECHO} "${BACKUPTO}" | sed 's/:.*//'`
	TAPEDEVICE=`${ECHO} "${BACKUPTO}" | sed 's/.*://'`
else
	TAPEDEVICE="${BACKUPTO}"
fi

getfsinfo

# If we have any more arguments, they are the mount points or filesytems
# to backup.  Otherwise, we just backup every local filesystem on the box.
if [ $# -gt 0 ]
then
	BACKUPLIST="$@"
else
	BACKUPLIST=`getfs | awk '{ print $1 }'`
fi

# Go through each mount point or filesystem and make sure that it's mounted,
# and get some statistics on each device.  Also, make a list of mount points
# to be backed up.
for BACKUPITEM in ${BACKUPLIST}
do
	if ${ECHO} ${BACKUPITEM} | egrep "${PARTITIONREGEXP}" > /dev/null
	then
		# It's a disk partition
		# Try to get the mount point -- we may not be able to get it
		MOUNTPOINT=`getfs | grep "^${BACKUPITEM}[ \t]" | \
			awk '{ print $2 }'`
		if [ "x${MOUNTPOINT}" = "x" ]
		then
			die "could not find mount point for ${BACKUPITEM}"
		fi

		PARTLIST="${PARTLIST}\n${BACKUPITEM} ${MOUNTPOINT}: `fssizes \"^${BACKUPITEM}[ \t]\"`"
		MOUNTPOINTS="${MOUNTPOINTS} ${MOUNTPOINT}"
	else
		# It should be mount point
		PARTITION=`getfs | grep "[ \t]${BACKUPITEM}\$" | \
			awk '{ print $1 }'`
		if [ "x${PARTITION}" = "x" ]
		then
			die "could not find partition for ${BACKUPITEM}"
		fi

		PARTLIST="${PARTLIST}\n${PARTITION} ${BACKUPITEM}: `fssizes \"^${PARTITION}[ \t]\"`"
		MOUNTPOINTS="${MOUNTPOINTS} ${BACKUPITEM}"
	fi
done

# output some useful data
${ECHO} HOST: ${BACKUPHOST} >&3
${ECHO} MOUNTPOINTS: ${MOUNTPOINTS} >&3
${ECHO} BACKUPLIST: ${BACKUPLIST} >&3
${ECHO} PARTLIST: ${PARTLIST} >&3
${ECHO} TAPEHOST: ${TAPEHOST} >&3
${ECHO} TAPEDEVICE: ${TAPEDEVICE} >&3
${ECHO} LEVEL: ${LEVEL} >&3

# rewind the tape if specified
if [ "x$REWIND" != "x" ]; then
	rewindtape || die "Error rewinding tape"
fi

if [ x"${BACKUPTYPE}" = x"gtar" ]; then
	GTARNOTE="NOTE: The next tape file is a copy of gtar for $OS"
else
	GTARNOTE="\c"
fi

# write a header file to the tape describing what is backed up on the tape.
PARTITION="header"
( ${ECHO} "BACKUP HEADER" ; \
	${ECHO} "BACKUPTYPE: ${BACKUPTYPE}" ;\
	${ECHO} "HOST: ${BACKUPHOST}" ;\
	${ECHO} "DATE: `date`" ;\
	${ECHO} "MOUNTPOINTS: ${MOUNTPOINTS}" ;\
	${ECHO} "LEVEL: ${LEVEL}" ;\
	${ECHO} "${GTARNOTE}" ;\
	${ECHO} "${PARTLIST}" ) | writetotape || \
		die "Error writing header data to tape"

if [ x"${BACKUPTYPE}" = x"gtar" ]; then
	# Slap a copy of gtar for $OS on the tape, since the boot media
	# may not have it, and in a pinch it would be nice to not have
	# to find and install it.
	PARTITION="gtar"
	command="cat \`which gtar\`"
	
	if [ x"$DRYRUN" = x"yes" ]
	then
		${ECHO} "${command}" >&4
		writetotape
	else
		$ONBACKUPHOST "${command}" | writetotape || \
			die "error placing a copy of gtar on the tape"
	fi
fi

# Back up each partition
for MOUNTPOINT in ${MOUNTPOINTS}
do
	${ECHO} "Backing up ${MOUNTPOINT}" >&3

	PARTITION="`${ECHO} ${MOUNTPOINT} | \
		sed -e 's/\///' -e 's/\//./g' -e 's/^$/root/'`"

	if [ x"$TIMESTAMPDIR" != x"" ]
	then
		CREATETIMESTAMP="date > ${TIMESTAMPDIR}/${PARTITION}.${LEVEL}.new &&"
		MOVETIMESTAMP="&& mv ${TIMESTAMPDIR}/${PARTITION}.${LEVEL}.new \
			${TIMESTAMPDIR}/${PARTITION}.${LEVEL}"
	fi
	if [ x"$LEVEL" != x"0" ]
	then
		NEWER="`cat $TEMPDIR/timestamps | grep "${PARTITION}\.[0-9]\$" | \
			sed \"/${LEVEL}\\\$/,\\\$d\" | tail -1`"
		if [ x"${NEWER}" != x"" ]
		then
			if [ x"${BACKUPTYPE}" = x"gtar" ]; then
				NEWER="-N `cat ${TIMESTAMPDIR}/${NEWER}`"
			else
				NEWER="-newer ${TIMESTAMPDIR}/${NEWER}"
			fi
		fi
	fi

	# XXX If cpio has _any_ errors while backing up, it will exit with
	# XXX a non-zero return value and the backup will croak.  For now,
	# XXX we just ignore the exist value and hope that things will go
	# XXX well.  We should watch the errors from cpio and consider the
	# XXX backup to be okay if the number of errors produced is less than
	# XXX a certain threshold.
	
	if [ x"${BACKUPTYPE}" = x"gtar" ]; then
		command="${CREATETIMESTAMP} \
				gtar -clf - ${NEWER} -C ${MOUNTPOINT} \
					${GTAROPTIONS} . ; true \
					${MOVETIMESTAMP}"
	else
		command="cd ${MOUNTPOINT} && \
				${CREATETIMESTAMP} \
				find . -xdev -depth ${NEWER} -print | \
					cpio -o ${CPIOOPTIONS} ; true \
					${MOVETIMESTAMP}"
	fi

	if [ x"$DRYRUN" = x"yes" ]
	then
		${ECHO} "${command}" >&4
		writetotape
	else
		if [ x"${BACKUPTYPE}" = x"gtar" ]; then
			$ONBACKUPHOST "${command}" | writetotape || \
				die "error backing up ${MOUNTPOINT}"
		else
			quietcpio $ONBACKUPHOST "${command}" | writetotape || \
				die "error backing up ${MOUNTPOINT}"
		fi
	fi
done

# Write an end-of-tape trailer.  This really isn't needed, because we can
# get a list of partitions very easily, however it will make a "read back"
# verify script very easy.
PARTITION="trailer"
${ECHO} "END OF HOST" | writetotape || die "Error writing end of tape trailer"

exit 0

