#!/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"