Near Native FreeBSD Full and Incremental Backups to a Removable USB Storage Drive

UPDATE 2011/03/09 – I updated the code to backup to an NFS mount, and to include the “-h 0″ flag to skip all nodump flags. That was causing me serious problems.

Summary

I’ve given quite a bit of thought to backup procedures at home since my FreeBSD 8.1 box dropped my mirrored filesystem. The signs of impending apocalypse were there, I just didn’t pay them proper heed. Fortunately, all of my data was salvaged; unfortunately, I lost all the custom PHP code I wrote over the last 6 months, my wordpress themes, plugins and modifications, and everything else that actually DID anything with all that data. So, while I’ve been rewriting that, I’ve been giving equal, if not more attention to backing it up. I’ll catch up again, but before I do that, I’ll make sure I won’t fall behind again.

I did a few searches for FreeBSD backup solutions, and rolled my own little backup script using dump. It was decent, but it didn’t do everything I wanted as well as I wanted it to. Every night was a full backup, and there were no incrementals. I had to implement some pretty inelegant code to accomplish a couple things simply b/c I didn’t know how else to do it. So I kept looking and eventually zeroed in on David Andrzejewski’s work. He clearly states what he put out there is a use-at-your-own risk kind of script. I took it anyway as a starting block, and fleshed it out for my own purposes.

My requirements were similar to his, with the exception that I don’t have a cloud based storage account at the time of this writing and instead will be using a removable USB connected storage drive.

Project Goals

  • Run with native or easily accessible tools.
  • Full off-system backup of entire system once a week.
  • Incremental off-system backup of entire system nightly.
  • Separate off-system backups of individual critical files to make future restores easier.

Future Goals

  • Play with ${DUMPCACHE} to see how it affects the time to execute in my environment. Drop it back to 8MB for a week. Ramp it up to 64MB for a week. Recommended is 32MB, but it’s a party! Let’s see what happens.
  • Continue monitoring and fine tuning the hardware, OS environment and script to ensure maximum performance and stability. I haven’t recompiled a kernel in a while, maybe I’ll see about that.
  • These are “as money allows” goals. I’m sure my wife is getting tired of me spending money on hardware. Then again, she does appreciate that I have a hobby that keeps me off the streets and out of the brothels.
    • Continue looking for consumer level, but sufficiently robust NAS solutions featuring RAID5 mirroring and access via secure and/or open protocols (ssh, smb, rsync, etc.) to replace (or augment) the removable drives I’m using now. No Windows-Only solutions please.
    • Evaluate cloud based storage for off-site backups. I’m looking at SpiderOak right now at the recommendation of a friend. I like their zero-knowledge solution and pricing, but more research is required. We’ll have upwards of 500GB of storage requirements, so we’ll have to weigh the monetary costs of cloud storage and bandwidth usage carefully against the risk of my solution failing when (!if) I need to restore. For the moment, I’m relatively comfortable with dumping the filesystems to removable drives, and keeping certain ultra-critical bits of recovery text (bsdlabels, fstabs, choice config files, etc.) in Google Docs.

My Environment

Two physically identical servers built from the ground up running FreeBSD 8.1. Each system houses a 150GB system drive (/dev/ad4s1) and a 500GB data/storage drive (/dev/ad6s1), and runs with 2GB of RAM.

I have /, /usr and /var mounted individually on the 150GB drive, and /home (containing /users and /www) mounted on the 500GB drive. I thought about getting separate drives for /www and /home, but decided I didn’t want to deal with planning for storage allocation. Instead I created /home/www for web files, and /home/users for user accounts. It’s not exactly standard, but it’s not unprecedented, and I make it work.

Off my “production” server I’ve hung a 2.5″ 320GB USB2.0 removable drive. Off my “development” server I’ve hung a 2.5″ 100GB USB2.0 removable drive. I’ll adjust the size of the drives as needed. That’s just what I had on hand. Both were UFS formatted using fdisk.

During the backup job, those drives are mounted at /backup. The rest of the time they’re plugged in, but not mounted.

The Script

You’re welcome to this, but be warned, if it borks up your machine, destroys your pr0n collection, or sends terrifying space monkeys into your engine room, don’t blame me. Use at your own risk. There, now that I’m all disclaimed…

#!/usr/local/bin/bash

# Much appreciation to David Andrzejewski, and the work he started at
# http://www.davidandrzejewski.com/2010/03/01/freebsd-backup-using-dump-and-duplicity/
# I'm sure his current script/processes far outstrip this, but this
# is my (f)stab at it

# Version: 0.5
# * Provides 1 set of full backups and 6 associated incrementals
# * Backup files stored on mounted USB drive only

# I would like to see...
# * Writing to NAS with RAID5 and standard access (ssh, SMB, etc.)
# * Retrieve the cloud based storage interaction I stripped out

# DUMPLVL: provided via a command line flag ${1})
# WEEKDAY: provided via a command line flag ${2})

# HOSTNAME: The host being backed up. Used in informational messages
HOSTNAME=$( hostname )

# FSLIST: The list of file systems that will be dumped along with the
# name of the dump Example: /dev/ad4s1a=root will dump the /dev/ad4s1a
# volume and name it DDD.root.dump.levelN.bz2 where "N" is the dump level
# and "DDD" is the weekday
FSLIST="/dev/ad4s1a=root /dev/ad4s1d=var /dev/ad4s1f=usr /dev/ad6s1d=home"

# BSDLABEL_PARTITIONS: The list of partitions to run `bsdlabel` on
# This will be saved in the backup directory during runtime as
# ${WEEKDAY}.bsdlabel_${PARTITION}.txt
BSDLABEL_PARTITIONS="ad4s1 ad6s1"

# DUMPDEVICE: The location the files will be dumped to
DUMPDEVICE=sosaria:/home/dumps/${HOSTNAME}

# DUMPDIR: The directory that the dumps will be written to
DUMPDIR=/backup

