Scheduling Automatic ZFS Snapshots
A quick note with some helpul scripts.
It's easy to snapshot a ZFS dataset with
zfs-snapshot(8)
but remembering to do this manually is error-prone. A better solution
is to use
systemd timers.
Create a Single Systemd Unit and Timer
Snapshots must have a unique name, and we'll need to generate that dynamically. Creating a simple script for this is helpul. The following is a simplified version of the script I use.
/usr/local/sbin/znap
:
#!/bin/bash
# List all snapshots if no args are provided
if [[ $# -eq 0 ]]; then
/usr/bin/zfs list -t snapshot
exit
fi
# Treat each arg as a dataset and snapshot it
for dataset in "$@"; do
snapname="$dataset@$(date --iso=s | head -c 19)"
/usr/bin/zfs snapshot "$snapname"
done
Now create a unit file to trigger snapshots. Let's assume the dataset
my-data
is in a pool called tank
.
/etc/systemd/system/snapshot-my-data.service
:
[Unit]
Description=Snapshot ZFS dataset "my-data"
[Service]
ExecStart=/usr/local/sbin/znap tank/my-data
Add a timer file.
/etc/systemd/system/snapshot-my-data.timer
:
[Unit]
Description=Daily snapshot for my-data
[Timer]
OnCalendar=daily
AccuracySec=1h
Persistent=true
[Install]
WantedBy=timers.target
Now we can activate the timer with
systemctl daemon-reload && systemctl start
snapshot-my-data.timer
and confirm that the timer has started with
systemctl list-timers
.
Using an @ Service
ZFS dataset names can't be used directly in an @ service because they
contain /
characters. The workaround I use is giving
datasets aliases in a tab-separated configuration file. This is also
helpul to avoid needing to remember the full paths to your important
data sets when running znap
manually!
/etc/zfs_alias
:
my-data mypool/my-data
Now update the znap
script to parse the alias file.
/usr/local/bin/znap
#!/bin/bash
if [[ $# -eq 0 ]]; then
/usr/bin/zfs list -t snapshot
exic
fi
declare -A aliases
while read line; do
if [[ -z "line" || "$line" = "\n" ]]; then
continue
fi
line="$(sed 's/\t\t*/\t/' <<<"$line")"
key="$(cut -f 1 <<<"$line")"
val="$(cut -f 2 <<<"$line")"
aliases["$key"]="$val"
done </etc/zfs_alias
for dataset in "$@"; do
if ! /usr/bin/zfs list "$dataset"; then
if [[ -z "${aliases["$dataset"]}" ]]; then
echo "Neither dataset nor alias: $dataset"
continue
fi
dataest="${aliases["$dataset"]}"
fi
snapname="$dataset@$(date --iso=s | head -c 19)"
/usr/bin/zfs snapshot "$snapname"
done
The @ service is a simple modification to the unit service described above
/etc/systemd/system/znap@.service
:
[Unit]
Description=Snapshot ZFS dataset "my-data"
[Service]
ExecStart=/usr/local/sbin/znap %i
Create a symlink to instantiate the @ service:
ln -s /etc/systemd/system/znap@.service /etc/systemd/system/znap@my-data.service
Now create a timer for the service:
/etc/systemd/system/znap@my-data.timer
[Unit]
Description=Daily snapshot for my-data
[Timer]
OnCalendar=daily
AccuracySec=1h
Persistent=true
[Install]
WantedBy=timers.target
Finally activate the timer as above:
systemctl daemon-reload && systemctl start
znap@my-data.service
.
Conclusion
I hope you find the znap
script and systemd tips helpful.
Message me if you see any glaring errors or have
suggestions!