Compare commits

...

4 Commits

  1. 9
      .gitmodules
  2. 32
      Makefile
  3. 24
      packaging/zvirt.spec
  4. 157
      src/bin/libvirt-hook
  5. 89
      src/bin/snapshot-libvirt-domains
  6. 67
      src/bin/zvirt
  7. 581
      src/lib/zvirt/core.sh
  8. 16
      test/e2e/cloud-init/standard-user-data
  9. 19
      test/e2e/cloud-init/with-fs-user-data
  10. 31
      test/e2e/cloud-init/with-zvol-user-data
  11. 836
      test/e2e/zvirt.bats
  12. 1
      test/test_helper/bats-assert
  13. 1
      test/test_helper/bats-mock
  14. 1
      test/test_helper/bats-support
  15. 885
      test/unit/core.bats
  16. 86
      test/unit/usage.bats

9
.gitmodules

@ -1,9 +0,0 @@
[submodule "bats-support"]
path = test/test_helper/bats-support
url = https://github.com/bats-core/bats-support.git
[submodule "bats-assert"]
path = test/test_helper/bats-assert
url = https://github.com/bats-core/bats-assert.git
[submodule "bats-mock"]
path = test/test_helper/bats-mock
url = https://github.com/grayhemp/bats-mock.git

32
Makefile

@ -1,13 +1,13 @@
PREFIX ?= /usr/local
.PHONY: all test unit-test syntax-test e2e-test lint clean prerequisites install uninstall release tarball install-tarball srpm rpm copr-build copr-whoami git-tag
.PHONY: all test syntax-test lint clean prerequisites install uninstall release tarball install-tarball srpm rpm copr-build copr-whoami git-tag
VERSION := $(shell git describe --tags --abbrev=0)
all: syntax-test lint unit-test e2e-test release
all: syntax-test lint release
syntax-test:
@echo "Running syntax tests..."
@/bin/bash -nv src/bin/zvirt
@/bin/bash -nv src/lib/zvirt/core.sh
@/bin/bash -nv src/bin/libvirt-hook
@/bin/bash -nv src/bin/snapshot-libvirt-domains
prerequisites:
@echo "Installing prerequisites..."
@ -18,25 +18,18 @@ prerequisites:
@/bin/bash -Eeuo pipefail -c 'if ! rpmbuild --version &>/dev/null; then dnf install -y rpm-build; fi'
@/bin/bash -Eeuo pipefail -c 'if ! copr-cli --version &>/dev/null; then dnf install -y copr-cli; fi'
@/bin/bash -Eeuo pipefail -c 'if ! git --version &>/dev/null; then dnf install -y git; fi'
unit-test: prerequisites
@echo "Running unit tests..."
@LANG=C LC_ALL=C BATS_LIB_PATH=$(PWD)/test/test_helper bats test/unit
e2e-test: prerequisites
@echo "Running end-to-end tests..."
@LANG=C LC_ALL=C BATS_LIB_PATH=$(PWD)/test/test_helper bats test/e2e
@/bin/bash -Eeuo pipefail -c 'if [ -z "$$(which zfs-autobackup)" ]; then pip install --upgrade zfs-autobackup; fi'
install:
@echo "Installing zvirt..."
@install -d $(PREFIX)/lib/zvirt $(PREFIX)/bin
@install -m 755 src/bin/zvirt $(PREFIX)/bin/zvirt
@install -m 644 src/lib/zvirt/core.sh $(PREFIX)/lib/zvirt/core.sh
@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
uninstall:
@echo "Uninstalling zvirt..."
@rm -f $(PREFIX)/bin/zvirt
@rm -rf $(PREFIX)/lib/zvirt
@rm -f $(PREFIX)/bin/libvirt-hook
@rm -f $(PREFIX)/bin/snapshot-libvirt-domains
tarball:
@echo "Creating release tarball..."
@ -53,6 +46,9 @@ srpm: prerequisites
@git ls-files | sed 's|^|./|' > build/filelist.txt
@mkdir -p build/zvirt-$(VERSION)/SOURCES
@tar --verbatim-files-from --files-from=build/filelist.txt -cvzf build/zvirt-$(VERSION)/SOURCES/zvirt-$(VERSION).tar.gz --transform "s|^./|zvirt-$(VERSION)/|"
@pip download --no-deps zfs-autobackup -d build/zvirt-$(VERSION)/SOURCES/ --quiet
@ZFS_AB_WHL=$$(ls build/zvirt-$(VERSION)/SOURCES/zfs_autobackup-*.whl | head -1 | xargs basename); \
sed -i "s|^Source1:.*|Source1: $${ZFS_AB_WHL}|" packaging/zvirt.spec
@rpmbuild --define "_topdir $$(pwd)/build/zvirt-$(VERSION)" --define "dist %{nil}" -bs packaging/zvirt.spec
rpm: prerequisites srpm
@ -91,4 +87,4 @@ clean:
lint: prerequisites
@echo "Linting..."
@cd src && shellcheck --severity=error bin/zvirt lib/zvirt/*.sh
@cd src && shellcheck --severity=error bin/*.sh

24
packaging/zvirt.spec

@ -1,5 +1,6 @@
%global __brp_python_bytecompile %{nil}
Name: zvirt
Version: 0.0.5
Version: 0.0.6
%if %{defined dist}
Release: 1%{?dist}
%else
@ -10,13 +11,18 @@ Summary: Libvirt ZFS snapshots utility
License: MIT
URL: https://github.com/nmasse-itix/zvirt
Source0: %{name}-%{version}.tar.gz
Source1: zfs_autobackup-3.3-py3-none-any.whl
BuildArch: noarch
Requires: bash
Requires: libvirt
Requires: zfs
Requires: python3-colorama
BuildRequires: make
BuildRequires: python3-pip
BuildRequires: python3-rpm-macros
BuildRequires: python3-colorama
%description
Zvirt takes snapshots of Libvirt domains using ZFS.
@ -26,6 +32,8 @@ At the end, all components of a domain (Domain definition, TPM, NVRAM,
VirtioFS, ZFS snapshots of the underlying storage volumes) are captured
as a set of consistent ZFS snapshots.
It is implemented as a set of hooks for the zfs_autobackup script.
%prep
%setup -q
@ -34,12 +42,20 @@ as a set of consistent ZFS snapshots.
%install
make PREFIX=%{buildroot}%{_prefix} install
pip install --root %{buildroot} --prefix %{_prefix} --no-compile --no-deps --no-index --ignore-installed --find-links %{_sourcedir} zfs-autobackup
%files
%{_bindir}/zvirt
%{_prefix}/lib/zvirt/core.sh
%dir %{_prefix}/lib/zvirt
%{_bindir}/libvirt-hook
%{_bindir}/snapshot-libvirt-domains
%{_bindir}/zfs-autobackup
%{_bindir}/zfs-autoverify
%{_bindir}/zfs-check
%{python3_sitelib}/zfs_autobackup/
%{python3_sitelib}/zfs_autobackup-*.dist-info/
%changelog
* Wed Apr 22 2026 Nicolas Massé <nicolas.masse@itix.fr> - 0.0.6-1
- Switch to zfs-autobackup + hooks
* Mon Nov 24 2025 Nicolas Massé <nicolas.masse@itix.fr> - 0.0.1-1
- Initial package release

157
src/bin/libvirt-hook

@ -0,0 +1,157 @@
#!/bin/bash
set -Eeuo pipefail
hook=""
verbose=0
live=0
domain=""
domain_dir=""
function show_help () {
cat << EOF
pre/post hook for zfs-autobackup to manage snapshots of KVM/QEMU virtual machines using virsh.
Usage: ${0##*/} -k {pre|post} [-h] [-l] [-v] domain
Options:
-h display this help and exit
-v verbose mode
-l live snapshot mode (default is crash-consistent)
-r dir specify the root directory of the domain's storage
-k {pre|post} specify the hook type (pre or post)
Examples:
pre-hook for a crash-consistent snapshot of domain 'vm1':
${0##*/} -k pre vm1
post-hook for a crash-consistent snapshot of domain 'vm1':
${0##*/} -k post vm1
pre-hook for a live snapshot of domain 'vm1':
${0##*/} -k pre -l -r /var/lib/libvirt/images/vm1 vm1
post-hook for a live snapshot of domain 'vm1':
${0##*/} -k post -l -r /var/lib/libvirt/images/vm1 vm1
EOF
}
function run () {
if [ "$verbose" -eq 1 ]; then
echo "$*" >&2
fi
"$@"
}
OPTIND=1 # Reset in case getopts has been used previously in the shell.
while getopts "h?lvk:r:" opt; do
case "$opt" in
h|\?)
show_help
exit 0
;;
v) verbose=1
;;
l) live=1
;;
k) hook="$OPTARG"
;;
r) domain_dir="$OPTARG"
;;
*) show_help >&2
exit 1
;;
esac
done
shift $((OPTIND-1))
[ "${1:-}" = "--" ] && shift
if [ $# -ne 1 ]; then
echo "Error: Unexpected number of positional arguments: $#" >&2
show_help >&2
exit 1
fi
domain="$1"
if [ "$hook" == "" ]; then
echo "Error: Hook type not specified. Use -k to specify 'pre' or 'post'." >&2
show_help >&2
exit 1
fi
if ! virsh dominfo "$domain" &> /dev/null; then
echo "Error: Domain '$domain' does not exist." >&2
exit 1
fi
state=$(virsh domstate "$domain")
if [ "$live" -eq 1 ]; then
if [ "$hook" == "pre" ] && [ "$state" != "running" ]; then
echo "Error: Domain '$domain' is not running. Pre-hook can only work on running domains." >&2
exit 1
fi
if [ "$hook" == "post" ] && [ "$state" != "shut off" ]; then
echo "Error: Domain '$domain' is not shut off. Post-hook can only work on shut off domains." >&2
exit 1
fi
if [ "$domain_dir" == "" ]; then
echo "Error: Domain storage directory must be specified for live snapshots using the -r option." >&2
show_help >&2
exit 1
fi
if [ ! -d "$domain_dir" ]; then
echo "Error: Specified domain directory '$domain_dir' does not exist." >&2
exit 1
fi
if [ "$hook" == "pre" ] && [ -f "$domain_dir/domain.save" ]; then
echo "Error: Specified domain directory '$domain_dir' already contains a save file." >&2
exit 1
fi
if [ "$hook" == "post" ] && [ ! -f "$domain_dir/domain.save" ]; then
echo "Error: Specified domain directory '$domain_dir' does not contain a save file." >&2
exit 1
fi
fi
case "$hook" in
pre)
if [ "$live" -eq 1 ]; then
virsh_args=()
if [ "$verbose" -eq 1 ]; then
virsh_args+=(--verbose)
fi
run virsh save "$domain" "${domain_dir}/domain.save" "${virsh_args[@]}" --running --image-format raw
else
if [ "$state" == "running" ]; then
run virsh domfsfreeze "$domain"
fi
fi
;;
post)
if [ "$live" -eq 1 ]; then
run virsh restore "${domain_dir}/domain.save" --running
run rm -f "${domain_dir}/domain.save"
else
if [ "$state" == "running" ]; then
run virsh domfsthaw "$domain"
fi
fi
;;
*)
echo "Error: Invalid hook type specified: '$hook'. Must be 'pre' or 'post'." >&2
show_help >&2
exit 1
;;
esac
exit 0

