Automatic ZFS Snapshot Rotation on FreeBSD

OpenSolaris has ZFS Automatic Snapshots; FreeBSD, while it has ZFS, doesn’t have a comparable feature that I’m aware of. So I wrote my own, zfs-snapshot.sh:

#!/usr/local/bin/bash

# Path to ZFS executable:
ZFS=/sbin/zfs

# Parse arguments:
TARGET=$1
SNAP=$2
COUNT=$3

# Function to display usage:
usage() {
    scriptname=`/usr/bin/basename $0`
    echo "$scriptname: Take and rotate snapshots on a ZFS file system"
    echo
    echo "  Usage:"
    echo "  $scriptname target snap_name count"
    echo
    echo "  target:    ZFS file system to act on"
    echo "  snap_name: Base name for snapshots, to be followed by a '.' and"
    echo "             an integer indicating relative age of the snapshot"
    echo "  count:     Number of snapshots in the snap_name.number format to"
    echo "             keep at one time.  Newest snapshot ends in '.0'."
    echo
    exit
}

# Basic argument checks:
if [ -z $COUNT ] ; then
    usage
fi

if [ ! -z $4 ] ; then
    usage
fi

# Snapshots are number starting at 0; $max_snap is the highest numbered
# snapshot that will be kept.
max_snap=$(($COUNT -1))

# Clean up oldest snapshot:
if [ -d /${TARGET}/.zfs/snapshot/${SNAP}.${max_snap} ] ; then
    $ZFS destroy -r ${TARGET}@${SNAP}.${max_snap}
fi

# Rename existing snapshots:
dest=$max_snap
while [ $dest -gt 0 ] ; do
    src=$(($dest - 1))
    if [ -d /${TARGET}/.zfs/snapshot/${SNAP}.${src} ] ; then
	$ZFS rename -r ${TARGET}@${SNAP}.${src} ${TARGET}@${SNAP}.${dest}
    fi
    dest=$(($dest - 1))
done

# Create new snapshot:
$ZFS snapshot -r ${TARGET}@${SNAP}.0

From the command line, call the script something like the following:

./zfs-snapshot.sh tank weekly 5

This would take a recursive snapshot of the “tank” zpool with the basename weekly, rotating through five snapshots with names “weekly.0” through “weekly.4”. This allows you to implement a snapshot scheme approximately similar to NetApp’s hourly-daily-weekly scheme, if you like. Because my FreeBSD workstation isn’t on 24×7, I run hourly snapshots out of /etc/crontab:

# Automated ZFS backups (hourly):
0 * * * * root /root/bin/zfs-snapshot.sh tank hourly 25

And daily/weekly/monthly snapshots out of /usr/local/etc/anacrontab (from the sysutils/anacron port):

PATH=/bin:/sbin:/usr/bin:/usr/sbin

# days		make sure the command is executed at least every 'days' days
# delay		delay in minutes, before a command starts
# id		unique id of a command

# days	delay	id		command
1	5	daily		periodic daily
1	10	daily_snap	/root/bin/zfs-snapshot.sh tank daily 8
7	15	weekly		periodic weekly
7	30	weekly_snap	/root/bin/zfs-snapshot.sh tank weekly 5
30	60	monthly		periodic monthly
30	90	monthly_snap	/root/bin/zfs-snapshot.sh tank monthly 13

(Of course, this isn’t as cool as the Gnome-integrated Time Slider in OpenSolaris, but it scratches my itch sufficiently.)

Advertisement

