crzsnap
is my (cr1901) attempt at a ZFS snapshotting engine built upon
the doit
Python task automater. It is built
specifically for my use case, and is designed to back up a ZFS pool from one
set of disks to another using incremental zfs send
and zfs recv
.
-
Either install this script with
pip install .
, or copy/symlink__init__.py
to/from your personalbin
directory; either works! -
Fill out a JSON file under
~/.config/crzsnap/crzsnap.json
. All datasets in thebookmark
andsnapshot
arrays should include the pool name specified in thefrom
key in the JSON file. -
Run
crzsnap init_dataset [-s] [dataset]
for each dataset named in the JSON file. Use-s
for each dataset where intermediatesnapshots
should be preserved. The dataset should include the pool name specified in thefrom
key in the JSON file. -
(Optional): If you want to see what would be executed, run the following to simulate a "dry run", ignoring checks that are expected to fail if you just want to print out all results (TODO: can this be incorporated into the script?):
crzsnap echo=1 all check:send_snapshots -f check:rotate_sender -f | grep -v -- ^[.-] && crzsnap forget > /dev/null
-
crzsnap init_dataset
does a full backup of datasets, and prepares the pools for subsequent runs of itself. After some amount of time has passed and you want to do another backup (your first incrementalsend
andrecv
for those datasets), runcrzsnap all
. -
After
crzsnap all
has run, wait some more time before another backup. Then runcrzsnap forget
and thencrzsnap all
(you must runcrzsnap forget
!).- If you want to add a new dataset to this script, modify the JSON file with
the new dataset, run
crzsnap forget
, and then runcrzsnap init_dataset [-s] [dataset]
as appropriate. The script will be ready for another backup usingcrzsnap all
with your newly added dataset. I do not suggest runninginit_dataset
without runningcrzsnap forget
first, but at present I don't enforce this.
- If you want to add a new dataset to this script, modify the JSON file with
the new dataset, run
Since crzsnap
is built upon doit
, crzsnap
inherits all of doit
's
subcommands. This includes how to
invoke help, which is done with crzsnap help
instead of e.g. crzsnap -h
.
-
crzsnap help help
gives help on global command line parameters tocrzsnap
. -
crzsnap list
lists all public-facing tasks created bydoit
, along with some basic help text.crzsnap --all
will list sub-tasks of public-facing tasks ("tasks with a colon"), andcrzsnap --all -p
will additionally include private tasks and subtasks. -
Usually you will be invoking the
run
subcommand, which is the default if no otherdoit
subcommand is not specified. The abovelist
command will give the name of tasks to run.crzsnap help [task]
will give help for the any task displayed from thelist
command, along with command-line arguments it takes. -
doit
's command line parameter format is reminiscent ofmake
; e.g.make PREFIX=$HOME/.local all install
. Unlikemake
,doit
tasks can take additional flag arguments. If you want to pass command-line args to multiple tasks at once, you must specify all those tasks on the command-line. See the "dry-run" example above, which also demonstrates themake
-like way to pass theecho
global variable todoit
. -
All
check
tasks can be forced to complete by passing in an-f
option; the script assumes you will manually fix problems for failing checks instead. See the "dry run" example above; in a "dry run" just before doing a backup, thecheck:send_snapshots
andcheck:rotate_sender
tasks are generally expected to fail. -
crzsnap forget
needs to be run to "reset" the script state just prior to starting another backup. If you do not runcrzsnap forget
after a successful backup, the script will think there's nothing to do whencrzsnap all
is run again!
ZFS has a bit of terminology to describe the snapshots that I also use (see zfs send
and zfs recv
pages for more info). Given the following command:
zfs send -i tank/ds{#,@}src tank/ds@dst | zfs recv pipe/ds@dst
I'm using the following naming scheme, mostly derived from the OpenZFS man pages:
- Incremental source: the
tank/ds{#,@}src
snapshot/bookmark. - Incremental target: the
tank/ds@dst
snapshot. - Sender: the
tank
pool. - Recevier: the
pipe
pool. - Source: the
tank/ds
dataset. - Destination: the
pipe/ds
dataset. - Target: the
pipe/ds@dst
snapshot; OpenZFS doesn't seem to use "incremental" descriptor for the receiver/destination. - Matched snapshots: During an incremental send, a "source" snapshot is implied
in the
zfs recv
command. OpenZFS doesn't seem to give a special name to it. The man page forzfs recv
describes this snapshot as the "most recent snapshot" and "must match the incremental stream's source". So I propose the name "matched snapshots", as in "receiver snapshots matched to sender's incremental source".
A config file is a single JSON object with the following key-value pairs:
from
(string): Name of pool used inzfs send
.to
(string): Name of pool used inzfs recv
.suffix
(string): Basename of snapshots and bookmarks created bycrzsnap
. when rotating snapshots, snapshots named$suffix-prev
will be destroyed, and snapshots named$suffix
will be renamed to$suffix-prev
.bookmark
(array[string]): Datasets on the sending pool which will not have snapshots (beyond those created bycrzsnap
) preserved on the receiver. Instead,zfs
bookmarks are leveraged.bookmark
(array[string]): Datasets on the sending pool which will have snapshots preserved on the receiver.
Datasets which are not specified in the JSON file will be ignored by crzsnap
.
{
"from": "tank",
"to": "pipe",
"suffix": "from-main",
"bookmark": [
"tank/backups",
"tank/cat-videos"
],
"snapshot": [
"tank/workdir"
]
}
crzsnap
is essentially an orchestrator of a sequence of zfs
destroy
, rename
,
bookmark
, snapshot
, send
, and recv
shell commands generated from a JSON
input file. In this sense, crzsnap
is a shell script generator and executor
(using subprocess
with
shell=True
). It leverages doit
for its database/dependency support, so that
the script knows where to pick up from if it is interrupted, something tedious
to create in a raw shell script.
The script is divided into 4 main tasks:
create_snapshots
: create$suffix
snapshots on sender.rotate_receiver
: delete$suffix-prev
snapshots on receiver, rename$suffix
->$suffix-prev
.send_snapshots
: incremental send of$suffix-prev
to$suffix
to receiver. Receiver datasets match sender after this step.rotate_sender
: delete$suffix-prev
snapshots on sender, rename$suffix
->$suffix-prev
.
Each one of these tasks has a corresponding check
subtask, e.g.
check:create_snapshots
. These tasks essentially check whether the state
of the sending and receiving pools match what is expected before each task
runs, and bails if not. Combined with doit
's database, these checks are
intended to make it difficult to accidentally forget to create (or accidentally
delete!) the required snapshots/bookmarks.
A fifth task, all
, is the intended entry point for normal operation:
crzsnap all
.
FIXME: This is all how crzsnap
is supposed to work. I need to write an
integration test for it with an on-disk ZFS pool populated with fake files.
Suppose you have three datasets in pool tank
: backups
, workdir
, and
cat-videos
. You don't care about keeping snapshots on backups
because e.g.
you're using you backup software's rotation scheme. On the other hand, you want
to keep snapshots of workdir
because you're testing software you wrote and
need to quickly revert to a pristine state. cat-videos
is a recent addition,
and recently populated with all your favorite feline goodness.
You have successfully set up backups
and workdir
previously to be
transferred from tank
to pipe
in the JSON config (i.e. you have run
crzsnap init_dataset backups
and crzsnap init_dataset -s workdir
and both
succeeded). Additionally, you have run crzsnap all
successfully once (and
not crzsnap forget
yet). Now you want to add the cat-videos
dataset to be
managed by crzsnap
. You don't care about snapshotting cat-videos
, so you
add cat-videos
to the bookmarks
section of your JSON config.
Assuming your snapshot suffix is from-main
, right now your JSON config should
match the config in the JSON example section. We'll
start preparing a backup from here.
Right now, your zfs
datasets should look (something) like this:
user@system$ zfs list -tfilesystem,snap,bookmark -oname
pipe/backups
pipe/backups@from-main-prev
pipe/backups@from-main
pipe/workdir
pipe/workdir@from-main-prev
pipe/workdir@from-main
tank/backups
tank/backups#from-main-prev
tank/cat-videos
tank/workdir
tank/workdir@from-main-prev
tank/workdir@weekly
Given the context above:
- The
from-main-prev
snapshots onpipe
reflect the contents of the equivalenttank
datasets just afterinit_dataset {workdir,backups}
was run. - The
from-main
snapshots onpipe
andfrom-main-prev
snapshots/bookmarks ontank
reflect the contents oftank
just aftercrzsnap all
was run. - The
tank/cat-videos
dataset has no snapshots because it was just recently created and was not registered withcrzsnap
. tank/workdir@weekly
is a snapshot created between the run ofcrzsnap all
and now (whether it was created before/aftertank/cat-videos
is irrelevant).
After we run init_dataset tank/cat-videos
, your zfs
datasets should look
(something) like this:
user@system$ crzsnap init_dataset tank/cat-videos
...
user@system$ zfs list -tfilesystem,snap,bookmark -oname
pipe/backups
pipe/backups@from-main-prev
pipe/backups@from-main
pipe/cat-videos
pipe/cat-videos@from-main
pipe/workdir
pipe/workdir@from-main-prev
pipe/workdir@from-main
tank/backups
tank/backups#from-main-prev
tank/cat-videos
tank/cat-videos#from-main-prev
tank/workdir
tank/workdir@from-main-prev
tank/workdir@weekly
init_dataset
does a "dummy" full send
and recv
to prepare future
incremental backups to be managed by crzsnap
:
pipe/cat-videos
,pipe/cat-videos@from-main
, andtank/cat-videos
, should have very similar, if not identical, content. Likewise, thetank/cat-videos#from-main-prev
bookmark should point very close to the current state oftank/cat-videos
.- All other datasets, snapshots, and bookmarks are the same as previously.
Note that crzsnap all
is equivalent to crzsnap create_snapshots rotate_receiver send_snapshots rotate_sender
.
The worked example goes through each task one at a time.
user@system$ crzsnap create_snapshots
...
user@system$ zfs list -tfilesystem,snap,bookmark -oname
pipe/backups
pipe/backups@from-main-prev
pipe/backups@from-main
pipe/cat-videos
pipe/cat-videos@from-main
pipe/workdir
pipe/workdir@from-main-prev
pipe/workdir@from-main
tank/backups
tank/backups#from-main-prev
tank/backups@from-main
tank/cat-videos
tank/cat-videos#from-main-prev
tank/cat-videos@from-main
tank/workdir
tank/workdir@from-main-prev
tank/workdir@weekly
tank/workdir@from-main
from-main
snapshots are created on tank
with the current contents of each
dataset. Because we just initialized tank/cat-videos
to be managed by crzsnap
,
the state of the tank/cat-videos@from-main
is likely to be very close
(if not identical) to tank/cat-videos#from-main-prev
and friends, as
above. An incremental send will still work just fine.
user@system$ crzsnap rotate_receiver
...
user@system$ zfs list -tfilesystem,snap,bookmark -oname
pipe/backups
pipe/backups@from-main-prev
pipe/cat-videos
pipe/cat-videos@from-main-prev
pipe/workdir
pipe/workdir@from-main-prev
tank/backups
tank/backups#from-main-prev
tank/backups@from-main
tank/cat-videos
tank/cat-videos#from-main-prev
tank/cat-videos@from-main
tank/workdir
tank/workdir@from-main-prev
tank/workdir@weekly
tank/workdir@from-main
tank
is unmodified compared to the previous step;
the from-main
snapshots on pipe
are moved to from-main-prev
, and the
snapshots previously named from-main-prev
are destroyed. pipe
is now ready
for an incremental send
and recv
.
user@system$ crzsnap send_snapshots
...
user@system$ zfs list -tfilesystem,snap,bookmark -oname
pipe/backups
pipe/backups@from-main-prev
pipe/backups@from-main
pipe/cat-videos
pipe/cat-videos@from-main-prev
pipe/cat-videos@from-main
pipe/workdir
pipe/workdir@from-main-prev
pipe/workdir@weekly
pipe/workdir@from-main
tank/backups
tank/backups#from-main-prev
tank/backups@from-main
tank/cat-videos
tank/cat-videos#from-main-prev
tank/cat-videos@from-main
tank/workdir
tank/workdir@from-main-prev
tank/workdir@weekly
tank/workdir@from-main
tank
datasets (via the from-main
snapshots) are incrementally copied from
to pipe
. The pipe
datasets are rolled back automatically to the
from-main-prev
snapshot before recv
. Upon receipt, datasets on pipe
will
match the corresponding datasets on tank
(as of the from-main
snapshots).
We could delete the -prev
snapshots on pipe
now,
as they will not be needed again after the send
/recv
is done. However, I
opt to keep the -prev
snapshots afterwards just in case. This way, a user
has time from the end of backup X
all the way to the rotate_receiver
step
of backup X + 2
to grab data they realize they in fact still need!
Right now (8/5/2024), I have no plans to add keeping additional backups on the
receiver. $suffix
and $suffix-prev
are sufficient for my needs to
efficiently copy datasets after one full send
/recv
.
user@system$ crzsnap rotate_sender
...
user@system$ zfs list -tfilesystem,snap,bookmark -oname
pipe/backups
pipe/backups@from-main-prev
pipe/backups@from-main
pipe/cat-videos
pipe/cat-videos@from-main-prev
pipe/cat-videos@from-main
pipe/workdir
pipe/workdir@from-main-prev
pipe/workdir@weekly
pipe/workdir@from-main
tank/backups
tank/backups#from-main-prev
tank/cat-videos
tank/cat-videos#from-main-prev
tank/workdir
tank/workdir@weekly
tank/workdir@from-main-prev
After snapshots from-main
snapshots on tank
have been moved to
from-main-prev
snapshots and bookmarks, the backup is done. Subsequent runs
of crzsnap all
(or running any of the individual tasks that all
composes)
report "up to date". You must run crzsnap forget
to "reset" the script. At
this point, the backup cycle restarts, and now is a good time
to add/initialize new datasets.
At this point in time (8/5/2024), swapping between snapshot and bookmark
strategies requires destroy
ing the relevant dataset and children on pipe
as well as the matching dataset's bookmarks and snapshots on tank
.
Running init_dataset
on said dataset will then reinitialize the pools so
that crzsnap
uses the alternate strategy.