249 lines
9.3 KiB
Bash
249 lines
9.3 KiB
Bash
#!/bin/bash
|
|
# -----------------------------------------------------------------------------------------------
|
|
# --------------------------------- SMART Health Monitor ---------------------------------------
|
|
# -----------------------------------------------------------------------------------------------
|
|
# Checks SMART health attributes for all drives on the system.
|
|
# Reads data live from each drive via smartctl — no persistent writes.
|
|
#
|
|
# Monitored attributes:
|
|
# Reallocated_Sector_Ct — bad sectors remapped — any > 0 is concerning
|
|
# Current_Pending_Sector — sectors waiting for reallocation — any > 0 is concerning
|
|
# Offline_Uncorrectable — sectors that could not be corrected — any > 0 is critical
|
|
# Temperature_Celsius — drive temperature vs SMART_TEMP_WARN / SMART_TEMP_CRIT
|
|
# Power_On_Hours — informational — drive age estimation
|
|
# SMART overall status — pass/fail per drive
|
|
#
|
|
# Discovers drives automatically — no configuration needed for drive list.
|
|
# SMART_IGNORE_DRIVES allows skipping specific drives (e.g. USB flash drives).
|
|
#
|
|
# All configuration in Master.conf under SMART Health section.
|
|
# Supports --dry-run to show which drives would be checked without running smartctl.
|
|
# -----------------------------------------------------------------------------------------------
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
source "$SCRIPT_DIR/../Master.conf"
|
|
source "$SCRIPT_DIR/../common.sh"
|
|
|
|
parse_args "$@"
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# ━━━ $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"
|
|
|
|
if ! command -v smartctl >/dev/null 2>&1; then
|
|
error "smartctl not found — install smartmontools"
|
|
notify "SMART health check failed on $(hostname) — smartmontools not installed" "SMART Health" "warning"
|
|
exit 1
|
|
fi
|
|
|
|
success "smartctl available"
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# ━━━ $ICON_SUMMARY Status ━━━
|
|
# -----------------------------------------------------------------------------------------------
|
|
if [[ "$SHOW_STATUS" == true ]]; then
|
|
echo ""
|
|
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
|
|
echo "$ICON_SMART Temp warn: ${SMART_TEMP_WARN}°C"
|
|
echo "$ICON_SMART Temp crit: ${SMART_TEMP_CRIT}°C"
|
|
echo "$ICON_SMART Ignore drives: ${SMART_IGNORE_DRIVES[*]:-none}"
|
|
echo "$ICON_GEAR Dry Run: $DRY_RUN"
|
|
echo ""
|
|
|
|
# Show drives that would be checked
|
|
echo "━━━ Discovered Drives ━━━"
|
|
for drive in /dev/sd? /dev/nvme?; do
|
|
[[ ! -e "$drive" ]] && continue
|
|
drive_name=$(basename "$drive")
|
|
ignored=false
|
|
for ignore in "${SMART_IGNORE_DRIVES[@]}"; do
|
|
[[ "$drive_name" == "$ignore" ]] && ignored=true && break
|
|
done
|
|
if [[ "$ignored" == true ]]; then
|
|
echo " $ICON_WARN $drive — ignored"
|
|
else
|
|
echo " $ICON_SMART $drive — would check"
|
|
fi
|
|
done
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
|
exit 0
|
|
fi
|
|
|
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — showing drive list only, no SMART data read"
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# HELPER — extract SMART attribute value
|
|
# Usage: get_smart_attr "/dev/sda" "Reallocated_Sector_Ct"
|
|
# -----------------------------------------------------------------------------------------------
|
|
get_smart_attr() {
|
|
local drive="$1" attr="$2"
|
|
smartctl -A "$drive" 2>/dev/null | \
|
|
awk -v attr="$attr" '$2 == attr {print $10}'
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# ━━━ $ICON_SMART SMART Health Check ━━━
|
|
# -----------------------------------------------------------------------------------------------
|
|
echo ""
|
|
echo "━━━ $ICON_SMART SMART Health Check — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
|
|
echo ""
|
|
|
|
START=$(date +%s)
|
|
DRIVES_OK=()
|
|
DRIVES_WARN=()
|
|
DRIVES_CRIT=()
|
|
DRIVES_SKIP=()
|
|
|
|
for drive in /dev/sd? /dev/nvme?; do
|
|
[[ ! -e "$drive" ]] && continue
|
|
drive_name=$(basename "$drive")
|
|
|
|
# Check ignore list
|
|
ignored=false
|
|
for ignore in "${SMART_IGNORE_DRIVES[@]}"; do
|
|
[[ "$drive_name" == "$ignore" ]] && ignored=true && break
|
|
done
|
|
|
|
if [[ "$ignored" == true ]]; then
|
|
info "$drive_name — ignored (in SMART_IGNORE_DRIVES)"
|
|
DRIVES_SKIP+=("$drive_name")
|
|
continue
|
|
fi
|
|
|
|
echo "━━━ $ICON_SMART $drive_name ━━━"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
warn "DRY RUN — would check $drive_name"
|
|
echo ""
|
|
continue
|
|
fi
|
|
|
|
# Check if drive supports SMART
|
|
if ! smartctl -i "$drive" 2>/dev/null | grep -q "SMART support is: Enabled"; then
|
|
warn "$drive_name — SMART not enabled or not supported"
|
|
DRIVES_SKIP+=("$drive_name")
|
|
echo ""
|
|
continue
|
|
fi
|
|
|
|
# Overall SMART status
|
|
SMART_STATUS=$(smartctl -H "$drive" 2>/dev/null | grep "overall-health" | awk '{print $NF}')
|
|
if [[ "$SMART_STATUS" == "PASSED" ]]; then
|
|
success "Overall status: PASSED"
|
|
else
|
|
error "Overall status: $SMART_STATUS"
|
|
fi
|
|
|
|
# Key attributes
|
|
DRIVE_WARN=false
|
|
DRIVE_CRIT=false
|
|
|
|
# Reallocated sectors
|
|
REALLOC=$(get_smart_attr "$drive" "Reallocated_Sector_Ct")
|
|
if [[ -n "$REALLOC" ]]; then
|
|
if [[ "$REALLOC" -gt 0 ]]; then
|
|
warn "$ICON_SMART Reallocated sectors: $REALLOC — drive showing wear"
|
|
DRIVE_WARN=true
|
|
else
|
|
success "$ICON_SMART Reallocated sectors: $REALLOC"
|
|
fi
|
|
fi
|
|
|
|
# Pending sectors
|
|
PENDING=$(get_smart_attr "$drive" "Current_Pending_Sector")
|
|
if [[ -n "$PENDING" ]]; then
|
|
if [[ "$PENDING" -gt 0 ]]; then
|
|
warn "$ICON_SMART Pending sectors: $PENDING — sectors awaiting reallocation"
|
|
DRIVE_WARN=true
|
|
else
|
|
success "$ICON_SMART Pending sectors: $PENDING"
|
|
fi
|
|
fi
|
|
|
|
# Uncorrectable sectors
|
|
UNCORR=$(get_smart_attr "$drive" "Offline_Uncorrectable")
|
|
if [[ -n "$UNCORR" ]]; then
|
|
if [[ "$UNCORR" -gt 0 ]]; then
|
|
error "$ICON_SMART Uncorrectable sectors: $UNCORR — CRITICAL"
|
|
DRIVE_CRIT=true
|
|
else
|
|
success "$ICON_SMART Uncorrectable sectors: $UNCORR"
|
|
fi
|
|
fi
|
|
|
|
# Temperature
|
|
TEMP=$(get_smart_attr "$drive" "Temperature_Celsius")
|
|
# NVMe uses different attribute name
|
|
[[ -z "$TEMP" ]] && TEMP=$(smartctl -A "$drive" 2>/dev/null | \
|
|
awk '/Temperature/{print $2}' | head -1)
|
|
|
|
if [[ -n "$TEMP" ]]; then
|
|
if [[ "$TEMP" -ge "$SMART_TEMP_CRIT" ]]; then
|
|
error "$ICON_SMART Temperature: ${TEMP}°C — CRITICAL (threshold: ${SMART_TEMP_CRIT}°C)"
|
|
DRIVE_CRIT=true
|
|
elif [[ "$TEMP" -ge "$SMART_TEMP_WARN" ]]; then
|
|
warn "$ICON_SMART Temperature: ${TEMP}°C — warning (threshold: ${SMART_TEMP_WARN}°C)"
|
|
DRIVE_WARN=true
|
|
else
|
|
success "$ICON_SMART Temperature: ${TEMP}°C"
|
|
fi
|
|
fi
|
|
|
|
# Power on hours — informational
|
|
POH=$(get_smart_attr "$drive" "Power_On_Hours")
|
|
if [[ -n "$POH" ]]; then
|
|
POH_DAYS=$(( POH / 24 ))
|
|
info "$ICON_SMART Power on hours: $POH (${POH_DAYS} days)"
|
|
fi
|
|
|
|
# Classify drive
|
|
if [[ "$DRIVE_CRIT" == true ]]; then
|
|
DRIVES_CRIT+=("$drive_name")
|
|
elif [[ "$DRIVE_WARN" == true ]]; then
|
|
DRIVES_WARN+=("$drive_name")
|
|
else
|
|
DRIVES_OK+=("$drive_name")
|
|
fi
|
|
|
|
echo ""
|
|
done
|
|
|
|
END=$(date +%s)
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# ━━━ $ICON_SUMMARY Summary ━━━
|
|
# -----------------------------------------------------------------------------------------------
|
|
echo "━━━━━ $ICON_SUMMARY SMART HEALTH SUMMARY ━━━━━"
|
|
echo "$ICON_TIME Duration: $(format_duration $((END - START)))"
|
|
echo ""
|
|
echo " $ICON_SUCCESS Healthy: ${#DRIVES_OK[@]} $ICON_WARN Warning: ${#DRIVES_WARN[@]} $ICON_ERROR Critical: ${#DRIVES_CRIT[@]} skipped: ${#DRIVES_SKIP[@]}"
|
|
echo ""
|
|
|
|
[[ ${#DRIVES_OK[@]} -gt 0 ]] && echo " $ICON_SUCCESS ${DRIVES_OK[*]}"
|
|
[[ ${#DRIVES_WARN[@]} -gt 0 ]] && echo " $ICON_WARN ${DRIVES_WARN[*]}"
|
|
[[ ${#DRIVES_CRIT[@]} -gt 0 ]] && echo " $ICON_ERROR ${DRIVES_CRIT[*]}"
|
|
|
|
echo ""
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
echo "$ICON_WARN Status: DRY RUN"
|
|
elif [[ ${#DRIVES_CRIT[@]} -gt 0 ]]; then
|
|
echo "$ICON_ERROR Status: CRITICAL — ${DRIVES_CRIT[*]}"
|
|
notify "SMART CRITICAL on $(hostname) — drives need immediate attention: ${DRIVES_CRIT[*]}" "SMART Health" "warning"
|
|
elif [[ ${#DRIVES_WARN[@]} -gt 0 ]]; then
|
|
echo "$ICON_WARN Status: WARNING — ${DRIVES_WARN[*]}"
|
|
notify "SMART WARNING on $(hostname) — drives showing wear: ${DRIVES_WARN[*]}" "SMART Health" "warning"
|
|
else
|
|
echo "$ICON_DONE Status: $ICON_SUCCESS ALL DRIVES HEALTHY"
|
|
notify "SMART health check passed on $(hostname) — ${#DRIVES_OK[@]} drives healthy" "SMART Health" "normal"
|
|
fi
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" |