Skip to content

FreeBSD - Periodic ZFS Snapshots

Install zfs-periodic

doas pkg install zfs-periodic

Output

Message from zfs-periodic-1.0.20130213:

--
In order to enable periodic snapshots you need
to add these lines to your /etc/periodic.conf

hourly_output="root"
hourly_show_success="NO"
hourly_show_info="YES"
hourly_show_badconfig="NO"
hourly_zfs_snapshot_enable="YES"
hourly_zfs_snapshot_pools="tank"
hourly_zfs_snapshot_keep=6
daily_zfs_snapshot_enable="YES"
daily_zfs_snapshot_pools="tank"
daily_zfs_snapshot_keep=7
weekly_zfs_snapshot_enable="YES"
weekly_zfs_snapshot_pools="tank"
weekly_zfs_snapshot_keep=5
monthly_zfs_snapshot_enable="YES"
monthly_zfs_snapshot_pools="tank"
monthly_zfs_snapshot_keep=2

To get hourly snapshots you also need to add
something like this to /etc/crontab:

2       *       *       *       *       root    periodic hourly

Customize /etc/periodic.conf

# Hourly options
hourly_output="root"                                    # user or /file
hourly_show_success="NO"                                # scripts returning 0
hourly_show_info="YES"                                  # scripts returning 1
hourly_show_badconfig="NO"                              # scripts returning 2

# 000.zfs-snapshot
hourly_zfs_snapshot_enable="YES"
hourly_zfs_snapshot_pools="zroot"                       # space seperated list of pools or datasets
hourly_zfs_snapshot_keep=24
hourly_zfs_snapshot_skip=""                             # space seperated list of datasets to skip

# Quarter hourly options

# 000.zfs-snapshot
quarter_hourly_zfs_snapshot_enable="YES"
quarter_hourly_zfs_snapshot_pools="zroot"
quarter_hourly_zfs_snapshot_keep=4
quarter_hourly_zfs_snapshot_skip=""

# Daily options

# 000.zfs-snapshot
daily_zfs_snapshot_enable="YES"
daily_zfs_snapshot_pools="zroot"
daily_zfs_snapshot_keep=7
daily_zfs_snapshot_skip=""


# 404.status-zfs
daily_status_zfs_enable="YES"

# Weekly options

# 000.zfs-snapshot
weekly_zfs_snapshot_enable="YES"
weekly_zfs_snapshot_pools="zroot"
weekly_zfs_snapshot_keep=5
weekly_zfs_snapshot_skip=""

# Monthly options

# 000.zfs-snapshot
monthly_zfs_snapshot_enable="YES"
monthly_zfs_snapshot_pools="zroot"
monthly_zfs_snapshot_keep=3
monthly_zfs_snapshot_skip=""

# 998.zfs-scrub
monthly_zfs_scrub_enable="YES"
monthly_zfs_scrub_pools="zroot"

Customize /etc/crontab

# /etc/crontab - root's crontab for FreeBSD
#
#
SHELL=/bin/sh
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
#
#minute hour    mday    month   wday    who     command
#
# Save some entropy so that /dev/random can re-seed on boot.
*/11    *       *       *       *       operator /usr/libexec/save-entropy
#
# Rotate log files every hour, if necessary.
0       *       *       *       *       root    newsyslog
#
# Perform daily/weekly/monthly maintenance.
*/15    *       *       *       *       root    periodic quarter_hourly
1       *       *       *       *       root    periodic hourly
1       3       *       *       *       root    periodic daily
15      4       *       *       6       root    periodic weekly
30      5       1       *       *       root    periodic monthly
#
# Adjust the time zone if the CMOS clock keeps local time, as opposed to
# UTC time.  See adjkerntz(8) for details.
1,31    0-5     *       *       *       root    adjkerntz -a

Enable quarter hourly snapshots

see https://github.com/ross/zfs-periodic/pull/10/commits/b37103444a0dcac58937e134cbbeb347831b967a and https://evilham.com/en/blog/2023-ZFS-replication-tools/ for more details

doas mkdir /usr/local/etc/periodic/quarter_hourly
doas cp /usr/local/etc/periodic/hourly/000.zfs-snapshot /usr/local/etc/periodic//quarter_hourly/000.zfs-snapshot

customize /usr/local/etc/periodic//quarter_hourly/000.zfs-snapshot -> change hourlyto quarter_hourly

#!/bin/sh

