#!/bin/sh - # # $Id: cpiobackup,v 1.14 2002/06/11 14:22:24 dgregor Exp $ # # Address correspondence to # # 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 [] \n\ \t\t[]\ " VERBOSEUSAGE="$USAGE\n\ \n\ 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\ 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 \tPerform a remote backup on .\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 \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