195 lines
7.3 KiB
Bash
195 lines
7.3 KiB
Bash
#!/bin/bash
|
|
# -----------------------------------------------------------------------------------------------
|
|
# --------------------------------- ZFS Pool Scrub ---------------------------------------------
|
|
# -----------------------------------------------------------------------------------------------
|
|
# Triggers a ZFS scrub on all pools (or a specific pool) and waits for completion.
|
|
# Sends a notification when scrub completes with a summary of any errors found.
|
|
#
|
|
# ZFS scrub reads every block on every pool and verifies checksums — it catches
|
|
# silent data corruption that would otherwise only surface when you try to read
|
|
# the corrupted data. Running monthly is recommended for all ZFS pools.
|
|
#
|
|
# Usage:
|
|
# zfs_pool_scrub.sh — scrub all pools
|
|
# zfs_pool_scrub.sh poolname — scrub specific pool only
|
|
# zfs_pool_scrub.sh --status — show scrub status for all pools
|
|
# zfs_pool_scrub.sh --dry-run — show what would be scrubbed
|
|
#
|
|
# Pools in ZFS_REPORT_IGNORE_POOLS are skipped unless specified explicitly.
|
|
# Scrub runs in background — script polls until complete then reports.
|
|
# Safe to run while the pool is in use — scrub does not interrupt normal I/O.
|
|
# -----------------------------------------------------------------------------------------------
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
source "$SCRIPT_DIR/../Master.conf"
|
|
source "$SCRIPT_DIR/../common.sh"
|
|
|
|
parse_args "$@"
|
|
|
|
TARGET_POOL="${PARSED_ARGS[0]:-}"
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# ━━━ $ICON_GEAR Setup ━━━
|
|
# -----------------------------------------------------------------------------------------------
|
|
echo ""
|
|
echo "━━━ $ICON_GEAR Setup ━━━"
|
|
|
|
if [[ "$EUID" -ne 0 ]]; then
|
|
error "Must be run as root"
|
|
exit 1
|
|
fi
|
|
|
|
success "Running as root"
|
|
|
|
acquire_lock
|
|
|
|
if ! command -v zpool >/dev/null 2>&1; then
|
|
error "ZFS not available on this system"
|
|
exit 1
|
|
fi
|
|
|
|
success "ZFS available"
|
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no scrubs will be started"
|
|
|
|
# Build ignore map
|
|
declare -A IGNORE_MAP
|
|
for pool in "${ZFS_REPORT_IGNORE_POOLS[@]}"; do
|
|
[[ -n "$pool" ]] && IGNORE_MAP["$pool"]=1
|
|
done
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# ━━━ $ICON_SUMMARY Status ━━━
|
|
# -----------------------------------------------------------------------------------------------
|
|
if [[ "$SHOW_STATUS" == true ]]; then
|
|
echo ""
|
|
echo "━━━━━ $ICON_SUMMARY SCRUB STATUS ━━━━━"
|
|
zpool list -H -o name 2>/dev/null | while read -r pool; do
|
|
SCAN=$(zpool status "$pool" 2>/dev/null | grep "scan:")
|
|
echo " $ICON_ZFS $pool — $SCAN"
|
|
done
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
|
exit 0
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# Build pool list to scrub
|
|
# -----------------------------------------------------------------------------------------------
|
|
POOLS_TO_SCRUB=()
|
|
|
|
if [[ -n "$TARGET_POOL" ]]; then
|
|
# Specific pool requested — validate it exists
|
|
if ! zpool list "$TARGET_POOL" >/dev/null 2>&1; then
|
|
error "Pool not found: $TARGET_POOL"
|
|
exit 1
|
|
fi
|
|
POOLS_TO_SCRUB=("$TARGET_POOL")
|
|
else
|
|
# All pools — skip ignored ones
|
|
while IFS= read -r pool; do
|
|
[[ -z "$pool" ]] && continue
|
|
if [[ -n "${IGNORE_MAP[$pool]:-}" ]]; then
|
|
info "Skipping $pool (in ZFS_REPORT_IGNORE_POOLS)"
|
|
continue
|
|
fi
|
|
POOLS_TO_SCRUB+=("$pool")
|
|
done < <(zpool list -H -o name 2>/dev/null)
|
|
fi
|
|
|
|
if [[ ${#POOLS_TO_SCRUB[@]} -eq 0 ]]; then
|
|
warn "No pools to scrub"
|
|
exit 0
|
|
fi
|
|
|
|
info "Pools to scrub: ${POOLS_TO_SCRUB[*]}"
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# ━━━ $ICON_ZFS Start Scrubs ━━━
|
|
# -----------------------------------------------------------------------------------------------
|
|
echo ""
|
|
echo "━━━ $ICON_ZFS Starting ZFS Scrubs ━━━"
|
|
START=$(date +%s)
|
|
|
|
for pool in "${POOLS_TO_SCRUB[@]}"; do
|
|
info "$ICON_ZFS Starting scrub on $pool..."
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
zpool scrub "$pool" 2>/dev/null && \
|
|
success "$pool scrub started" || \
|
|
error "Failed to start scrub on $pool"
|
|
else
|
|
warn "DRY RUN — would scrub: $pool"
|
|
fi
|
|
done
|
|
|
|
[[ "$DRY_RUN" == true ]] && {
|
|
echo ""
|
|
echo "━━━━━ $ICON_SUMMARY ZFS SCRUB SUMMARY ━━━━━"
|
|
echo "$ICON_WARN Status: DRY RUN — no scrubs started"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
exit 0
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# ━━━ Poll until complete ━━━
|
|
# -----------------------------------------------------------------------------------------------
|
|
echo ""
|
|
echo "━━━ $ICON_TIME Waiting for scrubs to complete ━━━"
|
|
info "Polling every 60 seconds — this may take a while on large pools"
|
|
info "Safe to leave running — scrub continues even if this script is stopped"
|
|
|
|
STILL_RUNNING=true
|
|
while [[ "$STILL_RUNNING" == true ]]; do
|
|
sleep 60
|
|
STILL_RUNNING=false
|
|
for pool in "${POOLS_TO_SCRUB[@]}"; do
|
|
STATUS=$(zpool status "$pool" 2>/dev/null | grep "scan:" | grep -c "in progress" || true)
|
|
if [[ "$STATUS" -gt 0 ]]; then
|
|
STILL_RUNNING=true
|
|
REPAIRED=$(zpool status "$pool" 2>/dev/null | grep "scan:" | grep -oE "[0-9]+ repaired")
|
|
log "$pool — scrub in progress ${REPAIRED:+($REPAIRED)}"
|
|
fi
|
|
done
|
|
done
|
|
|
|
END=$(date +%s)
|
|
success "All scrubs complete"
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# ━━━ $ICON_SUMMARY Results ━━━
|
|
# -----------------------------------------------------------------------------------------------
|
|
echo ""
|
|
echo "━━━ $ICON_ZFS Scrub Results ━━━"
|
|
|
|
POOLS_OK=()
|
|
POOLS_ERRORS=()
|
|
|
|
for pool in "${POOLS_TO_SCRUB[@]}"; do
|
|
SCAN_LINE=$(zpool status "$pool" 2>/dev/null | grep "scan:")
|
|
ERRORS=$(zpool status "$pool" 2>/dev/null | grep "errors:" | grep -v "No known data errors")
|
|
|
|
if [[ -n "$ERRORS" ]]; then
|
|
error "$pool — $SCAN_LINE"
|
|
error "$pool — $ERRORS"
|
|
POOLS_ERRORS+=("$pool")
|
|
else
|
|
success "$pool — $SCAN_LINE"
|
|
POOLS_OK+=("$pool")
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo "━━━━━ $ICON_SUMMARY ZFS SCRUB SUMMARY ━━━━━"
|
|
echo "$ICON_ZFS Pools scrubbed: ${#POOLS_TO_SCRUB[@]}"
|
|
echo "$ICON_SUCCESS Clean: ${#POOLS_OK[@]}"
|
|
echo "$ICON_ERROR Errors: ${#POOLS_ERRORS[@]}"
|
|
echo "$ICON_TIME Duration: $(format_duration $((END - START)))"
|
|
echo ""
|
|
|
|
if [[ ${#POOLS_ERRORS[@]} -gt 0 ]]; then
|
|
echo "$ICON_ERROR Status: ERRORS FOUND — ${POOLS_ERRORS[*]}"
|
|
notify "ZFS scrub complete on $(hostname) — ERRORS found in pools: ${POOLS_ERRORS[*]}" "ZFS Scrub" "warning"
|
|
else
|
|
echo "$ICON_DONE Status: $ICON_SUCCESS ALL POOLS CLEAN"
|
|
notify "ZFS scrub complete on $(hostname) — ${#POOLS_OK[@]} pools clean in $(format_duration $((END - START)))" "ZFS Scrub" "normal"
|
|
fi
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" |