Back when I first started using FreeBSD and ZFS, I needed tools with an extremely low barrier of entry, that did their job well.
When it comes to creating and pruning snapshots
, the job was first done by
sysutils/zfs-periodic.
Over time however, even with minor improvements on my part, it has come short for my current needs.
As inspired by Dan Langille’s wonderful blog (Dan always writes great blog posts, that end up being lovely complementary documentation), I document here my somewhat bumpy road when it comes to ZFS replication tools and why you might want to use something different at each step.
ZFS is nowadays (at least on FreeBSD and Linux) actually OpenZFS. Let’s see how they define it:
OpenZFS is an advanced file system and volume manager which was originally developed for Solaris and is now maintained by the OpenZFS community. This repository contains the code for running OpenZFS on Linux and FreeBSD.
Amongst the many beautiful things of ZFS, there is the fact that it works in a
Copy on Write fashion, which, amongst other things, enables it to create
snapshots
in an instantaneous fashion.
That is, we can assign a name to a known state with, e.g.
zfs snapshot -r zroot@goodstate
.
Keep on working and, if we get on to a bad state, discard any changes made
since then by issuing a rollback
.
This means, we can have a very handy way of having real-life save points, and work in a more relaxed fashion.
Together with zfs-send(8)
and zfs-receive(8)
, we
quickly have the basis for a remote backup system that is incremental.
Now that ZFS data set encryption (zfs-load-key(8)
) is a thing, it too can
be encrypted.
zfs-periodic(8)
Now, back in ~2019, I was mostly a Linux user that was jumping to FreeBSD and the last thing I wanted was to fiddle too much with a new file system.
Luckily, there is a very simple way to manage ZFS snapshots in an automatic
fashion right in the ports system: zfs-periodic
.
It profits from the periodic(8)
system, to run at regular
intervals, and gets configured in a minute with:
$ pkg install -y zfs-periodic
Updating FreeBSD repository catalogue...
FreeBSD repository is up to date.
[1/1] Fetching zfs-periodic-1.0.20130213.pkg: 100% 2 KiB 2.3kB/s 00:01
Checking integrity... done (0 conflicting)
[1/1] Installing zfs-periodic-1.0.20130213...
[1/1] Extracting zfs-periodic-1.0.20130213: 100%
=====
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
We literally do that (adding the crontab(5)
entry and configuring
zfs-periodic(8)
in /usr/local/etc/periodic.conf
(*)), and we have a simple
system that takes hourly, daily, weekly and monthly snapshots.
Half way there with zero effort, this is what has kept me using it for years.
(*): I prefer to use
/usr/local/etc/periodic.conf
for things outside the base system. This is a matter of taste and it helps easen administration.
quarter_hourly
Given how cheap these snapshots are, I (and people at work) started wanting to have them more often, which in my case meant every 15 minutes.
Luckily, periodic(8)
supports arbitrary directories:
directory An arbitrary directory containing a set of executables to be
run.
However, it wasn’t as simple as:
mkdir /usr/local/etc/periodic/quarter_hourly/
ln -s /usr/local/etc/periodic/daily/000.zfs-snapshot \
/usr/local/periodic/quarter_hourly/000.zfs-snapshot
Because the actual file in there depends on the name of the directory, so it
has to be customised.
And also because the /usr/local/bin/zfs-snapshot
script that gets installed
needs to understand all the periodic.conf
settings.
Luckily making these changes was fairly simple.
Since I won’t be using zfs-periodic(8)
any longer, and the patch has been
more than tested over the years, I’m opening a
Merge Request, which you can use to benefit from these
changes.
Take into account that it’ll also need a corresponding crontab(5)
entry:
*/15 * * * * root periodic quarter_hourly
zxfer(8)
Snapshots take care of local mess ups, but they don’t serve as a backup.
For that, I took to zxfer, which is simple to use and has worked great with some caveats.
A lovely thing of zxfer(8)
is that it supports an rsync(1)
mode, that
allows us to back up remote systems that do not support ZFS, to a ZFS-based
system that takes care of snapshots and so on.
Mostly, even with a pull-based approach, zxfer(8)
doesn’t protect much
against snapshots disappearing on the source, which means that there is a
somewhat plausible scenario where pulling data results in breaking certain
retention policies.
While there is a -g
flag to protect old snapshots, it means that we have to
replicate snapshot pruning on the destination system, but cannot create
snapshots as they could collide with the source snapshots.
That means zfs-periodic(8)
and zxfer(8)
are better than nothing and work
decently, but have important limitations.
When encrypted datasets got added to OpenZFS, I realised that
we need to use zfs send -w
, or else the backup will happen unencrypted.
So I wrote a patch for zxfer to do just that.
You can help land that patch in zxfer(8)
, it is basically
only missing the manpage and USAGE texts, but my manpage syntax is rusty
and time is not an abundantly available commodity.
I’d love to help you land this change though!
sanoid
After having read Dan Langille’s blog post on ZFS tools several months ago, I had been wanting to give it a try, since it looked simple to setup and more akin to my current needs.
I based my configuration mostly off Dan’s, so checked that great post :-).
zfs-periodic(8)
snapshots to sanoidThis ends up being somewhat simple with periodic2sanoid.sh
:
# Check locally and remotely that actions make sense (and results match)
# output lists "DS" as the base dataset, it acts recursively
./periodic2sanoid.sh zroot/usr/home
[...]
DS@daily-2023-06-28 --> DS@autosnap_2023-06-28_12:24:44_daily
[...]
# Actually apply changes
env DRY_RUN=NO ./periodic2sanoid.sh zroot/usr/home
And here is the code for that script, you can (and should!) audit it:
#!/bin/sh -eu
# Copyright (c) 2023, Evilham (https://evilham.com)
# BSD 2-Clause copyright and disclaimer apply.
# See: http://www.opensource.org/licenses/bsd-license.php
DS="$1"
DRY_RUN="${DRY_RUN:-YES}"
epoch_to_time() {
epoch="$1"
date -j -f '%s' "${epoch}" '+%Y-%m-%d_%H:%M:%S'
}
target_name() {
snap_epoch="$1"
snap_name="$2"
snap_time="$(epoch_to_time "${snap_epoch}")"
case "${snap_name}" in
quarter_hourly-*)
# quarter_hourly --> frequently
printf "autosnap_%s_%s" "${snap_time}" "frequently"
;;
hourly-*)
printf "autosnap_%s_%s" "${snap_time}" "hourly"
;;
daily-*)
printf "autosnap_%s_%s" "${snap_time}" "daily"
;;
weekly-*)
printf "autosnap_%s_%s" "${snap_time}" "weekly"
;;
monthly-*)
printf "autosnap_%s_%s" "${snap_time}" "monthly"
;;
yearly-*)
printf "autosnap_%s_%s" "${snap_time}" "yearly"
;;
*)
# Empty --> no renaming
;;
esac
}
zfs list -rpHt snap -s creation -o creation,name "$DS" | while IFS='' read -r line; do
snap_epoch="$(echo "${line}" | cut -f 1)"
full_snap="$(echo "${line}" | cut -f 2)"
ds_name="$(echo "${full_snap}" | cut -d '@' -f 1)"
snap_name="$(echo "${full_snap}" | cut -d '@' -f 2-)"
snap_target_name="$(target_name "${snap_epoch}" "${snap_name}")"
if [ -n "${snap_target_name}" ]; then
# We use DS to facilitate comparing local and remote actions
printf "%s@%s\t-->\t%s@%s\n" "DS" "${snap_name}" "DS" "${snap_target_name}"
if [ "${DRY_RUN}" != "YES" ]; then
echo "zfs rename \"${full_snap}\" \"${ds_name}@${snap_target_name}\""
zfs rename "${full_snap}" "${ds_name}@${snap_target_name}"
fi
fi
done
sanoid
This, as Dan documented it, works wonderfully after taking
into account the crontab(5)
caveat and using lockf(1)
.
syncoid
When installing sanoid
, we also get syncoid
as a replication tool.
While trying to replicate the datasets, I realised that it expects either
to run as root
or to have sudo
available.
Of course, that’s not ideal, as I prefer to rely on zfs-allow(8)
and do not
even have sudo
as an available command.
There is a flag --no-privilege-elevation
which helps circumvent these checks,
but the fact that it fails and requires the flag to work as non-root, is kind
of a red flag for me.
As with zxfer(8)
, I was setting up a pull-based approach over SSH.
Since this user and that SSH key should only be allowed to use zfs send
and little more, I use the authorized_keys
file to force the command
to a particular script.
This resulted in issues with syncoid
‘s shell escaping, which lead me to
this reported issue from 2022.
I believe the person that reported it was in a similar position to mine.
I ended up “fixing it” (the security implications are still to be determined),
by expanding the list of allowed commands and using sh -s
with
$SSH_ORIGINAL_COMMAND
:
#!/bin/sh
case "${SSH_ORIGINAL_COMMAND}" in
"uname")
;;
"zfs get "*)
;;
"/sbin/zfs get "*)
;;
"zfs list "*)
;;
"/sbin/zfs list "*)
;;
"zfs send -n "*)
;;
"zfs send -w "*)
;;
" zfs send -w "*)
;;
"/sbin/zfs send -w "*)
;;
"echo -n")
;;
"command -v lzop")
;;
"command -v mbuffer")
;;
"zpool get -o value -H feature@extensible_dataset "*)
;;
"exit")
;;
*)
echo "Command not allowed! See ya!"
exit 1
;;
esac
echo "${SSH_ORIGINAL_COMMAND}" >> /tmp/test
sh -s <<EOF
${SSH_ORIGINAL_COMMAND}
EOF
This is also a bit of an issue, and we need to specify --sendoptions="w"
.
syncoid -r --no-privilege-elevation --sendoptions="w" \
sucker@HOST:zroot/usr/home backups/pools/HOST/zroot/usr/home
This kind of works, but when looking to refine it, I realised that it does not
offer me any benefit regarding zxfer(8)
when it comes to protecting the
snapshots.
zrepl
After getting frustrated again, I realised this wasn’t quite looking as I was expecting (I was so sure this was doable!).
Turns out, I had read about zrepl some months ago, and checked the documentation and determined it was something I needed to use.
So, this misremembering caused me to look deeply into sanoid
and syncoid
.
But hey, at least now I am very sure about what I want, and I can probably quickly adapt my renaming script for a migration to zrepl.
I really should blog more often, if anything to save me some time.
This will have to be a two-parter, with this one being what I’ve tried and why I’m not keeping it, and the second one being how I get to setup zrepl to my liking.
I posted this on the fediverse, and some people shared what they use and why!
zfs_autobackup
@stefano@mdon.stefanomarinelli.it mentioned zfs_autobackup, and they have a very nice blog post series documenting this, and other steps they took on migrating from Linux to FreeBSD.
zrep
@dk3jf@radiosocial.de mentioned zrep: