FreeBSD Full / Incremental Tape Backup Shell Script

by Vivek Gite · 12 comments

#!/bin/sh
# A FreeBSD shell script to dump Filesystem with full and incremental backups to tape device connected to server.
# Tested on FreeBSD 6.x and 7.x - 32 bit and 64 bit systems.
# May work on OpenBSD / NetBSD.
# -------------------------------------------------------------------------
# Copyright (c) 2007 nixCraft project <http://www.cyberciti.biz/fb/>
# This script is licensed under GNU GPL version 2.0 or above
# -------------------------------------------------------------------------
# This script is part of nixCraft shell script collection (NSSC)
# Visit http://bash.cyberciti.biz/ for more information.
# ----------------------------------------------------------------------
LOGGER=/usr/bin/logger
DUMP=/sbin/dump
# FSL="/dev/aacd0s1a /dev/aacd0s1g"
FSL="/usr /var"
NOW=$(date +"%a")
LOGFILE="/var/log/dumps/$NOW.dump.log"
TAPE="/dev/sa0"
 
mk_auto_dump(){
	local fs=$1
	local level=$2
	local tape="$TAPE"
	local opts=""
 
	opts="-${level}uanL -f ${tape}"
        # run backup
	$DUMP ${opts} $fs
	if [ "$?" != "0" ];then
       		$LOGGER "$DUMP $fs FAILED!"
       		echo "*** DUMP COMMAND FAILED - $DUMP ${opts} $fs. ***"
	else
  		$LOGGER "$DUMP $fs DONE!"
	fi
}
 
dump_all_fs(){
	local level=$1
	for f in $FSL
	do
		mk_auto_dump $f $level
	done
}
 
init_backup(){
	local d=$(dirname $LOGFILE)
	[ ! -d ${d} ] && mkdir -p ${d}
}
 
init_backup
 
case $NOW in
	Mon)	dump_all_fs 0;;
	Tue)	dump_all_fs 1;;
	Wed)	dump_all_fs 2;;
	Thu)	dump_all_fs 3;;
	Fri) 	dump_all_fs 4;;
	Sat) 	dump_all_fs 5;;
	Sun) 	dump_all_fs 6;;
	*) ;;
esac > $LOGFILE 2>&1

How do I run this script?

Download this script and unzip in /root. Open script and customize tape device ($TAPE variable) and file systems ($FSL). Operator can run this script from a shell prompt:
# /root/tapebackup.sh
Or via a cron job:
@midnight /root/tapebackup.sh

Featured Articles:

Want to read Linux tips and tricks, but don't have time to check our blog everyday? Subscribe to our email newsletter to make sure you don't miss a single tip/tricks.

{ 12 comments… read them below or add one }

1 Hassan July 12, 2009 at 3:08 pm

Nice script
Can it be ported to solaris 10

Reply

2 Vivek Gite July 13, 2009 at 5:00 am

It should work under Solaris too with little changes.

Reply

3 Ville July 15, 2009 at 7:40 pm

That’s very handy! Two questions: the `die()’ routine doesn’t seem to be used by anything? And `BAK’ seems to be undefined.

I’ll use this as a base for a modified script (I save dumps onto a separate hard drive, and also want there to be separate dump files for each filesystem being dumped).

Reply

4 Vivek Gite July 15, 2009 at 7:47 pm

Actually I’ve two scripts, BAK was used to dump to $BAK file system (e.g. BAK=/usr/backup) and another for tape. $BAK and die are from my dump to FS. Feel free to modify it as per your setup. I will update script to avoid confusion.

Reply

5 Ville July 17, 2009 at 5:22 am

Inspired by your script I wrote a new one. It can be found on my blog.

Reply

6 Aaron Hurt September 16, 2009 at 4:12 pm

This gave me a the motivation to finally get around to writing a backup script for my server. It’s basic framework is a combination of this script and another one I found on this site. It’s optimized for a freebsd/zfs system with a local tapedrive…it’ll even automatically eject tapes and resume on a new tape load. Hope it helps someone.

