From 2d6c41048908825397f2d57f689dc77a5bea00e2 Mon Sep 17 00:00:00 2001 From: Tero Date: Fri, 4 Sep 2026 16:22:58 +0300 Subject: [PATCH] Add ability to test pool recovery --- README.md | 40 +++++++++++++++++- verify-zpool-recovery.sh | 90 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 verify-zpool-recovery.sh diff --git a/README.md b/README.md index a7a7144..d5d2494 100644 --- a/README.md +++ b/README.md @@ -95,4 +95,42 @@ sudo zdb -l /dev/sdX ``` `wipefs --no-act` reports signatures without modifying the device. `zdb -l` -should not find a readable ZFS label after a successful run. \ No newline at end of file +should not find a readable ZFS label after a successful run. + +## Recovery Assessment + +`verify-zpool-recovery.sh` checks whether ZFS can find and rewind an exported +or faulted pool. It cannot restore labels that have been completely overwritten +by the eraser, so recovery after a successful erase is unlikely. Use it before +erasing a device, or when ZFS still detects at least part of the pool metadata. + +List pools ZFS can discover without modifying anything: + +```bash +sudo ./verify-zpool-recovery.sh +``` + +Check whether a specific pool can be recovered with a transaction rewind. This +uses `zpool import -nF`, which is a dry run: + +```bash +sudo ./verify-zpool-recovery.sh tank +``` + +If vdevs must be found in a non-default directory, supply it explicitly: + +```bash +sudo ./verify-zpool-recovery.sh --directory /dev/disk/by-id tank +``` + +After reviewing the dry-run output, attempt the import with rewind recovery: + +```bash +sudo ./verify-zpool-recovery.sh --apply tank +``` + +`--apply` runs `zpool import -N -F`: `-F` may discard the most recent +transaction groups, and `-N` imports the pool without mounting datasets. The +script requires the exact confirmation `RECOVER tank` before issuing that +command. Inspect the result with `zpool status tank` before mounting datasets +or performing any further changes. \ No newline at end of file diff --git a/verify-zpool-recovery.sh b/verify-zpool-recovery.sh new file mode 100644 index 0000000..682172d --- /dev/null +++ b/verify-zpool-recovery.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# Assess whether ZFS can rewind and import an exported or faulted pool. + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: sudo ./verify-zpool-recovery.sh [--directory ] [--apply] [pool] + +Lists pools ZFS can discover, or checks whether a named pool can be recovered +with a transaction rewind. The default is a dry run and changes nothing. + +Options: + -d, --directory Search a specific directory for vdev devices. + --apply Import the named pool with rewind recovery. + -h, --help Show this help message. + +Without a pool name, the script only lists importable pools. --apply requires +a pool name and imports it with datasets left unmounted. +EOF +} + +die() { + printf 'Error: %s\n' "$*" >&2 + exit 1 +} + +search_directory= +apply=false +pool= + +while [[ $# -gt 0 ]]; do + case $1 in + -d|--directory) + [[ $# -ge 2 ]] || die "$1 requires a path." + search_directory=$2 + shift 2 + ;; + --apply) + apply=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + -*) + die "unknown option '$1'." + ;; + *) + [[ -z $pool ]] || die 'specify at most one pool name or GUID.' + pool=$1 + shift + ;; + esac +done + +[[ $EUID -eq 0 ]] || die 'run as root (for example, with sudo).' +command -v zpool >/dev/null 2>&1 || die "required command 'zpool' was not found." + +directory_args=() +if [[ -n $search_directory ]]; then + [[ -d $search_directory ]] || die "'$search_directory' is not a directory." + directory_args=(-d "$search_directory") +fi + +if [[ -z $pool ]]; then + [[ $apply == false ]] || die '--apply requires a pool name or GUID.' + printf 'Discovering importable pools. This does not modify any pool.\n\n' + zpool import "${directory_args[@]}" + exit 0 +fi + +if zpool list -H -o name "$pool" >/dev/null 2>&1; then + die "'$pool' is already imported; inspect it with 'zpool status $pool'." +fi + +if [[ $apply == false ]]; then + printf 'Dry-run rewind recovery check for pool %s. No changes will be made.\n\n' "$pool" + zpool import -nF "${directory_args[@]}" "$pool" + exit 0 +fi + +printf '\nWARNING: this will attempt to rewind and import pool:\n %s\n\n' "$pool" +printf 'This can discard the most recent transaction groups. Datasets will remain unmounted.\n' +read -r -p "Type exactly 'RECOVER $pool' to continue: " confirmation +[[ $confirmation == "RECOVER $pool" ]] || die 'confirmation did not match; no changes were made.' + +zpool import -N -F "${directory_args[@]}" "$pool" +printf "Pool '%s' was imported without mounting datasets. Inspect it with:\n zpool status %s\n" "$pool" "$pool" \ No newline at end of file