diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index ae6059f..0000000 --- a/.gitmodules +++ /dev/null @@ -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 diff --git a/Makefile b/Makefile index bca1946..bc43e3a 100644 --- a/Makefile +++ b/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 ! zfs-autobackup --version &>/dev/null; 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..." @@ -91,4 +84,4 @@ clean: lint: prerequisites @echo "Linting..." - @cd src && shellcheck --severity=error bin/zvirt lib/zvirt/*.sh + @cd src && shellcheck --severity=error bin/*.sh diff --git a/packaging/zvirt.spec b/packaging/zvirt.spec index 82a5917..45cfa1a 100644 --- a/packaging/zvirt.spec +++ b/packaging/zvirt.spec @@ -17,6 +17,7 @@ Requires: bash Requires: libvirt Requires: zfs BuildRequires: make +BuildRequires: python3-pip %description Zvirt takes snapshots of Libvirt domains using ZFS. @@ -26,6 +27,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,11 +37,11 @@ as a set of consistent ZFS snapshots. %install make PREFIX=%{buildroot}%{_prefix} install +pip install --target %{buildroot}%{python3_sitelib} foo-package %files -%{_bindir}/zvirt -%{_prefix}/lib/zvirt/core.sh -%dir %{_prefix}/lib/zvirt +%{_bindir}/libvirt-hook +%{_bindir}/snapshot-libvirt-domains %changelog * Mon Nov 24 2025 Nicolas Massé - 0.0.1-1 diff --git a/src/bin/libvirt-hook b/src/bin/libvirt-hook new file mode 100755 index 0000000..8ee1d84 --- /dev/null +++ b/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 diff --git a/src/bin/snapshot-libvirt-domains b/src/bin/snapshot-libvirt-domains new file mode 100755 index 0000000..0d5c5dc --- /dev/null +++ b/src/bin/snapshot-libvirt-domains @@ -0,0 +1,83 @@ +#!/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") + +for domain in $(virsh list --name); 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 diff --git a/src/bin/zvirt b/src/bin/zvirt deleted file mode 100755 index cc5b80f..0000000 --- a/src/bin/zvirt +++ /dev/null @@ -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 diff --git a/src/lib/zvirt/core.sh b/src/lib/zvirt/core.sh deleted file mode 100644 index f7dfdd6..0000000 --- a/src/lib/zvirt/core.sh +++ /dev/null @@ -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 -s - -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 -} diff --git a/test/e2e/cloud-init/standard-user-data b/test/e2e/cloud-init/standard-user-data deleted file mode 100644 index 70ce942..0000000 --- a/test/e2e/cloud-init/standard-user-data +++ /dev/null @@ -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 diff --git a/test/e2e/cloud-init/with-fs-user-data b/test/e2e/cloud-init/with-fs-user-data deleted file mode 100644 index a7cbb6f..0000000 --- a/test/e2e/cloud-init/with-fs-user-data +++ /dev/null @@ -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 diff --git a/test/e2e/cloud-init/with-zvol-user-data b/test/e2e/cloud-init/with-zvol-user-data deleted file mode 100644 index 1b0217e..0000000 --- a/test/e2e/cloud-init/with-zvol-user-data +++ /dev/null @@ -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 diff --git a/test/e2e/zvirt.bats b/test/e2e/zvirt.bats deleted file mode 100644 index 346b9c6..0000000 --- a/test/e2e/zvirt.bats +++ /dev/null @@ -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" </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" -} diff --git a/test/test_helper/bats-assert b/test/test_helper/bats-assert deleted file mode 160000 index 697471b..0000000 --- a/test/test_helper/bats-assert +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 697471b7a89d3ab38571f38c6c7c4b460d1f5e35 diff --git a/test/test_helper/bats-mock b/test/test_helper/bats-mock deleted file mode 160000 index 48fce74..0000000 --- a/test/test_helper/bats-mock +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 48fce74482a4d2bb879b904ccab31b6bc98e3224 diff --git a/test/test_helper/bats-support b/test/test_helper/bats-support deleted file mode 160000 index 0954abb..0000000 --- a/test/test_helper/bats-support +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0954abb9925cad550424cebca2b99255d4eabe96 diff --git a/test/unit/core.bats b/test/unit/core.bats deleted file mode 100644 index bda9ff4..0000000 --- a/test/unit/core.bats +++ /dev/null @@ -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 -} - diff --git a/test/unit/usage.bats b/test/unit/usage.bats deleted file mode 100644 index d14f8f0..0000000 --- a/test/unit/usage.bats +++ /dev/null @@ -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" -}