Main Part:

#!/bin/bash
## /root/dobackup.sh
## last update 09162009 by ahurt
#

# datasets to backup - use zfs paths not mount points
BACKUP_SETS="pool0/filebase pool0/vmware pool0/backuppc"

# number of snapshots to keep of each dataset
# snaps in excess of this number will be expired
# oldest snaps deleted first...this must be non zero
SNAP_KEEP="8"

# where you want your log files
LOGBASE=/root/logs

# where your tape drive is located
TAPE="/dev/nsa0"

# the length of the tape (in KBs)
TAPE_LENGTH="61440"

# tape swap script
TAPE_SWAP="/root/tapeswap.sh"

# the tape blocksize in bytes
BLOCKSIZE="32768"

# the tar blocksize in 512byte sectors
TAR_BLOCKS="64"

# any additional tar arguments
TAR_ARGS=""

# path to binaries
TAR=/usr/local/bin/gtar
MT=/usr/bin/mt
MKDIR=/bin/mkdir

# get the current date info
DOW=$(date +"%a")
MOY=$(date "+%m")
DOM=$(date "+%d")
YR=$(date "+%Y")

# backup log file
LOGFILE="${LOGBASE}/${DOW}-backup.log"

############################################
##### warning gremlins live below here #####
############################################

## init our backup paths
BACKUP_PATHS=""

## main backup function
full_backup(){
        ## do our snapshots..we don't want to tar a live fs
        zfs_snap
        ## initiate our tape
        $MT -f $TAPE comp on
        $MT -f $TAPE blocksize $BLOCKSIZE
        ## build our tar command
        if [ "${TAR_ARGS}x" != 'x' ]; then
                local tarcmd="$TAR $TAR_ARGS -F $TAPE_SWAP -H pax "
        else
                local tarcmd="$TAR -F $TAPE_SWAP -H pax "
        fi
        ## build the rest and run it
        tarcmd+="-L $TAPE_LENGTH -b $TAR_BLOCKS --transform=${XFROM} "
        tarcmd+="--show-transformed-names -clpMSvf $TAPE $BACKUP_PATHS"
        echo "RUNNING: $tarcmd"
        $tarcmd
        ## check the tar return
        if [ $? -eq 0 ]; then
                echo "FINISHED: Rewinding and ejecting tape"
                $MT -f $TAPE rewind
                $MT -f $TAPE offline
        else
                echo "ERROR: $TAR exited abnormally please check logs"
                exit 1
        fi
}

## partial/incremental fucntion
partial_backup(){
        ## we are doing an incremental...set our tar args
        ## only backup files newer than yesterday
        if [ "${TAR_ARGS}x" != 'x' ]; then
                TAR_ARGS+=" -N yesterday"
        else
                TAR_ARGS="-N yesterday"
        fi
        ## call our main backup function
        full_backup
}