# If there is a global system configuration file, suck it in.
#
if [ -r /etc/defaults/periodic.conf ]
then
    . /etc/defaults/periodic.conf
    source_periodic_confs
fi

pools=$quarter_hourly_zfs_snapshot_pools
if [ -z "$pools" ]; then
    pools='tank'
fi

keep=$quarter_hourly_zfs_snapshot_keep
if [ -z "$keep" ]; then
    keep=6
fi

case "$quarter_hourly_zfs_snapshot_enable" in
    [Yy][Ee][Ss])
        . /usr/local/bin/zfs-snapshot
        do_snapshots "$pools" $keep 'quarter_hourly' "$quarter_hourly_zfs_snapshot_skip"
        ;;
    *)
        ;;
esac

/usr/local/bin/zfs-snapshot

#!/bin/sh

# take the appropriately named snapshot
create_snapshot()
{
    pool=$1

    case "$type" in
        quarter_hourly)
        now=`date +"$type-%Y-%m-%d-%H-%M"`
        ;;
        hourly)
        now=`date +"$type-%Y-%m-%d-%H"`
        ;;
        daily)
        now=`date +"$type-%Y-%m-%d"`
        ;;
        weekly)
        now=`date +"$type-%Y-%U"`
        ;;
        monthly)
        now=`date +"$type-%Y-%m"`
        ;;
        yearly)
        now=`date +"$type-%Y"`
        ;;
        *)
        echo "unknown snapshot type: $type"
        exit 1
    esac

    # enumerate datasets under this pool or dataset, skip excluded datasets if requested
    if [ -n "$skip" ]; then
            egrep="($(echo $skip | sed "s/ /|/g"))"
            datasets=$(zfs list -r -H -o name $pool | egrep -v "$egrep")
    else
            datasets=$(zfs list -r -H -o name $pool)
    fi

    # loop through datasets, do snapshots of each
    for dataset in $datasets; do
        snapshot="$dataset@$now"
        # look for an existing snapshot with this name
        if zfs list $snapshot > /dev/null 2>&1; then
            echo "     snapshot $snapshot already exists, skipping..."
        else
            echo "     taking snapshot: $snapshot"
            zfs snapshot $snapshot
        fi
    done
}

# delete the named snapshot
delete_snapshot()
{
    snapshot=$1
    echo "      destroying old snapshot, $snapshot"
    zfs destroy -r $snapshot
}

# take a type snapshot of pool, keeping keep old ones
do_pool()
{
    pool=$1
    keep=$2
    type=$3
    skip=$4

    # create the regex matching the type of snapshots we're currently working
    # on
    case "$type" in
        quarter_hourly)
        # quarter_hourly-2009-01-01-00-15
        regex="^$pool@$type-[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]$"
        ;;
        hourly)
        # hourly-2009-01-01-00
        regex="^$pool@$type-[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]$"
        ;;
        daily)
        # daily-2009-01-01
        regex="^$pool@$type-[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$"
        ;;
        weekly)
        # weekly-2009-01
        regex="^$pool@$type-[0-9][0-9][0-9][0-9]-[0-9][0-9]"
        ;;
        monthly)
        # monthly-2009-01
        regex="^$pool@$type-[0-9][0-9][0-9][0-9]-[0-9][0-9]"
        ;;
        yearly)
        # yearly-2009
        regex="^$pool@$type-[0-9][0-9][0-9][0-9]"
        ;;
        *)
        echo "unknown snapshot type: $type"
        exit 1
    esac

    create_snapshot $pool $type

    # get a list of all of the snapshots of this type sorted alpha, which
    # effectively is increasing date/time
    # (using sort as zfs's sort seems to have bugs)
    snapshots=`zfs list -H -o name -t snapshot | sort | grep $regex`
    # count them
    count=`echo $snapshots | wc -w`
    if [ $count -ge 0 ]; then
        # how many items should we delete
        delete=`expr $count - $keep`
        count=0
        # walk through the snapshots, deleting them until we've trimmed deleted
        for snapshot in $snapshots; do
            if [ $count -ge $delete ]; then
                break
            fi
            delete_snapshot $snapshot
            count=`expr $count + 1`
        done
    fi
}

# take snapshots of type, for pools, keeping keep old ones,
do_snapshots()
{
    pools=$1
    keep=$2
    type=$3
    skip=$4

    echo ""
    echo "Doing zfs $type snapshots:"
    for pool in $pools; do
        do_pool $pool $keep $type "$skip"
    done
}