12sunflowers
Here is an updated script that safely inspects and optionally imports a ZFS pool in a read-only, non-destructive manner. It is designed to assist in data recovery when a pool appears damaged, was not cleanly exported, or is being accessed from a different system.
#!/bin/bash
# zfs-recovery-toolkit.sh – ZFS recovery script with detailed logs and structured report
set -euo pipefail
# === USER CONFIGURABLE ===
POOL="${1:-zroot}"
DEVICE_HINT="${2:-/dev}"
DEBUG="${3:-no}"
AUTO_IMPORT="yes"
AUTO_EXPORT="yes"
# ==========================
# --- Check for required commands early ---
for cmd in zdb zpool zfs; do
command -v "$cmd" >/dev/null 2>&1 || {
echo "[✘] Required command '$cmd' not found in PATH." >&2; exit 1;
}
done
[[ -z "$POOL" ]] && { echo "[✘] POOL name must not be empty." >&2; exit 1; }
[[ ! -d "$DEVICE_HINT" ]] && { echo "[✘] DEVICE_HINT '$DEVICE_HINT' is not a valid directory." >&2; exit 1; }
[[ "$DEBUG" == "yes" ]] && set -x
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
HOST=$(hostname)
OUTDIR="./zfs_recovery_${POOL}_${TIMESTAMP}"
REPORT="$OUTDIR/recovery-report.txt"
mkdir -p "$OUTDIR"
log() { echo "[*] $(date +%H:%M:%S) $1"; echo "[*] $1" >> "$REPORT"; }
warn() { echo "[!] $(date +%H:%M:%S) $1" >&2; echo "[!] $1" >> "$REPORT"; }
error() { echo "[✘] $(date +%H:%M:%S) $1" >&2; echo "[ERROR] $1" >> "$REPORT"; }
fail_exit() { error "$1"; echo "[ABORTED]" >> "$REPORT"; exit 1; }
trap 'echo "[!] Script interrupted or failed unexpectedly." >> "$REPORT"' EXIT
# === DISCLAIMER ===
echo "WARNING: This tool performs low-level read-only inspection of ZFS pools." >&2
echo "It attempts recovery in a non-destructive manner, but improper use may risk data loss." >&2
echo "Ensure you have backups. Continue at your own risk." >&2
read -rp "Do you wish to continue? (yes/no): " CONFIRM
if [[ "$CONFIRM" != "yes" ]]; then
fail_exit "User aborted script."
fi
# === BEGIN REPORT ===
{
echo "ZFS Recovery Report"
echo "==================="
echo "Host: $HOST"
echo "Time: $(date)"
echo "Pool: $POOL"
echo "Device Hint: $DEVICE_HINT"
echo "Output Dir: $OUTDIR"
echo
echo "Disclaimer acknowledged: user chose to proceed."
} > "$REPORT"
log "Step 1: Checking pool visibility..."
if ! zpool import 2> "$OUTDIR/import-visible-error.txt" | grep -q "$POOL"; then
warn "Pool not found in normal import list. Trying device hint scan..."
if ! zpool import -d "$DEVICE_HINT" > "$OUTDIR/import-scan.txt" 2> "$OUTDIR/import-scan-error.txt"; then
warn "Device scan failed. See $OUTDIR/import-scan-error.txt"
fi
if grep -q "$POOL" "$OUTDIR/import-scan.txt"; then
log "Pool '$POOL' found via device hint."
else
warn "Pool '$POOL' still not found. Continuing with caution."
fi
else
log "Pool '$POOL' is visible via normal import."
fi
log "Step 2: Analyzing pool metadata with zdb..."
if ! zdb -e -bcsvL "$POOL" > "$OUTDIR/zdb-output.txt" 2> "$OUTDIR/zdb-error.txt"; then
warn "zdb failed. Output saved to $OUTDIR/zdb-error.txt"
else
log "zdb completed successfully. Output: $OUTDIR/zdb-output.txt"
fi
log "Step 3: Checking for dirty log state (ZIL)..."
ZPOOL_LOGSTATE=$(zpool import -l -d "$DEVICE_HINT" "$POOL" 2>&1 || true)
echo "$ZPOOL_LOGSTATE" > "$OUTDIR/zpool-logstate.txt"
if echo "$ZPOOL_LOGSTATE" | grep -q "logs with pending data"; then
warn "Pool has uncommitted ZIL. Consider import with -FX."
read -rp "Attempt destructive rollback using -FX (last resort)? (yes/no): " DO_FX
if [[ "$DO_FX" == "yes" ]]; then
log "Attempting forced rollback with -FX..."
if ! zpool import -FX -o readonly=on -d "$DEVICE_HINT" "$POOL" > "$OUTDIR/zpool-import-FX.txt" 2>&1; then
warn "-FX import failed. See $OUTDIR/zpool-import-FX.txt"
else
log "-FX import succeeded."
fi
fi
fi
log "Step 4: Attempting dry-run import with rollback enabled..."
DRYRUN_OUTPUT=$(zpool import -F -n -o readonly=on -d "$DEVICE_HINT" "$POOL" 2>&1 || true)
echo "$DRYRUN_OUTPUT" > "$OUTDIR/zpool-dryrun-error.txt"
if echo "$DRYRUN_OUTPUT" | grep -q "was previously in use from another system"; then
warn "Pool was used on another system. Attempting forced read-only import."
elif echo "$DRYRUN_OUTPUT" | grep -q "cannot import"; then
warn "Dry-run import failed. See $OUTDIR/zpool-dryrun-error.txt"
else
log "Dry-run import succeeded."
fi
if [[ "$AUTO_IMPORT" == "yes" ]]; then
if zpool list | grep -q "^$POOL"; then
log "Pool '$POOL' already imported. Skipping read-only import."
else
log "Step 5: Performing safe read-only import..."
if ! zpool import -F -o readonly=on -f -d "$DEVICE_HINT" "$POOL" > "$OUTDIR/zpool-import.txt" 2> "$OUTDIR/zpool-import-error.txt"; then
warn "Read-only import failed. See $OUTDIR/zpool-import-error.txt"
else
log "Read-only import succeeded. See $OUTDIR/zpool-import.txt"
fi
fi
log "Step 6: Listing datasets..."
if ! zfs list -r "$POOL" > "$OUTDIR/zfs-list.txt" 2> "$OUTDIR/zfs-list-error.txt"; then
warn "Dataset listing failed. See $OUTDIR/zfs-list-error.txt"
else
log "Datasets listed successfully. See $OUTDIR/zfs-list.txt"
fi
if [[ "$AUTO_EXPORT" == "yes" ]]; then
log "Step 7: Exporting the pool..."
if ! zpool export "$POOL" > "$OUTDIR/zpool-export.txt" 2> "$OUTDIR/zpool-export-error.txt"; then
warn "Export failed. See $OUTDIR/zpool-export-error.txt"
else
log "Export completed successfully. See $OUTDIR/zpool-export.txt"
fi
else
log "AUTO_EXPORT disabled. Pool remains imported (read-only)."
fi
else
log "AUTO_IMPORT disabled. No pool was imported."
fi
# --- Final Summary ---
{
echo
echo "Final Notes:"
echo " - All operations were performed in non-destructive read-only mode."
echo " - No datasets were mounted."
echo " - Log and output directory: $OUTDIR"
echo "[COMPLETED]"
} >> "$REPORT"
log "Recovery script completed. Review $REPORT for details."
echo "{\"status\":\"complete\",\"pool\":\"$POOL\",\"output\":\"$OUTDIR\"}" > "$OUTDIR/summary.json"