From 9b4af01de28b444ca622bfcd471acc0f06e57422 Mon Sep 17 00:00:00 2001 From: hygienic-books Date: Wed, 16 Sep 2026 10:01:41 +0200 Subject: [PATCH] fix(apt): snapshot recursive roots separately --- apt-zfs-snapshot.sh | 52 +++++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/apt-zfs-snapshot.sh b/apt-zfs-snapshot.sh index 5ae41fb..f12f40a 100755 --- a/apt-zfs-snapshot.sh +++ b/apt-zfs-snapshot.sh @@ -2,13 +2,13 @@ set -o pipefail +set -xv + declare -r config_file='/etc/apt-zfs-snapshot.conf' declare -r zfs_property='space.quico:auto-snapshot' declare -r zfs_property_value='true' declare -r zfs_max_name_length=255 -set -xv - if [[ -r "${config_file}" ]]; then # shellcheck disable=SC1090 source "${config_file}" @@ -34,7 +34,7 @@ downgrade_suffix="${downgrade_suffix:-down}" mixed_suffix="${mixed_suffix:-mixd}" package_separator="${package_separator:-:}" -declare -a packages operations +declare -a packages operations successful_datasets declare existing_snapshots print_msg() { @@ -78,7 +78,7 @@ parse_apt_input() { continue fi - read -r package old_version _old_arch direction new_version _new_arch _multiarch action <<<"${line}" + read -r package old_version _old_arch _old_multiarch direction new_version _new_arch _new_multiarch action <<<"${line}" [[ -n "${package}" ]] || continue if [[ "${action}" == '**CONFIGURE**' ]]; then continue @@ -95,18 +95,31 @@ parse_apt_input() { } get_datasets() { - local dataset value + local dataset value candidate parent + local -a candidates + local mounted_datasets if [[ -n "${snapshot_roots}" ]]; then - read -r -a snappable_datasets <<<"${snapshot_roots}" + read -r -a candidates <<<"${snapshot_roots}" + snappable_datasets=("${candidates[@]}") return 0 fi + mounted_datasets="$(findmnt -rn -t zfs -o SOURCE)" while IFS=$'\t' read -r dataset value; do [[ "${value}" == "${zfs_property_value}" && "${dataset}" != *@* ]] || continue - if [[ "${snap_only_local_datasets}" != 'true' ]] || findmnt -rn -t zfs -o SOURCE | grep -Fxq -- "${dataset}"; then - snappable_datasets+=("${dataset}") + if [[ "${snap_only_local_datasets}" != 'true' ]] || grep -Fxq -- "${dataset}" <<<"${mounted_datasets}"; then + candidates+=("${dataset}") fi - done < <(zfs get -H -o name,value "${zfs_property}") + done < <(zfs get -H -t filesystem,volume -o name,value "${zfs_property}") + + # An inherited property marks every descendant. Keep only the highest + # selected dataset because the snapshot operation is recursive. + for dataset in "${candidates[@]}"; do + for parent in "${candidates[@]}"; do + [[ "${dataset}" == "${parent}"/* ]] && continue 2 + done + snappable_datasets+=("${dataset}") + done } operation_suffix() { @@ -181,7 +194,7 @@ prune_snapshots() { [[ "${dry_run}" == 'true' ]] && return 0 limit="${snapshots_trivial_keep}" [[ "${severity}" == "${important_suffix}" ]] && limit="${snapshots_important_keep}" - for dataset in "${snappable_datasets[@]}"; do + for dataset in "${successful_datasets[@]}"; do # Query only the root dataset. Destroying its recursive snapshot also # removes the matching descendant snapshots as one snapshot tree. mapfile -t snapshots < <(zfs list -H -t snapshot -o name -s creation "${dataset}" | grep -F "${dataset}@${snapshot_prefix}${field_separator}" | grep -F "${field_separator}sev:${severity}${field_separator}") @@ -220,10 +233,21 @@ main() { printf ' %s\n' "${names[@]}" >&2 return 0 fi - zfs snapshot -r "${names[@]}" || { print_msg WARN 'ZFS snapshot failed'; return 0; } - printf '[INFO] Created ZFS snapshots:\n' >&2 - printf ' %s\n' "${names[@]}" >&2 - prune_snapshots + successful_datasets=() + local name dataset snapshot_failed='false' + for name in "${names[@]}"; do + dataset="${name%%@*}" + if zfs snapshot -r "${name}"; then + successful_datasets+=("${dataset}") + printf '[INFO] Created ZFS snapshots for %s:\n' "${dataset}" >&2 + printf ' %s\n' "${name}" >&2 + else + snapshot_failed='true' + print_msg WARN "ZFS snapshot failed for ${dataset}" + fi + done + [[ "${snapshot_failed}" == 'false' ]] || print_msg WARN 'Snapshot set is incomplete' + ((${#successful_datasets[@]} > 0)) && prune_snapshots } main