17 comments

  1. Desper

    Thanks for this script!
    But, can You make change this script to:
    script creates a snapshot of the real date/time?
    Like tank@12:00:00-01-31-11

    Best Regards.

    P.s.
    Sorry for my English 🙂

    • Andy

      @Desper – The challenge there would be associating a particular time stamp with the type of snapshot. Would you require that your dailies/weeklies/monthlies always run at the same time of day and determine your snapshot type from that? Or would you add a prefix or change the naming convention for the non-hourlies?

      (One nice thing about the time stamp is that you could eliminate the zfs rename portion of the script.)

  2. Pingback: 飄狂山莊 | [引用]zfs snapshot script
  3. iMx

    Hi there,

    Am running FreeBSD 8.2 with the v28 pool patch, i think something must have changed in the command syntax – i vaguely remember this when i was running OpenSolaris that zfs list changed…

    cannot create snapshot ‘tank/vol0/users@hourly.0’: dataset already exists
    no snapshots were created

    Looks like the rename is not working, will investigate further…

  4. iMx

    Ah ha, its this bit:

    # Clean up oldest snapshot:
    if [ -d /${TARGET}/.zfs/snapshot/${SNAP}.${max_snap} ] ; then
    $ZFS destroy -r ${TARGET}@${SNAP}.${max_snap}
    fi

    its because i have the zfs mounted differently from the zpool name:

    ie tank/vol0/users@hourly.0

    is mounted as /vol0/users

  5. Ertug Karamatli

    Thanks for the nice script!

    I made it compatible with zfs-fuse which doesn’t have .zfs directories:


    #!/bin/bash
    ##
    # original code: https://andyleonard.com/2010/04/07/automatic-zfs-snapshot-rotation-on-freebsd/
    # 07/17/2011 – ertug: made it compatible with zfs-fuse which doesn't have .zfs directories
    ##
    # Path to ZFS executable:
    ZFS=/usr/local/sbin/zfs
    # Parse arguments:
    TARGET=$1
    SNAP=$2
    COUNT=$3
    # Function to display usage:
    usage() {
    scriptname=`/usr/bin/basename $0`
    echo "$scriptname: Take and rotate snapshots on a ZFS file system"
    echo
    echo " Usage:"
    echo " $scriptname target snap_name count"
    echo
    echo " target: ZFS file system to act on"
    echo " snap_name: Base name for snapshots, to be followed by a '.' and"
    echo " an integer indicating relative age of the snapshot"
    echo " count: Number of snapshots in the snap_name.number format to"
    echo " keep at one time. Newest snapshot ends in '.0'."
    echo
    exit
    }
    # Basic argument checks:
    if [ -z $COUNT ] ; then
    usage
    fi
    if [ ! -z $4 ] ; then
    usage
    fi
    # Snapshots are number starting at 0; $max_snap is the highest numbered
    # snapshot that will be kept.
    max_snap=$(($COUNT 1))
    # Clean up oldest snapshot:
    $ZFS list -t snapshot | grep -q ^${TARGET}@${SNAP}\.${max_snap}
    if [ $? -eq 0 ] ; then
    $ZFS destroy -r ${TARGET}@${SNAP}.${max_snap}
    fi
    # Rename existing snapshots:
    dest=$max_snap
    while [ $dest -gt 0 ] ; do
    src=$(($dest 1))
    $ZFS list -t snapshot | grep -q ^${TARGET}@${SNAP}\.${src}
    if [ $? -eq 0 ] ; then
    $ZFS rename -r ${TARGET}@${SNAP}.${src} ${TARGET}@${SNAP}.${dest}
    fi
    dest=$(($dest 1))
    done
    # Create new snapshot:
    $ZFS snapshot -r ${TARGET}@${SNAP}.0

    view raw

    zfs-snapshot.sh

    hosted with ❤ by GitHub

  6. Andy

    @Ertug – Thanks – and interesting timing. I had just written almost the same thing to work around FreeBSD’s kern/156781 bug (the .zfs/snapshot directory stops working).

  7. Aaron

    @Ertug – Thanks! the current version of zfsonlinux also lacks the .zfs directory. You just saved me a lot of time.

  8. sim

    I’m setting this up now on my home server, it looks like exactly what I was after. Many thanks for your efforts!

  9. John

    Great script and documentation — thanks for all of the time you saved me. Nice to see someone giving back with some KISS code/documentation.

  10. bbt

    For those interested in having the time stamp as the snapshot name, I have modified this script a bit and this is the result. Be aware that it hasn’t been fully tested, so use it at your own risk:

    #! /bin/sh

    # Path to ZFS executable:
    ZFS=`which zfs`

    # Parse arguments:
    TARGET=$1
    S_TYPE=$2
    COUNT=$3

    # Function to display usage:
    usage()
    {
    scriptname=`/usr/bin/basename $0`
    echo “$scriptname: Take and rotate snapshots on a ZFS file system”
    echo
    echo ” Usage:”
    echo ” $scriptname target snap_type count”
    echo
    echo ” target: ZFS file system to act on”
    echo ” snap_type: Internal identifier for snapshots of this kind”
    echo ” (e.g. hourly, daily, monthly, …)”
    echo ” count: Number of snapshots of the type to keep at one time”
    echo
    exit
    }

    # Basic argument checks:
    if [ -z $COUNT ]; then
    usage
    fi

    if [ ! -z $4 ]; then
    usage
    fi

    # Create new snapshot using current time stamp:
    DATE=`date ‘+%Y-%m-%d %H.%M.%S’`
    $ZFS snapshot -r -o snapshot:type=$S_TYPE “$TARGET@$DATE”

    # Get list of snapshots ordered by creation time and delete the ones which
    # are no longer needed
    snap_count=0
    $ZFS list -d 1 -H -o snapshot:type,name -S creation -t snapshot $TARGET |
    while read snap_type snap_name
    do
    if [ “$snap_type” == “$S_TYPE” ]; then
    snap_count=`expr $snap_count + 1`

    if [ $snap_count -gt $COUNT ]; then
    $ZFS destroy -r “$snap_name”
    fi
    fi
    done

  11. auk

    2bbt: nice script improvements, thanks a lot
    please be careful with copy-paste, some symbols become broken

  12. Fengchou

    Is it possible to have a synchronized copy of all pools/snapshots on another backup server? Probably by using zfs send/receive?

  13. John

    I’ve been using this script without issue for quite some time (using gimpe’s TARGET_MOUNT modifications). I’m running into some issues, which appear to be documented here. Figured it worth a mention. I’ll update if I uncover anything worth documenting.