Compare commits

...

4 Commits

Author SHA1 Message Date
Nicolas Massé 996f691c6a Bump version to 0.0.8 3 weeks ago
Nicolas Massé 023618bf62 Let zfs-autobackup decide which domains are configured 3 weeks ago
Nicolas Massé 585df61e6f Bump version to 0.0.7 3 weeks ago
Nicolas Massé c4a1d54d86 Make the snapshot name format queryable 3 weeks ago
  1. 6
      Makefile
  2. 76
      README.md
  3. 12
      packaging/zvirt.spec
  4. 105
      src/bin/snapshot-libvirt-domains
  5. 32
      src/share/zvirt/snapshot-format

6
Makefile

@ -25,11 +25,15 @@ install:
@install -d $(PREFIX)/bin
@install -m 755 src/bin/libvirt-hook $(PREFIX)/bin/libvirt-hook
@install -m 755 src/bin/snapshot-libvirt-domains $(PREFIX)/bin/snapshot-libvirt-domains
@install -d $(PREFIX)/share/zvirt
@install -m 644 src/share/zvirt/snapshot-format $(PREFIX)/share/zvirt/snapshot-format
uninstall:
@echo "Uninstalling zvirt..."
@rm -f $(PREFIX)/bin/libvirt-hook
@rm -f $(PREFIX)/bin/snapshot-libvirt-domains
@rm -f $(PREFIX)/share/zvirt/snapshot-format
@rmdir --ignore-fail-on-non-empty $(PREFIX)/share/zvirt
tarball:
@echo "Creating release tarball..."
@ -87,4 +91,4 @@ clean:
lint: prerequisites
@echo "Linting..."
@cd src && shellcheck --severity=error bin/*.sh
@cd src && shellcheck --severity=error bin/*

76
README.md

@ -13,6 +13,82 @@ At the end, all components of a domain - Domain definition, TPM, NVRAM, VirtioFS
- Support both crash-consistent and live snapshots.
- Support batch mode (pause all domains, take snapshots, then resume all domains)
## Selecting the domains to snapshot
`snapshot-libvirt-domains` iterates over the libvirt domains and hands each one to `zfs-autobackup`
under the backup name `libvirt-<domain>`, that is, the ZFS property `autobackup:libvirt-<domain>`.
Which datasets that property selects is `zfs-autobackup`'s decision, not zvirt's: it looks the
property up on every dataset, resolves inheritance, and tests its **value**:
| Value | Selected |
| -------- | ------------------------------------------------ |
| `true` | yes, on the dataset and everything inheriting it |
| `false` | no, explicitly excluded |
| `child` | only the datasets that *inherit* it, not this one |
| `parent` | only this dataset, not the ones inheriting it |
Inherited properties count, so the whole fleet can be configured on the parent dataset of all the
domains:
```console
$ zfs set autobackup:libvirt-quay=true data/domains/quay # this domain and its children
$ zfs set autobackup:libvirt=true data/domains # the shared property, for pruning
```
A domain that carries the property nowhere is skipped with a message on stderr and the run carries
on with the next one, exiting 0 — configuring only some domains is a legitimate setup.
> [!WARNING]
> The property selects **datasets**, not domains, and zvirt does not check that the selection
> actually covers a domain's storage. Set it on the domain's root dataset — set on a child only, the
> run reports success while the disks above it are never snapshotted. `zfs-autobackup --test
> --no-send --no-thinning libvirt-<domain>` prints the datasets it would select.
## Snapshot retention
`snapshot-libvirt-domains` runs `zfs-autobackup` with `--no-thinning`: it only creates snapshots and
never deletes any. Retention is left to a separate `zfs-autobackup` run in snapshot-only mode:
```console
$ zfs-autobackup --no-snapshot --keep-source 1w1d,1m1w,1y1m \
--snapshot-format "$(snapshot-libvirt-domains --print-snapshot-format)" libvirt
```
That run **must** be given the very same snapshot name format zvirt snapshotted with: `zfs-autobackup`
only thins snapshots whose name parses against the format it was given. On a mismatch it selects the
right datasets, matches none of their snapshots, deletes nothing and still exits 0 with
`All operations completed successfully` — so never hardcode the format, always query it.
Two ways to query it:
```console
$ snapshot-libvirt-domains --print-snapshot-format
libvirt-%Y-%m-%d-%H:%M:%S
$ . /usr/share/zvirt/snapshot-format && echo "$ZVIRT_SNAPSHOT_FORMAT"
libvirt-%Y-%m-%d-%H:%M:%S
```
`/usr/share/zvirt/snapshot-format` is the single source of truth: `snapshot-libvirt-domains` sources
it too, so the format cannot drift between the tool that writes the snapshots and the tool that
prunes them.
The format deliberately carries no `{}` placeholder (`zfs-autobackup`'s default is
`{}-%Y%m%d%H%M%S`, where `{}` expands to the backup name). All domains therefore share a single
snapshot name pattern, which is what allows one prune run over the shared `autobackup:libvirt`
property to cover every domain at once, instead of one invocation per domain.
### Pruning from a systemd unit
`%Y`, `%m`, `%d`, `%H`, `%M` and `%S` are systemd specifiers: written literally in an `ExecStart=`
they are silently expanded into paths and hostnames, and `systemd-analyze verify` does not catch it.
Either double them (`%%Y`, `%%m`, ...) or — better — let the unit call a wrapper that queries the
format:
```ini
ExecStart=/bin/bash -c '. /usr/share/zvirt/snapshot-format; exec /usr/bin/zfs-autobackup --no-snapshot --keep-source 1w1d,1m1w,1y1m --snapshot-format "$ZVIRT_SNAPSHOT_FORMAT" libvirt'
```
## License
MIT License

12
packaging/zvirt.spec

@ -1,6 +1,6 @@
%global __brp_python_bytecompile %{nil}
Name: zvirt
Version: 0.0.6
Version: 0.0.8
%if %{defined dist}
Release: 1%{?dist}
%else
@ -47,6 +47,8 @@ pip install --root %{buildroot} --prefix %{_prefix} --no-compile --no-deps --no-
%files
%{_bindir}/libvirt-hook
%{_bindir}/snapshot-libvirt-domains
%dir %{_datadir}/zvirt/
%{_datadir}/zvirt/snapshot-format
%{_bindir}/zfs-autobackup
%{_bindir}/zfs-autoverify
%{_bindir}/zfs-check
@ -54,6 +56,14 @@ pip install --root %{buildroot} --prefix %{_prefix} --no-compile --no-deps --no-
%{python3_sitelib}/zfs_autobackup-*.dist-info/
%changelog
* Wed Sep 02 2026 Nicolas Massé <nicolas.masse@itix.fr> - 0.0.8-1
- Let zfs-autobackup decide which domains are configured, so that an inherited
autobackup:libvirt-<domain> property is honoured
* Wed Sep 02 2026 Nicolas Massé <nicolas.masse@itix.fr> - 0.0.7-1
- Make the snapshot name format queryable (--print-snapshot-format)
- Ship /usr/share/zvirt/snapshot-format as the single source of truth
* Wed Apr 22 2026 Nicolas Massé <nicolas.masse@itix.fr> - 0.0.6-1
- Switch to zfs-autobackup + hooks

105
src/bin/snapshot-libvirt-domains

@ -4,7 +4,22 @@ set -Eeuo pipefail
verbose=0
live=0
print_snapshot_format=0
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
SNAPSHOT_FORMAT_FILE="$(readlink -f "$SCRIPT_DIR/../share/zvirt/snapshot-format")"
# The snapshot name format lives in a single sourceable file so that tools
# pruning zvirt's snapshots can read it instead of copying it. See that file.
if [ ! -r "$SNAPSHOT_FORMAT_FILE" ]; then
echo "Error: cannot read the snapshot format file: $SNAPSHOT_FORMAT_FILE" >&2
exit 1
fi
# shellcheck source=../share/zvirt/snapshot-format
source "$SNAPSHOT_FORMAT_FILE"
if [ -z "${ZVIRT_SNAPSHOT_FORMAT:-}" ]; then
echo "Error: ZVIRT_SNAPSHOT_FORMAT is not set in $SNAPSHOT_FORMAT_FILE" >&2
exit 1
fi
function show_help () {
cat << EOF
@ -12,11 +27,27 @@ Snapshots all libvirt domains on the host. It uses the zfs-autobackup tool
to create snapshots of the domains' using the ZFS native tools.
Usage: ${0##*/} [-l] [-h] [-v]
${0##*/} --print-snapshot-format
Options:
-h display this help and exit
-v verbose mode
-l live snapshot mode (default is crash-consistent)
--print-snapshot-format
print the snapshot name format used by zvirt and exit
Snapshots are created with --no-thinning: retention is left to a separate
zfs-autobackup run in snapshot-only mode, which must be given the very same
snapshot format, otherwise it matches no snapshot, deletes nothing and still
exits successfully:
${0##*/} --print-snapshot-format
zfs-autobackup --no-snapshot --keep-source 1w1d,1m1w,1y1m \\
--snapshot-format "\$(${0##*/} --print-snapshot-format)" libvirt
The format can also be sourced from ${SNAPSHOT_FORMAT_FILE}
as \$ZVIRT_SNAPSHOT_FORMAT.
EOF
}
@ -27,9 +58,21 @@ function run () {
"$@"
}
# Same as run(), but keeps the command's stderr in the given file instead of
# letting it through: the caller needs to read it before deciding what it means.
function run_capturing_stderr () {
local stderr_file="$1"
shift
if [ "$verbose" -eq 1 ]; then
echo "$*" >&2
fi
"$@" 2>"$stderr_file"
}
OPTIND=1 # Reset in case getopts has been used previously in the shell.
while getopts "h?lv" opt; do
while getopts "h?lv-:" opt; do
case "$opt" in
h|\?)
show_help
@ -39,6 +82,18 @@ while getopts "h?lv" opt; do
;;
l) live=1
;;
-) case "$OPTARG" in
help) show_help
exit 0
;;
print-snapshot-format) print_snapshot_format=1
;;
*) echo "Error: Unknown option: --$OPTARG" >&2
show_help >&2
exit 1
;;
esac
;;
*) show_help >&2
exit 1
;;
@ -55,12 +110,17 @@ if [ $# -ne 0 ]; then
exit 1
fi
if [ "$print_snapshot_format" -eq 1 ]; then
echo "$ZVIRT_SNAPSHOT_FORMAT"
exit 0
fi
declare -a zfs_autobackup_args=()
if [ "$verbose" -eq 1 ]; then
zfs_autobackup_args+=("-v")
fi
zfs_autobackup_args+=("--no-send" "--no-thinning")
zfs_autobackup_args+=("--snapshot-format" "libvirt-%Y-%m-%d-%H:%M:%S")
zfs_autobackup_args+=("--snapshot-format" "$ZVIRT_SNAPSHOT_FORMAT")
declare -a virsh_args=()
if [ "$live" -eq 1 ]; then
@ -68,12 +128,11 @@ if [ "$live" -eq 1 ]; then
else
virsh_args+=("--all")
fi
# zfs-autobackup's stderr is kept aside for the length of one domain, see below.
stderr_file="$(mktemp)"
trap 'rm -f "$stderr_file"' EXIT
for domain in $(virsh list --name "${virsh_args[@]}"); do
if [ "$(zfs get -t filesystem,volume autobackup:libvirt-${domain} -o value -H -s local)" == "" ]; then
echo "Skipping domain ${domain} because it is not configured for autobackup" >&2
continue
fi
declare -a zfs_autobackup_hooks_args=()
if [ "$live" -eq 1 ]; then
zfs_autobackup_hooks_args+=("-l" "-r" "/var/lib/libvirt/images/${domain}")
@ -82,8 +141,36 @@ for domain in $(virsh list --name "${virsh_args[@]}"); do
zfs_autobackup_hooks_args+=("-v")
fi
run zfs-autobackup "${zfs_autobackup_args[@]}" \
# Whether a domain is configured for autobackup is zfs-autobackup's question to
# answer, not ours: it looks the autobackup:libvirt-<domain> property up on
# every dataset, resolves inheritance and tests the *value* (true, false,
# child, parent). A 'zfs get -s local' guard here could only test the presence
# of a *local* property, pool-wide: it skipped every domain configured by
# inheritance from a parent dataset - the normal way to configure a fleet -
# and, having no dataset operand, let through any domain whose property
# happened to be set on some unrelated dataset.
#
# zfs-autobackup reports "nothing selected" as exit 255 with a distinctive
# message, so keep its stderr aside long enough to tell that case (skip this
# domain, carry on with the others) from a genuine failure (abort).
rc=0
run_capturing_stderr "$stderr_file" \
zfs-autobackup "${zfs_autobackup_args[@]}" \
--pre-snapshot-cmd "$SCRIPT_DIR/libvirt-hook ${zfs_autobackup_hooks_args[*]} -k pre $domain" \
--post-snapshot-cmd "$SCRIPT_DIR/libvirt-hook ${zfs_autobackup_hooks_args[*]} -k post $domain" \
"libvirt-${domain}"
"libvirt-${domain}" || rc=$?
if [ "$rc" -ne 0 ] && grep -qF "No source filesystems selected" "$stderr_file"; then
# Not an error here: the domain simply carries no autobackup property.
# zfs-autobackup's own wording is swallowed on purpose, it reads as a
# failure while nothing failed.
echo "Skipping domain ${domain} because it is not configured for autobackup" >&2
continue
fi
cat "$stderr_file" >&2
if [ "$rc" -ne 0 ]; then
echo "Error: zfs-autobackup failed on domain ${domain} with exit code ${rc}" >&2
exit "$rc"
fi
done

32
src/share/zvirt/snapshot-format

@ -0,0 +1,32 @@
# shellcheck shell=bash
# Snapshot name format used by zvirt.
#
# This file is the single source of truth: snapshot-libvirt-domains sources it
# instead of hardcoding the format, so any tool that prunes zvirt's snapshots
# can source it too and cannot drift from what was actually written.
#
# Consumers:
#
# . /usr/share/zvirt/snapshot-format
# zfs-autobackup --no-snapshot --keep-source 1w1d,1m1w,1y1m \
# --snapshot-format "$ZVIRT_SNAPSHOT_FORMAT" libvirt
#
# or, without sourcing:
#
# snapshot-libvirt-domains --print-snapshot-format
#
# A mismatched format is silently harmless-looking: zfs-autobackup only thins
# snapshots whose name parses against the format it was given, so a wrong
# format destroys nothing and still exits 0.
#
# Note: the format carries no '{}' placeholder on purpose (zfs-autobackup's
# default is '{}-%Y%m%d%H%M%S', where '{}' expands to the backup name). All
# domains therefore share a single snapshot name pattern, which is what allows
# one prune run over the shared 'autobackup:libvirt' property to cover every
# domain at once, instead of one invocation per domain. Do not add '{}' here.
#
# Note for systemd units: '%Y', '%m', '%d', '%H', '%M' and '%S' are systemd
# specifiers and must be doubled in ExecStart= ('%%Y', '%%m', ...). Sourcing
# this file from a wrapper script avoids the problem entirely.
ZVIRT_SNAPSHOT_FORMAT='libvirt-%Y-%m-%d-%H:%M:%S'
Loading…
Cancel
Save