Zvirt takes snapshots of Libvirt domains using ZFS.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

864 lines
20 KiB

#!/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/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; 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
get_zfs_datasets_from_domain() {
if [[ "$*" == "foo" ]]; then
echo "data/domains/foo"
return 0
fi
return 1
}
get_zfs_snapshots_from_dataset() {
if [[ "$*" == "data/domains/foo" ]]; then
echo "snapshot1
snapshot2"
return 0
fi
return 1
}
export -f get_zfs_datasets_from_domain get_zfs_snapshots_from_dataset
# Run the test
run in_bash list_snapshots foo
assert_success
assert_output "Snapshots for domain 'foo':
- snapshot1
- snapshot2"
}
@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
}