# STAGINGDIR: The directory where dumps are stored before being written
# to ${DUMPDIR}
STAGINGDIR=/home/dumps/stage

# ARCHIVEDIR: The local directory dumps are stored after being written
# to ${DUMPDIR}
ARCHIVEDIR=/home/dumps/${HOSTNAME}

# NODUMP_DIRS: List of directories to set the nodump flag
NODUMP_DIRS="/usr/ports /usr/obj /usr/src /home/www/logs /home/www/src /home/dumps"

# DUMPCACHE: The amount of memory to give dump
DUMPCACHE=32

# DUMPFLAGS: The flags to feed dump
DUMPFLAGS="uanL -h 0 -f"

# FSTYPE: The filesystem type of the mounted partition
FSTYPE=nfs

# These should be standard

# BSDLABELCMD: The bsdlabel command
BSDLABELCMD=/sbin/bsdlabel

# DUMPCMD: The dump command
DUMPCMD=/sbin/dump

# MOUNTCMD: The mount command
MOUNTCMD=/sbin/mount

# UMOUNTCMD: The mount command
UMOUNTCMD=/sbin/umount

##---------------------------------------------------------------------
# Shouldn't have to edit anything below here

# Get the start time so we can gauge how long this is taking. Useful in
# tweaking ${DUMPCACHE}
START=$( date +%s )

# Get the directory we're running from
SCRIPTDIR=$( dirname $0 )

cd ${SCRIPTDIR}
if [ $? -ne 0 ]; then
       echo "ERROR: Unable to cd to ${SCRIPTDIR}! Aborting!"
       exit 1
fi

# If we were executed like "./whatever.sh" - set SCRIPTDIR to the pwd
if [ "${SCRIPTDIR}" == "." ]; then
       SCRIPTDIR=$( pwd )
fi

echo "Script is running from ${SCRIPTDIR}"

# Check the command line to make sure we have what we need from it
# First check for the dump level
if [ "${1}" == "" ]; then
       echo "Must specify dump level. Aborting!"
       exit
else
       DUMPLVL=${1}
fi

# Sanity check
if [ "${DUMPLVL}" == "" ]; then
       echo "ERROR: For some reason DUMPLVL never got set! Aborting!"
       exit 1
fi

# Then get the weekday name off the command line
if [ "${2}" == "" ]; then
       echo "Must specify weekday name. Aborting!"
       exit
else
       WEEKDAY=${2}
fi

# Sanity check
if [ "${WEEKDAY}" == "" ]; then
       echo "ERROR: For some reason WEEKDAY never got set! Aborting!!"
       exit 1
fi

# Create the flag file so we can't run more than one instance
if [ -f "${SCRIPTDIR}/myself.flg" ]; then
       echo "Script running?! ${SCRIPTDIR}/myself.flg exists! Aborting!"
       exit 1
else
       echo "Touching myself at ${SCRIPTDIR}/myself.flg"
       touch ${SCRIPTDIR}/myself.flg
fi

# Check for the existance of ${STAGINGDIR}
if [ ! -d "${STAGINGDIR}" ]; then
       mkdir ${STAGINGDIR}
       if [ $? = 1 ]; then
               echo "Could not create ${STAGINGDIR}!  Aborting!"
               echo "Removing ${SCRIPTDIR}/myself.flg"
               rm -f ${SCRIPTDIR}/myself.flg
               exit 1
       fi
fi

# Check for the existance of ${ARCHIVEDIR}
if [ ! -d "${ARCHIVEDIR}" ]; then
       mkdir ${ARCHIVEDIR}
       if [ $? = 1 ]; then
               echo "Could not create ${ARCHIVEDIR}!  Aborting!"
               echo "Removing ${SCRIPTDIR}/myself.flg"
               rm -f ${SCRIPTDIR}/myself.flg
               exit 1
       fi
fi

echo ""
for DIR in ${NODUMP_DIRS}; do
       echo "Setting nodump on ${DIR}"
       chflags -R nodump ${DIR}
done

echo ""
echo "Dump Level: ${DUMPLVL}"

# Preserve a copy of root's crontab (/root/crontab is
# manually created with `crontab -l > ~/crontab` with every change
echo ""
echo "Copying /root/crontab to ${STAGINGDIR}/${WEEKDAY}.root_crontab"
cp -f /root/crontab ${STAGINGDIR}/${WEEKDAY}.root_crontab

# Preserve a copy of fstab
echo "Copying fstab to ${STAGINGDIR}/${WEEKDAY}.fstab.txt"
cp -f /etc/fstab ${STAGINGDIR}/${WEEKDAY}.fstab.txt

# Preserve a week's worth of bsdlabel copies for each partition
for PARTITION in ${BSDLABEL_PARTITIONS}; do
       echo "Writing bsdlabel for ${PARTITION} -> ${STAGINGDIR}/${WEEKDAY}.bsdlabel_${PARTITION}.txt"
       ${BSDLABELCMD} ${PARTITION} > ${STAGINGDIR}/${WEEKDAY}.bsdlabel_${PARTITION}.txt
done

# Dump the filesystems!
for FSITEM in ${FSLIST}; do
       # Get the devicename
       FS=$( echo ${FSITEM} | awk -F= '{ print $1 }' )
       # Get the filesystem name
       NAME=$( echo ${FSITEM} | awk -F= '{ print $2 }' )
       DUMPFILE=${WEEKDAY}.${NAME}.level${DUMPLVL}.dump
       echo ""
       echo "Dumping ${FS} to ${STAGINGDIR}/${DUMPFILE} at dump level ${DUMPLVL}"
       echo ""
       echo "${DUMPCMD} -C${DUMPCACHE} -${DUMPLVL}${DUMPFLAGS} ${STAGINGDIR}/${DUMPFILE} ${FS}"
       ${DUMPCMD} -C${DUMPCACHE} -${DUMPLVL}${DUMPFLAGS} ${STAGINGDIR}/${DUMPFILE} ${FS}
