Add scripterase-zfs.sh which does fulla / metadata erase and verification

This commit is contained in:
2026-09-05 12:02:31 +03:00
parent 2d6c410489
commit 2bd6159b05
2 changed files with 235 additions and 0 deletions
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env bash
# Fully erase a device or erase its partition-table and ZFS metadata.
set -euo pipefail
readonly DEFAULT_PASSES=10
readonly MIB=$((1024 * 1024))
readonly METADATA_MIB=4
readonly METADATA_BYTES=$((METADATA_MIB * MIB))
usage() {
cat <<'EOF'
Usage: sudo ./erase-zfs.sh [--wipe|--metadata] [--source zeroes|random] [--passes <count>] [--verify] /dev/<device>
Fully overwrites the target device ten times by default. --metadata overwrites
only the first and last 4 MiB, covering partition tables and ZFS vdev labels.
Options:
--wipe Fully overwrite the device (default).
--metadata Overwrite only partition-table and ZFS metadata regions.
--source <value> Write zeroes (default) or random data.
--passes <count> Use a positive number of overwrite passes (default: 10).
--verify Run signature, ZFS-label, and pool-discovery checks after wiping.
-h, --help Show this help message.
EOF
}
die() {
printf 'Error: %s\n' "$*" >&2
exit 1
}
wipe_mode=wipe
write_source=/dev/zero
source_name=zeroes
passes=$DEFAULT_PASSES
verify=false
device=
while [[ $# -gt 0 ]]; do
case $1 in
--wipe)
wipe_mode=wipe
shift
;;
--metadata)
wipe_mode=metadata
shift
;;
--source)
[[ $# -ge 2 ]] || die '--source requires zeroes or random.'
case $2 in
zeroes)
write_source=/dev/zero
source_name=zeroes
;;
random)
write_source=/dev/urandom
source_name=random
;;
*) die "invalid source '$2'; use zeroes or random." ;;
esac
shift 2
;;
--passes)
[[ $# -ge 2 ]] || die '--passes requires a positive integer.'
[[ $2 =~ ^[1-9][0-9]*$ ]] || die "invalid pass count '$2'; use a positive integer."
passes=$2
shift 2
;;
--verify)
verify=true
shift
;;
-h|--help)
usage
exit 0
;;
-*) die "unknown option '$1'." ;;
*)
[[ -z $device ]] || die 'specify exactly one block device.'
device=$1
shift
;;
esac
done
[[ -n $device ]] || { usage >&2; exit 2; }
[[ $EUID -eq 0 ]] || die 'run as root (for example, with sudo).'
[[ -b $device ]] || die "'$device' is not a block device."
[[ -r $write_source ]] || die "'$write_source' is not readable."
for command in blockdev dd findmnt lsblk zpool; do
command -v "$command" >/dev/null 2>&1 || die "required command '$command' was not found."
done
if [[ $verify == true ]]; then
for command in wipefs zdb; do
command -v "$command" >/dev/null 2>&1 || die "--verify requires '$command'."
done
if [[ $source_name == zeroes ]]; then
command -v cmp >/dev/null 2>&1 || die "--verify with zeroes requires 'cmp'."
fi
fi
if findmnt --noheadings --source "$device" >/dev/null 2>&1; then
die "'$device' is mounted; unmount it before continuing."
fi
if lsblk --noheadings --raw --output MOUNTPOINT "$device" | grep -q '[^[:space:]]'; then
die "'$device' or one of its children is mounted; unmount it before continuing."
fi
if zpool status -P 2>/dev/null | grep -Fq -- "$device"; then
die "'$device' belongs to an imported ZFS pool; export or detach it before continuing."
fi
size_bytes=$(blockdev --getsize64 "$device")
(( size_bytes > 0 )) || die "'$device' has no addressable capacity."
if [[ $wipe_mode == metadata ]]; then
(( size_bytes >= 2 * METADATA_BYTES )) || die "'$device' is smaller than $((2 * METADATA_MIB)) MiB."
fi
printf '\nWARNING: this permanently performs a %s wipe on:\n %s\nusing %s data for %d passes.\n\n' \
"$wipe_mode" "$device" "$source_name" "$passes"
lsblk --output NAME,SIZE,TYPE,MOUNTPOINTS "$device"
read -r -p "Type exactly 'ERASE $device' to continue: " confirmation
[[ $confirmation == "ERASE $device" ]] || die 'confirmation did not match; no data was changed.'
wipe_metadata() {
local last_offset=$((size_bytes - METADATA_BYTES))
dd if="$write_source" of="$device" bs="$MIB" count="$METADATA_MIB" iflag=fullblock conv=fsync,notrunc status=none
dd if="$write_source" of="$device" bs=1 count="$METADATA_BYTES" seek="$last_offset" iflag=fullblock conv=fsync,notrunc status=none
}
wipe_full_device() {
local whole_mib=$((size_bytes / MIB))
local remainder=$((size_bytes % MIB))
if (( whole_mib > 0 )); then
dd if="$write_source" of="$device" bs="$MIB" count="$whole_mib" iflag=fullblock conv=fsync,notrunc status=progress
fi
if (( remainder > 0 )); then
dd if="$write_source" of="$device" bs=1 count="$remainder" seek=$((whole_mib * MIB)) iflag=fullblock conv=fsync,notrunc status=none
fi
}
for ((pass = 1; pass <= passes; pass++)); do
printf 'Pass %d/%d...\n' "$pass" "$passes"
if [[ $wipe_mode == wipe ]]; then
wipe_full_device
else
wipe_metadata
fi
done
sync
printf 'Completed %d %s %s passes on %s.\n' "$passes" "$source_name" "$wipe_mode" "$device"
[[ $verify == true ]] || exit 0
verification_failed=false
printf '\nVerification: checking for remaining signatures.\n'
if wipefs --all --no-act "$device"; then
printf 'PASS: wipefs found no signatures.\n'
else
printf 'FAIL: wipefs reported one or more signatures.\n' >&2
verification_failed=true
fi
printf '\nVerification: checking for readable ZFS labels.\n'
if zdb -l "$device"; then
printf 'FAIL: zdb found a readable ZFS label.\n' >&2
verification_failed=true
else
printf 'PASS: zdb found no readable ZFS label.\n'
fi
printf '\nVerification: checking whether ZFS discovers an importable pool near this device.\n'
discovery_output=
if ! discovery_output=$(zpool import -d "$(dirname -- "$device")" 2>&1); then
:
fi
if grep -Eq '^[[:space:]]*pool:[[:space:]]' <<<"$discovery_output"; then
printf '%s\n' "$discovery_output"
printf 'FAIL: zpool found an importable pool, possibly using this device.\n' >&2
verification_failed=true
elif grep -Fq 'no pools available to import' <<<"$discovery_output"; then
printf 'PASS: zpool found no importable pools in the device directory.\n'
else
printf '%s\n' "$discovery_output" >&2
printf 'FAIL: zpool discovery did not produce a conclusive result.\n' >&2
verification_failed=true
fi
if [[ $wipe_mode == wipe && $source_name == zeroes ]]; then
printf '\nVerification: comparing the full device with zeroes.\n'
if cmp --silent /dev/zero "$device"; then
printf 'PASS: the complete readable device contains zeroes.\n'
else
printf 'FAIL: the complete readable device is not all zeroes.\n' >&2
verification_failed=true
fi
fi
[[ $verification_failed == false ]] || exit 1
printf '\nVerification completed successfully.\n'