Here’s a simple little Bourne shell-compatible script I wrote to create and delete rolling ZFS snapshots on a small home server running the FreeBSD operating system. Sure there’s ports/packages available in FreeBSD to accomplish this, but there’s something to be said for keeping it simple and having one less piece of software to update and maintain.
As currently configured, the script recursively creates a daily snapshot of /pool_0/dataset_0. The name of each snapshot consists of the word “snap,” followed by the date and time the snapshot was taken (e.g., snap-202002102300). Snapshots are retained in pool_0/dataset_0/.zfs/snapshots. The script will then destroy any snapshot older that 90 days and log what its done to the file cronlog located in my home directory. A typical ~/cronlog entry looks like the following:
1 2 3 4 5 6 7 8 9 |
zfssnap.sh Mon Feb 10 23:00:00 EST 2020 Taking today's snapshot: pool_0/dataset_0@snap-202002102300 Attempting to destroy old snapshots... Destroying the following old snapshots: pool_0/dataset_0@snap-202001312300 ********** |
Using the script
To use the script, I save it as zfssnap.sh in ~/bin, where I keep most of my helper scripts, and make it executable:
1 |
chmod +x ~/bin/zfssnap.sh |
Then I delegate some ZFS permissions to user iceflatline so snapshots can be created and destroyed without becoming the root user. However, this command is issued as the user root:
1 |
zfs allow -u iceflatline create,destroy,hold,mount,receive,send,snapshot pool_0 |
Note that I’ve set permissions at the zpool level, which means that all datasets under pool_0 will inherit these settings. Alternatively I could have applied the permissions just to dataset_0.
Then I make the snapshots retained in pool_0/dataset_0/.zfs/snapshots. Here too, the command must be issued as the user root:
1 |
zfs set snapdir=visible pool_0 |
Here again, I’ve applied applied this setting at the zpool level so when additional datasets are created in pool_0 they will inherit this setting. And here too, I could have applied this setting just to dataset_0.
Finally I configure a cronjob in the crontab for user iceflatline so that the script runs daily at 23:00:
1 2 3 4 |
### DAILY # Run backup scripts every day at 2300 0 23 * * * /home/iceflatline/bin/zfssnap.sh |
If you’d like to use the script as is then simply modify the variable src_0 to reflect the name of your zpool and dataset. Else, modify the script to suite your needs.
Prefer to run the script more frequently than daily? Modify your cronjob. Here are couple of examples for running the script more frequently:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
### HOURLY # Run snapshot script every 15 minutes #*/18 * * * * /home/iceflatine/bin/zfssnap.sh # Run snapshot script every 30 minutes #*/33 * * * * /home/iceflatine/bin/zfssnap.sh # Run snapshot script at the top of every hour #3 * * * * /home/iceflatine/bin/zfssnap.sh # Run snapshot script every three hours at the top of each hour #3 */3 * * * /home/iceflatine/bin/zfssnap.sh # Run snapshot script every six hours at the top of each hour #3 */6 * * * /home/iceflatine/bin/zfssnap.sh |
Don’t like the snapshot naming convention. No problem, just modify the variable snap_prefix.
Prefer to have more or less than 90 snapshots? Modify the retention variable.
Want to add other zpools and/or datasets? Add them to another variable (e.g., src_1), then modify the remainder of the script to recognize and take action on them.
Prefer to have the script log its output to a different location? Modify the log variable.
You get the idea.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
#!/bin/sh ### BEGIN INFO # PROVIDE: # REQUIRE: # KEYWORD: # Description: # This script is used to create zfs snapshots and remove older snapshots. # The number of snapshots to retain is defined in the variable retention. # Author: iceflatline <iceflatline@gmail.com> # # OPTIONS: # -v: Be verbose ### END INFO ### START OF SCRIPT # These variables are named first because they are nested in other variables. snap_prefix=snap retention=90 # Full paths to these utilities are needed when running the script from cron. date=/bin/date grep=/usr/bin/grep sed=/usr/bin/sed sort=/usr/bin/sort xargs=/usr/bin/xargs zfs=/sbin/zfs src_0="pool_0/dataset_0" today="$snap_prefix-`date +%Y%m%d%H%M`" snap_today="$src_0@$today" snap_old=`$zfs list -t snapshot -o name | $grep "$src_0@$snap_prefix*" | $sort -r | $sed 1,${retention}d | $xargs -n 1` log=/home/iceflatline/cronlog # Create a blank line between the previous log entry and this one. echo >> $log # Print the name of the script. echo "zfssnap.sh" >> $log # Print the current date/time. $date >> $log echo >> $log # Look for today's snapshot and, if not found, create it. if $zfs list -H -o name -t snapshot | $grep "$snap_today" > /dev/null then echo "Today's snapshot '$snap_today' already exists." >> $log # Uncomment if you want the script to exit when it does not create today's snapshot: #exit 1 else echo "Taking today's snapshot: $snap_today" >> $log $zfs snapshot -r $snap_today >> $log 2>&1 fi echo >> $log # Remove snapshot(s) older than the value assigned to $retention. echo "Attempting to destroy old snapshots..." >> $log if [ -n "$snap_old" ] then echo "Destroying the following old snapshots:" >> $log echo "$snap_old" >> $log $zfs list -t snapshot -o name | $grep "$src_0@$snap_prefix*" | $sort -r | $sed 1,${retention}d | $xargs -n 1 $zfs destroy -r >> $log 2>&1 else echo "Could not find any snapshots to destroy." >> $log fi # Mark the end of the script with a delimiter. echo "**********" >> $log # END OF SCRIPT |
Conclusion
There you have it. A simple way to create and destroy ZFS snapshots on a rolling basis under FreeBSD.