plex32-scripts/plex32_update_check.sh

384 lines
18 KiB
Bash
Raw Normal View History

2026-07-26 21:35:57 -04:00
#!/bin/bash
# =============================================================
# PLEX32 — Daily Update Check & Notification
# KingDezigns Infrastructure
# Schedule: Daily at 7:00 AM
# Location: /usr/scripts/plex32/plex32_update_check.sh
# =============================================================
HOSTNAME="plex32"
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-updates"
LOG_FILE="$LOG_DIR/update-check-$DATE_SHORT.log"
mkdir -p "$LOG_DIR"
exec > >(tee -a "$LOG_FILE") 2>&1
echo "[$TIMESTAMP] Starting update check..."
# =============================================================
# STEP 1 — CHECK OS/APT UPDATES
# =============================================================
echo "[INFO] Checking apt updates..."
apt_output=$(apt-get update -qq 2>&1)
apt_check_status=$?
if [ $apt_check_status -ne 0 ]; then
APT_FAILED=true
APT_UPDATES=0
APT_DETAILS="apt-get update failed:\n$apt_output"
else
APT_FAILED=false
apt_list=$(apt list --upgradable 2>/dev/null | grep -v "Listing..." | grep -v "^$")
APT_UPDATES=$(echo "$apt_list" | grep -c "/" || true)
APT_DETAILS="$apt_list"
fi
echo "[INFO] APT updates available: $APT_UPDATES"
# =============================================================
# STEP 2 — CHECK PLEX DOCKER IMAGE UPDATE
# =============================================================
echo "[INFO] Checking Plex Docker image..."
PLEX_FAILED=false
PLEX_UPDATE_AVAILABLE=false
PLEX_ERROR_HTML=""
PLEX_PULL_OUTPUT=$(docker pull lscr.io/linuxserver/plex:latest 2>&1)
PLEX_PULL_STATUS=$?
PLEX_IMAGE_VERSION=$(docker inspect lscr.io/linuxserver/plex:latest --format '{{index .Config.Labels "build_version"}}' 2>/dev/null || echo "unknown")
CURRENT_IMAGE_DATE=$(docker inspect --format='{{.Created}}' lscr.io/linuxserver/plex:latest 2>/dev/null | cut -d'T' -f1)
if [ $PLEX_PULL_STATUS -ne 0 ]; then
PLEX_FAILED=true
PLEX_STATUS_TEXT="Check failed — see error detail below"
echo "[ERROR] docker pull exited $PLEX_PULL_STATUS. Full output:"
echo "$PLEX_PULL_OUTPUT"
PLEX_ERROR_HTML="<div style='margin-top:8px;padding:10px 14px;background:#fff1f2;border-left:4px solid #b91c1c;border-radius:4px;'><div style='font-size:11px;font-weight:700;color:#b91c1c;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px;'>⚠ Docker Pull Error (exit $PLEX_PULL_STATUS)</div><pre style='margin:0;font-family:monospace;font-size:12px;color:#7f1d1d;white-space:pre-wrap;'>$(echo "$PLEX_PULL_OUTPUT" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')</pre></div>"
elif echo "$PLEX_PULL_OUTPUT" | grep -q "Pull complete"; then
PLEX_UPDATE_AVAILABLE=true
PLEX_STATUS_TEXT="Update available — new image pulled"
else
PLEX_STATUS_TEXT="Up to date"
fi
echo "[INFO] Plex status: $PLEX_STATUS_TEXT"
# =============================================================
# STEP 3 — CHECK FAIL2BAN VERSION
# =============================================================
echo "[INFO] Checking Fail2Ban..."
F2B_UPDATE_AVAILABLE=false
F2B_CANDIDATE=$(apt-cache policy fail2ban 2>/dev/null | grep Candidate | awk '{print $2}' | cut -d'-' -f1)
F2B_INSTALLED=$(apt-cache policy fail2ban 2>/dev/null | grep Installed | awk '{print $2}' | cut -d'-' -f1)
if [ "$F2B_CANDIDATE" != "$F2B_INSTALLED" ] && [ -n "$F2B_CANDIDATE" ]; then
F2B_UPDATE_AVAILABLE=true
F2B_STATUS_TEXT="Update available: $F2B_INSTALLED$F2B_CANDIDATE"
else
F2B_STATUS_TEXT="Up to date ($F2B_INSTALLED)"
fi
echo "[INFO] Fail2Ban status: $F2B_STATUS_TEXT"
# =============================================================
# STEP 4 — CHECK DOCKER ENGINE UPDATE
# =============================================================
echo "[INFO] Checking Docker..."
DOCKER_UPDATE_AVAILABLE=false
DOCKER_CANDIDATE=$(apt-cache policy docker-ce 2>/dev/null | grep Candidate | awk '{print $2}')
DOCKER_INSTALLED=$(apt-cache policy docker-ce 2>/dev/null | grep Installed | awk '{print $2}')
if [ "$DOCKER_CANDIDATE" != "$DOCKER_INSTALLED" ] && [ -n "$DOCKER_CANDIDATE" ]; then
DOCKER_UPDATE_AVAILABLE=true
DOCKER_STATUS_TEXT="Update available: $DOCKER_INSTALLED$DOCKER_CANDIDATE"
else
DOCKER_STATUS_TEXT="Up to date ($DOCKER_INSTALLED)"
fi
echo "[INFO] Docker status: $DOCKER_STATUS_TEXT"
# =============================================================
# STEP 5 — DETERMINE OVERALL STATUS
# =============================================================
ANY_FAILED=false
ANY_UPDATES=false
[ "$APT_FAILED" = true ] && ANY_FAILED=true
[ "$PLEX_FAILED" = true ] && ANY_FAILED=true
[ "$APT_UPDATES" -gt 0 ] && ANY_UPDATES=true
[ "$PLEX_UPDATE_AVAILABLE" = true ] && ANY_UPDATES=true
[ "$F2B_UPDATE_AVAILABLE" = true ] && ANY_UPDATES=true
[ "$DOCKER_UPDATE_AVAILABLE" = true ] && ANY_UPDATES=true
if [ "$ANY_FAILED" = true ]; then
STATUS_COLOR="#b45309"
STATUS_BADGE="⚠ CHECK FAILED"
STATUS_BANNER_MSG="⚠ &nbsp;One or more update checks failed — manual verification required"
SUBJECT="⚠ PLEX32 — Update Check Failed — $DATE_SHORT"
elif [ "$ANY_UPDATES" = true ]; then
STATUS_COLOR="#1d6fa8"
STATUS_BADGE="🔄 UPDATES AVAILABLE"
STATUS_BANNER_MSG="🔄 &nbsp;Updates are available for one or more components"
SUBJECT="🔄 PLEX32 — Updates Available — $DATE_SHORT"
else
STATUS_COLOR="#1a7f4b"
STATUS_BADGE="✅ ALL CURRENT"
STATUS_BANNER_MSG="✅ &nbsp;All components are up to date — no action required"
SUBJECT="✅ PLEX32 — All Systems Current — $DATE_SHORT"
fi
# =============================================================
# STEP 6 — BUILD COMMAND BLOCKS (click to select)
# =============================================================
make_cmd_block() {
local step_num="$1"
local step_label="$2"
local cmd="$3"
local cmd_html=$(echo "$cmd" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
echo "<div style='margin-bottom:16px;'>
<div style='font-size:12px;font-weight:600;color:#374151;margin-bottom:4px;'>$step_num. $step_label</div>
<div style='font-size:11px;color:#6b7280;margin-bottom:4px;'>👆 Click inside to select all — then Ctrl+C</div>
<pre style='margin:0;background:#1e293b;color:#e2e8f0;padding:14px;border-radius:6px;font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-all;cursor:pointer;user-select:all;-webkit-user-select:all;-moz-user-select:all;'>$cmd_html</pre>
</div>"
}
UPDATE_COMMANDS_HTML=""
if [ "$ANY_UPDATES" = true ]; then
UPDATE_COMMANDS_HTML="<div style='margin-top:24px;'><div style='font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;'>🛠 Commands to Apply Updates (run in order)</div>"
STEP=1
if [ "$APT_UPDATES" -gt 0 ]; then
CMD="sudo apt update && sudo apt upgrade -y"
UPDATE_COMMANDS_HTML+=$(make_cmd_block "$STEP" "OS &amp; Package Updates" "$CMD")
STEP=$((STEP+1))
fi
if [ "$PLEX_UPDATE_AVAILABLE" = true ]; then
CMD="cd ~/docker/plex
docker compose up -d"
UPDATE_COMMANDS_HTML+=$(make_cmd_block "$STEP" "Plex Media Server (Docker)" "$CMD")
STEP=$((STEP+1))
fi
if [ "$DOCKER_UPDATE_AVAILABLE" = true ]; then
CMD="sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
docker --version"
UPDATE_COMMANDS_HTML+=$(make_cmd_block "$STEP" "Docker Engine" "$CMD")
STEP=$((STEP+1))
fi
if [ "$F2B_UPDATE_AVAILABLE" = true ]; then
CMD="sudo apt update && sudo apt install -y fail2ban
sudo systemctl restart fail2ban
sudo systemctl status fail2ban"
UPDATE_COMMANDS_HTML+=$(make_cmd_block "$STEP" "Fail2Ban" "$CMD")
fi
UPDATE_COMMANDS_HTML+="</div>"
fi
# =============================================================
# STEP 7 — BUILD APT PACKAGES TABLE
# =============================================================
APT_PACKAGES_HTML=""
if [ "$APT_UPDATES" -gt 0 ] && [ "$APT_FAILED" = false ]; then
APT_PACKAGES_HTML='<div style="margin-top:24px;">
<div style="font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;">📦 Packages Awaiting Update</div>
<table width="100%" cellspacing="0" cellpadding="0" style="background:white;border-radius:8px;border:1px solid #e5e7eb;overflow:hidden;">
<tr style="background:#f8fafc;">
<th style="text-align:left;padding:8px 14px;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;">Package</th>
<th style="text-align:left;padding:8px 14px;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;">Available Version</th>
</tr>'
while IFS= read -r line; do
[ -z "$line" ] && continue
pkg=$(echo "$line" | awk '{print $1}' | cut -d'/' -f1)
ver=$(echo "$line" | awk '{print $2}')
APT_PACKAGES_HTML+="<tr><td style='padding:8px 14px;font-size:13px;color:#111827;border-bottom:1px solid #f3f4f6;font-family:monospace;'>$pkg</td><td style='padding:8px 14px;font-size:13px;color:#1d6fa8;font-weight:600;border-bottom:1px solid #f3f4f6;'>$ver</td></tr>"
done <<< "$APT_DETAILS"
APT_PACKAGES_HTML+='</table></div>'
fi
# =============================================================
# STEP 8 — BADGES
# =============================================================
make_badge() {
local update="$1"
local failed="$2"
if [ "$failed" = true ]; then
echo "<span style='background:#b45309;color:white;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;margin-left:8px;'>CHECK FAILED</span>"
elif [ "$update" = true ]; then
echo "<span style='background:#1d6fa8;color:white;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;margin-left:8px;'>UPDATE AVAILABLE</span>"
else
echo "<span style='background:#1a7f4b;color:white;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;margin-left:8px;'>CURRENT</span>"
fi
}
PLEX_BADGE=$(make_badge "$PLEX_UPDATE_AVAILABLE" "$PLEX_FAILED")
F2B_BADGE=$(make_badge "$F2B_UPDATE_AVAILABLE" "false")
DOCKER_BADGE=$(make_badge "$DOCKER_UPDATE_AVAILABLE" "false")
# =============================================================
# STEP 9 — BUILD & SEND EMAIL
# =============================================================
python3 << PYEOF
import smtplib, ssl
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
apt_updates = $APT_UPDATES
plex_update = "$PLEX_UPDATE_AVAILABLE" == "true"
f2b_update = "$F2B_UPDATE_AVAILABLE" == "true"
docker_update = "$DOCKER_UPDATE_AVAILABLE" == "true"
any_failed = "$ANY_FAILED" == "true"
status_color = "$STATUS_COLOR"
status_badge = "$STATUS_BADGE"
status_banner = "$STATUS_BANNER_MSG"
subject = "$SUBJECT"
plex_status = "$PLEX_STATUS_TEXT"
f2b_status = "$F2B_STATUS_TEXT"
docker_status = "$DOCKER_STATUS_TEXT"
plex_badge = """$PLEX_BADGE"""
f2b_badge = """$F2B_BADGE"""
docker_badge = """$DOCKER_BADGE"""
update_commands = """$UPDATE_COMMANDS_HTML"""
apt_packages_html = """$APT_PACKAGES_HTML"""
plex_error_html = """$PLEX_ERROR_HTML"""
timestamp = "$TIMESTAMP"
plex_image_ver = "$PLEX_IMAGE_VERSION"
current_image_date= "$CURRENT_IMAGE_DATE"
from_email = "$EMAIL_FROM"
total_updates = apt_updates + (1 if plex_update else 0) + (1 if f2b_update else 0) + (1 if docker_update else 0)
apt_card_color = "#1d6fa8" if apt_updates > 0 else "#111827"
plex_card_color = "#1d6fa8" if plex_update else "#111827"
total_card_color = "#1d6fa8" if total_updates > 0 else "#1a7f4b"
other_update = f2b_update or docker_update
other_card_color = "#1d6fa8" if other_update else "#111827"
other_count = sum([1 if f2b_update else 0, 1 if docker_update else 0])
html = f"""<!DOCTYPE html>
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"></head>
<body style="margin:0;padding:0;background:#f1f5f9;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
<table width="100%" cellspacing="0" cellpadding="0" style="background:#f1f5f9;padding:32px 16px;">
<tr><td align="center">
<table width="100%" cellspacing="0" cellpadding="0" style="max-width:720px;background:white;border-radius:10px;box-shadow:0 1px 3px rgba(0,0,0,.1);border:1px solid #e5e7eb;overflow:hidden;">
<tr><td style="background:#0f172a;border-radius:10px 10px 0 0;padding:28px 32px;">
<table width="100%" cellspacing="0" cellpadding="0"><tr>
<td>
<div style="font-size:11px;color:#94a3b8;letter-spacing:1px;text-transform:uppercase;">Update Monitor</div>
<div style="font-size:26px;font-weight:800;color:white;margin-top:4px;">🖥 PLEX32</div>
<div style="font-size:13px;color:#94a3b8;margin-top:4px;">{timestamp} | VLAN 50 — 192.168.150.45</div>
</td>
<td align="right">
<div style="background:{status_color};color:white;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:700;">{status_badge}</div>
</td>
</tr></table>
</td></tr>
<tr><td style="background:{status_color};padding:12px 32px;">
<span style="color:white;font-size:14px;font-weight:600;">{status_banner}</span>
</td></tr>
<tr><td style="padding:28px 32px;">
<table width="100%" cellspacing="0" cellpadding="0">
<tr>
<td style="width:24%;padding:8px 6px 8px 0;">
<div style="background:#f9fafb;border-radius:6px;padding:12px;text-align:center;border:1px solid #e5e7eb;">
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Total Updates</div>
<div style="font-size:28px;font-weight:700;color:{total_card_color};margin-top:4px;">{total_updates}</div>
</div>
</td>
<td style="width:24%;padding:8px 6px;">
<div style="background:#f9fafb;border-radius:6px;padding:12px;text-align:center;border:1px solid #e5e7eb;">
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">OS Packages</div>
<div style="font-size:28px;font-weight:700;color:{apt_card_color};margin-top:4px;">{apt_updates}</div>
</div>
</td>
<td style="width:24%;padding:8px 6px;">
<div style="background:#f9fafb;border-radius:6px;padding:12px;text-align:center;border:1px solid #e5e7eb;">
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Plex</div>
<div style="font-size:28px;font-weight:700;color:{plex_card_color};margin-top:4px;">{'1' if plex_update else '0'}</div>
</div>
</td>
<td style="width:24%;padding:8px 0 8px 6px;">
<div style="background:#f9fafb;border-radius:6px;padding:12px;text-align:center;border:1px solid #e5e7eb;">
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Other</div>
<div style="font-size:28px;font-weight:700;color:{other_card_color};margin-top:4px;">{other_count}</div>
</div>
</td>
</tr>
</table>
<div style="margin-top:24px;">
<div style="font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;">📋 Component Status</div>
<table width="100%" cellspacing="0" cellpadding="0" style="background:white;border-radius:8px;border:1px solid #e5e7eb;overflow:hidden;">
<tr style="background:#f8fafc;">
<th style="text-align:left;padding:8px 14px;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;">Component</th>
<th style="text-align:left;padding:8px 14px;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;">Status</th>
</tr>
<tr>
<td style="padding:10px 14px;font-size:13px;color:#374151;font-weight:600;border-bottom:1px solid #f3f4f6;">🐧 Ubuntu Server (apt)</td>
<td style="padding:10px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;">{apt_updates} package(s) available</td>
</tr>
<tr>
<td style="padding:10px 14px;font-size:13px;color:#374151;font-weight:600;border-bottom:1px solid #f3f4f6;">🎬 Plex Media Server {plex_badge}</td>
<td style="padding:10px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;">{plex_status}<br><span style="font-size:11px;color:#9ca3af;">Image: {plex_image_ver} | Built: {current_image_date}</span>{plex_error_html}</td>
</tr>
<tr>
<td style="padding:10px 14px;font-size:13px;color:#374151;font-weight:600;border-bottom:1px solid #f3f4f6;">🛡 Fail2Ban {f2b_badge}</td>
<td style="padding:10px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;">{f2b_status}</td>
</tr>
<tr>
<td style="padding:10px 14px;font-size:13px;color:#374151;font-weight:600;">🐳 Docker Engine {docker_badge}</td>
<td style="padding:10px 14px;font-size:13px;color:#374151;">{docker_status}</td>
</tr>
</table>
</div>
{update_commands}
{apt_packages_html}
</td></tr>
<tr><td style="background:#0f172a;border-radius:0 0 10px 10px;padding:16px 32px;text-align:center;">
<span style="font-size:11px;color:#64748b;">Update Monitor &nbsp;|&nbsp; PLEX32 &nbsp;|&nbsp; KingDezigns Infrastructure</span>
</td></tr>
</table>
</td></tr>
</table>
</body></html>"""
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = f"KingDezigns PLEX32 <{from_email}>"
msg["To"] = "$EMAIL_TO"
msg.attach(MIMEText(html, "html"))
context = ssl.create_default_context()
try:
with smtplib.SMTP_SSL("$SMTP_SERVER", $SMTP_PORT, context=context, timeout=15) as server:
server.login("$SMTP_USER", "$SMTP_PASS")
server.sendmail("$EMAIL_FROM", "$EMAIL_TO", msg.as_string())
print("[$TIMESTAMP] Email sent successfully.")
except Exception as e:
print(f"[$TIMESTAMP] Email send failed: {e}")
exit(1)
PYEOF
echo "[$TIMESTAMP] Update check complete."