## create our zfs snapshots
zfs_snap(){
        ## set our snap name
        local sname="${DOW}-${MOY}${DOM}${YR}"
        ## remove snapshot paths from filenames...this is passed to tar
        ## with --tramsform in the backup function above
        XFROM="s/\\/\\.zfs\\/snapshot\\/${sname}//g"
        ## generate snapshot list and cleanup old snapshots
        for dset in $BACKUP_SETS; do
                ## get current existing snapshots that look like
                ## they were made by this script
                local temps=`zfs list|grep -e "${dset}\@[Mon|Tue|Wed|Thu]"|\
                awk '{print $1}'`
                ## just a counter var
                local index=0
                ## our snapshot array
                declare -a snaps
                ## to the loop...
                for sn in $temps; do
                        ## while we are here...check for our current snap name
                        if [ $sn == $sname ]; then
                                ## looks like it's here...we better kill it
                                ## this shouldn't happen normally
                                echo "Destroying OLD snapshot ${dset}@${sname}"
                                zfs destroy ${dset}@${sname}
                        else
                                ## append this snap to an array
                                snaps[$index]=$sn
                                ## increase our index counter
                                let "index += 1"
                        fi
                done
                ## set our snap count and reset/reuse our index
                local scount=${#snaps[@]}; index=0
                ## how many snapshots did we end up with..
                if [ $scount -ge $SNAP_KEEP ]; then
                        ## oops...too many snapshots laying around
                        ## we need to destroy some of these
                        while [ $scount -ge $SNAP_KEEP ]; do
                                ## zfs list always shows newest last
                                ## we can use that to our advantage
                                echo "Destroying OLD snapshot ${snaps[$index]}"
                                zfs destroy ${snaps[$scount]}
                                ## decrease scount and increase index
                                let "scount -= 1"; let "index += 1"
                        done
                fi
                ## come on already...make that snapshot
                echo "Creating ZFS snapshot ${dset}@${sname}"
                zfs snapshot ${dset}@${sname}
                ## build our backup paths/snapshot paths
                local mnt=`zfs get mountpoint ${dset}|tail -1|awk '{print $3}'`
                ## add this to our global var
                if [ "${BACKUP_PATHS}x" != 'x' ]; then
                        BACKUP_PATHS+=" ${mnt}/.zfs/snapshot/${sname}"
                else
                        BACKUP_PATHS="${mnt}/.zfs/snapshot/${sname}"
                fi
        done
}

## make sure our log dir exits
[ ! -d $LOGBASE ] && $MKDIR -p $LOGBASE

## this is where it all starts - do either a full
## or partial depening on day
## office is closed friday-sunday
case $DOW in
        Mon)    full_backup;;
        Tue|Wed|Thu)    partial_backup;;
        *) ;;
esac > $LOGFILE 2>&1
[/code]

Second Part: (this is the tar info-script)
[code]
#! /bin/sh
##
### tar info script to automate tape changes
### below should match settings in backup script
##

# where your tape drive is located
TAPE="/dev/nsa0"

# path to binaries
MT=/usr/bin/mt

## script loop below ##

echo "Preparing volume $TAR_VOLUME of $TAR_ARCHIVE...."
sleep 15s
echo "Rewinding tape $TAPE ..."
$MT -f $TAPE rewind
sleep 5s
echo "Ejecting tape $TAPE ..."
$MT -f $TAPE offline
sleep 5s
echo "Waiting for next tape..."

while true; do
        ## wait 15 seconds between checks
        sleep 15s
        ## issue a status command and squelch output
        ## this will return an error if there is no tape
        $MT -f $TAPE status >/dev/null 2>&1
        ## check if status returned error or good
        if [ $? -eq 0 ];
        then
                ## if we are in here status worked
                echo "New tape detected...preparing..."
                ## make sure take is ready...and let's quick erase it
                $MT -f $TAPE rewind
                $MT -f $TAPE erase 0
                ## that's it...
                echo "Tape ready...resuming operation..."
                ## exit clean and tar will continue
                exit 0
        fi
done

Not sure if anyone else could use it...but I hope it helps someone get a start anyways.

Reply

7 Aaron Hurt September 16, 2009 at 4:16 pm

Err…that’s not exactly what I wanted…guess [code] tags aren't supported here. It looses all the formatting/tabs like that...but it should still work just doesn't look very pretty.

Reply

8 Vivek Gite September 16, 2009 at 7:01 pm

I’ve edited out your script and posted using <pre> … </pre> tags

Thanks for sharing your code!

Reply

9 Aaron Hurt September 22, 2009 at 8:56 pm

No problem, just a little update I basically rewrote it again and think I finally have what I really want. I use ZFS here on my freebsd system and I wanted to make sure 1) my backups were sane and stable ….and 2) I didn’t want to always have to goto tape to grab a file unless there was just a total catastrophic failure. Hence I integrated ZFS snapshot creation into the process. Then just to make the archives pretty, I told tar to transform the snapshot paths to the actual system paths before writing it to tape. This uses the same code for tapeswap…here it is…

