#!/bin/bash # # An enterprise business continuity / disaster survival tool # based on my old script that backed up a bunch of hosts by # pure brute force with Mike Rubel's rsync technique. # # Charlie Brooks, HBCS LTD. # This script is licensed for use under the GNU GPLv2. # Removal of this notice and/or author attribution is not permitted. # # Avoiding file globbing (to minimize the risk of shell command # buffer overrun) and call-outs from bash to external programs # like find and expr (to increase reliability by decreasing the # size of the source code base) has resulted in an (inefficient, # ugly, slow) but (robust, comprehensible, maintainable) script. # # I am OK with that. # # Note that although the script is not optimized for speed, # script execution time is not really a significant part # of the total time required for rsync-based backups. # # 2005-10-31 Charlie Brooks Original (bash) version # 2005-12-25 CTB Added config file, loop for multiple hosts # 2006-01-02 CTB replaced cp -lpr with rsync --link-dest # 2006-02-07 CTB RSYNC_RSH, control temp file locations # 2006-02-16 CTB fielded read to allow more individual host controls # 2006-02-17 CTB detect failures, allow comment lines in configuration file # 2006-02-?? CTB enumerate existing backups, decipher intervals # 2006-03-07 CTB purge unwanted backups, establish KillLimit # 2006-03-10 CTB evil algorithm for exclude parameter composition # 2006-03-13 CTB somewhat less evil exclude and include algorithm # 2006-03-20 CTB better structure and function delineation # 2006-03-21 CTB typo fix, loop optimization, variable renaming # 2006-03-22 CTB improve comments, lay groundwork for archival # 2006-06-02 CTB port to RHEL4 (rsync 2.6.3 protocol version 28) # 2006-06-06 CTB never step on existing backup from same date # 2007-01-23 CTB deletion logic rewritten to handle failed backups properly # 2007-01-24 CTB clean up comments and default value definitions # 2007-01-29 CTB reduce diagnostics and eliminate redundant dir check # 2007-02-07 CTB add archive detection, rename some variables # 2007-02-08 CTB add archival code as echo rather than execute # 2007-02-09 CTB light up the archive engine for real # 2007-02-14 CTB fix logic bugs in archive determination loops # 2007-02-23 CTB clean up reporting messages and default values # 2007-02-27 CTB revise tar parameters to make more useful archives # 2007-06-07 CTB more optimization of archive output files # 2008-10-22 CTB beginning the long-awaited BATCH MODE revision # 2008-11-06 CTB add test for rsync batch mode availability # 2008-11-14 CTB per-backup lockfiles, move site specific vars to top # 2008-11-20 CTB remove media-specific stuff for writing tars to DVD # 2008-12-02 CTB more batch mode, including 1st cut at transfers # 2008-12-03 CTB don't try to archive what has never been backed up # 2008-12-03 CTB fix egregiously stupid logic error in batch loop # 2008-12-04 CTB another batch bug fix, revisions to default excludes # 2008-12-15 CTB reset rsync parms before each backup # 2008-12-17 CTB fix array syntax errors in batch file management # 2008-12-26 CTB boxing day... detect & acommodate error 23 # 2008-12-27 CTB revert ill-considered boxing day patch, rewrite filtering # 2009-01-06 CTB clean up error and information messages # 2009-01-08 CTB add timers, TimeSince subroutine # 2009-01-13 CTB add command line parameter, tune debug level reporting # 2009-04-27 CTB display run-times longer than 24 hours correctly # 2009-05-04 CTB prepend hostname and timestamp to any rsync errors # 2009-11-17 CTB define all default values in config file # ############################################################################# # # Debug Levels: -1 do not generate any output (bad idea!!) # 0 normal, report major errors only # 1 progress reporting & all errors # 2 detailed progress reports & errors, warnings # 3 all of above & print rsync command # 4 all of above & list individual files export Debug=0 # # First we'll define values that are likely to be site-specific # KEEP IN MIND that if you are using the remote archive feature # to maintain offsite backups you should use the same filesystem # hierarchy on all participating nodes (we use four sites & hosts) # export sshKey="/shootsnap/.ssh/id_dsa.shootsnap" export BackDir="/var/backup" export ArchDir="/var/backup/archive" # # Establish limits that will have major effect on runtimes # NOTE "yearly" intervals will not work properly if BSL is less than 365 # export BackScanLimit=412 # max days in the past to check for existing backups export KillLimit=3 # max backups to delete per host per run export ArchLimit=1 # max archive tarballs to create per run # # Define server-specific variables such as location of # configuration file, backup device and external commands # export ConFile="/etc/shootsnap.conf" export PidFile="currently_running_job.pid" export Rsync="/usr/bin/rsync" export RsyncBaseParms="-az --numeric-ids --delete-excluded" # more added later export Tar="/bin/tar" # use Star if you need ACLs on HPUX export TarParms="-cvpz --numeric-owner --atime-preserve" # # # Set up default field values. These can be overridden by config file # export DefaultTreeRoot="/" export DefaultBackupInterval="1" # one means daily export DefaultArchiveInterval="365" # zero means never export DefaultKeepLimit="7" export DefaultExcludeList="/proc/**,chroot/proc/**,/sys/**,/dev/**,/var/log/messages,/var/log/maillog" # /tmp/**? export DefaultIncludeList="/proc/,/proc/partitions" export DefaultRemoteSite="" # null string means no default off-site storage # Flog the GNU "date" command mercilessly export Today=`date -I` # ISO formatted date, YYYY-MM-DD export JulianDay=`date -d"$Today" +%j` export DayOfMonth=`date -d"$Today" +%d` export DayOfWeek06=`date -d"$Today" +%w` declare -a DayNames=(sun mon tue wed thu fri sat) export TodayShortName=${DayNames[DayOfWeek06]} # guarantees lower case # If this seems tedious, think how bad it would be if # we did it inside the test loop for every single host # There is a "clever" and incomprehensible way to do this too DaysAgo[0]="$Today" typeset -i LoopCounter=0 while ( test "$LoopCounter" -lt $BackScanLimit ) ; do let LoopCounter=$LoopCounter+1 DaysAgo[$LoopCounter]=`date -I -d "$Today -$LoopCounter days"` || exit 1 done # Make rsync use passwordless SSH as base transport mechanism # this depends on the previously defined key being set up right export RSYNC_RSH="ssh -2 -i $sshKey" # Check that our rsync supports required features, if not blow up if ! strings $Rsync | grep '^\-\-link\-dest$' >/dev/null ; then [ $Debug -ge 0 ] && echo "Unsupported version of rsync (lacks --link-dest switch)" exit -1 fi if ! strings $Rsync | grep '^\-\-write\-batch$' >/dev/null ; then [ $Debug -ge 0 ] && echo "Unsupported version of rsync (lacks --write-batch option)" exit -1 fi # Check that our hierarchy exists (should test for partition, too) if ! ( test -d "$BackDir" ); then [ $Debug -ge 0 ] && echo FATAL ERROR: Master Backup directory $BackDir does not exist exit 1 fi # Subroutine to determine whether a given interval value requires something # to be done. This is used to convert values like "monthly" to a functional # instruction like "do this now" or "it's not time to do this yet". # # If function returns SUCCESS (do it now) nothing will be written on any # output stream. If the interval we are trying to decipher is completely # incomprehensible (say, "walpurgis nacht") the function returns FAIL and # also writes diagnostic text to the standard error. # # There are several global variables that must be filled before calling, # including Host, DayOfWeek06, JulianDay, DayOfMonth, and DayNames[] # Positional parameters are 1 = Frequency 2 = DaysSinceLast 3 = FlavorText function TimeToDoIt { local ReturnStatus=0 # If in doubt DO THE BACKUP # test \( $# -eq 3 \) || exit 5 # must have 3 parameters if test "$1" -eq "$1" 2>/dev/null ; then # test for integer if ( test "$1" -eq "0" -o "$1" -gt "$2" ) ; then ReturnStatus=255 fi else # 1=`echo $1 | tr "[A-Z]" "[a-z]"` #uncomment to lose case sensitivity case $1 in never) ReturnStatus=255 ;; daily) ReturnStatus=0 ;; weekly) if test "$2" -lt 7 ; then ReturnStatus=255 fi ;; monthly) if test "$2" -le 31 -a "$DayOfMonth" -ne 1 ; then ReturnStatus=255 fi ;; yearly) if test "$2" -lt 365 -a "$JulianDay" -ne 1 ; then ReturnStatus=255 fi ;; *) typeset -i Found=0 Day2Find=${1:0:3} # extract first three chars for TestDay in ${DayNames[*]}; do if test "$TestDay" = "$Day2Find" ; then Found=1 if test "${DayNames[DayOfWeek06]}" != "$Day2Find" ; then ReturnStatus=255 # valid day, just not today fi break fi done if test "$Found" -eq 0 ; then [ $Debug -ge 0 ] && echo $Host\: String $1 is not a valid $3 interval >&2 ReturnStatus=255 fi ;; esac fi return $ReturnStatus } # Subroutine to determine whether a job of the given type (either backup # or archive) is already running. We do not want to duplicate processes # that are either already in progress or hung in some kind of error state. # # This routine is obviously dependent on creation and deletion of pidfiles # by all currently running copies of this program. # # If function returns SUCCESS (job is already running) a brief diagnostic # text will be written to the standard error. If it returns FAIL nothing # will be written on any output stream. I realize this seems backwards. # # Positional parameter 1 is the fully pathed name of the lockfile, which # is constructed from global variables Host, BackDir, and PidFile. # In practice, this logic has been found to produce ugly output # when the pid file is deleted after the -f test and before the cat # which happens more often than you'd think but causes no real # problems other than the aforementioned ugly output. It should be # re-written to use the cat or an open as the actual file test function RunningAlready { local ReturnStatus=255 # If in doubt DO THE BACKUP if [ -f $1 ] ; then OtherJobPid=`cat $1` [ $Debug -ge 2 ] && echo $Host\: other job pid is $OtherJobPid if [ -e /proc/partitions ] ; then if [ -d /proc/$OtherJobPid ] ; then ReturnStatus=0 fi else [ $Debug -ge 1 ] && echo "WARNING: No /proc filesystem found, using lame workaround." if ps -e |grep "^ *$OtherJobPid " ; then ReturnStatus=0 fi fi if [ "$ReturnStatus" -eq 255 ] ; then StaleLockFile=`dirname $1`/stale_lock.$$ [ $Debug -ge 1 ] && echo "WARNING - moving stale lock file to $StaleLockFile!" mv $1 $StaleLockFile fi fi return $ReturnStatus } # set up a comment-skipping fielded read function # that won't pollute IFS inside the main program loop # someday I may create a trendy modern configuration format function ReadFields () { local IFS=: Host="#" while test "${Host:0:1}" == "#" ; do read Host BackWhen KeepLimit TreeRoot Exclude Include ArchWhen RemoteSite|| return if test "${#Host}" -eq 0 ; then Host="# blank line"; fi done } # Function to stamp messages from stderr with hostname and wall time function StampErrors() { # while read -r -s -u1; do while read -r -s ; do if [ $Debug -ge 2 -o "${REPLY:0:18}" != "file has vanished:" ] ; then echo $Host\: `date +%H:%M:%S` $REPLY fi done } # Function to output nicely formatted elapsed time # Clever use of the "time" function would also give # CPU time, this uses "date" which only gives wall time function TimeSince() { local StartTime=$1 EndTime=`date +%s` if [ -z "$StartTime" ] ; then echo "No START_TIME supplied to elapsed time calculation" else ElapsedTime=$((EndTime - StartTime)) deltasec=$((ElapsedTime % 60)) deltamin=$(((ElapsedTime / 60) % 60)) deltahour=$(((ElapsedTime / 3600) % 24)) deltaday=$((ElapsedTime / 86400)) if test "$deltaday" != "0" ; then echo -en "$deltaday days " fi date -d $deltahour:$deltamin:$deltasec +%H:%M:%S fi } # remember when we started so we can output total elapsed time eventually export StartTime=`date +%s` # initialize counter for the archive array (which will contain the names of all # the backups that should be tarballed once we've finished using the network). typeset -i ArchCount=0 # Begin main program loop # for every host listed in the config file do while ReadFields ; do # Internet host names aren't case sensitive, so case sensitivity here # allows you to have a host named "default" if you want to get crazy if [ "$Host" == "DEFAULT" ] ; then export DefaultTreeRoot="$TreeRoot" export DefaultBackupInterval="$BackWhen" export DefaultArchiveInterval="$ArchWhen" # zero means never export DefaultKeepLimit="$KeepLimit" export DefaultExcludeList="$Exclude" export DefaultIncludeList="$Include" export DefaultRemoteSite="$RemoteSite" # null means no off-site storage elif [ "x$1" == "x" -o "$1" == "$Host" ] ; then # Anything not defined will get a default value either from the DEFAULT line # in the configuration file or from hardcoded values here [ -n "$TreeRoot" ] || export TreeRoot="${DefaultTreeRoot}" [ -n "$BackWhen" ] || export BackWhen="${DefaultBackupInterval}" [ -n "$ArchWhen" ] || export ArchWhen="${DefaultArchiveInterval}" [ -n "$KeepLimit" ] || export KeepLimit="${DefaultKeepLimit}" [ -n "$RemoteSite" ] || export RemoteSite="$DefaultRemoteSite" [ $Debug -ge 2 ] && echo -e \\n [ $Debug -ge 1 ] && echo -e $Host\: -------------------------------------------------------- # Initialise the "kill" array to include all failed backups # (this ignores BackScanLimit). This is so that the failed # backups will be deleted first by the kill routine, in case # the KillLimit is reached and we don't get around to all KillList=( $(find $BackDir/$Host -maxdepth 1 -name '20??-??-??.failed.*') ) typeset -i KillCount=${#KillList[@]} if test "$KillCount" -gt "$KeepLimit" ; then [ $Debug -ge 2 ] && echo $Host\: WARNING\: $KillCount existing failed backups in $BackDir/$Host fi if test -d $BackDir/$Host/$Today ; then [ $Debug -ge 1 ] && echo $Host\: Backup for $Today already exists. DaysSinceBackup=0 else # This loop finds all existing successful backups within # the date range bounded by today and the BackScanLimit # and sets the DaysSinceBackup counter to the number of days # since the last time a backup completed successfully. # Note you can derive last backup name from DaysSinceBackup using # the DaysAgo array. Also note I have stopped using "break n" # because I have found it to be buggy in some versions of bash. typeset -i LoopIndex=0 typeset -i KeepCount=0 DaysSinceBackup=$BackScanLimit while ( test "$LoopIndex" -lt "$BackScanLimit" ) ; do let LoopIndex=$LoopIndex+1 TestFolder=$BackDir/$Host/${DaysAgo[$LoopIndex]} if ( test -d "$TestFolder" ) ; then if test "$KeepCount" -lt "$KeepLimit" ; then let KeepCount=$KeepCount+1 if test "$KeepCount" -eq 1 ; then DaysSinceBackup=$LoopIndex LastBackup=$TestFolder fi else if test "$KillCount" -lt "$KillLimit" ; then KillList[$KillCount]="$TestFolder" let KillCount=$KillCount+1 else LoopIndex=$BackScanLimit # to avoid break 2+ break # Bust out of loop to save time fi fi fi done # Check backup interval (see subroutine for comments) if TimeToDoIt $BackWhen $DaysSinceBackup "backup" ; then # Check for already running backup job (see subroutine for comments) export LockFile=$BackDir/$Host/$PidFile if RunningAlready "$LockFile" ; then [ $Debug -ge 0 ] && echo "Skipping $Host, job with pid `cat $LockFile` already in progress." else if test "$DaysSinceBackup" -eq "$BackScanLimit" ; then [ $Debug -ge 2 ] && echo $Host\: No backups found within the last $BackScanLimit days else [ $Debug -ge 2 ] && echo $Host\: Backup interval is $BackWhen [ $Debug -ge 1 ] && echo -n $Host\: Last successful backup was ${DaysAgo[$DaysSinceBackup]} [ $Debug -ge 1 ] && echo \ \($DaysSinceBackup days ago\). fi # Check that the individual host's subfolder exists if ! ( test -d "$BackDir/$Host" ); then [ $Debug -ge 1 ] && echo $Host\: Creating backup container directory $BackDir/$Host mkdir $BackDir/$Host fi # Check that today's subfolder exists mkdir $BackDir/$Host/$Today if ! ( cd $BackDir/$Host/$Today ); then [ $Debug -ge 0 ] && echo $Host\: FATAL ERROR: unable to use directory $BackDir/$Host/$Today continue fi # File globbing has to be turned off in the shell now or else any asterisks # will get expanded inappropriately based on the content of the local system # # I did this with file globbing turned on in earlier versions, but the # code got absurdly abstruse because of the syntax overlap between bash # file glob characters and rsync's exclude parameter, complicated # by the syntax overlap between bash's substitution string delimiter # and the filesystem's directory delimiter. set -f # turn off file globbing # The rsync switches and parameters contain the deep magic, which is # really happening inside rsync and not within any of my code. export RsyncParms="$RsyncBaseParms" [ $Debug -ge 4 ] && RsyncParms="-v $RsyncParms" # Build an arbitrarily complex filter specification # # proc means any file anywhere named proc # /proc means only the specific file named /proc # /proc/ means the folder /proc # proc/ means any folder named proc # /proc/** means everything contained by /proc # /proc/*** means the folder /proc and its contents # # Include parameters will be specified on the command line BEFORE the # excludes and rsync will handle any conflicts as documented in the # rsync man page. It's essentially first-match-and-out (which is why # the includes come first) but there are complexities worth study. IncludeList="${DefaultIncludeList},${Include}" for IncludeItem in ${IncludeList//,/ } ; do RsyncParms="$RsyncParms --include=$IncludeItem" done ExcludeList="${DefaultExcludeList},${Exclude}" for ExcludeItem in ${ExcludeList//,/ } ; do RsyncParms="$RsyncParms --exclude=$ExcludeItem" done # The rsync linkdest option will be set to the most recent # successful backup if there is one. If not we get very slow. if test "$KeepCount" -eq "0" ; then [ $Debug -ge 0 ] && echo $Host\: ERROR: unable to find previous backup for linking [ $Debug -ge 0 ] && echo $Host\: Proceeding to do SLOW, BANDWIDTH-HOGGING backup RsyncParms="$RsyncParms --stats" # might as well do something useful else [ $Debug -ge 2 ] && echo $Host\: $LastBackup will be used for linking RsyncParms="$RsyncParms --link-dest=$LastBackup" [ $Debug -ge 2 ] && echo $Host\: $KeepCount existing successful backups will be retained [ $Debug -ge 2 ] && echo $Host\: ${#KillList[@]} backups are scheduled for deletion fi # Most sites will only make a batch if there's been a remote host # explicitly specified in the configuration file. if ! ( test "no$RemoteSite" == "no" ); then [ $Debug -ge 2 ] && echo $Host\: Batch file will be built for $RemoteSite let BatchCount=$BatchCount+1 BatchFileArray[$BatchCount]="$LastBackup-$Today.batch" BatchHostArray[$BatchCount]=$RemoteSite RsyncParms="$RsyncParms --write-batch=$LastBackup-$Today.batch" fi # By the rude bridge that arched the flood, # Their flag to April's breeze unfurled; # Here once the embattled farmers stood; # And fired the shot heard 'round the world. # --Emerson, /Concord Hymn/, 1837 [ $Debug -ge 1 ] && echo $Host\: Attempting backup `date +"%Y-%m-%d %T"` logger -i -p syslog.info "Attempting to back up $Host" BackupStartTime=`date +%s` echo $$ >$LockFile [ $Debug -ge 3 ] && echo $Rsync $RsyncParms ${Host}:${TreeRoot} $BackDir/$Host/$Today $Rsync $RsyncParms ${Host}:${TreeRoot} $BackDir/$Host/$Today &> >(StampErrors) Rstatus=${PIPESTATUS[0]} rm $LockFile [ $Debug -ge 0 ] && echo "$Host: Backup elapsed time: $(TimeSince $BackupStartTime)" set +f # re-enable file globbing # The way this code works, if the backups fail repeatedly they will # start to pile up and human intervention will be required to prevent # the disk volume from being entirely consumed. THIS BEHAVIOUR IS # BY DESIGN. If at any point the keep-limit has been reached and # today's backup is successful, failed backups will be deleted. if test "$Rstatus" -eq 0 -o "$Rstatus" -eq 24; then DaysSinceBackup=0 [ $Debug -ge 1 ] && echo $Host\: Backup completed `date +"%Y-%m-%d %T"` logger -i -p syslog.info "Backup of $Host completed." [ $Debug -ge 1 ] && echo $Host\: $KeepCount existing successful backups will be retained [ $Debug -ge 2 ] && echo $Host\: $KillCount backups are eligible for deletion if test $KillLimit -lt $KillCount; then [ $Debug -ge 2 ] && echo $Host\: but only $KillLimit will be deleted today KillCount=$KillLimit fi typeset -i LoopCounter=0 while test $LoopCounter -lt $KillCount ; do RmStartTime=`date +%s` [ $Debug -ge 1 ] && echo $Host\: Purging backup ${KillList[$LoopCounter]}... rm -rf ${KillList[$LoopCounter]} [ $Debug -ge 0 ] && echo "$Host: Deletion elapsed time: $(TimeSince $RmStartTime)" let LoopCounter=$LoopCounter+1 done else [ $Debug -ge 0 ] && echo $Host\: ERROR\: Backup Failed `date +"%Y-%m-%d %T"` logger -i -p syslog.info "Backup of $Host FAILED with status $Rstatus" mv $BackDir/$Host/$Today $BackDir/$Host/$Today.failed.$$ fi fi # if/else running already else [ $Debug -ge 2 ] && echo $Host\: No backup scheduled for today. fi fi # Use DaysAgo array to find out when (if ever) the last archive was made # and build a list of hosts needing archive after all backups are done # This code is based on the loop that finds all existing successful # backups, and shares variables (such as BackScanLimit and the DaysAgo # array) with that loop. Don't muck about in here without considering # consequences for preceding code, i.e. don't change any shared globals. # note the use of "let" and "typeset" to force arithmetic - I am not sure # if it would be more portable to use $((expression)) instead and I would # like to use the most portable form compatible with busybox... if ( test "$ArchWhen" == "never" -o "$ArchWhen" == "0" ) ; then [ $Debug -ge 2 ] && echo $Host\: This host is not configured for permanent archives else DaysSinceArchive="" typeset -i LoopIndex=0 while ( test -z "$DaysSinceArchive" ) ; do if ( test -s "$ArchDir/$Host.${DaysAgo[$LoopIndex]}.tgz" ) ; then let DaysSinceArchive=$LoopIndex else let LoopIndex=$LoopIndex+1 if ( test "$LoopIndex" -gt "$BackScanLimit" ) ; then let DaysSinceArchive=$BackScanLimit+1 fi fi done if [ "$DaysSinceArchive" -le "$BackScanLimit" ] ; then if TimeToDoIt $ArchWhen $DaysSinceArchive "archive" ; then let ArchCount=$ArchCount+1 ArchHostArray[$ArchCount]="$Host" ArchLastBackArray[$ArchCount]="$DaysSinceBackup" fi fi fi fi done < $ConFile # Once we've completed all the rsync operations, we can ship any batch files # we've created off to the remote hosts where they may or may not be applied # by jobs running on those hosts... we aren't going to kick the batches off # from this script, because the remote host presumably has a better idea of # exactly when it would be most appropriate to do so. if ! [ "X$BatchCount" == "X" ] ; then [ $Debug -ge 0 ] && echo -e \\n\\n---- Beginning Batchfile transfers ---- while [ "$BatchCount" -gt "0" ] ; do TreeBranch="${BatchFileArray[$BatchCount]}" RemoteSite="${BatchHostArray[$BatchCount]}" if test -s $TreeBranch ; then [ $Debug -ge 0 ] && echo "Securely transferring $Treebranch to $RemoteSite" TransferStartTime=`date +%s` scp -i $sshKey $TreeBranch $RemoteSite:$TreeBranch scp -i $sshKey $TreeBranch.sh $RemoteSite:${TreeBranch}.sh [ $Debug -ge 0 ] && echo "Transfer elapsed time: $(TimeSince $TransferStartTime)" else [ $Debug -ge 0 ] && echo "ERROR: $TreeBranch missing or empty!" fi let BatchCount=$BatchCount-1 done else [ $Debug -ge 2 ] && echo "No batch files created today, so none will be transferred." fi # After all the backups are complete we can start working on tar archives # At this time I do not believe any mainstream tar engine is capable of tarring # any Unix Domain sockets embedded in the filesystem, and most tars (with the # exception of Schilling's star) cannot deal with Access Control Lists either. # # Because of this, and the sheer size of our data volumes, we no longer make # tarballs - instead, we maintain duplicate file trees on our remote sites # which can be used for business continuity and/or disaster recovery. If you # haven't got any remote sites and do not want to rent space from rsync.net, # you probably will want to make tarballs and set up a separate job to stream # them to tape or chunk them up for writing to DVD. Just keep in mind that # you will not get as good a restore from a tarball if you use ACLs etc. # # if the tarballs should be written to DVD, split them into roughly 8 GB chunks # like this "split -da 3 -b 8353711391 tarfile.tgz tarfile.part" where tarfile is # the name of the .tgz file you want to split. This will create 8GB files that # look like tarfile.part000, tarfile.part001, [etc.] that can be reassembled by # copying all the parts to disk and executing "cat tarfile.part??? >tarfile.tgz". # Obviously you are going to need huge amounts of disk space for this, but you # should be able to conserve some by cat'ting the parts directly into the untar # command without bothering to rebuild the .tgz along the way (assuming you are # using an operating system with true pipes, such as unix or linux). # # Keep in mind that tarballing and zipping titanic data archives can potentially # take several days to complete, so you will need to set your ArchLimit wisely. if ( test "$ArchCount" -ne "0" ) ; then [ $Debug -ge 0 ] && echo -e \\n\\n---- Beginning Archive Run ---- if ( test -d "$ArchDir" ) ; then while ( test "$ArchCount" -gt "0" ) ; do Host="${ArchHostArray[$ArchCount]}" if( test "$ArchCount" -gt "$ArchLimit" ) ; then [ $Debug -ge 1 ] && echo ${Host}\: No archive today, \$ArchLimit exceeded else BackupDate="${DaysAgo[${ArchLastBackArray[$ArchCount]}]}" [ $Debug -ge 2 ] && echo ${Host}\: archiving from backup made ${BackupDate} if cd $BackDir/$Host/$BackupDate ; then TarStartTime=`date +%s` if $Tar $TarParms . -f $ArchDir/$Host.$BackupDate.tgz \ > $ArchDir/$Host.$BackupDate.tarlog 2>&1 ; then [ $Debug -ge 1 ] && echo ${Host}\: archive to $ArchDir/$Host.$BackupDate.tgz complete else [ $Debug -ge 0 ] && echo ${Host}\: archive to $ArchDir/$Host.$BackupDate.tgz FAILED [ $Debug -ge 0 ] && echo ${Host}\: see $ArchDir/$Host.$BackupDate.tarlog for details fi [ $Debug -ge 0 ] && echo "Archival elapsed time: $(TimeSince $TarStartTime)" else [ $Debug -ge 0 ] && echo Could not cd to $BackDir/$Host/$BackupDate fi fi let ArchCount=$ArchCount-1 done else [ $Debug -ge 0 ] && echo -e \\nFATAL ERROR: Backup Archive directory $ArchDir does not exist exit 1 fi fi # Let's finish up by supplying the total elapsed time since this program was invoked [ $Debug -ge 0 ] && echo -e "\nTotal elapsed time: $(TimeSince $StartTime)" # T-t-t-that's all, folks!