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