#!/bin/bash
## /root/dobackup.sh
## last update 09212009 by ahurt
#

# datasets to backup - use zfs paths not mount points
BACKUP_SETS="pool0/filebase pool0/vmware pool0/backuppc"

# number of snapshots to keep of each dataset
# snaps in excess of this number will be expired
# oldest snaps deleted first...this must be non zero
SNAP_KEEP="8"

# where you want your log files
# and gnu tar incremental snaphots
LOGBASE=/root/logs

# where your tape drive is located
TAPE="/dev/nsa0"

# the length of the tape (in KBs)
TAPE_LENGTH="62914560"

# tape swap script
TAPE_SWAP="/root/tapeswap.sh"

# the tape blocksize in bytes
BLOCKSIZE="32768"

# the tar blocksize in 512byte sectors
TAR_BLOCKS="64"

# any additional tar arguments
TAR_ARGS=""

# path to binaries
TAR=/usr/local/bin/gtar
MT=/usr/bin/mt
MKDIR=/bin/mkdir
GZIP=/usr/bin/gzip

# get the current date info
DOW=$(date +"%a")
MOY=$(date "+%m")
DOM=$(date "+%d")
YR=$(date "+%Y")

# backup log file
LOGFILE="${LOGBASE}/${DOW}-backup.log"

# gnu tar incremental snapshot file
SNAR="${LOGBASE}/gtar-backup.snar"

############################################
##### warning gremlins live below here #####
############################################

## init our backup paths
BACKUP_PATHS=""

## main backup function
do_backup(){
	## do our snapshots..we don't want to tar a live fs
	zfs_snap
	## initiate our tape
	$MT -f $TAPE comp on
	$MT -f $TAPE blocksize $BLOCKSIZE
	## if it's monday and we have a tar snapfile
	## we need to delete this to get a full backup
	if [ -f ${SNAR} ] && [ $DOW == 'Mon' ]; then
		rm ${SNAR}
	fi
	## build our tar command
	if [ "${TAR_ARGS}x" != 'x' ]; then
		local tarcmd="$TAR $TAR_ARGS -F $TAPE_SWAP -H pax "
	else
		local tarcmd="$TAR -F $TAPE_SWAP -H pax "
	fi
	## build the rest and run it
	tarcmd+="-L $TAPE_LENGTH -b $TAR_BLOCKS --transform=${XFROM} "
	tarcmd+="--show-transformed-names --listed-incremental=${SNAR} "
	tarcmd+="-clpMSvf $TAPE $BACKUP_PATHS"
	echo "RUNNING: $tarcmd"
	$tarcmd
	## check the tar return
	if [ $? -eq 0 ]; then
		echo "FINISHED: Rewinding and ejecting tape"
		$MT -f $TAPE rewind
		$MT -f $TAPE offline
	else
		echo "ERROR: $TAR exited abnormally please check logs"
	fi
	## finish up...zip the log...they get huge
	echo "Cleaning up...zipping log ${LOGFILE}.gz"
	echo "This log can be viewed with zcat"
	$GZIP ${LOGFILE}
	## delete lockfile
	if [ -f "${LOGBASE}/.backup.lock" ]; then
		rm "${LOGBASE}/.backup.lock"
	fi
}

