html email
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# server-health-check.sh — Argento health monitoring
|
# server-health-check.sh — Argento health monitoring
|
||||||
# Checks: SMART disk health, ZFS pool status, disk space
|
# Checks: SMART disk health, ZFS pool status, disk space
|
||||||
# Outputs to stdout by default, or sends email with --email.
|
# Outputs plain text to stdout by default, or sends an HTML email with --email.
|
||||||
# Use --error-only to suppress output when everything is healthy.
|
# Use --error-only to suppress output when everything is healthy.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
@@ -37,6 +37,28 @@ done
|
|||||||
PROBLEMS=""
|
PROBLEMS=""
|
||||||
STATUS=""
|
STATUS=""
|
||||||
|
|
||||||
|
# HTML table rows for the email body, built alongside the plain-text STATUS.
|
||||||
|
# Styles are inlined on every element because most mail clients strip
|
||||||
|
# <style> blocks. nowrap keeps device paths and sizes from wrapping.
|
||||||
|
TH='style="padding:2px 10px;text-align:left;border-bottom:1px solid #ccc;white-space:nowrap"'
|
||||||
|
TD='style="padding:2px 10px;text-align:left;white-space:nowrap"'
|
||||||
|
TDR='style="padding:2px 10px;text-align:right;white-space:nowrap"'
|
||||||
|
TABLE='style="border-collapse:collapse;margin:4px 0 16px"'
|
||||||
|
HTML_SATA=""
|
||||||
|
HTML_NVME=""
|
||||||
|
HTML_ZFS=""
|
||||||
|
HTML_SPACE=""
|
||||||
|
|
||||||
|
# Green for healthy, red for anything else. UNKNOWN is amber because it
|
||||||
|
# means smartctl couldn't read the device, not that the device failed.
|
||||||
|
state_html() {
|
||||||
|
case "$1" in
|
||||||
|
PASSED|ONLINE) printf '<span style="color:#1a7f37;font-weight:bold">%s</span>' "$1" ;;
|
||||||
|
UNKNOWN) printf '<span style="color:#9a6700;font-weight:bold">%s</span>' "$1" ;;
|
||||||
|
*) printf '<span style="color:#cf222e;font-weight:bold">%s</span>' "$1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# ─── SMART Disk Health (SATA/SAS) ───────────────────────────────
|
# ─── SMART Disk Health (SATA/SAS) ───────────────────────────────
|
||||||
STATUS+="SMART (SATA):\n"
|
STATUS+="SMART (SATA):\n"
|
||||||
for disk in /dev/sd?; do
|
for disk in /dev/sd?; do
|
||||||
@@ -50,11 +72,15 @@ for disk in /dev/sd?; do
|
|||||||
temp=$(smartctl -A "$disk" 2>/dev/null | awk '$1 == 194 || $1 == 190 {print $10; exit}') || true
|
temp=$(smartctl -A "$disk" 2>/dev/null | awk '$1 == 194 || $1 == 190 {print $10; exit}') || true
|
||||||
temp="${temp:-?}"
|
temp="${temp:-?}"
|
||||||
hours=$(smartctl -A "$disk" 2>/dev/null | awk '$1 == 9 {print $10; exit}') || true
|
hours=$(smartctl -A "$disk" 2>/dev/null | awk '$1 == 9 {print $10; exit}') || true
|
||||||
|
# Some Seagates report attribute 9 raw as "11170h+42m+51.807s" —
|
||||||
|
# keep only the whole hours.
|
||||||
|
hours=${hours%%[^0-9]*}
|
||||||
hours="${hours:-?}"
|
hours="${hours:-?}"
|
||||||
model=$(smartctl -i "$disk" 2>/dev/null | awk -F': +' '/Device Model|Model Number/{print $2; exit}') || true
|
model=$(smartctl -i "$disk" 2>/dev/null | awk -F': +' '/Device Model|Model Number/{print $2; exit}') || true
|
||||||
model="${model:-unknown}"
|
model="${model:-unknown}"
|
||||||
|
|
||||||
STATUS+=" $disk ($model): $health, ${temp}C, ${hours}h\n"
|
STATUS+=" $disk ($model): $health, ${temp}C, ${hours}h\n"
|
||||||
|
HTML_SATA+="<tr><td $TD>$disk</td><td $TD>$model</td><td $TD>$(state_html "$health")</td><td $TDR>${temp}C</td><td $TDR>${hours}h</td></tr>"$'\n'
|
||||||
|
|
||||||
if [ "$health" != "PASSED" ] && [ "$health" != "UNKNOWN" ]; then
|
if [ "$health" != "PASSED" ] && [ "$health" != "UNKNOWN" ]; then
|
||||||
PROBLEMS+="[SMART] $disk: overall health check FAILED ($health)\n"
|
PROBLEMS+="[SMART] $disk: overall health check FAILED ($health)\n"
|
||||||
@@ -93,14 +119,18 @@ for disk in /dev/nvme[0-9]*; do
|
|||||||
health="${health:-UNKNOWN}"
|
health="${health:-UNKNOWN}"
|
||||||
model=$(smartctl -i "$ns" 2>/dev/null | awk -F': +' '/Model Number/{print $2; exit}') || true
|
model=$(smartctl -i "$ns" 2>/dev/null | awk -F': +' '/Model Number/{print $2; exit}') || true
|
||||||
model="${model:-unknown}"
|
model="${model:-unknown}"
|
||||||
|
# smartctl prints "38 Celsius" — keep just the number to match the SATA column
|
||||||
temp=$(smartctl -A "$ns" 2>/dev/null | awk -F': +' '/Temperature:/{print $2; exit}') || true
|
temp=$(smartctl -A "$ns" 2>/dev/null | awk -F': +' '/Temperature:/{print $2; exit}') || true
|
||||||
|
temp=${temp%% *}
|
||||||
temp="${temp:-?}"
|
temp="${temp:-?}"
|
||||||
hours=$(smartctl -A "$ns" 2>/dev/null | awk -F': +' '/Power On Hours/{print $2; exit}') || true
|
# strip thousands separator ("9,852")
|
||||||
|
hours=$(smartctl -A "$ns" 2>/dev/null | awk -F': +' '/Power On Hours/{print $2; exit}' | tr -d ',') || true
|
||||||
hours="${hours:-?}"
|
hours="${hours:-?}"
|
||||||
# NVMe percentage used — 100% means full rated write endurance consumed
|
# NVMe percentage used — 100% means full rated write endurance consumed
|
||||||
pct_used=$(smartctl -A "$ns" 2>/dev/null | awk -F': +' '/Percentage Used/{print $2; exit}' | tr -d '%') || true
|
pct_used=$(smartctl -A "$ns" 2>/dev/null | awk -F': +' '/Percentage Used/{print $2; exit}' | tr -d '%') || true
|
||||||
|
|
||||||
STATUS+=" $ns ($model): $health, ${temp}, ${hours}h\n"
|
STATUS+=" $ns ($model): $health, ${temp}C, ${hours}h\n"
|
||||||
|
HTML_NVME+="<tr><td $TD>$ns</td><td $TD>$model</td><td $TD>$(state_html "$health")</td><td $TDR>${temp}C</td><td $TDR>${hours}h</td></tr>"$'\n'
|
||||||
|
|
||||||
if [ "$health" != "PASSED" ] && [ "$health" != "UNKNOWN" ]; then
|
if [ "$health" != "PASSED" ] && [ "$health" != "UNKNOWN" ]; then
|
||||||
PROBLEMS+="[SMART] $ns: overall health check FAILED ($health)\n"
|
PROBLEMS+="[SMART] $ns: overall health check FAILED ($health)\n"
|
||||||
@@ -116,9 +146,14 @@ STATUS+="\nZFS Pools:\n"
|
|||||||
while IFS= read -r pool; do
|
while IFS= read -r pool; do
|
||||||
[ -n "$pool" ] || continue
|
[ -n "$pool" ] || continue
|
||||||
state=$(zpool list -H -o health "$pool" 2>/dev/null) || true
|
state=$(zpool list -H -o health "$pool" 2>/dev/null) || true
|
||||||
|
state="${state:-UNKNOWN}"
|
||||||
pool_info=$(zpool list -H -o name,size,alloc,free,cap "$pool" 2>/dev/null) || true
|
pool_info=$(zpool list -H -o name,size,alloc,free,cap "$pool" 2>/dev/null) || true
|
||||||
STATUS+=" $pool_info $state\n"
|
STATUS+=" $pool_info $state\n"
|
||||||
|
|
||||||
|
# zpool -H output is tab-separated
|
||||||
|
IFS=$'\t' read -r p_name p_size p_alloc p_free p_cap <<< "$pool_info" || true
|
||||||
|
HTML_ZFS+="<tr><td $TD>${p_name:-$pool}</td><td $TDR>$p_size</td><td $TDR>$p_alloc</td><td $TDR>$p_free</td><td $TDR>$p_cap</td><td $TD>$(state_html "$state")</td></tr>"$'\n'
|
||||||
|
|
||||||
if [ "$state" != "ONLINE" ]; then
|
if [ "$state" != "ONLINE" ]; then
|
||||||
PROBLEMS+="[ZFS] Pool '$pool' state: $state\n"
|
PROBLEMS+="[ZFS] Pool '$pool' state: $state\n"
|
||||||
fi
|
fi
|
||||||
@@ -155,9 +190,12 @@ while IFS= read -r line; do
|
|||||||
used=$(echo "$line" | awk '{print $3}')
|
used=$(echo "$line" | awk '{print $3}')
|
||||||
avail=$(echo "$line" | awk '{print $4}')
|
avail=$(echo "$line" | awk '{print $4}')
|
||||||
STATUS+=" $mount: ${usage}% (${used}/${size}, ${avail} free)\n"
|
STATUS+=" $mount: ${usage}% (${used}/${size}, ${avail} free)\n"
|
||||||
|
usage_cell="${usage}%"
|
||||||
if [ "$usage" -gt "$SPACE_THRESHOLD" ] 2>/dev/null; then
|
if [ "$usage" -gt "$SPACE_THRESHOLD" ] 2>/dev/null; then
|
||||||
PROBLEMS+="[SPACE] $mount is ${usage}% full\n"
|
PROBLEMS+="[SPACE] $mount is ${usage}% full\n"
|
||||||
|
usage_cell="<span style=\"color:#cf222e;font-weight:bold\">${usage}%</span>"
|
||||||
fi
|
fi
|
||||||
|
HTML_SPACE+="<tr><td $TD>$mount</td><td $TDR>$usage_cell</td><td $TDR>$used</td><td $TDR>$size</td><td $TDR>$avail</td></tr>"$'\n'
|
||||||
done < <(df -h --output=source,size,used,avail,pcent,target -x tmpfs -x devtmpfs -x overlay -x efivarfs 2>/dev/null | tail -n +2 || true)
|
done < <(df -h --output=source,size,used,avail,pcent,target -x tmpfs -x devtmpfs -x overlay -x efivarfs 2>/dev/null | tail -n +2 || true)
|
||||||
|
|
||||||
# ─── Output ──────────────────────────────────────────────────────
|
# ─── Output ──────────────────────────────────────────────────────
|
||||||
@@ -181,12 +219,47 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$USE_EMAIL" = true ]; then
|
if [ "$USE_EMAIL" = true ]; then
|
||||||
|
HTML_PROBLEMS=""
|
||||||
|
while IFS= read -r p; do
|
||||||
|
[ -n "$p" ] || continue
|
||||||
|
HTML_PROBLEMS+="<li style=\"color:#cf222e;font-weight:bold;margin:2px 0\">$p</li>"$'\n'
|
||||||
|
done < <(printf '%b' "$PROBLEMS")
|
||||||
|
|
||||||
|
# Built with a newline after each row/section — SMTP caps lines at
|
||||||
|
# ~1000 chars and one giant line can get mangled in transit.
|
||||||
|
HTML_BODY="<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#222\">"$'\n'
|
||||||
|
if [ "$ERROR_ONLY" = true ]; then
|
||||||
|
HTML_BODY+="<p>Health check found issues on <b>$HOSTNAME</b> at $(date):</p>"$'\n'
|
||||||
|
HTML_BODY+="<ul style=\"padding-left:20px\">"$'\n'"$HTML_PROBLEMS</ul>"$'\n'
|
||||||
|
else
|
||||||
|
HTML_BODY+="<p>Health report for <b>$HOSTNAME</b> at $(date):</p>"$'\n'
|
||||||
|
if [ -n "$HTML_PROBLEMS" ]; then
|
||||||
|
HTML_BODY+="<h3 style=\"color:#cf222e\">Issues</h3>"$'\n'
|
||||||
|
HTML_BODY+="<ul style=\"padding-left:20px\">"$'\n'"$HTML_PROBLEMS</ul>"$'\n'
|
||||||
|
fi
|
||||||
|
HTML_BODY+="<h3>SMART (SATA)</h3><table $TABLE>"$'\n'
|
||||||
|
HTML_BODY+="<tr><th $TH>Disk</th><th $TH>Model</th><th $TH>Health</th><th $TH>Temp</th><th $TH>Hours</th></tr>"$'\n'
|
||||||
|
HTML_BODY+="$HTML_SATA</table>"$'\n'
|
||||||
|
HTML_BODY+="<h3>SMART (NVMe)</h3><table $TABLE>"$'\n'
|
||||||
|
HTML_BODY+="<tr><th $TH>Disk</th><th $TH>Model</th><th $TH>Health</th><th $TH>Temp</th><th $TH>Hours</th></tr>"$'\n'
|
||||||
|
HTML_BODY+="$HTML_NVME</table>"$'\n'
|
||||||
|
HTML_BODY+="<h3>ZFS Pools</h3><table $TABLE>"$'\n'
|
||||||
|
HTML_BODY+="<tr><th $TH>Pool</th><th $TH>Size</th><th $TH>Alloc</th><th $TH>Free</th><th $TH>Cap</th><th $TH>State</th></tr>"$'\n'
|
||||||
|
HTML_BODY+="$HTML_ZFS</table>"$'\n'
|
||||||
|
HTML_BODY+="<h3>Disk Space</h3><table $TABLE>"$'\n'
|
||||||
|
HTML_BODY+="<tr><th $TH>Mount</th><th $TH>Used %</th><th $TH>Used</th><th $TH>Size</th><th $TH>Free</th></tr>"$'\n'
|
||||||
|
HTML_BODY+="$HTML_SPACE</table>"$'\n'
|
||||||
|
fi
|
||||||
|
HTML_BODY+="</div>"
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "Subject: $SUBJECT"
|
echo "Subject: $SUBJECT"
|
||||||
echo "From: $HOSTNAME <argento@fastmail.com>"
|
echo "From: $HOSTNAME <argento@fastmail.com>"
|
||||||
echo "To: $MAILTO"
|
echo "To: $MAILTO"
|
||||||
|
echo "MIME-Version: 1.0"
|
||||||
|
echo "Content-Type: text/html; charset=utf-8"
|
||||||
echo ""
|
echo ""
|
||||||
printf '%b' "$BODY"
|
printf '%s\n' "$HTML_BODY"
|
||||||
} | sendmail "$MAILTO"
|
} | sendmail "$MAILTO"
|
||||||
else
|
else
|
||||||
printf '%b' "$BODY"
|
printf '%b' "$BODY"
|
||||||
|
|||||||
Reference in New Issue
Block a user