89
src/bin/snapshot-libvirt-domains

@ -0,0 +1,89 @@
#!/bin/bash
set -Eeuo pipefail
verbose=0
live=0
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
function show_help () {
cat << EOF
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]
Options:
-h display this help and exit
-v verbose mode
-l live snapshot mode (default is crash-consistent)
EOF
}
function run () {
if [ "$verbose" -eq 1 ]; then
echo "$*" >&2
fi
"$@"
}
OPTIND=1 # Reset in case getopts has been used previously in the shell.
while getopts "h?lv" opt; do
case "$opt" in
h|\?)
show_help
exit 0
;;
v) verbose=1
;;
l) live=1
;;
*) show_help >&2
exit 1
;;
esac
done
shift $((OPTIND-1))
[ "${1:-}" = "--" ] && shift
if [ $# -ne 0 ]; then
echo "Error: Unexpected number of positional arguments: $#" >&2
show_help >&2
exit 1
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")
declare -a virsh_args=()
if [ "$live" -eq 1 ]; then
virsh_args+=("--state-running")
else
virsh_args+=("--all")
fi
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}")
fi
if [ "$verbose" -eq 1 ]; then
zfs_autobackup_hooks_args+=("-v")
fi
run 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}"
done

67
src/bin/zvirt

@ -1,67 +0,0 @@
#!/bin/bash
# This script takes snapshots or revert snapshots of libvirt domains using ZFS.
#
# It can take two kinds of snapshots: crash-consistent snapshots and live snapshots.
#
# - Crash-consistent snapshots are taken when the VM is powered off or powered on.
# They capture only the disk state at the time of the snapshot.
# Especially, those items are NOT included in crash-consistent snapshots: TPM, NVRAM, domain definition (XML).
#
# Taking crash-consistent snapshots makes use of ZFS filesystem snapshots of the underlying storage volumes
#
# Restoring from crash-consistent snapshots involves destroying the domain, reverting the ZFS snapshots and
# restarting the domain.
#
# - Live snapshots are taken while the VM is running and capture the entire state of the VM, including memory,
# CPU state, TPM, NVRAM, and domain definition (XML).
#
# Taking live snapshots makes use of libvirt's "save" functionality. The domain is paused, its state is saved
# to disk, the ZFS snapshots of the underlying storage volumes are taken, and then the domain is resumed.
#
# Restoring from live snapshots involves destroying the domain, reverting the ZFS snapshots, restoring the saved state,
# and restarting the domain.
#
set -Eeuo pipefail
# Make sure the output of underlying tool won't be altered by locale settings
export LANG=C
export LC_ALL=C
# Load core library
script_dir="$(realpath "$(dirname "${BASH_SOURCE[0]}")/../")"
source "$script_dir/lib/zvirt/core.sh"
# Parse command line arguments and act accordingly
init_global_variables
if ! parse_args "$@"; then
echo
show_help >&2
exit 1
fi
case "$action" in
snapshot)
preflight_checks "$action" "$snapshot_name" "${domains[@]}" || fatal "Pre-flight checks failed."
take_snapshots || fatal "Failed to take snapshots."
;;
revert)
preflight_checks "$action" "$snapshot_name" "${domains[@]}" || fatal "Pre-flight checks failed."
revert_snapshots || fatal "Failed to revert snapshots."
;;
list)
preflight_checks "$action" "${domains[@]}" || fatal "Pre-flight checks failed."
list_snapshots "${domains[@]}" || fatal "Failed to list snapshots."
;;
prune)
preflight_checks "$action" "${domains[@]}" || fatal "Pre-flight checks failed."
prune_snapshots "${domains[@]}" || fatal "Failed to prune snapshots."
;;
*)
fatal "Unknown action '$action'."
;;
esac
log_verbose "Operation '$action' completed successfully."
exit 0

581
src/lib/zvirt/core.sh

