#!/bin/bash # ============================================================= # PLEX32 — Daily Server Health Report # KingDezigns Infrastructure # Schedule: Daily at 7:05 AM (run after update check) # Location: /usr/scripts/plex32/plex32_health_report.sh # ============================================================= SMTP_SERVER="smtppro.zoho.com" SMTP_PORT="465" SMTP_USER="rufus.king@kingdezigns.com" SMTP_PASS="Valerie8545" EMAIL_TO="rufus.king@kingdezigns.com" EMAIL_FROM="rufus.king@kingdezigns.com" TIMESTAMP=$(date "+%A, %B %d, %Y at %I:%M %p") DATE_SHORT=$(date "+%Y-%m-%d") LOG_DIR="/var/log/plex32-health" LOG_FILE="$LOG_DIR/health-$DATE_SHORT.log" mkdir -p "$LOG_DIR" exec > >(tee -a "$LOG_FILE") 2>&1 echo "[$TIMESTAMP] Starting health check..." # ============================================================= # DISK USAGE — eMMC + NFS MOUNTS # ============================================================= DISK_WARN=85 DISK_CRIT=95 DISK_ISSUES="" DISK_STATUS="OK" check_disk() { local label="$1" local mount="$2" local pct=$(df -h "$mount" 2>/dev/null | awk 'NR==2{gsub(/%/,"",$5); print $5}') local size=$(df -h "$mount" 2>/dev/null | awk 'NR==2{print $2}') local used=$(df -h "$mount" 2>/dev/null | awk 'NR==2{print $3}') local avail=$(df -h "$mount" 2>/dev/null | awk 'NR==2{print $4}') if [ -z "$pct" ]; then echo "UNAVAILABLE|$label|--|--|--|--" return fi local status="OK" if [ "$pct" -ge "$DISK_CRIT" ]; then status="CRITICAL" elif [ "$pct" -ge "$DISK_WARN" ]; then status="WARNING" fi echo "$status|$label|$size|$used|$avail|$pct%" } ROOT_DISK=$(check_disk "eMMC — OS (/)." "/") BOOT_DISK=$(check_disk "eMMC — /boot" "/boot") NFS_CONFIG=$(check_disk "NAS08 — Plex Config" "/mnt/plex/config") NFS_MEDIA=$(check_disk "NAS08 — Media" "/mnt/plex/media") for disk in "$ROOT_DISK" "$BOOT_DISK" "$NFS_CONFIG" "$NFS_MEDIA"; do status=$(echo "$disk" | cut -d'|' -f1) label=$(echo "$disk" | cut -d'|' -f2) if [ "$status" = "CRITICAL" ]; then DISK_STATUS="CRITICAL" DISK_ISSUES+="CRITICAL: $label is nearly full."$'\n' elif [ "$status" = "WARNING" ] && [ "$DISK_STATUS" != "CRITICAL" ]; then DISK_STATUS="WARNING" DISK_ISSUES+="WARNING: $label is getting full."$'\n' elif [ "$status" = "UNAVAILABLE" ]; then [ "$DISK_STATUS" = "OK" ] && DISK_STATUS="WARNING" DISK_ISSUES+="WARNING: $label is unavailable."$'\n' fi done echo "[INFO] Disk status: $DISK_STATUS" # ============================================================= # NFS MOUNT HEALTH # ============================================================= NFS_STATUS="OK" NFS_ISSUES="" check_nfs() { local label="$1" local mount="$2" if mountpoint -q "$mount" 2>/dev/null && timeout 5 ls "$mount" > /dev/null 2>&1; then echo "OK|$label" else echo "DOWN|$label" fi } NFS_CONFIG_STATUS=$(check_nfs "Plex Config" "/mnt/plex/config") NFS_TRANSCODE_STATUS=$(check_nfs "Plex Transcode" "/mnt/plex/transcode") NFS_MEDIA_STATUS=$(check_nfs "Media Library" "/mnt/plex/media") for nfs in "$NFS_CONFIG_STATUS" "$NFS_TRANSCODE_STATUS" "$NFS_MEDIA_STATUS"; do status=$(echo "$nfs" | cut -d'|' -f1) label=$(echo "$nfs" | cut -d'|' -f2) if [ "$status" = "DOWN" ]; then NFS_STATUS="CRITICAL" NFS_ISSUES+="CRITICAL: NFS mount $label is down."$'\n' fi done echo "[INFO] NFS status: $NFS_STATUS" # ============================================================= # MEMORY # ============================================================= MEM_TOTAL=$(free -m | awk 'NR==2{print $2}') MEM_USED=$(free -m | awk 'NR==2{print $3}') MEM_AVAIL=$(free -m | awk 'NR==2{print $7}') MEM_PCT=$(awk "BEGIN{printf \"%d\", ($MEM_USED/$MEM_TOTAL)*100}") MEM_TOTAL_H=$(free -h | awk 'NR==2{print $2}') MEM_USED_H=$(free -h | awk 'NR==2{print $3}') MEM_AVAIL_H=$(free -h | awk 'NR==2{print $7}') MEM_STATUS="OK" MEM_ISSUES="" if [ "$MEM_PCT" -ge 95 ]; then MEM_STATUS="CRITICAL" MEM_ISSUES="CRITICAL: Memory usage at ${MEM_PCT}% — system may be under pressure." elif [ "$MEM_PCT" -ge 85 ]; then MEM_STATUS="WARNING" MEM_ISSUES="WARNING: Memory usage at ${MEM_PCT}%." fi echo "[INFO] Memory: ${MEM_PCT}% used" # ============================================================= # CPU LOAD # ============================================================= LOAD_1=$(cat /proc/loadavg | awk '{print $1}') LOAD_5=$(cat /proc/loadavg | awk '{print $2}') LOAD_15=$(cat /proc/loadavg | awk '{print $3}') CPU_CORES=$(nproc) LOAD_PCT=$(awk "BEGIN{printf \"%d\", ($LOAD_5/$CPU_CORES)*100}") CPU_STATUS="OK" CPU_ISSUES="" if [ "$LOAD_PCT" -ge 90 ]; then CPU_STATUS="CRITICAL" CPU_ISSUES="CRITICAL: CPU load average is very high (${LOAD_5} on ${CPU_CORES} cores)." elif [ "$LOAD_PCT" -ge 70 ]; then CPU_STATUS="WARNING" CPU_ISSUES="WARNING: CPU load average is elevated (${LOAD_5} on ${CPU_CORES} cores)." fi echo "[INFO] CPU load: $LOAD_5 (5min avg), $CPU_CORES cores" # ============================================================= # UPTIME & REBOOT CHECK # ============================================================= UPTIME_SECONDS=$(awk '{print int($1)}' /proc/uptime) UPTIME_DAYS=$((UPTIME_SECONDS / 86400)) UPTIME_HOURS=$(( (UPTIME_SECONDS % 86400) / 3600 )) UPTIME_MINS=$(( (UPTIME_SECONDS % 3600) / 60 )) UPTIME_DISPLAY="${UPTIME_DAYS}d ${UPTIME_HOURS}h ${UPTIME_MINS}m" REBOOT_STATUS="OK" REBOOT_ISSUES="" if [ -f /var/run/reboot-required ]; then REBOOT_STATUS="WARNING" REBOOT_ISSUES="Reboot required to apply kernel or system updates." fi echo "[INFO] Uptime: $UPTIME_DISPLAY | Reboot required: $REBOOT_STATUS" # ============================================================= # PLEX CONTAINER STATUS # ============================================================= PLEX_CONTAINER_STATUS="OK" PLEX_CONTAINER_ISSUES="" PLEX_RUNNING=$(docker ps --filter "name=plex" --filter "status=running" --format "{{.Names}}" 2>/dev/null) PLEX_UPTIME=$(docker ps --filter "name=plex" --format "{{.Status}}" 2>/dev/null) if [ -z "$PLEX_RUNNING" ]; then PLEX_CONTAINER_STATUS="CRITICAL" PLEX_CONTAINER_ISSUES="CRITICAL: Plex container is not running." fi echo "[INFO] Plex container: $PLEX_CONTAINER_STATUS — $PLEX_UPTIME" # ============================================================= # FAILED SYSTEMD UNITS # ============================================================= SYSTEMD_STATUS="OK" SYSTEMD_ISSUES="" FAILED_UNITS=$(systemctl --failed --no-pager --plain 2>/dev/null | grep "●" | awk '{print $2}') if [ -n "$FAILED_UNITS" ]; then SYSTEMD_STATUS="WARNING" SYSTEMD_ISSUES="Failed units: $FAILED_UNITS" fi echo "[INFO] Systemd failed units: $([ -z "$FAILED_UNITS" ] && echo 'none' || echo "$FAILED_UNITS")" # ============================================================= # RECENT JOURNAL ERRORS (last 24h, filter known-harmless) # ============================================================= JOURNAL_ERRORS=$(journalctl -p err -b --no-pager 2>/dev/null \ | grep -v "SGX disabled by BIOS" \ | grep -v "^-- " \ | tail -10) JOURNAL_COUNT=$(echo "$JOURNAL_ERRORS" | grep -c "." || true) [ "$JOURNAL_COUNT" -le 1 ] && JOURNAL_COUNT=0 JOURNAL_STATUS="OK" [ "$JOURNAL_COUNT" -gt 0 ] && JOURNAL_STATUS="WARNING" echo "[INFO] Journal errors: $JOURNAL_COUNT" # ============================================================= # FAIL2BAN STATUS # ============================================================= F2B_RUNNING=$(systemctl is-active fail2ban 2>/dev/null) F2B_BANS=$(fail2ban-client status sshd 2>/dev/null | grep "Total banned" | awk '{print $NF}' || echo "0") F2B_CURRENTLY_BANNED=$(fail2ban-client status sshd 2>/dev/null | grep "Currently banned" | awk '{print $NF}' || echo "0") F2B_HEALTH_STATUS="OK" F2B_HEALTH_ISSUES="" if [ "$F2B_RUNNING" != "active" ]; then F2B_HEALTH_STATUS="CRITICAL" F2B_HEALTH_ISSUES="CRITICAL: Fail2Ban is not running." fi echo "[INFO] Fail2Ban: $F2B_RUNNING | Total bans: $F2B_BANS | Current: $F2B_CURRENTLY_BANNED" # ============================================================= # OVERALL STATUS # ============================================================= OVERALL="OK" for s in "$DISK_STATUS" "$NFS_STATUS" "$MEM_STATUS" "$CPU_STATUS" "$REBOOT_STATUS" "$PLEX_CONTAINER_STATUS" "$SYSTEMD_STATUS" "$F2B_HEALTH_STATUS"; do if [ "$s" = "CRITICAL" ]; then OVERALL="CRITICAL"; break; fi if [ "$s" = "WARNING" ] && [ "$OVERALL" != "CRITICAL" ]; then OVERALL="WARNING"; fi done if [ "$OVERALL" = "CRITICAL" ]; then STATUS_COLOR="#b91c1c" STATUS_BADGE="🚨 ACTION REQUIRED" STATUS_BANNER_MSG="🚨 One or more critical issues detected — immediate attention required" SUBJECT="🚨 PLEX32 — Critical Health Alert — $DATE_SHORT" elif [ "$OVERALL" = "WARNING" ]; then STATUS_COLOR="#b45309" STATUS_BADGE="⚠ ATTENTION NEEDED" STATUS_BANNER_MSG="⚠ One or more items need your attention" SUBJECT="⚠ PLEX32 — Health Warning — $DATE_SHORT" else STATUS_COLOR="#1a7f4b" STATUS_BADGE="✅ HEALTHY" STATUS_BANNER_MSG="✅ All systems are healthy — no action required" SUBJECT="✅ PLEX32 — Daily Health Report — $DATE_SHORT" fi echo "[INFO] Overall status: $OVERALL" # ============================================================= # BUILD DISK TABLE HTML # ============================================================= build_disk_row() { local row="$1" local status=$(echo "$row" | cut -d'|' -f1) local label=$(echo "$row" | cut -d'|' -f2) local size=$(echo "$row" | cut -d'|' -f3) local used=$(echo "$row" | cut -d'|' -f4) local avail=$(echo "$row" | cut -d'|' -f5) local pct=$(echo "$row" | cut -d'|' -f6) local icon="✅" local row_bg="white" local pct_color="#1a7f4b" [ "$status" = "WARNING" ] && icon="⚠" && row_bg="#fffbeb" && pct_color="#b45309" [ "$status" = "CRITICAL" ] && icon="🚨" && row_bg="#fff1f2" && pct_color="#b91c1c" [ "$status" = "UNAVAILABLE" ] && icon="❌" && row_bg="#fff1f2" && pct_color="#b91c1c" echo "
sudo reboot
$JOURNAL_ESCAPED