## create and manage our zfs snapshots
zfs_snap(){
	## set our snap name
	local sname="${DOW}-${MOY}${DOM}${YR}"
	## remove snapshot paths from filenames...this is passed to tar
	## with --tramsform in the backup function above
	XFROM="s/\\/\\.zfs\\/snapshot\\/${sname}//g"
	## generate snapshot list and cleanup old snapshots
	for dset in $BACKUP_SETS; do
                ## get current existing snapshots that look like
		## they were made by this script
		local temps=`zfs list|grep -e "${dset}\@[Mon|Tue|Wed|Thu]"|\
		awk '{print $1}'`
		## just a counter var
		local index=0
		## our snapshot array
		declare -a snaps
		## to the loop...
		for sn in $temps; do
			## while we are here...check for our current snap name
                	if [ $sn == $sname ]; then
                        	## looks like it's here...we better kill it
							## this shouldn't happen normally
                        	echo "Destroying OLD snapshot ${dset}@${sname}"
                        	zfs destroy ${dset}@${sname}
                	else
				## append this snap to an array
				snaps[$index]=$sn
				## increase our index counter
				let "index += 1"
			fi
		done
		## set our snap count and reset/reuse our index
		local scount=${#snaps[@]}; index=0
		## how many snapshots did we end up with..
		if [ $scount -ge $SNAP_KEEP ]; then
			## oops...too many snapshots laying around
			## we need to destroy some of these
			while [ $scount -ge $SNAP_KEEP ]; do
				## zfs list always shows newest last
				## we can use that to our advantage
				echo "Destroying OLD snapshot ${snaps[$index]}"
				zfs destroy ${snaps[$scount]}
				## decrease scount and increase index
				let "scount -= 1"; let "index += 1"
			done
		fi
		## come on already...make that snapshot
		echo "Creating ZFS snapshot ${dset}@${sname}"
		zfs snapshot ${dset}@${sname}
		## build our backup paths/snapshot paths
		local mnt=`zfs get mountpoint ${dset}|tail -1|awk '{print $3}'`
		## add this to a global
		if [ "${BACKUP_PATHS}x" != 'x' ]; then
			BACKUP_PATHS+=" ${mnt}/.zfs/snapshot/${sname}"
		else
			BACKUP_PATHS="${mnt}/.zfs/snapshot/${sname}"
		fi
	done
}

## check/create lock file and proceed
init(){
	## check our lockfile status
	if [ -f "${LOGBASE}/.backup.lock" ]; then
		## get lockfile contents
		local lpid=`cat "${LOGBASE}/.backup.lock"`
		## see if this pid is still running
		local ps=`ps auxww|grep $lpid|grep -v grep`
		if [ "${ps}x" != 'x' ]; then
			## looks like it's still running
			echo "ERROR: This script is already running as: $ps"
		else
			## well the lockfile is there...stale?
			echo "ERROR: Lockfile exists '${LOGBASE}/.backup.lock'"
			echo -n "However, the contents do not match any "
			echo "currently running process...stale lockfile?"

		fi
		## tell em what to do...
		echo -n "To force script run please delete "
		echo "'${LOGBASE}/.backup.lock'"
		## exit now...
		exit 0
	else
		## well no lockfile..let's make a new one
		echo $$ > "${LOGBASE}/.backup.lock"
	fi
	## start our backup function
	do_backup
}
## make sure our log dir exits
[ ! -d $LOGBASE ] && $MKDIR -p $LOGBASE

## this is where it all starts
## office is closed friday-sunday
case $DOW in
	Mon|Tue|Wed|Thu) init;;
	*) ;;
esac > $LOGFILE 2>&1

Reply

10 Aaron Hurt September 22, 2009 at 8:57 pm

It doesn’t like my tags … guess I don’t have html tag permissions ;p

Reply

11 Vivek Gite September 26, 2009 at 11:51 am

You need to put them between pre tags. BTW, I’ve edited out your post. Thanks for sharing your solution.

Reply

12 Aaron Hurt October 27, 2009 at 12:57 pm

I’ve updated the above to include lockfile management, enabling setting snapshot only days, tape full days and, tape incremental days or just to skip certain days. You can see the full script here: http://woodstock.anbcs.com/shell/dobackup.sh

Thanks again Vivek for giving me the motivation to get that setup…it’s been a real lifesaver for us here.

– Aaron

Reply

Previous post:

Next post: