Browse Source

implement the prune action

main 0.0.3
Nicolas Massé 2 weeks ago
parent
commit
c1d3f9177f
  1. 4
      Makefile
  2. 2
      README.md
  3. 9
      src/bin/zvirt
  4. 84
      src/lib/zvirt/core.sh
  5. 147
      test/e2e/zvirt.bats
  6. 55
      test/unit/core.bats
  7. 21
      test/unit/usage.bats

4
Makefile

@ -6,8 +6,8 @@ all: syntax-test lint unit-test e2e-test release
syntax-test: syntax-test:
@echo "Running syntax tests..." @echo "Running syntax tests..."
@/bin/bash -nv src/zvirt @/bin/bash -nv src/bin/zvirt
@/bin/bash -nv src/lib/core.sh @/bin/bash -nv src/lib/zvirt/core.sh
prerequisites: prerequisites:
@echo "Installing prerequisites..." @echo "Installing prerequisites..."

2
README.md

@ -5,7 +5,7 @@
Zvirt takes snapshots of Libvirt domains using ZFS. Zvirt takes snapshots of Libvirt domains using ZFS.
It supports both crash-consistent and live snapshots. It supports both crash-consistent and live snapshots.
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. At the end, all components of a domain - Domain definition, TPM, NVRAM, VirtioFS, disks (either files on a ZFS dataset or raw zvols) - are captured as a set of consistent ZFS snapshots.
## Features ## Features

9
src/bin/zvirt

@ -51,12 +51,13 @@ case "$action" in
revert_snapshots || fatal "Failed to revert snapshots." revert_snapshots || fatal "Failed to revert snapshots."
;; ;;
list) list)
if [ ${#domains[@]} -eq 0 ]; then preflight_checks "$action" "${domains[@]}" || fatal "Pre-flight checks failed."
# Get all domains
mapfile -t domains < <(virsh list --all --name | grep -v '^$')
fi
list_snapshots "${domains[@]}" || fatal "Failed to list snapshots." 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'." fatal "Unknown action '$action'."
;; ;;

84
src/lib/zvirt/core.sh

@ -33,11 +33,13 @@ Options:
-d DOMAIN specify domain name (you can specify multiple -d options) -d DOMAIN specify domain name (you can specify multiple -d options)
-s SNAPSHOT specify snapshot name -s SNAPSHOT specify snapshot name
-b batch mode (pause all domains, take snapshots, then resume all domains) -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: Actions:
snapshot take a snapshot of the specified domain(s) snapshot take a snapshot of the specified domain(s)
revert revert to 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) 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: Examples:
Take a crash-consistent snapshot of domain 'vm1' named 'backup1': Take a crash-consistent snapshot of domain 'vm1' named 'backup1':
@ -54,6 +56,9 @@ Examples:
List snapshots of all domains: List snapshots of all domains:
${0##*/} list ${0##*/} list
Prune snapshots of all domains, keeping at most 5 snapshots:
${0##*/} prune -k 5
EOF EOF
} }
@ -66,6 +71,7 @@ function init_global_variables () {
action="" action=""
batch=0 batch=0
live=0 live=0
keep=0
# Cache for domain parameters to avoid redundant calls to the zfs command # Cache for domain parameters to avoid redundant calls to the zfs command
declare -gA domain_params_cache=( ) declare -gA domain_params_cache=( )
@ -83,7 +89,7 @@ function parse_args () {
OPTIND=1 # Reset in case getopts has been used previously in the shell. OPTIND=1 # Reset in case getopts has been used previously in the shell.
while getopts "h?blvd:s:" opt; do while getopts "h?blvd:s:k:" opt; do
case "$opt" in case "$opt" in
h|\?) h|\?)
show_help show_help
@ -99,6 +105,8 @@ function parse_args () {
;; ;;
l) live=1 l) live=1
;; ;;
k) keep="$OPTARG"
;;
*) show_help >&2 *) show_help >&2
exit 1 exit 1
;; ;;
@ -114,6 +122,11 @@ function parse_args () {
should_exit=1 should_exit=1
fi fi
if [ ${#domains[@]} -eq 0 ]; then
# Get all domains
mapfile -t domains < <(virsh list --all --name | grep -v '^$')
fi
case "$action" in case "$action" in
snapshot) snapshot)
if [ ${#domains[@]} -eq 0 ] || [ -z "$snapshot_name" ]; then if [ ${#domains[@]} -eq 0 ] || [ -z "$snapshot_name" ]; then
@ -139,6 +152,12 @@ function parse_args () {
;; ;;
list) 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'." echo "Error: Unsupported action '$action'."
should_exit=1 should_exit=1
@ -164,7 +183,10 @@ function domain_exists () {
function domain_checks () { function domain_checks () {
local action="$1" local action="$1"
local domain="$2" local domain="$2"
local snapshot_name="$3" local snapshot_name
if [ "$action" == "snapshot" ] || [ "$action" == "revert" ]; then
snapshot_name="$3"
fi
local error=0 local error=0
local state="" local state=""
@ -194,8 +216,6 @@ function domain_checks () {
if [ -z "$zfs_mountpoint" ] || [[ ! "$zfs_mountpoint" =~ ^/ ]]; then if [ -z "$zfs_mountpoint" ] || [[ ! "$zfs_mountpoint" =~ ^/ ]]; then
error "$domain: Wrong ZFS mountpoint for dataset '$zfs_dataset': '$zfs_mountpoint'." ; error=1 error "$domain: Wrong ZFS mountpoint for dataset '$zfs_dataset': '$zfs_mountpoint'." ; error=1
# elif [ ! -d "$zfs_mountpoint" ]; then
# error "$domain: ZFS mountpoint '$zfs_mountpoint' does not exist." ; error=1
fi fi
state=$(domain_state "$domain") state=$(domain_state "$domain")
@ -205,6 +225,7 @@ function domain_checks () {
domain_params_cache["$domain/dataset"]="${zfs_dataset}" domain_params_cache["$domain/dataset"]="${zfs_dataset}"
domain_params_cache["$domain/mountpoint"]="${zfs_mountpoint}" domain_params_cache["$domain/mountpoint"]="${zfs_mountpoint}"
domain_params_cache["$domain/zvols"]="${zfs_zvols[*]}" domain_params_cache["$domain/zvols"]="${zfs_zvols[*]}"
domain_params_cache["$domain/snapshots"]="${zfs_dataset_snapshots[*]}"
case "$action" in case "$action" in
snapshot) snapshot)
@ -251,7 +272,15 @@ function domain_checks () {
fi fi
done 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'." error "$domain: Unknown action '$action'."
;; ;;
esac esac
@ -381,13 +410,20 @@ function resume_all_domains () {
# Performs pre-flight checks for all specified domains according to the action. # Performs pre-flight checks for all specified domains according to the action.
function preflight_checks () { function preflight_checks () {
local action="$1" ; shift local action="$1" ; shift
local snapshot_name="$1" ; shift local snapshot_name
if [ "$action" == "snapshot" ] || [ "$action" == "revert" ]; then
snapshot_name="$1" ; shift
fi
local error=0 local error=0
local domains=( "$@" ) local domains=( "$@" )
for domain in "${domains[@]}"; do for domain in "${domains[@]}"; do
log_verbose "$domain: Performing domain pre-flight checks for $action..." log_verbose "$domain: Performing domain pre-flight checks for $action..."
if ! domain_checks "$action" "$domain" "$snapshot_name"; then 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 error=1
fi fi
done done
@ -395,6 +431,7 @@ function preflight_checks () {
return $error return $error
} }
# Removes the save file for the specified domain. # Removes the save file for the specified domain.
function remove_save_file () { function remove_save_file () {
local domain="$1" local domain="$1"
@ -510,18 +547,35 @@ function revert_snapshots () {
# Lists snapshots for all specified domains. # Lists snapshots for all specified domains.
function list_snapshots () { function list_snapshots () {
local domains=( "$@" ) local domains=( "$@" )
local zfs_datasets
local zfs_dataset
local domain local domain
local snapshot
for domain in "${domains[@]}"; do for domain in "${domains[@]}"; do
zfs_datasets=( $(get_zfs_datasets_from_domain "$domain") )
if [ ${#zfs_datasets[@]} -ne 1 ]; then
error "$domain: Wrong number of ZFS datasets (${#zfs_datasets[@]}) found." ; return 1
fi
zfs_dataset="${zfs_datasets[0]:-}"
echo "Snapshots for domain '$domain':" echo "Snapshots for domain '$domain':"
get_zfs_snapshots_from_dataset "$zfs_dataset" | sed 's/^/ - /' 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 done
} }

147
test/e2e/zvirt.bats

@ -228,6 +228,153 @@ teardown() {
e2e_test_debug_log "setup: provisioning completed" 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" { @test "zvirt: take live snapshot in batch mode" {
# Create witness files in all three domains before taking snapshots # Create witness files in all three domains before taking snapshots
qemu_exec standard touch /test/rootfs/witness-file qemu_exec standard touch /test/rootfs/witness-file

55
test/unit/core.bats

@ -19,7 +19,7 @@ setup() {
# and with access to the domain_params_cache associative array # and with access to the domain_params_cache associative array
in_bash() { in_bash() {
local vars="" local vars=""
for var in domain_params_cache snapshot_name domains verbose action batch live; do for var in domain_params_cache snapshot_name domains verbose action batch live keep; do
if declare -p "${var}" &>/dev/null; then if declare -p "${var}" &>/dev/null; then
vars+="$(declare -p "${var}") ; " vars+="$(declare -p "${var}") ; "
fi fi
@ -504,22 +504,7 @@ data/domains/baz/virtiofs"
@test "list_snapshots: nominal case" { @test "list_snapshots: nominal case" {
# Mock the underlying tools # Mock the underlying tools
get_zfs_datasets_from_domain() { declare -A domain_params_cache=( ["foo/snapshots"]="snapshot1 snapshot2" ["bar/snapshots"]="snapshot3 snapshot4" )
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 the test
run in_bash list_snapshots foo run in_bash list_snapshots foo
@ -529,6 +514,42 @@ snapshot2"
- snapshot2" - 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" { @test "preflight_checks: nominal case" {
# Mock the underlying tools # Mock the underlying tools
domain_checks() { domain_checks() {

21
test/unit/usage.bats

@ -11,7 +11,7 @@ setup() {
init_global_variables init_global_variables
parse_args "$@" parse_args "$@"
ret=$? ret=$?
declare -p action batch live verbose domains snapshot_name declare -p action batch live verbose domains snapshot_name keep
return $ret return $ret
} }
} }
@ -65,3 +65,22 @@ setup() {
assert_output --partial 'live="0"' assert_output --partial 'live="0"'
} }
@test "call_parse_args: prune snapshots for all domains" {
virsh() {
if [[ "$*" == "list --all --name" ]]; then
echo -e "foo\nbar"
return 0
fi
return 1
}
run call_parse_args prune -k 5
assert_success
assert_output --partial 'action="prune"'
assert_output --partial 'domains=([0]="foo" [1]="bar")'
assert_output --partial 'keep="5"'
run call_parse_args prune
assert_failure
assert_output --partial "The -k option with a positive integer value must be specified for the 'prune' action"
}

Loading…
Cancel
Save