@ -1,581 +0,0 @@
#!/bin/bash
##
## zvirt core library - Provides functions for taking and reverting snapshots of libvirt domains using ZFS.
##
# Reports a verbose message to stdout if verbose mode is enabled.
function log_verbose () {
if [ "$verbose" -eq 1 ]; then
echo "$@" 2>&1
fi
}
# Reports a fatal error message to stderr and exits with a non-zero exit code.
function fatal () {
echo "Error: $*" 2>&1
exit 1
}
# Reports an error message to stderr.
function error () {
echo "Error: $*" 2>&1
}
function show_help () {
cat << EOF
Usage: ${0##*/} action [-h] [-l] [-v] -d <domain_name> -s <snapshot_name>
Options:
-h display this help and exit
-v verbose mode
-l live snapshot mode (default is crash-consistent)
-d DOMAIN specify domain name (you can specify multiple -d options)
-s SNAPSHOT specify snapshot name
-b batch mode (pause all domains, take snapshots, then resume all domains)
-k N keep at most N snapshots per domain (used with 'prune' action)
Actions:
snapshot take a snapshot of the specified domain(s)
revert revert to a snapshot of the specified domain(s)
list list snapshots of the specified domain(s) (or all domains if none specified)
prune prune old snapshots of the specified domain(s) according to retention policy
Examples:
Take a crash-consistent snapshot of domain 'vm1' named 'backup1':
${0##*/} snapshot -d vm1 -s backup1
Take a live snapshot of domains 'vm1' and 'vm2' in batch mode, named 'livebackup':
${0##*/} snapshot -l -b -d vm1 -d vm2 -s livebackup
Revert domain 'vm1' to snapshot 'backup1':
${0##*/} revert -d vm1 -s backup1
List snapshots of domain 'vm1':
${0##*/} list -d vm1
List snapshots of all domains:
${0##*/} list
Prune snapshots of all domains, keeping at most 5 snapshots:
${0##*/} prune -k 5
EOF
}
# Initialize the global variables
function init_global_variables () {
# Command line parsing variables
snapshot_name=""
domains=()
verbose=0
action=""
batch=0
live=0
keep=0
# Cache for domain parameters to avoid redundant calls to the zfs command
declare -gA domain_params_cache=( )
}
# Parses the command-line arguments.
function parse_args () {
local should_exit=0
# Try to get the action from the first positional argument
if [ -n "${1:-}" ] && [[ ! "${1:-}" =~ ^- ]]; then
action="${1:-}"
shift || true
fi
OPTIND=1 # Reset in case getopts has been used previously in the shell.
while getopts "h?blvd:s:k:" opt; do
case "$opt" in
h|\?)
show_help
exit 0
;;
v) verbose=1
;;
d) domains+=( "$OPTARG" )
;;
s) snapshot_name="$OPTARG"
;;
b) batch=1
;;
l) live=1
;;
k) keep="$OPTARG"
;;
*) show_help >&2
exit 1
;;
esac
done
shift $((OPTIND-1))
[ "${1:-}" = "--" ] && shift
if [ $# -ne 0 ]; then
echo "Error: Unexpected positional arguments: $*"
should_exit=1
fi
if [ ${#domains[@]} -eq 0 ]; then
# Get all domains
mapfile -t domains < <(virsh list --all --name | grep -v '^$')
fi
case "$action" in
snapshot)
if [ ${#domains[@]} -eq 0 ] || [ -z "$snapshot_name" ]; then
echo "Error: Domain name(s) and snapshot name must be specified."
should_exit=1
fi
if [[ ! "$snapshot_name" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo "Error: Snapshot name '$snapshot_name' contains invalid characters. Only alphanumeric characters, dots (.), underscores (_) and hyphens (-) are allowed."
should_exit=1
fi
;;
revert)
if [ ${#domains[@]} -eq 0 ] || [ -z "$snapshot_name" ]; then
echo "Error: Domain name(s) and snapshot name must be specified."
should_exit=1
fi
if [ "$live" -eq 1 ]; then
echo "Error: Live mode is only supported for the 'snapshot' action."
should_exit=1
fi
;;
list)
;;
prune)
if [ "$keep" -le 0 ]; then
echo "Error: The -k option with a positive integer value must be specified for the 'prune' action."
should_exit=1
fi
;;
*)
echo "Error: Unsupported action '$action'."
should_exit=1
;;
esac
return $should_exit
}
# Checks if the specified domain exists.
function domain_exists () {
local domain="$1"
if virsh dominfo "$domain" &> /dev/null; then
return 0
else
return 1
fi
}
# Performs various checks on the specified domain before taking or reverting a snapshot.
# All the checks are performed according to the specified action (snapshot or revert).
# Any errors are reported via stderr and the function returns a non-zero exit code.
function domain_checks () {
local action="$1"
local domain="$2"
local snapshot_name
if [ "$action" == "snapshot" ] || [ "$action" == "revert" ]; then
snapshot_name="$3"
fi
local error=0
local state=""
if ! domain_exists "$domain"; then
error "Domain '$domain' does not exist."
return 1 # There is no point in continuing checks if the domain does not exist
fi
# ZFS dataset checks
zfs_datasets=( $(get_zfs_datasets_from_domain "$domain") )
if [ ${#zfs_datasets[@]} -ne 1 ]; then
error "$domain: Wrong number of ZFS datasets (${#zfs_datasets[@]}) found." ; error=1
fi
zfs_dataset="${zfs_datasets[0]:-}"
# Zvols checks
zfs_zvols=( $(get_zfs_zvols_from_domain "$domain") )
for zvol in "${zfs_zvols[@]}"; do
# Check if zvol is a child of $zfs_dataset
if [[ "$zvol" != "$zfs_dataset"* ]]; then
error "$domain: ZFS zvol '$zvol' is not a child of dataset '$zfs_dataset'." ; error=1
fi
done
zfs_dataset_snapshots=( $(get_zfs_snapshots_from_dataset "${zfs_dataset}") )
zfs_mountpoint=$(get_zfs_dataset_mountpoint "${zfs_dataset}")
if [ -z "$zfs_mountpoint" ] || [[ ! "$zfs_mountpoint" =~ ^/ ]]; then
error "$domain: Wrong ZFS mountpoint for dataset '$zfs_dataset': '$zfs_mountpoint'." ; error=1
fi
state=$(domain_state "$domain")
# Store those values in cache for later use
domain_params_cache["$domain/state"]="${state}"
domain_params_cache["$domain/dataset"]="${zfs_dataset}"
domain_params_cache["$domain/mountpoint"]="${zfs_mountpoint}"
domain_params_cache["$domain/zvols"]="${zfs_zvols[*]}"
domain_params_cache["$domain/snapshots"]="${zfs_dataset_snapshots[*]}"
case "$action" in
snapshot)
# Check domain state
if [ "$state" != "shut off" ] && [ "$state" != "running" ]; then
error "$domain: Domain must be either 'shut off' or 'running' to take a snapshot (current state: '$state')." ; error=1
fi
# Check if live snapshot requested on powered-off domain
if [ "$live" -eq 1 ] && [ "$state" != "running" ]; then
log_verbose "$domain: Live snapshot requested but domain is not running."
fi
# Check if snapshot already exists
if printf '%s\n' "${zfs_dataset_snapshots[@]}" | grep -Fqx "$snapshot_name" ; then
error "$domain: Snapshot '$snapshot_name' already exists." ; error=1
fi
for zvol in "${zfs_zvols[@]}"; do
zfs_zvol_snapshots=( $(get_zfs_snapshots_from_dataset "$zvol") )
if printf '%s\n' "${zfs_zvol_snapshots[@]}" | grep -Fqx "$snapshot_name" ; then
error "$domain: Snapshot '$snapshot_name' already exists for ZFS zvol '$zvol'." ; error=1
fi
done
# Check if save file already exists for live snapshot
if [ "$live" -eq 1 ] && has_save_file "$domain"; then
error "$domain: Save file '${zfs_mountpoint}/domain.save' already exists." ; error=1
fi
;;
revert)
# Check domain state
if [ "$state" != "shut off" ]; then
error "$domain: Domain must be 'shut off' to revert a snapshot (current state: '$state')." ; error=1
fi
# Check if snapshot exists
if ! printf '%s\n' "${zfs_dataset_snapshots[@]}" | grep -Fqx "$snapshot_name" ; then
error "$domain: Snapshot '$snapshot_name' does not exist for domain '$domain'." ; error=1
fi
for zvol in "${zfs_zvols[@]}"; do
zfs_zvol_snapshots=( $(get_zfs_snapshots_from_dataset "$zvol") )
if ! printf '%s\n' "${zfs_zvol_snapshots[@]}" | grep -Fqx "$snapshot_name" ; then
error "$domain: Snapshot '$snapshot_name' does not exist for ZFS zvol '$zvol'." ; error=1
fi
done
;;
list)
;;
prune)
if [ ${#zfs_dataset_snapshots[@]} -le "$keep" ]; then
log_verbose "$domain: No snapshots to prune (total: ${#zfs_dataset_snapshots[@]}, keep: $keep)."
fi
;;
*)
# Should not reach here due to prior validation
error "$domain: Unknown action '$action'."
;;
esac
if [ $error -ne 0 ]; then
error "$domain: Domain checks failed."
return 1
fi
return 0
}
# Gets the mountpoint of the specified ZFS dataset.
function get_zfs_dataset_mountpoint () {
local zfs_dataset="$1"
zfs get -H -o value mountpoint "${zfs_dataset}"
}
# Gets the current state of the specified domain.
function domain_state () {
local domain="$1"
virsh domstate "$domain"
}
# Gets the list of ZFS datasets used by the specified domain (excluding zvols)
function get_zfs_datasets_from_domain () {
local domain="$1"
virsh domblklist "$domain" --details | awk '$1 == "file" && $2 == "disk" { print $4 }' | while read -r file; do df --output=source "$file" | tail -n 1; done | sort | uniq
}
# Gets the list of ZFS zvols used by the specified domain
function get_zfs_zvols_from_domain () {
local domain="$1"
virsh domblklist "$domain" --details | awk '$1 == "block" && $2 == "disk" && $4 ~ /^\/dev\/zvol\// { gsub(/\/dev\/zvol\//, "", $4); print $4 }'
}
# Gets the list of ZFS snapshots for the specified dataset.
function get_zfs_snapshots_from_dataset () {
local dataset="$1"
zfs list -H -t snapshot -o name "$dataset" | awk -F'@' '{print $2}' | sort | uniq
}
# Takes a live snapshot of the specified domain.
function take_live_snapshot () {
local domain="$1"
local snapshot="$2"
log_verbose "$domain: Taking live snapshot '$snapshot'..."
zfs_dataset="${domain_params_cache["$domain/dataset"]}"
zfs_mountpoint="${domain_params_cache["$domain/mountpoint"]}"
virsh save "$domain" "${zfs_mountpoint}/domain.save" --running --verbose --image-format raw
zfs snapshot -r "${zfs_dataset}@${snapshot}"
}
# Takes a crash-consistent snapshot of the specified domain.
function take_crash_consistent_snapshot () {
local domain="$1"
local snapshot="$2"
log_verbose "$domain: Taking crash-consistent snapshot '$snapshot'..."
zfs_dataset="${domain_params_cache["$domain/dataset"]}"
zfs snapshot -r "${zfs_dataset}@${snapshot}"
}
# Reverts the specified snapshot for the given domain.
function revert_snapshot () {
local domain="$1"
local snapshot="$2"
log_verbose "$domain: Reverting snapshot '$snapshot'..."
zfs_dataset="${domain_params_cache["$domain/dataset"]}"
zfs list -H -r -o name "$zfs_dataset" | while read dataset; do
zfs rollback -Rrf "$dataset@$snapshot"
done
}
# Restores a saved domain.
function restore_domain () {
local domain="$1"
log_verbose "$domain: Restoring live snapshot..."
zfs_dataset="${domain_params_cache["$domain/dataset"]}"
zfs_mountpoint="${domain_params_cache["$domain/mountpoint"]}"
virsh_restore_opts=( )
if [ "$batch" -eq 1 ]; then
virsh_restore_opts+=( "--paused" )
else
virsh_restore_opts+=( "--running" )
fi
virsh restore "${zfs_mountpoint}/domain.save" "${virsh_restore_opts[@]}"
}
# Pauses all domains in the list.
function pause_all_domains () {
local domains=( "$@" )
for domain in "${domains[@]}"; do
log_verbose "$domain: Pausing domain..."
state="${domain_params_cache["$domain/state"]}"
if [ "$state" == "running" ]; then
virsh suspend "$domain"
fi
done
}
# Resumes all domains in the list.
function resume_all_domains () {
local domains=( "$@" )
for domain in "${domains[@]}"; do
log_verbose "$domain: Resuming domain..."
state="$(domain_state "$domain")"
case "$state" in
paused)
virsh resume "$domain" || true
;;
"shut off")
virsh start "$domain" || true
;;
*)
continue
;;
esac
done
}
# Performs pre-flight checks for all specified domains according to the action.
function preflight_checks () {
local action="$1" ; shift
local snapshot_name
if [ "$action" == "snapshot" ] || [ "$action" == "revert" ]; then
snapshot_name="$1" ; shift
fi
local error=0
local domains=( "$@" )
for domain in "${domains[@]}"; do
log_verbose "$domain: Performing domain pre-flight checks for $action..."
local -a domain_checks_args=( "$action" "$domain" )
if [ "$action" == "snapshot" ] || [ "$action" == "revert" ]; then
domain_checks_args+=( "$snapshot_name" )
fi
if ! domain_checks "${domain_checks_args[@]}"; then
error=1
fi
done
return $error
}
# Removes the save file for the specified domain.
function remove_save_file () {
local domain="$1"
zfs_mountpoint="${domain_params_cache["$domain/mountpoint"]}"
if [ -f "${zfs_mountpoint}/domain.save" ]; then
log_verbose "$domain: Removing save file '${zfs_mountpoint}/domain.save'..."
rm -f "${zfs_mountpoint}/domain.save"
fi
}
# Checks if the save file exists for the specified domain.
function has_save_file () {
local domain="$1"
zfs_mountpoint="${domain_params_cache["$domain/mountpoint"]}"
if [ -f "${zfs_mountpoint}/domain.save" ]; then
return 0
else
return 1
fi
}
# Thaws the specified domain filesystem.
function fsthaw_domain () {
local domain="$1"
virsh domfsthaw "$domain"
}
# Freezes the specified domain filesystem.
function fsfreeze_domain () {
local domain="$1"
virsh domfsfreeze "$domain"
}
# Thaws all domains in the list.
function fsthaw_all_domains () {
local domains=( "$@" )
for domain in "${domains[@]}"; do
log_verbose "$domain: Thawing domain..."
state="${domain_params_cache["$domain/state"]}"
if [ "$state" == "running" ]; then
fsthaw_domain "$domain"
fi
done
}
# Freezes all domains in the list.
function fsfreeze_all_domains () {
local domains=( "$@" )
for domain in "${domains[@]}"; do
log_verbose "$domain: Freezing domain..."
state="${domain_params_cache["$domain/state"]}"
if [ "$state" == "running" ]; then
fsfreeze_domain "$domain"
fi
done
}
# Takes snapshots for all specified domains.
function take_snapshots () {
if [ "$batch" -eq 1 ] && [ "$live" -eq 1 ]; then
pause_all_domains "${domains[@]}"
elif [ "$batch" -eq 1 ] && [ "$live" -eq 0 ]; then
fsfreeze_all_domains "${domains[@]}"
fi
for domain in "${domains[@]}"; do
state="${domain_params_cache["$domain/state"]}"
if [ "$live" -eq 1 ] && [ "$state" == "running" ]; then
take_live_snapshot "$domain" "$snapshot_name"
restore_domain "$domain"
if [ "$batch" -eq 1 ]; then
remove_save_file "$domain"
fi
else
if [ "$batch" -eq 0 ] && [ "$state" == "running" ]; then
fsfreeze_domain "$domain"
fi
take_crash_consistent_snapshot "$domain" "$snapshot_name"
if [ "$batch" -eq 0 ] && [ "$state" == "running" ]; then
fsthaw_domain "$domain"
fi
fi
done
if [ "$batch" -eq 1 ] && [ "$live" -eq 1 ]; then
resume_all_domains "${domains[@]}"
elif [ "$batch" -eq 1 ] && [ "$live" -eq 0 ]; then
fsthaw_all_domains "${domains[@]}"
fi
return 0
}
# Reverts snapshots for all specified domains.
function revert_snapshots () {
for domain in "${domains[@]}"; do
revert_snapshot "$domain" "$snapshot_name"
if has_save_file "$domain"; then
restore_domain "$domain"
if [ "$batch" -eq 1 ]; then
remove_save_file "$domain"
fi
fi
done
if [ "$batch" -eq 1 ]; then
resume_all_domains "${domains[@]}"
fi
}
# Lists snapshots for all specified domains.
function list_snapshots () {
local domains=( "$@" )
local domain
local snapshot
for domain in "${domains[@]}"; do
echo "Snapshots for domain '$domain':"
for snapshot in ${domain_params_cache["$domain/snapshots"]}; do
echo " - $snapshot"
done
done
}
# Prunes old snapshots for all specified domains according to the retention policy.
function prune_snapshots () {
local domains=( "$@" )
local dataset
local snapshots
local domain
for domain in "${domains[@]}"; do
snapshots=( ${domain_params_cache["$domain/snapshots"]} )
dataset="${domain_params_cache["$domain/dataset"]}"
if [ "${#snapshots[@]}" -le "$keep" ]; then
continue
fi
local first_to_delete_idx=$(( ${#snapshots[@]} - keep - 1 ))
local first_to_delete="${snapshots[$first_to_delete_idx]}"
if [ -z "$first_to_delete" ]; then
continue
fi
zfs destroy -r "${dataset}@%${first_to_delete}"
done
}

16
test/e2e/cloud-init/standard-user-data

@ -1,16 +0,0 @@
#cloud-config
bootcmd:
- setsebool -P virt_qemu_ga_run_unconfined on
- setsebool -P virt_qemu_ga_read_nonsecurity_files on
- setsebool -P virt_rw_qemu_ga_data on
- install -o root -g root -m 0777 --context=system_u:object_r:virt_qemu_ga_data_t:s0 -d /test/rootfs
users:
- name: e2e
gecos: End-to-End Test User
sudo: ALL=(ALL) NOPASSWD:ALL
groups: wheel
lock_passwd: false
# echo -n test | mkpasswd -m bcrypt -s
passwd: $2b$05$Oh13XsRSrGrL/iSvV0Rax.w7rQMx/6lyBTCuaEVXrdh/qiagci9bS

19
test/e2e/cloud-init/with-fs-user-data

@ -1,19 +0,0 @@
#cloud-config
mounts:
- [ data, /test/virtiofs, virtiofs, "defaults,context=system_u:object_r:virt_qemu_ga_data_t:s0", "0", "0" ]
bootcmd:
- setsebool -P virt_qemu_ga_run_unconfined on
- setsebool -P virt_qemu_ga_read_nonsecurity_files on
- setsebool -P virt_rw_qemu_ga_data on
- install -o root -g root -d /test/virtiofs
users:
- name: e2e
gecos: End-to-End Test User
sudo: ALL=(ALL) NOPASSWD:ALL
groups: wheel
lock_passwd: false
# echo -n test | mkpasswd -m bcrypt -s
passwd: $2b$05$Oh13XsRSrGrL/iSvV0Rax.w7rQMx/6lyBTCuaEVXrdh/qiagci9bS

31
test/e2e/cloud-init/with-zvol-user-data

@ -1,31 +0,0 @@
#cloud-config
disk_setup:
/dev/vdb:
table_type: gpt
layout: true
overwrite: true
fs_setup:
- label: zvol
filesystem: xfs
device: /dev/vdb
partition: auto
mounts:
- [ LABEL=zvol, /test/zvol, xfs, "defaults,context=system_u:object_r:virt_qemu_ga_data_t:s0", "0", "2" ]
bootcmd:
- setsebool -P virt_qemu_ga_run_unconfined on
- setsebool -P virt_qemu_ga_read_nonsecurity_files on
- setsebool -P virt_rw_qemu_ga_data on
- mkdir -p /test/zvol
users:
- name: e2e
gecos: End-to-End Test User
sudo: ALL=(ALL) NOPASSWD:ALL
groups: wheel
lock_passwd: false
# echo -n test | mkpasswd -m bcrypt -s
passwd: $2b$05$Oh13XsRSrGrL/iSvV0Rax.w7rQMx/6lyBTCuaEVXrdh/qiagci9bS

836
test/e2e/zvirt.bats

@ -1,836 +0,0 @@
#!/usr/bin/env bats
setup() {
bats_load_library 'bats-support'
bats_load_library 'bats-assert'
set -Eeuo pipefail
export LANG=C LC_ALL=C
zvirt () {
"${BATS_TEST_DIRNAME}/../../src/bin/zvirt" "$@"
}
declare -g e2e_test_enable_debug=1
e2e_test_debug_log(){
if [ "$e2e_test_enable_debug" -eq 1 ]; then
echo "$@" >&3
fi
}
qemu_exec() {
domain="$1"
shift || true
local json_args=""
for arg in "${@:2}"; do
if [ -n "$json_args" ]; then
json_args+=", "
fi
json_args+="\"$arg\""
done
local command="{\"execute\": \"guest-exec\", \"arguments\": {\"path\": \"$1\", \"arg\": [ $json_args ], \"capture-output\": true }}"
output="$(virsh qemu-agent-command "$domain" "$command")"
#e2e_test_debug_log "qemu_exec: command output: $output"
pid="$(echo "$output" | jq -r '.return.pid')"
if [ -z "$pid" ] || [ "$pid" == "null" ]; then
e2e_test_debug_log "qemu_exec: failed to get pid from command output"
return 1
fi
sleep .25
while true; do
local status_command="{\"execute\": \"guest-exec-status\", \"arguments\": {\"pid\": $pid}}"
status_output="$(virsh qemu-agent-command "$domain" "$status_command")"
#e2e_test_debug_log "qemu_exec: status output: $status_output"
exited="$(echo "$status_output" | jq -r '.return.exited')"
if [ "$exited" == "true" ]; then
stdout_base64="$(echo "$status_output" | jq -r '.return["out-data"]')"
if [ "$stdout_base64" != "null" ]; then
echo "$stdout_base64" | base64 --decode
fi
stderr_base64="$(echo "$status_output" | jq -r '.return["err-data"]')"
if [ "$stderr_base64" != "null" ]; then
echo "$stderr_base64" | base64 --decode >&2
fi
exit_code="$(echo "$status_output" | jq -r '.return.exitcode')"
return $exit_code
fi
sleep 1
done
}
create_cloud_init_iso () {
local domain="$1"
local iso_path="/var/lib/libvirt/images/${domain}/cloud-init.iso"
local user_data_path="/var/lib/libvirt/images/${domain}/cloud-init/user-data"
local meta_data_path="/var/lib/libvirt/images/${domain}/cloud-init/meta-data"
# Create cloud-init user-data and meta-data files
mkdir -p "/var/lib/libvirt/images/${domain}/cloud-init"
cp "${BATS_TEST_DIRNAME}/cloud-init/${domain}-user-data" "$user_data_path"
cat > "$meta_data_path" <<EOF
instance-id: ${domain}
local-hostname: ${domain}
EOF
# Create ISO image
genisoimage -output "$iso_path" -volid cidata -joliet -rock "$user_data_path" "$meta_data_path"
}
convert_cloud_image() {
local src="$1"
local dest="$2"
# Convert qcow2 to raw and resize to 20G
qemu-img convert -f qcow2 -O raw "$src" "$dest"
qemu-img resize -f raw "$dest" 20G
}
cleanup() {
e2e_test_debug_log "teardown: Cleaning up created domains and images..."
for domain in standard with-fs with-zvol; do
state="$(virsh domstate "$domain" 2>/dev/null || true)"
if [[ -n "$state" && "$state" != "shut off" ]]; then
virsh destroy "$domain"
fi
if virsh dominfo "$domain" &>/dev/null; then
virsh undefine "$domain" --nvram
fi
done
sleep 1
sync
sleep 1
for domain in standard with-fs with-zvol; do
if zfs list data/domains/"$domain" &>/dev/null; then
zfs destroy -rR data/domains/"$domain"
fi
sleep .2
rm -rf "/var/lib/libvirt/images/${domain}"
done
}
create_domains() {
# Create the standard VM
e2e_test_debug_log "setup: Creating the standard VM..."
mkdir -p /var/lib/libvirt/images/standard
zfs create -p data/domains/standard -o mountpoint=/var/lib/libvirt/images/standard
convert_cloud_image "$fedora_img" "/var/lib/libvirt/images/standard/root.img"
create_cloud_init_iso "standard"
virt-install --noautoconsole \
--name=standard \
--cpu=host-passthrough \
--vcpus=1 \
--ram=4096 \
--os-variant=fedora-rawhide \
--disk=path=/var/lib/libvirt/images/standard/root.img,target.dev=vda,bus=virtio,driver.discard=unmap,driver.io=io_uring,format=raw,sparse=True,blockio.logical_block_size=512,blockio.physical_block_size=512,serial=root,format=raw \
--network=none \
--console=pty,target.type=virtio \
--serial=pty \
--disk=path=/var/lib/libvirt/images/standard/cloud-init.iso,readonly=True \
--import \
--sysinfo=system.serial=ds=nocloud \
--boot=uefi
# Create the with-fs VM
e2e_test_debug_log "setup: Creating the with-fs VM..."
mkdir -p /var/lib/libvirt/images/with-fs /srv/with-fs
chmod 0777 /srv/with-fs
zfs create -p data/domains/with-fs -o mountpoint=/var/lib/libvirt/images/with-fs
zfs create -p data/domains/with-fs/virtiofs -o mountpoint=/srv/with-fs
convert_cloud_image "$fedora_img" "/var/lib/libvirt/images/with-fs/root.img"
create_cloud_init_iso "with-fs"
virt-install --noautoconsole \
--name=with-fs \
--cpu=host-passthrough \
--vcpus=1 \
--ram=4096 \
--os-variant=fedora-rawhide \
--disk=path=/var/lib/libvirt/images/with-fs/root.img,target.dev=vda,bus=virtio,driver.discard=unmap,driver.io=io_uring,format=raw,sparse=True,blockio.logical_block_size=512,blockio.physical_block_size=512,serial=root,format=raw \
--network=none \
--console=pty,target.type=virtio \
--serial=pty \
--disk=path=/var/lib/libvirt/images/with-fs/cloud-init.iso,readonly=True \
--import \
--sysinfo=system.serial=ds=nocloud \
--boot=uefi \
--memorybacking=access.mode=shared,source.type=memfd \
--filesystem=type=mount,accessmode=passthrough,driver.type=virtiofs,driver.queue=1024,source.dir=/srv/with-fs,target.dir=data
# Create the with-zvol VM
e2e_test_debug_log "setup: Creating the with-zvol VM..."
mkdir -p /var/lib/libvirt/images/with-zvol
zfs create -p data/domains/with-zvol -o mountpoint=/var/lib/libvirt/images/with-zvol
zfs create -V 10G data/domains/with-zvol/data
convert_cloud_image "$fedora_img" "/var/lib/libvirt/images/with-zvol/root.img"
create_cloud_init_iso "with-zvol"
virt-install --noautoconsole \
--name=with-zvol \
--cpu=host-passthrough \
--vcpus=1 \
--ram=4096 \
--os-variant=fedora-rawhide \
--disk=path=/var/lib/libvirt/images/with-zvol/root.img,target.dev=vda,bus=virtio,driver.discard=unmap,driver.io=io_uring,format=raw,sparse=True,blockio.logical_block_size=512,blockio.physical_block_size=512,serial=root,format=raw \
--disk=path=/dev/zvol/data/domains/with-zvol/data,target.dev=vdb,bus=virtio,cache=directsync,blockio.logical_block_size=4096,blockio.physical_block_size=4096,driver.discard=unmap,driver.io=io_uring,serial=zvol \
--network=none \
--console=pty,target.type=virtio \
--serial=pty \
--disk=path=/var/lib/libvirt/images/with-zvol/cloud-init.iso,readonly=True \
--import \
--sysinfo=system.serial=ds=nocloud \
--boot=uefi
}
readiness_wait() {
e2e_test_debug_log "setup: Waiting for VMs to become ready..."
for domain in standard with-fs with-zvol; do
e2e_test_debug_log "setup: Waiting for qemu guest agent to be running in domain '$domain'..."
until virsh qemu-agent-command "$domain" '{"execute":"guest-ping"}' &>/dev/null; do
sleep 2
done
done
e2e_test_debug_log "setup: all VMs started successfully"
for domain in standard with-fs with-zvol; do
e2e_test_debug_log "setup: Waiting for cloud-init to complete in domain '$domain'..."
until qemu_exec "$domain" test -f /var/lib/cloud/instance/boot-finished; do
sleep 2
done
done
if ! qemu_exec with-fs grep -q /test/virtiofs /proc/mounts; then
e2e_test_debug_log "setup: virtiofs not mounted in 'with-fs' domain"
return 1
fi
if ! qemu_exec with-zvol grep -q /test/zvol /proc/mounts; then
e2e_test_debug_log "setup: zvol not mounted in 'with-zvol' domain"
return 1
fi
e2e_test_debug_log "setup: VMs are ready"
}
local fedora_url="https://download.fedoraproject.org/pub/fedora/linux/releases/42/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-42-1.1.x86_64.qcow2"
local fedora_img="/var/lib/libvirt/images/$(basename "$fedora_url")"
if [ ! -f "$fedora_img" ]; then
e2e_test_debug_log "setup: downloading Fedora Cloud image to $fedora_img"
mkdir -p /var/lib/libvirt/images/library
curl -sSfL -o "$fedora_img" "$fedora_url"
fi
e2e_test_debug_log "setup: Fedora Cloud image is at $fedora_img"
# Cleanup any leftover artifacts from previous runs
cleanup
create_domains
readiness_wait
}
teardown() {
cleanup
}
@test "zvirt: setup selftest" {
e2e_test_debug_log "setup: provisioning completed"
}
@test "zvirt: prune snapshots" {
# Take five snapshots in a row, each time creating and deleting a witness file
for snap in s1 s2 s3 s4 s5; do
# Create witness files in all three domains before taking snapshots
qemu_exec standard touch /test/rootfs/witness-file.$snap
qemu_exec with-fs touch /test/virtiofs/witness-file.$snap
qemu_exec with-zvol touch /test/zvol/witness-file.$snap
# Verify that the witness files exist in the virtiofs host mount
run test -f /srv/with-fs/witness-file.$snap
assert_success
# Take crash-consistent snapshots for all three domains
run zvirt snapshot -d standard -d with-zvol -d with-fs -s $snap
assert_success
# Verify that the domains are still running
run virsh domstate standard
assert_success
assert_output "running"
run virsh domstate with-fs
assert_success
assert_output "running"
run virsh domstate with-zvol
assert_success
assert_output "running"
# Assert that the files created before the snapshot exist
run qemu_exec standard ls -1 /test/rootfs
assert_success
assert_output "witness-file.$snap"
run qemu_exec with-fs ls -1 /test/virtiofs
assert_success
assert_output "witness-file.$snap"
run qemu_exec with-zvol ls -1 /test/zvol
assert_success
assert_output "witness-file.$snap"
# Delete the witness files
run qemu_exec standard rm /test/rootfs/witness-file.$snap
assert_success
run qemu_exec with-fs rm /test/virtiofs/witness-file.$snap
assert_success
run qemu_exec with-zvol rm /test/zvol/witness-file.$snap
assert_success
# Sync all filesystems
run qemu_exec standard sync
assert_success
run qemu_exec with-fs sync
assert_success
run qemu_exec with-zvol sync
assert_success
# Wait a moment to ensure all writes are flushed
sleep 2
# Verify that the witness files have been deleted in the virtiofs host mount
run test -f /srv/with-fs/witness-file.$snap
assert_failure
done
# List snapshots and verify their existence
run zvirt list -d standard -d with-zvol -d with-fs
assert_success
assert_output "Snapshots for domain 'standard':
- s1
- s2
- s3
- s4
- s5
Snapshots for domain 'with-zvol':
- s1
- s2
- s3
- s4
- s5
Snapshots for domain 'with-fs':
- s1
- s2
- s3
- s4
- s5"
# Prune snapshots to keep only the latest two
run zvirt prune -k 2 -d standard -d with-zvol -d with-fs
assert_success
# List snapshots and verify their existence
run zvirt list -d standard -d with-zvol -d with-fs
assert_success
assert_output "Snapshots for domain 'standard':
- s4
- s5
Snapshots for domain 'with-zvol':
- s4
- s5
Snapshots for domain 'with-fs':
- s4
- s5"
# Stop all domains
run virsh destroy standard
assert_success
run virsh destroy with-fs
assert_success
run virsh destroy with-zvol
assert_success
# Revert snapshots in batch mode
run zvirt revert -d standard -d with-zvol -d with-fs -s s4
assert_success
# Check all domains have been shut off
run virsh domstate standard
assert_success
assert_output "shut off"
run virsh domstate with-fs
assert_success
assert_output "shut off"
run virsh domstate with-zvol
assert_success
assert_output "shut off"
# Start all domains
run virsh start standard
assert_success
run virsh start with-fs
assert_success
run virsh start with-zvol
assert_success
# Wait for all domains to be fully ready
readiness_wait
# Verify that the witness files still exist after revert
run qemu_exec standard ls -1 /test/rootfs
assert_success
assert_output "witness-file.s4"
run qemu_exec with-fs ls -1 /test/virtiofs
assert_success
assert_output "witness-file.s4"
run qemu_exec with-zvol ls -1 /test/zvol
assert_success
assert_output "witness-file.s4"
}
@test "zvirt: take live snapshot in batch mode" {
# Create witness files in all three domains before taking snapshots
qemu_exec standard touch /test/rootfs/witness-file
qemu_exec with-fs touch /test/virtiofs/witness-file
qemu_exec with-zvol touch /test/zvol/witness-file
# Verify that the witness files exist in the virtiofs host mount
run test -f /srv/with-fs/witness-file
assert_success
# Take live snapshots for all three domains
run zvirt snapshot -b -d standard -d with-zvol -d with-fs -s backup1 -l
assert_success
# Verify that the domains are still running
run virsh domstate standard
assert_success
assert_output "running"
run virsh domstate with-fs
assert_success
assert_output "running"
run virsh domstate with-zvol
assert_success
assert_output "running"
# Assert that the files created before the snapshot exist
run qemu_exec standard ls -1 /test/rootfs
assert_success
assert_output "witness-file"
run qemu_exec with-fs ls -1 /test/virtiofs
assert_success
assert_output "witness-file"
run qemu_exec with-zvol ls -1 /test/zvol
assert_success
assert_output "witness-file"
# List snapshots and verify their existence
run zvirt list -d standard -d with-zvol -d with-fs
assert_success
assert_output "Snapshots for domain 'standard':
- backup1
Snapshots for domain 'with-zvol':
- backup1
Snapshots for domain 'with-fs':
- backup1"
# Attempt to take the same snapshot again and expect failure
run zvirt snapshot -b -d standard -d with-zvol -d with-fs -s backup1 -l
assert_failure
assert_output --partial "Snapshot 'backup1' already exists."
assert_output --partial "standard:"
assert_output --partial "with-zvol:"
assert_output --partial "with-fs:"
assert_output --partial "Pre-flight checks failed."
# Delete the witness files
run qemu_exec standard rm /test/rootfs/witness-file
assert_success
run qemu_exec with-fs rm /test/virtiofs/witness-file
assert_success
run qemu_exec with-zvol rm /test/zvol/witness-file
assert_success
# Sync all filesystems
run qemu_exec standard sync
assert_success
run qemu_exec with-fs sync
assert_success
run qemu_exec with-zvol sync
assert_success
# Verify that the witness files have been deleted in the virtiofs host mount
run test -f /srv/with-fs/witness-file
assert_failure
# Stop all domains
run virsh destroy standard
assert_success
run virsh destroy with-fs
assert_success
run virsh destroy with-zvol
assert_success
# Revert snapshots in batch mode
run zvirt revert -b -d standard -d with-zvol -d with-fs -s backup1
assert_success
# Check all domains are running again
run virsh domstate standard
assert_success
assert_output "running"
run virsh domstate with-fs
assert_success
assert_output "running"
run virsh domstate with-zvol
assert_success
assert_output "running"
# Verify that the witness files still exist after revert
run qemu_exec standard ls -1 /test/rootfs
assert_success
assert_output "witness-file"
run qemu_exec with-fs ls -1 /test/virtiofs
assert_success
assert_output "witness-file"
run qemu_exec with-zvol ls -1 /test/zvol
assert_success
assert_output "witness-file"
}
@test "zvirt: take live snapshot without batch mode" {
# Create witness files in all three domains before taking snapshots
qemu_exec standard touch /test/rootfs/witness-file
qemu_exec with-fs touch /test/virtiofs/witness-file
qemu_exec with-zvol touch /test/zvol/witness-file
# Verify that the witness files exist in the virtiofs host mount
run test -f /srv/with-fs/witness-file
assert_success
# Take live snapshots for all three domains
run zvirt snapshot -d standard -d with-zvol -d with-fs -s backup1 -l
assert_success
# Verify that the domains are still running
run virsh domstate standard
assert_success
assert_output "running"
run virsh domstate with-fs
assert_success
assert_output "running"
run virsh domstate with-zvol
assert_success
assert_output "running"
# Assert that the files created before the snapshot exist
run qemu_exec standard ls -1 /test/rootfs
assert_success
assert_output "witness-file"
run qemu_exec with-fs ls -1 /test/virtiofs
assert_success
assert_output "witness-file"
run qemu_exec with-zvol ls -1 /test/zvol
assert_success
assert_output "witness-file"
# List snapshots and verify their existence
run zvirt list -d standard -d with-zvol -d with-fs
assert_success
assert_output "Snapshots for domain 'standard':
- backup1
Snapshots for domain 'with-zvol':
- backup1
Snapshots for domain 'with-fs':
- backup1"
# Attempt to take the same snapshot again and expect failure
run zvirt snapshot -d standard -d with-zvol -d with-fs -s backup1 -l
assert_failure
assert_output --partial "Snapshot 'backup1' already exists."
assert_output --partial "standard:"
assert_output --partial "with-zvol:"
assert_output --partial "with-fs:"
assert_output --partial "Pre-flight checks failed."
# Delete the witness files
run qemu_exec standard rm /test/rootfs/witness-file
assert_success
run qemu_exec with-fs rm /test/virtiofs/witness-file
assert_success
run qemu_exec with-zvol rm /test/zvol/witness-file
assert_success
# Sync all filesystems
run qemu_exec standard sync
assert_success
run qemu_exec with-fs sync
assert_success
run qemu_exec with-zvol sync
assert_success
# Verify that the witness files have been deleted in the virtiofs host mount
run test -f /srv/with-fs/witness-file
assert_failure
# Stop all domains
run virsh destroy standard
assert_success
run virsh destroy with-fs
assert_success
run virsh destroy with-zvol
assert_success
# Revert snapshots in batch mode
run zvirt revert -d standard -d with-zvol -d with-fs -s backup1
assert_success
# Check all domains are running again
run virsh domstate standard
assert_success
assert_output "running"
run virsh domstate with-fs
assert_success
assert_output "running"
run virsh domstate with-zvol
assert_success
assert_output "running"
# Verify that the witness files still exist after revert
run qemu_exec standard ls -1 /test/rootfs
assert_success
assert_output "witness-file"
run qemu_exec with-fs ls -1 /test/virtiofs
assert_success
assert_output "witness-file"
run qemu_exec with-zvol ls -1 /test/zvol
assert_success
assert_output "witness-file"
}
@test "zvirt: take crash-consistent snapshot without batch mode" {
# Create witness files in all three domains before taking snapshots
qemu_exec standard touch /test/rootfs/witness-file
qemu_exec with-fs touch /test/virtiofs/witness-file
qemu_exec with-zvol touch /test/zvol/witness-file
# Verify that the witness files exist in the virtiofs host mount
run test -f /srv/with-fs/witness-file
assert_success
# Take crash-consistent snapshots for all three domains
run zvirt snapshot -d standard -d with-zvol -d with-fs -s backup1
assert_success
# Verify that the domains are still running
run virsh domstate standard
assert_success
assert_output "running"
run virsh domstate with-fs
assert_success
assert_output "running"
run virsh domstate with-zvol
assert_success
assert_output "running"
# Assert that the files created before the snapshot exist
run qemu_exec standard ls -1 /test/rootfs
assert_success
assert_output "witness-file"
run qemu_exec with-fs ls -1 /test/virtiofs
assert_success
assert_output "witness-file"
run qemu_exec with-zvol ls -1 /test/zvol
assert_success
assert_output "witness-file"
# List snapshots and verify their existence
run zvirt list -d standard -d with-zvol -d with-fs
assert_success
assert_output "Snapshots for domain 'standard':
- backup1
Snapshots for domain 'with-zvol':
- backup1
Snapshots for domain 'with-fs':
- backup1"
# Attempt to take the same snapshot again and expect failure
run zvirt snapshot -d standard -d with-zvol -d with-fs -s backup1
assert_failure
assert_output --partial "Snapshot 'backup1' already exists."
assert_output --partial "standard:"
assert_output --partial "with-zvol:"
assert_output --partial "with-fs:"
assert_output --partial "Pre-flight checks failed."
# Delete the witness files
run qemu_exec standard rm /test/rootfs/witness-file
assert_success
run qemu_exec with-fs rm /test/virtiofs/witness-file
assert_success
run qemu_exec with-zvol rm /test/zvol/witness-file
assert_success
# Sync all filesystems
run qemu_exec standard sync
assert_success
run qemu_exec with-fs sync
assert_success
run qemu_exec with-zvol sync
assert_success
# Wait a moment to ensure all writes are flushed
sleep 2
# Verify that the witness files have been deleted in the virtiofs host mount
run test -f /srv/with-fs/witness-file
assert_failure
# Stop all domains
run virsh destroy standard
assert_success
run virsh destroy with-fs
assert_success
run virsh destroy with-zvol
assert_success
# Revert snapshots in batch mode
run zvirt revert -d standard -d with-zvol -d with-fs -s backup1
assert_success
# Check all domains have been shut off
run virsh domstate standard
assert_success
assert_output "shut off"
run virsh domstate with-fs
assert_success
assert_output "shut off"
run virsh domstate with-zvol
assert_success
assert_output "shut off"
# Start all domains
run virsh start standard
assert_success
run virsh start with-fs
assert_success
run virsh start with-zvol
assert_success
# Wait for all domains to be fully ready
readiness_wait
# Verify that the witness files still exist after revert
run qemu_exec standard ls -1 /test/rootfs
assert_success
assert_output "witness-file"
run qemu_exec with-fs ls -1 /test/virtiofs
assert_success
assert_output "witness-file"
run qemu_exec with-zvol ls -1 /test/zvol
assert_success
assert_output "witness-file"
}
@test "zvirt: take crash-consistent snapshot with batch mode" {
# Create witness files in all three domains before taking snapshots
qemu_exec standard touch /test/rootfs/witness-file
qemu_exec with-fs touch /test/virtiofs/witness-file
qemu_exec with-zvol touch /test/zvol/witness-file
# Verify that the witness files exist in the virtiofs host mount
run test -f /srv/with-fs/witness-file
assert_success
# Take crash-consistent snapshots for all three domains
run zvirt snapshot -b -d standard -d with-zvol -d with-fs -s backup1
assert_success
# Verify that the domains are still running
run virsh domstate standard
assert_success
assert_output "running"
run virsh domstate with-fs
assert_success
assert_output "running"
run virsh domstate with-zvol
assert_success
assert_output "running"
# Assert that the files created before the snapshot exist
run qemu_exec standard ls -1 /test/rootfs
assert_success
assert_output "witness-file"
run qemu_exec with-fs ls -1 /test/virtiofs
assert_success
assert_output "witness-file"
run qemu_exec with-zvol ls -1 /test/zvol
assert_success
assert_output "witness-file"
# List snapshots and verify their existence
run zvirt list -d standard -d with-zvol -d with-fs
assert_success
assert_output "Snapshots for domain 'standard':
- backup1
Snapshots for domain 'with-zvol':
- backup1
Snapshots for domain 'with-fs':
- backup1"
# Attempt to take the same snapshot again and expect failure
run zvirt snapshot -b -d standard -d with-zvol -d with-fs -s backup1
assert_failure
assert_output --partial "Snapshot 'backup1' already exists."
assert_output --partial "standard:"
assert_output --partial "with-zvol:"
assert_output --partial "with-fs:"
assert_output --partial "Pre-flight checks failed."
# Delete the witness files
run qemu_exec standard rm /test/rootfs/witness-file
assert_success
run qemu_exec with-fs rm /test/virtiofs/witness-file
assert_success
run qemu_exec with-zvol rm /test/zvol/witness-file
assert_success
# Sync all filesystems
run qemu_exec standard sync
assert_success
run qemu_exec with-fs sync
assert_success
run qemu_exec with-zvol sync
assert_success
# Wait a moment to ensure all writes are flushed
sleep 2
# Verify that the witness files have been deleted in the virtiofs host mount
run test -f /srv/with-fs/witness-file
assert_failure
# Stop all domains
run virsh destroy standard
assert_success
run virsh destroy with-fs
assert_success
run virsh destroy with-zvol
assert_success
# Revert snapshots in batch mode
run zvirt revert -b -d standard -d with-zvol -d with-fs -s backup1
assert_success
# Check all domains are running again
run virsh domstate standard
assert_success
assert_output "running"
run virsh domstate with-fs
assert_success
assert_output "running"
run virsh domstate with-zvol
assert_success
assert_output "running"
# Wait for all domains to be fully ready
readiness_wait
# Verify that the witness files still exist after revert
run qemu_exec standard ls -1 /test/rootfs
assert_success
assert_output "witness-file"
run qemu_exec with-fs ls -1 /test/virtiofs
assert_success
assert_output "witness-file"
run qemu_exec with-zvol ls -1 /test/zvol
assert_success
assert_output "witness-file"
}

1
test/test_helper/bats-assert

@ -1 +0,0 @@
Subproject commit 697471b7a89d3ab38571f38c6c7c4b460d1f5e35

1
test/test_helper/bats-mock

@ -1 +0,0 @@
Subproject commit 48fce74482a4d2bb879b904ccab31b6bc98e3224

1
test/test_helper/bats-support

@ -1 +0,0 @@
Subproject commit 0954abb9925cad550424cebca2b99255d4eabe96

885
test/unit/core.bats

@ -1,885 +0,0 @@
#!/usr/bin/env bats
setup() {
bats_load_library 'bats-support'
bats_load_library 'bats-assert'
bats_load_library 'bats-mock'
# Load the core library and export its functions
local fn_before="$(declare -F | cut -d ' ' -f 3 | sort)"
set -Eeuo pipefail
source "${BATS_TEST_DIRNAME}/../../src/lib/zvirt/core.sh"
local fn_after="$(declare -F | cut -d ' ' -f 3 | sort)"
declare -a zvirt_fn=( $(comm -13 <(echo "$fn_before") <(echo "$fn_after")) )
for fn in "${zvirt_fn[@]}"; do
export -f "${fn}"
done
# Helper to run commands in a separate bash process with the proper flags
# and with access to the domain_params_cache associative array
in_bash() {
local vars=""
for var in domain_params_cache snapshot_name domains verbose action batch live keep; do
if declare -p "${var}" &>/dev/null; then
vars+="$(declare -p "${var}") ; "
fi
done
bash -Eeuo pipefail -c "init_global_variables ; $vars \"\$@\"" zvirt "$@"
}
}
@test "domain_state: nominal case" {
# Mock the underlying tools
virsh() {
[[ "$*" == "domstate foo" ]] && echo "running"
}
export -f virsh
# Run the test
run in_bash domain_state "foo"
assert_success
assert_output "running"
}
@test "domain_exists: nominal case" {
# Mock the underlying tools
virsh() {
[[ "$*" == "dominfo foo" ]] && return 0
return 1
}
export -f virsh
# Run the test
run in_bash domain_exists "foo"
assert_success
run in_bash domain_exists "bar"
assert_failure
}
@test "get_zfs_datasets_from_domain: nominal case" {
# Mock the underlying tools
virsh() {
if [[ "$*" == "domblklist foo --details" ]]; then
cat <<-EOF
Type Device Target Source
------------------------------------------------------------------------
file disk vda /var/lib/libvirt/images/foo/root.img
file disk vdb /var/lib/libvirt/images/foo/data.img
block disk vdc /dev/zvol/data/domains/foo/data-vol
block disk vdd /dev/sda1
EOF
return 0
fi
return 1
}
df() {
if [[ "$*" == "--output=source /var/lib/libvirt/images/foo/root.img" ]] || [[ "$*" == "--output=source /var/lib/libvirt/images/foo/data.img" ]]; then
echo Filesystem
echo "/var/lib/libvirt/images/foo"
return 0
fi
return 1
}
export -f virsh df
# Run the test
run in_bash get_zfs_datasets_from_domain "foo"
assert_output "/var/lib/libvirt/images/foo"
assert_success
run in_bash get_zfs_datasets_from_domain "bar"
assert_failure
}
@test "get_zfs_zvols_from_domain: nominal case" {
# Mock the underlying tools
virsh() {
if [[ "$*" == "domblklist foo --details" ]]; then
cat <<-EOF
Type Device Target Source
------------------------------------------------------------------------
file disk vda /var/lib/libvirt/images/foo/root.img
file disk vdb /var/lib/libvirt/images/foo/data.img
block disk vdc /dev/zvol/data/domains/foo/data-vol
block disk vdd /dev/sda1
EOF
return 0
fi
return 1
}
export -f virsh
# Run the test
run in_bash get_zfs_zvols_from_domain "foo"
assert_output "data/domains/foo/data-vol"
assert_success
run in_bash get_zfs_zvols_from_domain "bar"
assert_failure
}
@test "get_zfs_snapshots_from_dataset: nominal case" {
# Mock the underlying tools
zfs() {
if [[ "$*" == "list -H -t snapshot -o name data/domains/foo" ]]; then
cat <<-EOF
data/domains/foo@snapshot1
data/domains/foo/virtiofs@snapshot1
data/domains/foo@snapshot2
data/domains/foo/virtiofs@snapshot2
EOF
return 0
fi
return 1
}
export -f zfs
# Run the test
run in_bash get_zfs_snapshots_from_dataset "data/domains/foo"
assert_output "snapshot1
snapshot2"
assert_success
run in_bash get_zfs_snapshots_from_dataset "data/domains/bar"
assert_failure
}
@test "get_zfs_dataset_mountpoint: nominal case" {
# Mock the underlying tools
zfs() {
if [[ "$*" == "get -H -o value mountpoint data/domains/foo" ]]; then
echo "/var/lib/libvirt/images/foo"
return 0
fi
return 1
}
export -f zfs
# Run the test
run in_bash get_zfs_dataset_mountpoint "data/domains/foo"
assert_output "/var/lib/libvirt/images/foo"
assert_success
run in_bash get_zfs_dataset_mountpoint "data/domains/bar"
assert_failure
}
@test "has_save_file: nominal case" {
# Temporary directory for save files
local temp_dir="$(mktemp -d)"
mkdir -p "$temp_dir/foo" "$temp_dir/bar"
# Only foo has a save file
touch "$temp_dir/foo/domain.save"
# Fill up the cache
declare -A domain_params_cache=( ["foo/mountpoint"]="$temp_dir/foo" ["bar/mountpoint"]="$temp_dir/bar" )
# Run the test
run in_bash has_save_file foo
assert_success
run in_bash has_save_file bar
assert_failure
}
@test "take_live_snapshot: nominal case" {
# Mock the underlying tools
declare -A domain_params_cache=( ["foo/state"]="running" ["foo/dataset"]="data/domains/foo" ["foo/mountpoint"]="/var/lib/libvirt/images/foo" ["foo/zvols"]="" )
virsh_mock="$(mock_create)"
virsh() {
if [[ "$*" == "save foo /var/lib/libvirt/images/foo/domain.save --running --verbose --image-format raw" ]]; then
$virsh_mock "$@"
return $?
fi
return 1
}
zfs_mock="$(mock_create)"
zfs() {
if [[ "$*" == "snapshot -r data/domains/foo@backup1" ]]; then
$zfs_mock "$@"
return $?
fi
return 1
}
export -f virsh zfs
export virsh_mock zfs_mock
# Run the test
run in_bash take_live_snapshot foo backup1
assert_success
[[ "$(mock_get_call_num ${virsh_mock})" -eq 1 ]]
[[ "$(mock_get_call_num ${zfs_mock})" -eq 1 ]]
}
@test "take_crash_consistent_snapshot: nominal case" {
# Mock the underlying tools
declare -A domain_params_cache=( ["bar/state"]="running" ["bar/dataset"]="data/domains/bar" ["bar/mountpoint"]="/var/lib/libvirt/images/bar" ["bar/zvols"]="" )
zfs_mock="$(mock_create)"
zfs() {
if [[ "$*" == "snapshot -r data/domains/bar@backup2" ]]; then
$zfs_mock "$@"
return $?
fi
return 1
}
export -f zfs
export zfs_mock
# Run the test
run in_bash take_crash_consistent_snapshot bar backup2
assert_success
[[ "$(mock_get_call_num ${zfs_mock})" -eq 1 ]]
}
@test "revert_snapshot: nominal case" {
# Mock the underlying tools
verbose=1
declare -A domain_params_cache=( ["baz/state"]="running" ["baz/dataset"]="data/domains/baz" ["baz/mountpoint"]="/var/lib/libvirt/images/baz" ["baz/zvols"]="" )
zfs_mock="$(mock_create)"
zfs() {
rollback_pattern="^rollback -Rrf data/domains/baz(/virtiofs)?@backup3$"
if [[ "$*" == "list -H -r -o name data/domains/baz" ]]; then
echo "data/domains/baz
data/domains/baz/virtiofs"
return 0
elif [[ "$*" =~ $rollback_pattern ]]; then
$zfs_mock "$@"
return $?
fi
return 1
}
export -f zfs
export zfs_mock
# Run the test
run in_bash revert_snapshot baz backup3
assert_success
[[ "$(mock_get_call_num ${zfs_mock})" -eq 2 ]]
}
@test "restore_domain: batch mode" {
# Mock the underlying tools
batch=1
declare -A domain_params_cache=( ["foo/state"]="running" ["foo/dataset"]="data/domains/foo" ["foo/mountpoint"]="/var/lib/libvirt/images/foo" ["foo/zvols"]="" )
virsh_mock="$(mock_create)"
virsh() {
if [[ "$*" == "restore /var/lib/libvirt/images/foo/domain.save --paused" ]]; then
$virsh_mock "$@"
return $?
fi
return 1
}
export -f virsh
export virsh_mock
# Run the test
run in_bash restore_domain foo
assert_success
[[ "$(mock_get_call_num ${virsh_mock})" -eq 1 ]]
}
@test "restore_domain: nominal case" {
# Mock the underlying tools
batch=0
declare -A domain_params_cache=( ["foo/state"]="running" ["foo/dataset"]="data/domains/foo" ["foo/mountpoint"]="/var/lib/libvirt/images/foo" ["foo/zvols"]="" )
virsh_mock="$(mock_create)"
virsh() {
if [[ "$*" == "restore /var/lib/libvirt/images/foo/domain.save --running" ]]; then
$virsh_mock "$@"
return $?
fi
return 1
}
export -f virsh
export virsh_mock
# Run the test
run in_bash restore_domain foo
assert_success
[[ "$(mock_get_call_num ${virsh_mock})" -eq 1 ]]
}
@test "pause_all_domains: nominal case" {
# Mock the underlying tools
local domains=( "foo" "bar" )
declare -A domain_params_cache=( ["foo/state"]="running" ["bar/state"]="shut off" )
virsh_mock="$(mock_create)"
virsh() {
if [[ "$*" == "suspend foo" ]]; then
$virsh_mock "$@"
return $?
fi
return 1
}
export -f virsh
export virsh_mock
# Run the test
run in_bash pause_all_domains "${domains[@]}"
assert_success
[[ "$(mock_get_call_num ${virsh_mock})" -eq 1 ]]
}
@test "resume_all_domains: nominal case" {
# Mock the underlying tools
local domains=( "foo" "bar" )
virsh_mock="$(mock_create)"
virsh() {
if [[ "$*" == "resume foo" ]] || [[ "$*" == "start bar" ]]; then
$virsh_mock "$@"
return $?
fi
if [[ "$*" == "domstate foo" ]]; then
echo "paused"
return 0
elif [[ "$*" == "domstate bar" ]]; then
echo "shut off"
return 0
fi
return 1
}
export -f virsh
export virsh_mock
# Run the test
run in_bash resume_all_domains "${domains[@]}"
assert_success
[[ "$(mock_get_call_num ${virsh_mock})" -eq 2 ]]
}
@test "fsthaw_all_domains: nominal case" {
# Mock the underlying tools
local domains=( "foo" "bar" )
declare -A domain_params_cache=( ["foo/state"]="running" ["bar/state"]="shut off" )
fsthaw_mock="$(mock_create)"
fsthaw_domain() {
if [[ "$*" == "foo" ]]; then
$fsthaw_mock "$@"
return $?
fi
return 1
}
export -f fsthaw_domain
export fsthaw_mock
# Run the test
run in_bash fsthaw_all_domains "${domains[@]}"
assert_success
[[ "$(mock_get_call_num ${fsthaw_mock})" -eq 1 ]]
}
@test "fsfreeze_all_domains: nominal case" {
# Mock the underlying tools
local domains=( "foo" "bar" )
declare -A domain_params_cache=( ["foo/state"]="running" ["bar/state"]="shut off" )
fsfreeze_mock="$(mock_create)"
fsfreeze_domain() {
if [[ "$*" == "foo" ]]; then
$fsfreeze_mock "$@"
return $?
fi
return 1
}
export -f fsfreeze_domain
export fsfreeze_mock
# Run the test
run in_bash fsfreeze_all_domains "${domains[@]}"
assert_success
[[ "$(mock_get_call_num ${fsfreeze_mock})" -eq 1 ]]
}
@test "fsthaw_domain: nominal case" {
# Mock the underlying tools
virsh() {
[[ "$*" == "domfsthaw foo" ]] && return 0
return 1
}
export -f virsh
# Run the test
run in_bash virsh domfsthaw "foo"
assert_success
run in_bash virsh domfsthaw "bar"
assert_failure
}
@test "fsfreeze_domain: nominal case" {
# Mock the underlying tools
virsh() {
[[ "$*" == "domfsfreeze foo" ]] && return 0
return 1
}
export -f virsh
# Run the test
run in_bash virsh domfsfreeze "foo"
assert_success
run in_bash virsh domfsfreeze "bar"
assert_failure
}
@test "domain_checks: nominal case" {
# Mock the underlying tools
domain_exists() {
if [[ "$*" == "foo" ]] || [[ "$*" == "bar" ]]; then
return 0
fi
return 1
}
domain_state() {
if [[ "$*" == "foo" ]]; then
echo "running"
return 0
elif [[ "$*" == "bar" ]]; then
echo "shut off"
return 0
fi
return 1
}
get_zfs_datasets_from_domain() {
if [[ "$*" == "foo" ]]; then
echo "data/domains/foo"
return 0
elif [[ "$*" == "bar" ]]; then
echo "data/domains/bar"
return 0
fi
return 1
}
get_zfs_zvols_from_domain() {
if [[ "$*" == "foo" ]]; then
return 0
elif [[ "$*" == "bar" ]]; then
return 0
fi
return 1
}
get_zfs_snapshots_from_dataset() {
if [[ "$*" == "data/domains/foo" ]]; then
echo "backup1"
return 0
elif [[ "$*" == "data/domains/bar" ]]; then
echo "backup1"
return 0
fi
return 1
}
get_zfs_dataset_mountpoint() {
if [[ "$*" == "data/domains/foo" ]]; then
echo "/var/lib/libvirt/images/foo"
return 0
elif [[ "$*" == "data/domains/bar" ]]; then
echo "/var/lib/libvirt/images/bar"
return 0
fi
return 1
}
export -f domain_exists domain_state get_zfs_datasets_from_domain get_zfs_zvols_from_domain get_zfs_snapshots_from_dataset get_zfs_dataset_mountpoint
# Run the test
run in_bash domain_checks snapshot foo backup2
assert_success
run in_bash domain_checks revert bar backup1
assert_success
# Live mode with existing save file
has_save_file() {
return 0
}
export -f has_save_file
live=1
run in_bash domain_checks snapshot foo backup2
assert_failure
# Live mode with non-existing save file
has_save_file() {
return 1
}
export -f has_save_file
live=1
run in_bash domain_checks snapshot foo backup2
assert_success
}
@test "list_snapshots: nominal case" {
# Mock the underlying tools
declare -A domain_params_cache=( ["foo/snapshots"]="snapshot1 snapshot2" ["bar/snapshots"]="snapshot3 snapshot4" )
# Run the test
run in_bash list_snapshots foo
assert_success
assert_output "Snapshots for domain 'foo':
- snapshot1
- snapshot2"
}
@test "prune_snapshots: nominal case" {
# Mock the underlying tools
declare -A domain_params_cache=( ["foo/snapshots"]="s1 s2 s3 s4 s5" ["bar/snapshots"]="s1 s2 s3 s4 s5" ["baz/snapshots"]="s1" ["foo/dataset"]="data/domains/foo" ["bar/dataset"]="data/domains/bar" ["baz/dataset"]="data/domains/baz" )
zfs_destroy_mock="$(mock_create)"
zfs() {
if [[ "$*" == "destroy -r data/domains/foo@%s3" ]] || [[ "$*" == "destroy -r data/domains/bar@%s2" ]]; then
$zfs_destroy_mock "$@"
return $?
fi
return 1
}
export -f zfs
export zfs_destroy_mock
# Run the test
keep=2
run in_bash prune_snapshots foo
assert_success
[[ "$(mock_get_call_num ${zfs_destroy_mock})" -eq 1 ]] # Deletion up to s3
keep=3
run in_bash prune_snapshots bar
assert_success
[[ "$(mock_get_call_num ${zfs_destroy_mock})" -eq 2 ]] # Deletion up to s2
keep=5
run in_bash prune_snapshots bar
assert_success
[[ "$(mock_get_call_num ${zfs_destroy_mock})" -eq 2 ]] # No deletion should occur
keep=1
run in_bash prune_snapshots baz
assert_success
[[ "$(mock_get_call_num ${zfs_destroy_mock})" -eq 2 ]] # No deletion should occur
}
@test "preflight_checks: nominal case" {
# Mock the underlying tools
domain_checks() {
if [[ "$*" == "snapshot foo backup2" ]]; then
return 0
fi
return 1
}
export -f domain_checks
# Run the test
run in_bash preflight_checks snapshot backup2 foo
assert_success
}
@test "take_snapshots: batch=0, live=0" {
# Mock the underlying tools
take_crash_consistent_snapshot() {
regex="^(foo|bar) backup$"
if [[ "$*" =~ $regex ]]; then
return 0
fi
return 1
}
pause_all_domains() { return 1; }
take_live_snapshot() { return 1; }
restore_domain() { return 1; }
resume_all_domains() { return 1; }
remove_save_file() { return 1; }
fsfreeze_all_domains() { return 1; }
fsthaw_all_domains() { return 1; }
fsfreeze_domain() {
if [[ "$*" == "foo" ]]; then
return 0
fi
return 1
}
fsthaw_domain() {
if [[ "$*" == "foo" ]]; then
return 0
fi
return 1
}
export -f take_crash_consistent_snapshot pause_all_domains take_live_snapshot restore_domain resume_all_domains remove_save_file fsfreeze_all_domains fsthaw_all_domains fsfreeze_domain fsthaw_domain
declare -A domain_params_cache=( ["foo/state"]="running" ["bar/state"]="shut off" )
# Run the test
domains=( "foo" "bar" )
snapshot_name="backup"
batch=0
live=0
run in_bash take_snapshots
assert_success
# Add a non-existing domain to the list
domains+=( "baz" )
run in_bash take_snapshots
assert_failure
}
@test "take_snapshots: batch=1, live=0" {
# Mock the underlying tools
take_crash_consistent_snapshot() {
regex="^(foo|bar) backup$"
if [[ "$*" =~ $regex ]]; then
return 0
fi
return 1
}
pause_all_domains() { return 1; }
take_live_snapshot() { return 1; }
restore_domain() { return 1; }
resume_all_domains() { return 1; }
remove_save_file() { return 1; }
fsfreeze_all_domains() {
if [[ "$*" == "foo bar" ]]; then
return 0
fi
return 1
}
fsthaw_all_domains() {
if [[ "$*" == "foo bar" ]]; then
return 0
fi
return 1
}
fsfreeze_domain() { return 1; }
fsthaw_domain() { return 1; }
export -f take_crash_consistent_snapshot pause_all_domains take_live_snapshot restore_domain resume_all_domains remove_save_file fsfreeze_all_domains fsthaw_all_domains fsfreeze_domain fsthaw_domain
declare -A domain_params_cache=( ["foo/state"]="running" ["bar/state"]="shut off" )
# Run the test
domains=( "foo" "bar" )
snapshot_name="backup"
batch=1
live=0
run in_bash take_snapshots
assert_success
# Add a non-existing domain to the list
domains+=( "baz" )
run in_bash take_snapshots
assert_failure
}
@test "take_snapshots: batch=0, live=1" {
# Mock the underlying tools
take_crash_consistent_snapshot() {
if [[ "$*" == "bar backup" ]]; then
return 0
fi
return 1
}
pause_all_domains() { return 1; }
take_live_snapshot() {
if [[ "$*" == "foo backup" ]]; then
return 0
fi
return 1
}
restore_domain() {
if [[ "$*" == "foo" ]]; then
return 0
fi
return 1
}
resume_all_domains() { return 1; }
remove_save_file() { return 1; }
fsfreeze_all_domains() { return 1; }
fsthaw_all_domains() { return 1; }
fsfreeze_domain() {
if [[ "$*" == "bar" ]]; then
return 0
fi
return 1
}
fsthaw_domain() {
if [[ "$*" == "bar" ]]; then
return 0
fi
return 1
}
export -f take_crash_consistent_snapshot pause_all_domains take_live_snapshot restore_domain resume_all_domains remove_save_file fsfreeze_all_domains fsthaw_all_domains fsfreeze_domain fsthaw_domain
declare -A domain_params_cache=( ["foo/state"]="running" ["bar/state"]="shut off" )
# Run the test
domains=( "foo" "bar" )
snapshot_name="backup"
batch=0
live=1
run in_bash take_snapshots
assert_success
# Add a non-existing domain to the list
domains+=( "baz" )
run in_bash take_snapshots
assert_failure
}
@test "take_snapshots: batch=1, live=1" {
# Mock the underlying tools
take_crash_consistent_snapshot() {
if [[ "$*" == "bar backup" ]]; then
return 0
fi
return 1
}
pause_all_domains() {
if [[ "$*" == "foo bar" ]]; then
return 0
fi
return 1
}
take_live_snapshot() {
if [[ "$*" == "foo backup" ]]; then
return 0
fi
return 1
}
restore_domain() {
if [[ "$*" == "foo" ]]; then
return 0
fi
return 1
}
resume_all_domains() {
if [[ "$*" == "foo bar" ]]; then
return 0
fi
return 1
}
remove_save_file() { return 1; }
fsfreeze_all_domains() { return 1; }
fsthaw_all_domains() { return 1; }
fsfreeze_domain() {
if [[ "$*" == "bar" ]]; then
return 0
fi
return 1
}
fsthaw_domain() {
if [[ "$*" == "bar" ]]; then
return 0
fi
return 1
}
export -f take_crash_consistent_snapshot pause_all_domains take_live_snapshot restore_domain resume_all_domains remove_save_file fsfreeze_all_domains fsthaw_all_domains fsfreeze_domain fsthaw_domain
declare -A domain_params_cache=( ["foo/state"]="running" ["bar/state"]="shut off" )
# Run the test
domains=( "foo" "bar" )
snapshot_name="backup"
batch=0
live=1
run in_bash take_snapshots
assert_success
# Add a non-existing domain to the list
domains+=( "baz" )
run in_bash take_snapshots
assert_failure
}
@test "revert_snapshots: batch=0" {
# Mock the underlying tools
revert_snapshot() {
regex="^(foo|bar) backup$"
if [[ "$*" =~ $regex ]]; then
return 0
fi
return 1
}
restore_domain() {
if [[ "$*" == "foo" ]]; then
return 0
fi
return 1
}
resume_all_domains() { return 1; }
has_save_file() {
if [[ "$*" == "foo" ]]; then
return 0
fi
return 1
}
remove_save_file() { return 1; }
domain_state() {
if [[ "$*" == "foo" ]]; then
echo "paused"
return 0
elif [[ "$*" == "bar" ]]; then
echo "shut off"
return 0
fi
return 1
}
export -f revert_snapshot restore_domain resume_all_domains has_save_file remove_save_file domain_state
# Run the test
domains=( "foo" "bar" )
snapshot_name="backup"
batch=0
run in_bash revert_snapshots
assert_success
# Add a non-existing domain to the list
domains+=( "baz" )
run in_bash revert_snapshots
assert_failure
}
@test "revert_snapshots: batch=1" {
# Mock the underlying tools
revert_snapshot() {
regex="^(foo|bar) backup$"
if [[ "$*" =~ $regex ]]; then
return 0
fi
return 1
}
restore_domain() {
if [[ "$*" == "foo" ]]; then
return 0
fi
return 1
}
resume_all_domains() {
if [[ "$*" == "foo bar" ]]; then
return 0
fi
return 1
}
has_save_file() {
if [[ "$*" == "foo" ]]; then
return 0
fi
return 1
}
remove_save_file() {
if [[ "$*" == "foo" ]]; then
return 0
fi
return 1
}
domain_state() {
if [[ "$*" == "foo" ]]; then
echo "paused"
return 0
elif [[ "$*" == "bar" ]]; then
echo "shut off"
return 0
fi
return 1
}
export -f revert_snapshot restore_domain resume_all_domains has_save_file remove_save_file domain_state
# Run the test
domains=( "foo" "bar" )
snapshot_name="backup"
batch=1
run in_bash revert_snapshots
assert_success
# Add a non-existing domain to the list
domains+=( "baz" )
run in_bash revert_snapshots
assert_failure
}

86
test/unit/usage.bats

@ -1,86 +0,0 @@
#!/usr/bin/env bats
setup() {
bats_load_library 'bats-support'
bats_load_library 'bats-assert'
set -Eeuo pipefail
source "${BATS_TEST_DIRNAME}/../../src/lib/zvirt/core.sh"
function call_parse_args () {
init_global_variables
parse_args "$@"
ret=$?
declare -p action batch live verbose domains snapshot_name keep
return $ret
}
}
@test "call_parse_args: show help and exit" {
run call_parse_args -h
assert_success
assert_output --partial "Usage:"
}
@test "call_parse_args: no action provided" {
run call_parse_args
assert_failure
assert_output --partial "Unsupported action"
}
@test "call_parse_args: list snapshots for a single domain" {
run call_parse_args list -d foo
assert_success
assert_output --partial 'action="list"'
assert_output --partial 'domains=([0]="foo")'
}
@test "call_parse_args: take a snapshot for two domains in batch mode" {
run call_parse_args snapshot -b -d foo -d bar -s backup1 -l
assert_success
assert_output --partial 'action="snapshot"'
assert_output --partial 'batch="1"'
assert_output --partial 'domains=([0]="foo" [1]="bar")'
assert_output --partial 'snapshot_name="backup1"'
assert_output --partial 'live="1"'
}
@test "call_parse_args: take a crash-consistent snapshot for two domains" {
run call_parse_args snapshot -d foo -d bar -s backup2
assert_success
assert_output --partial 'action="snapshot"'
assert_output --partial 'batch="0"'
assert_output --partial 'domains=([0]="foo" [1]="bar")'
assert_output --partial 'snapshot_name="backup2"'
assert_output --partial 'live="0"'
}
@test "call_parse_args: revert snapshot for a domain" {
run call_parse_args revert -d foo -s backup2
assert_success
assert_output --partial 'action="revert"'
assert_output --partial 'batch="0"'
assert_output --partial 'domains=([0]="foo")'
assert_output --partial 'snapshot_name="backup2"'
assert_output --partial 'live="0"'
}
@test "call_parse_args: prune snapshots for all domains" {
virsh() {
if [[ "$*" == "list --all --name" ]]; then
echo -e "foo\nbar"
return 0
fi
return 1
}
run call_parse_args prune -k 5
assert_success
assert_output --partial 'action="prune"'
assert_output --partial 'domains=([0]="foo" [1]="bar")'
assert_output --partial 'keep="5"'
run call_parse_args prune
assert_failure
assert_output --partial "The -k option with a positive integer value must be specified for the 'prune' action"
}
Loading…
Cancel
Save