Add scripterase-zfs.sh which does fulla / metadata erase and verification
This commit is contained in:
@@ -4,6 +4,35 @@
|
|||||||
a device as a ZFS vdev. It is intended for decommissioning or repurposing a
|
a device as a ZFS vdev. It is intended for decommissioning or repurposing a
|
||||||
device after it has been cleanly removed from its pool.
|
device after it has been cleanly removed from its pool.
|
||||||
|
|
||||||
|
## Unified Eraser
|
||||||
|
|
||||||
|
`erase-zfs.sh` is the recommended entry point. It performs a full-device wipe
|
||||||
|
by default and uses 10 overwrite passes. Select `--metadata` when only the
|
||||||
|
partition tables and ZFS vdev labels should be overwritten.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x erase-zfs.sh
|
||||||
|
sudo ./erase-zfs.sh /dev/sdX
|
||||||
|
sudo ./erase-zfs.sh --metadata /dev/sdX
|
||||||
|
sudo ./erase-zfs.sh --passes 3 /dev/sdX
|
||||||
|
sudo ./erase-zfs.sh --source random --verify /dev/sdX
|
||||||
|
```
|
||||||
|
|
||||||
|
`--wipe` explicitly selects the default full-device mode. `--source zeroes`
|
||||||
|
is the default, while `--source random` uses `/dev/urandom`. `--passes` accepts
|
||||||
|
a positive integer and defaults to 10. `--verify` runs after wiping and checks
|
||||||
|
`wipefs` signatures, readable ZFS labels through `zdb -l`, and ZFS pool
|
||||||
|
discovery. A full zero wipe additionally compares every readable byte against
|
||||||
|
`/dev/zero`. Random data cannot be byte-for-byte checked after it is written,
|
||||||
|
so its verification is limited to the metadata and ZFS recovery checks. The
|
||||||
|
verifier marks unavailable or ambiguous ZFS discovery as a failure rather than
|
||||||
|
declaring the wipe successful.
|
||||||
|
|
||||||
|
Pool discovery searches the target device's parent directory. Any reported
|
||||||
|
pool may use a neighboring device, so inspect its vdev paths before treating
|
||||||
|
the result as evidence about the erased target. A failed verification exits
|
||||||
|
with a nonzero status and leaves all evidence in place for investigation.
|
||||||
|
|
||||||
## What It Erases
|
## What It Erases
|
||||||
|
|
||||||
The script performs 10 overwrite passes over the first 4 MiB and the last
|
The script performs 10 overwrite passes over the first 4 MiB and the last
|
||||||
|
|||||||
+206
@@ -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'
|
||||||
Reference in New Issue
Block a user