done

# Test for an existing backup device mount and either use the existing
# mountpoint or mount our backup directory

MOUNTRESULTS=$( ${MOUNTCMD} | grep "${DUMPDEVICE} on ${DUMPDIR}" )

if [ "${MOUNTRESULTS}" == "" ]; then
       echo ""
       echo "Mounting ${DUMPDEVICE} on ${DUMPDIR}"
       ${MOUNTCMD} -t ${FSTYPE} ${DUMPDEVICE} ${DUMPDIR}
       if [ $? = 1 ]; then
               echo "  ... failed. Aborting!"
               echo "Removing ${SCRIPTDIR}/myself.flg"
               rm -f ${SCRIPTDIR}/myself.flg
               exit 1
       else
               echo "  ... succeeded"
       fi
else
       echo "${HOSTNAME}:${DUMPDEVICE} already mounted on ${DUMPDIR}"
fi

# Copy the files to ${DUMPDIR} and archive them to {$ARCHIVEDIR}
cd ${STAGINGDIR}
echo ""
for FILE in *; do
       echo "Copying ${FILE} to ${DUMPDIR}"
       cp ${FILE} ${DUMPDIR}/${FILE}
       if [ $? = 1 ]; then
               echo "... Failed to copy ${FILE}! You might want to see to that."
       else
               echo "Moving ${FILE} to ${ARCHIVEDIR}"
               mv ${FILE} ${ARCHIVEDIR}/${FILE}
       fi
done

# Get a snapshot of how the dump directory looks for verification
echo ""
echo "Recent Additions to ${DUMPDIR}:"
echo ""
ls -lt ${DUMPDIR} | tail -n +2 | head -n 8

# Umount the backup filesystem
echo ""
echo "Unmounting ${DUMPDIR}"
${UMOUNTCMD} ${DUMPDIR}
if [ $? = 1 ]; then
       echo "  ... failed. You might want to see to that."
else
       echo "  ... succeeded"
fi

# Clear the running flag
echo ""
echo "Removing ${SCRIPTDIR}/myself.flg"
rm -f ${SCRIPTDIR}/myself.flg
if [ -f "${SCRIPTDIR}/myself.flg" ]; then
       echo "  ... failed. You might want to see to that."
else
       echo "  ... succeeded"
fi

echo ""
echo "Backup of ${HOSTNAME} Complete"

END=$( date +%s )
RUNTIME=$(( ${END} - ${START} ))
H=$(( ${RUNTIME}/3600 ))
M=$(( ( ${RUNTIME}/60 ) % 60 ))
S=$(( ${RUNTIME} % 60 ))

echo "It took ${H} hrs, ${M} mins and ${S} secs with -C${DUMPCACHE} (${RUNTIME} secs)"
exit 0

The Crontab

Here’s how I’ve set up my crontab. Like Mr. Andrzejewski, I opted to keep the specifics regarding the type of backup and the day it’s run in cron, rather than build it into the script. While it does make for a slightly longer crontab, it simplifies the logic in the script considerably. At the end of the day, I just feel better about telling the script what kind of backup to run (full or incremental), and the weekday name to embed in the resulting filenames, rather than letting it determine it itself. It’s a control thing.

# Daily Backups of filesystems
# Full backups on Sunday. Incremental backups every other day.
30 0 * * 0 /root/bin/backup/backup_script.sh 0 Sun 2>&1 /dev/null | mail -s "System Backup" dvicci
30 0 * * 1 /root/bin/backup/backup_script.sh 1 Mon 2>&1 /dev/null | mail -s "System Backup" dvicci
30 0 * * 2 /root/bin/backup/backup_script.sh 1 Tue 2>&1 /dev/null | mail -s "System Backup" dvicci
30 0 * * 3 /root/bin/backup/backup_script.sh 1 Wed 2>&1 /dev/null | mail -s "System Backup" dvicci
30 0 * * 4 /root/bin/backup/backup_script.sh 1 Thu 2>&1 /dev/null | mail -s "System Backup" dvicci
30 0 * * 5 /root/bin/backup/backup_script.sh 1 Fri 2>&1 /dev/null | mail -s "System Backup" dvicci
30 0 * * 6 /root/bin/backup/backup_script.sh 1 Sat 2>&1 /dev/null | mail -s "System Backup" dvicci

This will finally result in a list of files looking something like this come Sunday morning. Sort to taste.

backup/Sat.usr.level1.dump
backup/Sat.var.level1.dump
backup/Sat.root.level1.dump
backup/Sat.fstab.txt
backup/Sat.bsdlabel_ad6s1.txt
backup/Sat.bsdlabel_ad4s1.txt
backup/Sat.root_crontab.txt
backup/Fri.home.level1.dump
backup/Fri.usr.level1.dump
backup/Fri.var.level1.dump
backup/Fri.root.level1.dump
backup/Fri.fstab.txt
backup/Fri.bsdlabel_ad6s1.txt
backup/Fri.bsdlabel_ad4s1.txt
backup/Fri.root_crontab.txt
backup/Thu.home.level1.dump
backup/Thu.usr.level1.dump
backup/Thu.var.level1.dump
backup/Thu.root.level1.dump
backup/Thu.fstab.txt
backup/Thu.bsdlabel_ad6s1.txt
backup/Thu.bsdlabel_ad4s1.txt
backup/Thu.root_crontab.txt
backup/Wed.home.level1.dump
backup/Wed.usr.level1.dump
backup/Wed.var.level1.dump
backup/Wed.root.level1.dump
backup/Wed.fstab.txt
backup/Wed.bsdlabel_ad6s1.txt
backup/Wed.bsdlabel_ad4s1.txt
backup/Wed.root_crontab.txt
backup/Tue.home.level1.dump
backup/Tue.usr.level1.dump
backup/Tue.var.level1.dump
backup/Tue.root.level1.dump
backup/Tue.fstab.txt
backup/Tue.bsdlabel_ad6s1.txt
backup/Tue.bsdlabel_ad4s1.txt
backup/Tue.root_crontab.txt
backup/Mon.home.level1.dump
backup/Mon.usr.level1.dump
backup/Mon.var.level1.dump
backup/Mon.root.level1.dump
backup/Mon.fstab.txt
backup/Mon.bsdlabel_ad6s1.txt
backup/Mon.bsdlabel_ad4s1.txt
backup/Mon.root_crontab.txt
backup/Sun.home.level0.dump
backup/Sun.usr.level0.dump
backup/Sun.var.level0.dump
backup/Sun.root.level1.dump
backup/Sun.fstab.txt
backup/Sun.bsdlabel_ad6s1.txt
backup/Sun.bsdlabel_ad4s1.txt
backup/Sun.root_crontab.txt

