#!/bin/bash # ============================================================================= # NAS08 Weekly TRIM Report Script # ============================================================================= # Runs fstrim on all mounted filesystems, captures results, gathers SMART # health data for each SSD, and sends an HTML email report matching the # KingDezigns backup monitor style. # # Triggered by: systemd fstrim.service (via drop-in override) # Schedule: Weekly (Monday ~12:33 AM — managed by fstrim.timer) # # Script location: /usr/scripts/omv/fstrim-report.sh # Log location: /var/log/fstrim/ # ============================================================================= set -uo pipefail # ----------------------------------------------------------------------------- # CONFIGURATION # ----------------------------------------------------------------------------- HOSTNAME_LABEL=$(hostname -s 2>/dev/null || echo "NAS08") REPORT_TO="rufus.king@kingdezigns.com" REPORT_FROM="trim-monitor@nas08.local" # Display only — OMV uses your configured SMTP relay LOG_DIR="/var/log/fstrim" KEEP_LOGS=90 # days to retain log files DATE=$(date +%Y-%m-%d) REPORT_TIME=$(date "+%Y-%m-%d %H:%M:%S") LOG_FILE="${LOG_DIR}/fstrim-${DATE}.log" # ----------------------------------------------------------------------------- # STATUS TRACKING # OVERALL_STATUS: SUCCESS | WARNING | FAILED # ----------------------------------------------------------------------------- OVERALL_STATUS="SUCCESS" SECTION_ROWS_HTML="" WARNING_COUNT=0 ERROR_COUNT=0 # Append a result row to the section table # Usage: add_section_row "Section name" "STATUS" "Detail message" # STATUS: OK | WARNING | ERROR | SKIPPED add_section_row() { local section="$1" status="$2" detail="$3" case "$status" in OK) BADGE="✓ OK" ;; WARNING) BADGE="⚠ WARNING" WARNING_COUNT=$((WARNING_COUNT + 1)) [[ "$OVERALL_STATUS" == "SUCCESS" ]] && OVERALL_STATUS="WARNING" ;; ERROR) BADGE="✗ ERROR" ERROR_COUNT=$((ERROR_COUNT + 1)) OVERALL_STATUS="FAILED" ;; SKIPPED) BADGE="— SKIPPED" ;; esac SECTION_ROWS_HTML+=" $section $BADGE $detail " } # ----------------------------------------------------------------------------- # LOGGING # ----------------------------------------------------------------------------- mkdir -p "$LOG_DIR" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" } log_section() { echo "" | tee -a "$LOG_FILE" echo "------------------------------------------------------------" | tee -a "$LOG_FILE" log "$1" echo "------------------------------------------------------------" | tee -a "$LOG_FILE" } # ----------------------------------------------------------------------------- # SECTION 1: RUN FSTRIM # ----------------------------------------------------------------------------- log_section "Running fstrim on all mounted filesystems" TRIM_RAW=$(fstrim -av 2>&1) TRIM_EXIT=$? TRIM_ROWS_HTML="" TOTAL_BYTES=0 VOLUME_COUNT=0 while IFS= read -r line; do if [[ "$line" =~ ^(/.+):[[:space:]]+(.+)[[:space:]]+\(([0-9]+)[[:space:]]bytes\) ]]; then MOUNT="${BASH_REMATCH[1]}" HUMAN="${BASH_REMATCH[2]}" BYTES="${BASH_REMATCH[3]}" TOTAL_BYTES=$((TOTAL_BYTES + BYTES)) VOLUME_COUNT=$((VOLUME_COUNT + 1)) log "Trimmed ${MOUNT}: ${HUMAN}" TRIM_ROWS_HTML+=" $MOUNT $HUMAN freed " fi done <<< "$TRIM_RAW" # Convert total bytes to human readable if [ $TOTAL_BYTES -ge 1073741824 ]; then TOTAL_HUMAN=$(awk "BEGIN {printf \"%.1f GiB\", $TOTAL_BYTES/1073741824}") elif [ $TOTAL_BYTES -ge 1048576 ]; then TOTAL_HUMAN=$(awk "BEGIN {printf \"%.1f MiB\", $TOTAL_BYTES/1048576}") else TOTAL_HUMAN="${TOTAL_BYTES} bytes" fi if [ $TRIM_EXIT -eq 0 ]; then log "fstrim completed — $VOLUME_COUNT volume(s) · $TOTAL_HUMAN total freed" add_section_row "TRIM Execution" "OK" "$VOLUME_COUNT volume(s) processed · $TOTAL_HUMAN total freed" else log "ERROR: fstrim exited with code $TRIM_EXIT" add_section_row "TRIM Execution" "ERROR" "fstrim exited with code $TRIM_EXIT" fi # ----------------------------------------------------------------------------- # SECTION 2: SSD SMART HEALTH # ----------------------------------------------------------------------------- log_section "Gathering SSD SMART health data" SMART_ROWS_HTML="" SMART_WARN=0 SMART_ERR=0 for DISK in $(lsblk -dpno NAME | grep -E '^/dev/sd|^/dev/nvme'); do MODEL=$(smartctl -i "$DISK" 2>/dev/null | awk -F': ' '/Device Model|Model Number/{gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2; exit}') SERIAL=$(smartctl -i "$DISK" 2>/dev/null | awk -F': ' '/Serial Number/{gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2; exit}') CAPACITY=$(lsblk -dno SIZE "$DISK" 2>/dev/null) HEALTH=$(smartctl -H "$DISK" 2>/dev/null | awk '/overall-health|test result/{print $NF}') # Temperature: +0 forces numeric conversion, handling "32 (Min/Max 25/56)" Crucial format TEMP=$(smartctl -A "$DISK" 2>/dev/null | awk '/Temperature_Celsius|Temperature_Internal/{print $10+0; exit}') POWER_ON=$(smartctl -A "$DISK" 2>/dev/null | awk '/Power_On_Hours/{print $10; exit}') # Wear: PNY=SSD_Life_Left, Crucial=Percent_Lifetime_Remain, Samsung/WD=Wear_Leveling_Count, Intel=Media_Wearout_Indicator WEAR=$(smartctl -A "$DISK" 2>/dev/null | awk '/SSD_Life_Left|Percent_Lifetime_Remain|Wear_Leveling_Count|Media_Wearout_Indicator/{print $10; exit}') # Reallocated: PNY=Bad_Blk_Ct_Lat/Erl (RAW_VALUE is "0/88" — take first number), Crucial=Reallocate_NAND_Blk_Cnt, others=Reallocated_Sector_Ct REALLOCATED=$(smartctl -A "$DISK" 2>/dev/null | awk '/Bad_Blk_Ct_Lat\/Erl/{split($10,a,"/"); print a[1]; exit} /Reallocate_NAND_Blk_Cnt|Reallocated_Sector_Ct|Reallocated_Sector/{print $10; exit}') # Pending: Crucial=Current_Pending_ECC_Cnt, others=Current_Pending_Sector (PNY has no equivalent — will show N/A) PENDING=$(smartctl -A "$DISK" 2>/dev/null | awk '/Current_Pending_ECC_Cnt|Current_Pending_Sector/{print $10; exit}') # Uncorrectable: Crucial=Reported_Uncorrect, others=Offline_Uncorrectable (PNY has no equivalent — will show N/A) UNCORRECTABLE=$(smartctl -A "$DISK" 2>/dev/null | awk '/Reported_Uncorrect|Offline_Uncorrectable/{print $10; exit}') [ -z "$MODEL" ] && MODEL="Unknown" [ -z "$SERIAL" ] && SERIAL="Unknown" [ -z "$HEALTH" ] && HEALTH="Unknown" [ -z "$TEMP" ] && TEMP="N/A" [ -z "$POWER_ON" ] && POWER_ON="N/A" [ -z "$WEAR" ] && WEAR="N/A" [ -z "$REALLOCATED" ] && REALLOCATED="N/A" [ -z "$PENDING" ] && PENDING="N/A" [ -z "$UNCORRECTABLE" ] && UNCORRECTABLE="N/A" # Health colour if [ "$HEALTH" = "PASSED" ]; then HEALTH_COLOUR="#065f46" HEALTH_BG="#d1fae5" DISK_STATUS="OK" else HEALTH_COLOUR="#991b1b" HEALTH_BG="#fee2e2" DISK_STATUS="WARNING" SMART_WARN=$((SMART_WARN + 1)) fi # Flag non-zero reallocated/pending/uncorrectable sectors SECTOR_NOTE="" if [ "$REALLOCATED" != "N/A" ] && [ "$REALLOCATED" -gt 0 ] 2>/dev/null; then SECTOR_NOTE+=" ⚠ Reallocated: $REALLOCATED" SMART_WARN=$((SMART_WARN + 1)) [[ "$OVERALL_STATUS" == "SUCCESS" ]] && OVERALL_STATUS="WARNING" fi if [ "$PENDING" != "N/A" ] && [ "$PENDING" -gt 0 ] 2>/dev/null; then SECTOR_NOTE+=" ⚠ Pending: $PENDING" SMART_WARN=$((SMART_WARN + 1)) [[ "$OVERALL_STATUS" == "SUCCESS" ]] && OVERALL_STATUS="WARNING" fi if [ "$UNCORRECTABLE" != "N/A" ] && [ "$UNCORRECTABLE" -gt 0 ] 2>/dev/null; then SECTOR_NOTE+=" 🚨 Uncorrectable: $UNCORRECTABLE" SMART_ERR=$((SMART_ERR + 1)) OVERALL_STATUS="FAILED" fi log "Drive $DISK ($MODEL) — Health: $HEALTH | Temp: ${TEMP}°C | Wear: $WEAR | Power-on: ${POWER_ON}h${SECTOR_NOTE}" SMART_ROWS_HTML+=" $DISK  $MODEL  ·  $CAPACITY  ·  S/N: $SERIAL Overall health $HEALTH${SECTOR_NOTE:+$SECTOR_NOTE} Temperature ${TEMP}°C Power-on hours ${POWER_ON} hrs Wear leveling $WEAR Sectors (realloc / pending / uncorrectable) $REALLOCATED  /  $PENDING  /  $UNCORRECTABLE " done if [ $SMART_ERR -gt 0 ]; then add_section_row "SSD SMART Health" "ERROR" "$SMART_ERR drive(s) with uncorrectable sectors — immediate attention required" elif [ $SMART_WARN -gt 0 ]; then add_section_row "SSD SMART Health" "WARNING" "$SMART_WARN warning(s) — review drive details below" else add_section_row "SSD SMART Health" "OK" "All drives PASSED · no sector errors detected" fi # ----------------------------------------------------------------------------- # SECTION 3: FILESYSTEM USAGE # ----------------------------------------------------------------------------- log_section "Filesystem usage" FS_ROWS_HTML="" FS_WARN=0 while IFS= read -r line; do USE_PCT=$(echo "$line" | awk '{print $5}' | tr -d '%') MOUNT_PT=$(echo "$line" | awk '{print $6}') USED=$(echo "$line" | awk '{print $3}') SIZE=$(echo "$line" | awk '{print $2}') if [ "$USE_PCT" -ge 90 ] 2>/dev/null; then ROW_COLOUR="#fee2e2"; PCT_COLOUR="#991b1b" FS_WARN=$((FS_WARN + 1)) [[ "$OVERALL_STATUS" == "SUCCESS" ]] && OVERALL_STATUS="WARNING" elif [ "$USE_PCT" -ge 75 ] 2>/dev/null; then ROW_COLOUR="#fef3c7"; PCT_COLOUR="#92400e" FS_WARN=$((FS_WARN + 1)) [[ "$OVERALL_STATUS" == "SUCCESS" ]] && OVERALL_STATUS="WARNING" else ROW_COLOUR="transparent"; PCT_COLOUR="#065f46" fi log "Filesystem $MOUNT_PT: ${USED} used of ${SIZE} (${USE_PCT}%)" FS_ROWS_HTML+=" $MOUNT_PT $USED used of $SIZE ${USE_PCT}% " done < <(df -h | grep -E '^/dev/') if [ $FS_WARN -gt 0 ]; then add_section_row "Filesystem Usage" "WARNING" "$FS_WARN volume(s) above 75% — review recommended" else add_section_row "Filesystem Usage" "OK" "All volumes within normal range" fi # ----------------------------------------------------------------------------- # ROTATE OLD LOGS # ----------------------------------------------------------------------------- find "$LOG_DIR" -name "fstrim-*.log" -mtime +${KEEP_LOGS} -delete # ----------------------------------------------------------------------------- # SYSTEM CONTEXT # ----------------------------------------------------------------------------- OMV_VER=$(dpkg -l openmediavault 2>/dev/null | awk '/^ii/{print $3}' || echo "Unknown") KERNEL_VER=$(uname -r) HOST_IP=$(hostname -I | awk '{print $1}') UPTIME_STR=$(uptime -p) LOAD_STR=$(uptime | awk -F'load average:' '{print $2}' | xargs) MEM_USED=$(free -h | awk '/^Mem:/{print $3}') MEM_TOTAL=$(free -h | awk '/^Mem:/{print $2}') NEXT_RUN=$(systemctl show fstrim.timer --property=NextElapseUSecRealtime 2>/dev/null \ | awk -F= '{print $2}' || echo "See: systemctl status fstrim.timer") log_section "Report complete — Overall: $OVERALL_STATUS" # ----------------------------------------------------------------------------- # BUILD HTML EMAIL (style: KingDezigns monitor) # ----------------------------------------------------------------------------- # Overall banner & badge case "$OVERALL_STATUS" in SUCCESS) BADGE_HTML="
✅ SUCCESS
" BANNER_HTML="✅  TRIM completed successfully — no action required." ;; WARNING) BADGE_HTML="
⚠️ WARNING
" BANNER_HTML="⚠️  TRIM completed with warnings — review recommended." ;; FAILED) BADGE_HTML="
🚨 FAILED
" BANNER_HTML="🚨  TRIM run failed — immediate attention required!" ;; esac HTML_BODY=$(cat <
$BANNER_HTML
SSD Maintenance Monitor
💽 $HOSTNAME_LABEL
$REPORT_TIME  |  Weekly TRIM · $WARNING_COUNT warning(s) · $ERROR_COUNT error(s)
$BADGE_HTML
📋 Run Summary
$SECTION_ROWS_HTML
Section Status Detail
✂️ TRIM Results — $TOTAL_HUMAN Total Freed
$TRIM_ROWS_HTML
Mount point Space freed
🔬 SSD SMART Health
$SMART_ROWS_HTML
💾 Filesystem Usage
$FS_ROWS_HTML
Mount point Usage Used %
🖥️ System Info
OMV ${OMV_VER} Kernel ${KERNEL_VER}
IP Address ${HOST_IP} Uptime ${UPTIME_STR}
Load average ${LOAD_STR} Memory ${MEM_USED} / ${MEM_TOTAL}
Log file ${LOG_FILE}
Next TRIM run ${NEXT_RUN}
SSD Maintenance Monitor  |  $HOSTNAME_LABEL  |  KingDezigns Infrastructure
EOF ) # ----------------------------------------------------------------------------- # SEND EMAIL # ----------------------------------------------------------------------------- SUBJECT="[TRIM] $HOSTNAME_LABEL — $OVERALL_STATUS | $REPORT_TIME" { echo "To: $REPORT_TO" echo "From: $REPORT_FROM" echo "Subject: $SUBJECT" echo "MIME-Version: 1.0" echo "Content-Type: text/html; charset=UTF-8" echo "" echo "$HTML_BODY" } | sendmail -t log "Email sent to $REPORT_TO — Overall status: $OVERALL_STATUS" log "===== fstrim-report.sh finished ====="