14 thoughts on “Near Native FreeBSD Full and Incremental Backups to a Removable USB Storage Drive

  1. When I was a consultant sysadmin in charge of backups, we had one network attached storage volume (basically, a bunch of space on a Samba share). I opted for monthly fulls and nightly incrementals. It went something like this:

    A file called “nobackup.txt” contained a list of subdirs I didn’t want backed up.
    ^/tmp
    ^/dev
    ^/proc
    ^/var/tmp
    ^/backups

    Monthly, a full backup with tar:
    find / | grep -vf /backups/nobackup.txt | xargs tar czf `date +%Y%m%d`-full.tar.gz

    Then nightly, run something pretty similar, with a flag of -mtime -1, which tells find to show only files modded in the last 24 hours.

    find / -mtime -1 | grep -vf /backups/nobackup.txt | xargs tar czf `date +%Y%m%d`-inc.tar.gz

    Would be simple to make it do weekly fulls, but whatever. Tweak it with cron. Not nearly as elegant, mind you, but it was simple and effective. I had to recover from backups exactly twice, and the second time, I was really wishing I’d have gone with weekly full backups, but it would have eaten into how many months back we could recover.

  2. This is exactly what I was looking for… that said, I failed.
    Do you have any interest in holding a (relatively competent) newbs hand through setting this up? We could do it here or via email if you’d rather not clutter up the comments section.

    • I’d be more than willing to help, and here in the comments, I think, is preferred. That way, perhaps our conversation will benefit others. What seems to be the problem? Be as descriptive and detailed as possible.

  3. You sir, rock! Thank you!

    Well to start with, a lot of the errors I was seeing yesterday stemmed from the fact that I was being stupid… I was trying to test the script by running ./backup_script.sh, and needless to say it wasn’t receiving all of the vars that are provided via the command line examples you have above from your crontab.

    OK, so that squared away I was able to successfully test the script, and it (for the most part) worked great!

    I am seeing a little weirdness, but I know that it’s just my lack of BSD experience, so hopefully you’ll see what it is I’m doing wrong right away!

    I’ll explain my setup first, then add two more post, one with my config and one with the output.

    Server running the script is a FreeBSD 8.2 box, backing up to a FreeNAS 0.7.2 (FreeBSD 7.2) server via NFS.
    On my 8.2 box (hostname=hermes) I created /home/dumps (for stage and archive dirs) and created the mount /backup to my NFS export. These work fine. The script does create hermes.mydomain.tld inside /home/dumps for archiving, but does not move the files there… I think this has to do with the fact that I have somehow incorrectly defined my machine under the “HOSTNAME=$( hostname )” section of the script. (you’ll see what I mean at the beginning and end of my output file.

    Also, I’m really not sure how to add the crontab properly (I used ‘sudo crontab -e’) and the script seems to skip over the backing of it up. However it did successfully run last night, so that’s minor as far as I’m concerned.

    OK, I’ll now post my config and output… again, thank you so much for helping me!

  4. Script:

    #!/usr/local/bin/bash

    # Adapted from: http://www.dvicci.com/code/near-native-freebsd-full-and-incremental-backups-to-a-removable-usb-storage-drive
    # Much Thanks!

    # Version: 0.1

    # DUMPLVL: provided via a command line flag ${1})
    # WEEKDAY: provided via a command line flag ${2})

    # HOSTNAME: The host being backed up. Used in informational messages
    HOSTNAME=$( hermes )

    # FSLIST: The list of file systems that will be dumped along with the
    # name of the dump Example: /dev/ad4s1a=root will dump the /dev/ad4s1a
    # volume and name it DDD.root.dump.levelN.bz2 where “N” is the dump level
    # and “DDD” is the weekday
    FSLIST=”/dev/ad4s1a=root /dev/ad4s1d=var /dev/ad4s1f=usr”

    # BSDLABEL_PARTITIONS: The list of partitions to run `bsdlabel` on
    # This will be saved in the backup directory during runtime as
    # ${WEEKDAY}.bsdlabel_${PARTITION}.txt
    BSDLABEL_PARTITIONS=”ad4s1″

    # DUMPDEVICE: The location the files will be dumped to
    DUMPDEVICE=orion:/mnt/zpool/BackUps/dumps/${HOSTNAME}

    # DUMPDIR: The directory that the dumps will be written to
    DUMPDIR=/backup

    # STAGINGDIR: The directory where dumps are stored before being written
    # to ${DUMPDIR}
    STAGINGDIR=/home/dumps/stage

    # ARCHIVEDIR: The local directory dumps are stored after being written
    # to ${DUMPDIR}
    ARCHIVEDIR=/home/dumps/${HOSTNAME}

    # NODUMP_DIRS: List of directories to set the nodump flag
    NODUMP_DIRS=”/usr/ports /usr/obj /usr/src /home/dumps”

    # DUMPCACHE: The amount of memory to give dump
    DUMPCACHE=32

    # DUMPFLAGS: The flags to feed dump
    DUMPFLAGS=”uanL -h 0 -f”

    # FSTYPE: The filesystem type of the mounted partition
    FSTYPE=nfs

    # These should be standard

    # BSDLABELCMD: The bsdlabel command
    BSDLABELCMD=/sbin/bsdlabel

    # DUMPCMD: The dump command
    DUMPCMD=/sbin/dump

    # MOUNTCMD: The mount command
    MOUNTCMD=/sbin/mount

    # UMOUNTCMD: The mount command
    UMOUNTCMD=/sbin/umount

    ##———————————————————————
    # Shouldn’t have to edit anything below here

    # Get the start time so we can gauge how long this is taking. Useful in
    # tweaking ${DUMPCACHE}
    START=$( date +%s )

    # Get the directory we’re running from
    SCRIPTDIR=$( dirname $0 )

    cd ${SCRIPTDIR}
    if [ $? -ne 0 ]; then
    echo “ERROR: Unable to cd to ${SCRIPTDIR}! Aborting!”
    exit 1
    fi

    # If we were executed like “./whatever.sh” – set SCRIPTDIR to the pwd
    if [ "${SCRIPTDIR}" == "." ]; then
    SCRIPTDIR=$( pwd )
    fi

    echo “Script is running from ${SCRIPTDIR}”

    # Check the command line to make sure we have what we need from it
    # First check for the dump level
    if [ "${1}" == "" ]; then
    echo “Must specify dump level. Aborting!”
    exit
    else
    DUMPLVL=${1}
    fi

    # Sanity check
    if [ "${DUMPLVL}" == "" ]; then
    echo “ERROR: For some reason DUMPLVL never got set! Aborting!”
    exit 1
    fi

    # Then get the weekday name off the command line
    if [ "${2}" == "" ]; then
    echo “Must specify weekday name. Aborting!”
    exit
    else
    WEEKDAY=${2}
    fi

    # Sanity check
    if [ "${WEEKDAY}" == "" ]; then
    echo “ERROR: For some reason WEEKDAY never got set! Aborting!!”
    exit 1
    fi

    # Create the flag file so we can’t run more than one instance
    if [ -f "${SCRIPTDIR}/myself.flg" ]; then
    echo “Script running?! ${SCRIPTDIR}/myself.flg exists! Aborting!”
    exit 1
    else
    echo “Touching myself at ${SCRIPTDIR}/myself.flg”
    touch ${SCRIPTDIR}/myself.flg
    fi

    # Check for the existance of ${STAGINGDIR}
    if [ ! -d "${STAGINGDIR}" ]; then
    mkdir ${STAGINGDIR}
    if [ $? = 1 ]; then
    echo “Could not create ${STAGINGDIR}! Aborting!”
    echo “Removing ${SCRIPTDIR}/myself.flg”
    rm -f ${SCRIPTDIR}/myself.flg
    exit 1
    fi
    fi

    # Check for the existance of ${ARCHIVEDIR}
    if [ ! -d "${ARCHIVEDIR}" ]; then
    mkdir ${ARCHIVEDIR}
    if [ $? = 1 ]; then
    echo “Could not create ${ARCHIVEDIR}! Aborting!”
    echo “Removing ${SCRIPTDIR}/myself.flg”
    rm -f ${SCRIPTDIR}/myself.flg
    exit 1
    fi
    fi

    echo “”
    for DIR in ${NODUMP_DIRS}; do
    echo “Setting nodump on ${DIR}”
    chflags -R nodump ${DIR}
    done

    echo “”
    echo “Dump Level: ${DUMPLVL}”

    # Preserve a copy of root’s crontab (/root/crontab is
    # manually created with `crontab -l > ~/crontab` with every change
    echo “”
    echo “Copying /root/crontab to ${STAGINGDIR}/${WEEKDAY}.root_crontab”
    cp -f /root/crontab ${STAGINGDIR}/${WEEKDAY}.root_crontab

    # Preserve a copy of fstab
    echo “Copying fstab to ${STAGINGDIR}/${WEEKDAY}.fstab.txt”
    cp -f /etc/fstab ${STAGINGDIR}/${WEEKDAY}.fstab.txt

    # Preserve a week’s worth of bsdlabel copies for each partition
    for PARTITION in ${BSDLABEL_PARTITIONS}; do
    echo “Writing bsdlabel for ${PARTITION} -> ${STAGINGDIR}/${WEEKDAY}.bsdlabel_${PARTITION}.txt”
    ${BSDLABELCMD} ${PARTITION} > ${STAGINGDIR}/${WEEKDAY}.bsdlabel_${PARTITION}.txt
    done

    # Dump the filesystems!
    for FSITEM in ${FSLIST}; do
    # Get the devicename
    FS=$( echo ${FSITEM} | awk -F= ‘{ print $1 }’ )
    # Get the filesystem name
    NAME=$( echo ${FSITEM} | awk -F= ‘{ print $2 }’ )
    DUMPFILE=${WEEKDAY}.${NAME}.level${DUMPLVL}.dump
    echo “”
    echo “Dumping ${FS} to ${STAGINGDIR}/${DUMPFILE} at dump level ${DUMPLVL}”
    echo “”
    echo “${DUMPCMD} -C${DUMPCACHE} -${DUMPLVL}${DUMPFLAGS} ${STAGINGDIR}/${DUMPFILE} ${FS}”
    ${DUMPCMD} -C${DUMPCACHE} -${DUMPLVL}${DUMPFLAGS} ${STAGINGDIR}/${DUMPFILE} ${FS}
    done

    # Test for an existing backup device mount and either use the existing
    # mountpoint or mount our backup directory

    MOUNTRESULTS=$( ${MOUNTCMD} | grep “${DUMPDEVICE} on ${DUMPDIR}” )

    if [ "${MOUNTRESULTS}" == "" ]; then
    echo “”
    echo “Mounting ${DUMPDEVICE} on ${DUMPDIR}”
    ${MOUNTCMD} -t ${FSTYPE} ${DUMPDEVICE} ${DUMPDIR}
    if [ $? = 1 ]; then
    echo ” … failed. Aborting!”
    echo “Removing ${SCRIPTDIR}/myself.flg”
    rm -f ${SCRIPTDIR}/myself.flg
    exit 1
    else
    echo ” … succeeded”
    fi
    else
    echo “${HOSTNAME}:${DUMPDEVICE} already mounted on ${DUMPDIR}”
    fi

    # Copy the files to ${DUMPDIR} and archive them to {$ARCHIVEDIR}
    cd ${STAGINGDIR}
    echo “”
    for FILE in *; do
    echo “Copying ${FILE} to ${DUMPDIR}”
    cp ${FILE} ${DUMPDIR}/${FILE}
    if [ $? = 1 ]; then
    echo “… Failed to copy ${FILE}! You might want to see to that.”
    else
    echo “Moving ${FILE} to ${ARCHIVEDIR}”
    mv ${FILE} ${ARCHIVEDIR}/${FILE}
    fi
    done

    # Get a snapshot of how the dump directory looks for verification
    echo “”
    echo “Recent Additions to ${DUMPDIR}:”
    echo “”
    ls -lt ${DUMPDIR} | tail -n +2 | head -n 8

    # Umount the backup filesystem
    echo “”
    echo “Unmounting ${DUMPDIR}”
    ${UMOUNTCMD} ${DUMPDIR}
    if [ $? = 1 ]; then
    echo ” … failed. You might want to see to that.”
    else
    echo ” … succeeded”
    fi

    # Clear the running flag
    echo “”
    echo “Removing ${SCRIPTDIR}/myself.flg”
    rm -f ${SCRIPTDIR}/myself.flg
    if [ -f "${SCRIPTDIR}/myself.flg" ]; then
    echo ” … failed. You might want to see to that.”
    else
    echo ” … succeeded”
    fi

    echo “”
    echo “Backup of ${HOSTNAME} Complete”

    END=$( date +%s )
    RUNTIME=$(( ${END} – ${START} ))
    H=$(( ${RUNTIME}/3600 ))
    M=$(( ( ${RUNTIME}/60 ) % 60 ))
    S=$(( ${RUNTIME} % 60 ))

    echo “It took ${H} hrs, ${M} mins and ${S} secs with -C${DUMPCACHE} (${RUNTIME} secs)”
    exit 0

  5. Output:

    /root/bin/backup/backup_script.sh: line 12: hermes: command not found
    Script is running from /root/bin/backup
    Touching myself at /root/bin/backup/myself.flg

    Setting nodump on /usr/ports
    Setting nodump on /usr/obj
    Setting nodump on /usr/src
    Setting nodump on /home/dumps

    Dump Level: 1

    Copying /root/crontab to /home/dumps/stage/Fri.root_crontab
    cp: /root/crontab: No such file or directory
    Copying fstab to /home/dumps/stage/Fri.fstab.txt
    Writing bsdlabel for ad4s1 -> /home/dumps/stage/Fri.bsdlabel_ad4s1.txt

    Dumping /dev/ad4s1a to /home/dumps/stage/Fri.root.level1.dump at dump level 1

    /sbin/dump -C32 -1uanL -h 0 -f /home/dumps/stage/Fri.root.level1.dump /dev/ad4s1a
    DUMP: Date of this level 1 dump: Fri Jul 29 00:30:58 2011
    DUMP: Date of last level 0 dump: Thu Jul 28 22:30:02 2011
    DUMP: Dumping snapshot of /dev/ad4s1a (/) to /home/dumps/stage/Fri.root.level1.dump
    DUMP: mapping (Pass I) [regular files]
    DUMP: Cache 32 MB, blocksize = 65536
    DUMP: mapping (Pass II) [directories]
    DUMP: estimated 83 tape blocks.
    DUMP: dumping (Pass III) [directories]
    DUMP: dumping (Pass IV) [regular files]
    DUMP: DUMP: 67 tape blocks on 1 volume
    DUMP: finished in less than a second
    DUMP: level 1 dump on Fri Jul 29 00:30:58 2011
    DUMP: Closing /home/dumps/stage/Fri.root.level1.dump
    DUMP: DUMP IS DONE

    Dumping /dev/ad4s1d to /home/dumps/stage/Fri.var.level1.dump at dump level 1

    /sbin/dump -C32 -1uanL -h 0 -f /home/dumps/stage/Fri.var.level1.dump /dev/ad4s1d
    DUMP: Date of this level 1 dump: Fri Jul 29 00:31:02 2011
    DUMP: Date of last level 0 dump: Thu Jul 28 22:30:22 2011
    DUMP: Dumping snapshot of /dev/ad4s1d (/var) to /home/dumps/stage/Fri.var.level1.dump
    DUMP: mapping (Pass I) [regular files]
    DUMP: Cache 32 MB, blocksize = 65536
    DUMP: mapping (Pass II) [directories]
    DUMP: estimated 471 tape blocks.
    DUMP: dumping (Pass III) [directories]
    DUMP: dumping (Pass IV) [regular files]
    DUMP: DUMP: 375 tape blocks on 1 volume
    DUMP: finished in less than a second
    DUMP: level 1 dump on Fri Jul 29 00:31:02 2011
    DUMP: Closing /home/dumps/stage/Fri.var.level1.dump
    DUMP: DUMP IS DONE

    Dumping /dev/ad4s1f to /home/dumps/stage/Fri.usr.level1.dump at dump level 1

    /sbin/dump -C32 -1uanL -h 0 -f /home/dumps/stage/Fri.usr.level1.dump /dev/ad4s1f
    DUMP: Date of this level 1 dump: Fri Jul 29 00:40:47 2011
    DUMP: Date of last level 0 dump: Thu Jul 28 22:40:33 2011
    DUMP: Dumping snapshot of /dev/ad4s1f (/usr) to /home/dumps/stage/Fri.usr.level1.dump
    DUMP: mapping (Pass I) [regular files]
    DUMP: Cache 32 MB, blocksize = 65536
    DUMP: mapping (Pass II) [directories]
    DUMP: estimated 21838 tape blocks.
    DUMP: dumping (Pass III) [directories]
    DUMP: dumping (Pass IV) [regular files]
    DUMP: DUMP: 14566 tape blocks on 1 volume
    DUMP: finished in 1 seconds, throughput 14566 KBytes/sec
    DUMP: level 1 dump on Fri Jul 29 00:40:47 2011
    DUMP: Closing /home/dumps/stage/Fri.usr.level1.dump
    DUMP: DUMP IS DONE

    Mounting orion:/mnt/zpool/BackUps/dumps/ on /backup
    … succeeded

    Copying Fri.bsdlabel_ad4s1.txt to /backup
    Moving Fri.bsdlabel_ad4s1.txt to /home/dumps/
    Copying Fri.fstab.txt to /backup
    Moving Fri.fstab.txt to /home/dumps/
    Copying Fri.root.level1.dump to /backup
    Moving Fri.root.level1.dump to /home/dumps/
    Copying Fri.usr.level1.dump to /backup
    Moving Fri.usr.level1.dump to /home/dumps/
    Copying Fri.var.level1.dump to /backup
    Moving Fri.var.level1.dump to /home/dumps/

    Recent Additions to /backup:

    -rw-r–r– 1 root wheel 378880 Jul 29 00:45 Fri.var.level1.dump
    -rw-r–r– 1 root wheel 14909440 Jul 29 00:45 Fri.usr.level1.dump
    -rw-r–r– 1 root wheel 61440 Jul 29 00:45 Fri.root.level1.dump
    -rw-r–r– 1 root wheel 201 Jul 29 00:45 Fri.fstab.txt
    -rw-r–r– 1 root wheel 441 Jul 29 00:45 Fri.bsdlabel_ad4s1.txt
    -rw-r–r– 1 root wheel 130078720 Jul 28 22:50 Sun.var.level0.dump
    -rw-r–r– 1 root wheel 1392558080 Jul 28 22:50 Sun.usr.level0.dump
    -rw-r–r– 1 root wheel 178944000 Jul 28 22:48 Sun.root.level0.dump

    Unmounting /backup
    … succeeded

    Removing /root/bin/backup/myself.flg
    … succeeded

    Backup of Complete
    It took 0 hrs, 15 mins and 29 secs with -C32 (929 secs)

  6. @Ted No problem, man. A quick look over the output looks fine except for two things…
    1) The hostname variable should actually be left alone. The syntax “$( )” executes the command within the parens, so in your case, it’s trying to execute the command `hermes` which doesn’t exist. Set that back to “$( hostname )” and you’ll be fine. That should fix the file copy issues as well. Make sure the directory you create matches the output of `hostname`.
    2) The error “cp: /root/crontab: No such file or directory” means simply that the file “/root/crontab” doesn’t exist. I copy the root crontab manually whenever I make a change to it (just part of my routine) with `crontab -l > /root/crontab`. If you do that, the command in the script will succeed.
    Let me know if I’ve missed something… I have a very full and distracting houseful of guests at the moment.

  7. D’OH! Man, I swore that I had ‘hostname’ there, but I bet it was when I was screwing up in other ways that I changed it, then totally overlooked it until you just pointed it out!;-) I put it back in and it works as expected now, of course! I should have realized/seen that, sorry.

    I saw your comment in the code about ‘crontab -l > /root/crontab’, but kept getting permission denied, even with sudo… Then I realized I needed to sudo su to do that, bam!

    Sorry man, newb mistakes indeed… Thank you so much for the help and for posting this great script, it was exactly what I wanted!

    Have a great weekend!
    // Ted

    • No problem, man! It’s always the little things. I’m just glad something I put together (with inspiration from others, as well – :on the shoulders of giants” after all). Best of luck with it, and let me know if you have any other questions (or even improvements).

  8. I think the next thing I’m going to do is have another script run every Sunday before the backup that tar’s up and compresses the previous week’s backups and puts them on the NAS before overwriting them with the new week. I mean, I’ve got the storage, I might as well backup my backups.

    I probably won’t keep them all forever, maybe just a few months worth, but that way if need be I can step a little further back in time. (Sometimes it takes me a while before I notice that I’ve screwed something up!)

    After I hammer something together, I’ll shoot it back your way if you’re interest?

    • Sure I’m interested… when you get a working setup running, post back here how you did it. I originally had something similar, but slimmed it down for space reasons (I don’t have that much space to deal with at the moment, though I could easily given current prices).

  9. Hey Dave,

    So I whipped something together, and I hope you don’t mind, but I basically recycled most of the script you have above…

    I wanted something that was configurable in the same way and gave me basically the same sort of informational output as yours, so for the most part I just renamed a few variables, dropped the dump section of the script and added the tar.

    I thought for half a minute about just adding the tar routine to your original script, but then 86′ed the idea on the premiss that the correct operation of the dump functionality was vastly more important than the long term archiving of said dumps, so if for whatever reason the tar section borked the operation all together, I still want my dumps to happen.

    That said, of course feel free to do with it whatever you want… and again, I hope you don’t mind and thank you so much for the building block!

    // Ted

    PS- I may at some point add some sort of house keeping to it to remove tars over a certain age, but at this point I just plan on dealing with that via cron on backup server. If I do tho, I’ll be sure to push that back your way as well.

    Thank you, thank you, thank you! This has all turned out to be a perfect fit for my needs!

  10. #!/usr/local/bin/bash

    # TAR Dumps and Archive

    # The bulk of this script has been recycled from Dave Veatch’s wonderful
    # dump script, which you can find at http://www.dvicci.com under the title:
    # Near Native FreeBSD Full and Incremental Backups.

    # I followed his format in hopes of creating somewhat of a compainion
    # script to archive the weeks worth of dumps made by it and
    # that would hopefully preserver look, configuration and output.

    # Version: 0.3

    # HOSTNAME: The host being backed up. Used in informational messages
    HOSTNAME=$( hostname )

    # BUDEVICE: The location the files will be backedup to
    BUDEVICE=orion:/mnt/zpool/BackUps/dumps/${HOSTNAME}

    # STAGINGDIR: The directory where tars are stored before being written
    # to ${BUMNT}
    STAGINGDIR=/home/dumps/stage

    # ARCHIVEDIR: The local directory dumps are stored
    ARCHIVEDIR=/home/dumps/${HOSTNAME}

    # BUMNT: The directory (mnt) that the backups will be written to
    BUMNT=/backup

    # FSTYPE: The filesystem type of the mounted partition
    FSTYPE=nfs

    # TARFLAGS: The flags to feed tar
    TARFLAGS=czfv

    # Sets date for backup logging variable
    DATE=$( date +%Y%m%d )

    # These should be standard

    # TARCMD: The tar command
    TARCMD=/usr/bin/tar

    # MOUNTCMD: The mount command
    MOUNTCMD=/sbin/mount

    # UMOUNTCMD: The mount command
    UMOUNTCMD=/sbin/umount

    ##———————————————————————
    # Shouldn’t have to edit anything below here

    # Get the start time so we can gauge how long this is taking.
    START=$( date +%s )

    # Get the directory we’re running from
    SCRIPTDIR=$( dirname $0 )

    cd ${SCRIPTDIR}
    if [ $? -ne 0 ]; then
    echo “ERROR: Unable to cd to ${SCRIPTDIR}! Aborting!”
    exit 1
    fi

    # If we were executed like “./whatever.sh” – set SCRIPTDIR to the pwd
    if [ "${SCRIPTDIR}" == "." ]; then
    SCRIPTDIR=$( pwd )
    fi

    echo “Script is running from ${SCRIPTDIR}”

    # Create the flag file so we can’t run more than one instance
    if [ -f "${SCRIPTDIR}/tarmyself.flg" ]; then
    echo “Script running?! ${SCRIPTDIR}/tarmyself.flg exists! Aborting!”
    exit 1
    else
    echo “Touching myself at ${SCRIPTDIR}/tarmyself.flg”
    touch ${SCRIPTDIR}/tarmyself.flg
    fi

    # Check for the existance of ${STAGINGDIR}
    if [ ! -d "${STAGINGDIR}" ]; then
    mkdir ${STAGINGDIR}
    if [ $? = 1 ]; then
    echo “Could not create ${STAGINGDIR}! Aborting!”
    echo “Removing ${SCRIPTDIR}/tarmyself.flg”
    rm -f ${SCRIPTDIR}/tarmyself.flg
    exit 1
    fi
    fi

    # TAR last weeks dumps
    echo “”
    echo “Taring ${ARCHIVEDIR} to ${STAGINGDIR}”
    echo “”
    echo “${TARCMD} -${TARFLAGS} ${STAGINGDIR}/${HOSTNAME}.dumpbak.${DATE}.tar.gz ${ARCHIVEDIR}”
    ${TARCMD} ${TARFLAGS} ${STAGINGDIR}/${HOSTNAME}.dumpbak.${DATE}.tar.gz ${ARCHIVEDIR}

    # Test for an existing backup device mount and either use the existing
    # mountpoint or mount our backup directory

    MOUNTRESULTS=$( ${MOUNTCMD} | grep “${BUDEVICE} on ${BUMNT}” )

    if [ "${MOUNTRESULTS}" == "" ]; then
    echo “”
    echo “Mounting ${BUDEVICE} on ${BUMNT}”
    ${MOUNTCMD} -t ${FSTYPE} ${BUDEVICE} ${BUMNT}
    if [ $? = 1 ]; then
    echo ” … failed. Aborting!”
    echo “Removing ${SCRIPTDIR}/tarmyself.flg”
    rm -f ${SCRIPTDIR}/tarmyself.flg
    exit 1
    else
    echo ” … succeeded”
    fi
    else
    echo “${HOSTNAME}:${BUDEVICE} already mounted on ${BUMNT}”
    fi

    # Move tar to ${BUMNT}
    cd ${STAGINGDIR}
    echo “”
    for FILE in *; do
    echo “Moving ${FILE} to ${BUMNT}”
    mv ${FILE} ${BUMNT}/${FILE}
    if [ $? = 1 ]; then
    echo “… Failed to move ${FILE}! You might want to see to that.”
    echo “Removing ${SCRIPTDIR}/tarmyself.flg”
    rm -f ${SCRIPTDIR}/tarmyself.flg
    exit 1
    else
    echo ” … succeeded”
    fi
    done

    # Get a snapshot of how the dump directory looks for verification
    echo “”
    echo “Recent Additions to ${BUMNT}:”
    echo “”
    ls -lth ${BUMNT} | tail -n +2 | head -n 8

    # Umount the backup filesystem
    echo “”
    echo “Unmounting ${BUMNT}”
    ${UMOUNTCMD} ${BUMNT}
    if [ $? = 1 ]; then
    echo ” … failed. You might want to see to that.”
    else
    echo ” … succeeded”
    fi

    # Clear the running flag
    echo “”
    echo “Removing ${SCRIPTDIR}/tarmyself.flg”
    rm -f ${SCRIPTDIR}/tarmyself.flg
    if [ -f "${SCRIPTDIR}/tarmyself.flg" ]; then
    echo ” … failed. You might want to see to that.”
    else
    echo ” … succeeded”
    fi

    echo “”
    echo “The backup of last weeks dumps for ${HOSTNAME} is Complete”

    END=$( date +%s )
    RUNTIME=$(( ${END} – ${START} ))
    H=$(( ${RUNTIME}/3600 ))
    M=$(( ( ${RUNTIME}/60 ) % 60 ))
    S=$(( ${RUNTIME} % 60 ))

    echo “It took ${H} hrs, ${M} mins and ${S} secs – ${RUNTIME} secs”
    exit 0

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>