Initial import
This commit is contained in:
commit
2ae9363458
9 changed files with 3135 additions and 0 deletions
498
KingDezigns-Summary-2026-06-21.html
Normal file
498
KingDezigns-Summary-2026-06-21.html
Normal file
|
|
@ -0,0 +1,498 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>KingDezigns — PLEX32 Timezone Fix & Remote Reboot Script Summary</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #f7f6f2;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-alt: #f1f0eb;
|
||||||
|
--border: #e2e0d8;
|
||||||
|
--border-strong: #c8c5ba;
|
||||||
|
--text-primary: #1a1917;
|
||||||
|
--text-secondary: #6b6960;
|
||||||
|
--text-muted: #9b9890;
|
||||||
|
--green-bg: #eaf3de;
|
||||||
|
--green-border: #97c459;
|
||||||
|
--green-text: #2d5a0e;
|
||||||
|
--red-bg: #fcebeb;
|
||||||
|
--red-border: #f09595;
|
||||||
|
--red-text: #7a1f1f;
|
||||||
|
--amber-bg: #faeeda;
|
||||||
|
--amber-border: #fac775;
|
||||||
|
--amber-text: #633806;
|
||||||
|
--coral-bg: #faece7;
|
||||||
|
--coral-border: #f0997b;
|
||||||
|
--coral-text: #4a1b0c;
|
||||||
|
--mono: 'IBM Plex Mono', monospace;
|
||||||
|
--sans: 'IBM Plex Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--sans);
|
||||||
|
font-size: 14px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.7;
|
||||||
|
padding: 2.5rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page { max-width: 860px; margin: 0 auto; }
|
||||||
|
|
||||||
|
header {
|
||||||
|
border-bottom: 1px solid var(--border-strong);
|
||||||
|
padding-bottom: 1.25rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--mono);
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section { margin-bottom: 2.25rem; }
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title::before {
|
||||||
|
content: attr(data-num);
|
||||||
|
background: var(--text-primary);
|
||||||
|
color: var(--bg);
|
||||||
|
font-size: 10px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
p { margin-bottom: 0.75rem; color: var(--text-primary); }
|
||||||
|
p:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
|
.badge-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 1rem; }
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.red { background: var(--red-bg); color: var(--red-text); border-color: var(--red-border); }
|
||||||
|
.badge.green { background: var(--green-bg); color: var(--green-text); border-color: var(--green-border); }
|
||||||
|
.badge.amber { background: var(--amber-bg); color: var(--amber-text); border-color: var(--amber-border); }
|
||||||
|
|
||||||
|
.step {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-header {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-num {
|
||||||
|
background: var(--surface-alt);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: var(--surface-alt);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
white-space: pre;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 12px;
|
||||||
|
background: var(--surface-alt);
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.why {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
.why::before { content: '↳ why: '; font-family: var(--mono); }
|
||||||
|
|
||||||
|
.result { font-size: 12px; margin-top: 0.35rem; padding-left: 2px; }
|
||||||
|
.result.pass { color: var(--green-text); }
|
||||||
|
.result.pass::before { content: '✓ '; font-family: var(--mono); }
|
||||||
|
.result.fail { color: var(--red-text); }
|
||||||
|
.result.fail::before { content: '✗ '; font-family: var(--mono); }
|
||||||
|
.result.neutral { color: var(--text-secondary); }
|
||||||
|
.result.neutral::before { content: '→ '; font-family: var(--mono); }
|
||||||
|
|
||||||
|
.callout { border-radius: 6px; padding: 1rem 1.25rem; margin-bottom: 0.75rem; }
|
||||||
|
.callout.coral { background: var(--coral-bg); border: 1px solid var(--coral-border); }
|
||||||
|
.callout.coral p { color: var(--coral-text); }
|
||||||
|
.callout.coral pre { background: #fdf0eb; border-color: var(--coral-border); }
|
||||||
|
.callout.amber { background: var(--amber-bg); border: 1px solid var(--amber-border); }
|
||||||
|
.callout.amber p { color: var(--amber-text); }
|
||||||
|
.callout.amber pre { background: #fdf5e6; border-color: var(--amber-border); }
|
||||||
|
.callout.amber code { background: #fdf5e6; border-color: var(--amber-border); color: var(--amber-text); }
|
||||||
|
.callout.green { background: var(--green-bg); border: 1px solid var(--green-border); }
|
||||||
|
.callout.green p { color: var(--green-text); }
|
||||||
|
.callout.green pre { background: #d8efc0; border-color: var(--green-border); }
|
||||||
|
|
||||||
|
.clue-label {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
color: var(--amber-text);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.script-box { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
|
||||||
|
.script-row { padding: 0.75rem 1.25rem; border-bottom: 1px solid var(--border); }
|
||||||
|
.script-row:last-child { border-bottom: none; }
|
||||||
|
.script-label { font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-muted); margin-bottom: 6px; }
|
||||||
|
.script-row pre { margin: 0; }
|
||||||
|
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 13px; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
|
||||||
|
th { text-align: left; padding: 8px 14px; font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); background: var(--surface-alt); border-bottom: 1px solid var(--border); font-weight: 500; }
|
||||||
|
td { padding: 9px 14px; border-bottom: 1px solid var(--border); vertical-align: top; line-height: 1.5; }
|
||||||
|
tr:last-child td { border-bottom: none; }
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>KingDezigns — PLEX32 Timezone Fix & Remote Reboot Script summary</h1>
|
||||||
|
<div class="meta">
|
||||||
|
<span>📅 2026-06-21</span>
|
||||||
|
<span>🖥 PLEX32 / NAS08</span>
|
||||||
|
<span>⚙️ Debian / OpenMediaVault</span>
|
||||||
|
<span>🐳 Docker Compose</span>
|
||||||
|
<span>☁️ Plex Media Server</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SECTION 1 — THE PROBLEM -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title" data-num="1">The problem</div>
|
||||||
|
|
||||||
|
<p>The PLEX32 backup script ran at 10:00 PM instead of the scheduled 2:00 AM, indicating a scheduling problem. A separate gap was also identified: there was no way to remotely reboot PLEX32 when away from home if it became unresponsive.</p>
|
||||||
|
|
||||||
|
<div class="badge-row">
|
||||||
|
<span class="badge red">Backup ran at 10 PM instead of 2 AM</span>
|
||||||
|
<span class="badge amber">No remote reboot capability for PLEX32</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>The crontab entry on PLEX32 was correct (<code>0 2 */3 * *</code>), but the system timezone was set to <code>Etc/UTC</code> instead of <code>America/New_York</code>, causing all cron jobs to fire 4 hours early during EDT (UTC-4).</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SECTION 2 — DIAGNOSTIC COMMANDS -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title" data-num="2">Diagnostic commands run & why</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-header"><span class="step-num">1</span> Checked backup log timestamps</div>
|
||||||
|
<pre>ls -lt /var/log/plex32-backup/</pre>
|
||||||
|
<p class="why">Confirm the 10 PM run actually happened and check if it was recurring.</p>
|
||||||
|
<p class="result neutral">Showed Jun 19 ran at 23:17, Jun 20 at 02:03, Jun 22 at 02:09 — pattern inconsistent, not a simple crontab typo.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-header"><span class="step-num">2</span> Verified crontab entry on PLEX32</div>
|
||||||
|
<pre>sudo crontab -l</pre>
|
||||||
|
<p class="why">Rule out a bad cron expression as the cause.</p>
|
||||||
|
<p class="result pass">Crontab was correct: <code>0 2 */3 * *</code> — not the problem.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-header"><span class="step-num">3</span> Checked system timezone</div>
|
||||||
|
<pre>timedatectl</pre>
|
||||||
|
<p class="why">A 4-hour offset is the classic symptom of UTC vs EDT mismatch.</p>
|
||||||
|
<p class="result fail">Time zone: Etc/UTC — confirmed root cause. Cron was firing at 2 AM UTC = 10 PM EDT.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-header"><span class="step-num">4</span> Checked timezones on all other VLAN 50 servers</div>
|
||||||
|
<pre>ssh rufusking@192.168.150.35 "timedatectl | grep 'Time zone'" # NAS08
|
||||||
|
ssh rufusking@192.168.150.40 "timedatectl | grep 'Time zone'" # NAS16
|
||||||
|
ssh rufusking@192.168.150.30 "timedatectl | grep 'Time zone'" # HAS</pre>
|
||||||
|
<p class="why">If PLEX32 had the wrong timezone, other servers might too.</p>
|
||||||
|
<p class="result pass">All other servers were already on America/New_York — PLEX32 was the only one affected.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SECTION 3 — ROOT CAUSE (TIMEZONE) -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title" data-num="3">Root cause — cron firing at wrong time</div>
|
||||||
|
|
||||||
|
<div class="callout coral">
|
||||||
|
<p>PLEX32's system timezone was set to <code>Etc/UTC</code> (the Debian default). All cron jobs fired at the correct UTC time, which is 4 hours ahead of Eastern Daylight Time, causing the 2 AM backup to run at 10 PM EDT.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="callout amber">
|
||||||
|
<div class="clue-label">⚠ Key clue</div>
|
||||||
|
<p style="margin-bottom:6px;">The backup log on Jun 19 showed 23:17 and Jun 20 showed 02:03 — the jump from late-night to early-morning suggested the timezone was corrected mid-cycle, but <code>timedatectl</code> showed it was still UTC. The log timestamps themselves are in UTC, so both runs were actually at 2 AM UTC.</p>
|
||||||
|
<pre>-rw-r--r-- 1 root root 1559 Jun 22 02:09 backup-20260622.log
|
||||||
|
-rw-rw-r-- 1 rufusking rufusking 3120 Jun 20 02:03 backup-20260620.log
|
||||||
|
-rw-rw-r-- 1 rufusking rufusking 5879 Jun 19 23:17 backup-20260619.log</pre>
|
||||||
|
<p style="margin-top:8px;">The Jun 19 timestamp difference was due to the backup running at a slightly different interval that cycle — not a timezone change.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="callout green" style="margin-top:0.75rem;">
|
||||||
|
<p><strong>Resolution:</strong> Set the timezone to <code>America/New_York</code> with <code>sudo timedatectl set-timezone America/New_York</code>. No crontab changes needed — cron reads system time automatically.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SECTION 4 — REMOTE REBOOT SCRIPT -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title" data-num="4">New capability — PLEX32 remote reboot script</div>
|
||||||
|
|
||||||
|
<p>A remote reboot script was created on NAS08 to provide a way to reboot PLEX32 from outside the home network via OMV's Scheduled Jobs interface. The script was iteratively debugged through several test runs.</p>
|
||||||
|
|
||||||
|
<div class="badge-row" style="margin-top:0.75rem;">
|
||||||
|
<span class="badge red"><code>nc</code> not installed on NAS08 — port check always failed</span>
|
||||||
|
<span class="badge amber">docker compose down clears restart policy — containers didn't auto-start</span>
|
||||||
|
<span class="badge amber">flat <code>sleep 60</code> caused unnecessary delay in Plex detection</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-header"><span class="step-num">1</span> Verified SSH key from NAS08 root → PLEX32 already existed</div>
|
||||||
|
<pre>sudo ssh-copy-id -i /root/.ssh/id_ed25519.pub rufusking@192.168.150.45
|
||||||
|
sudo ssh -i /root/.ssh/id_ed25519 rufusking@192.168.150.45 "echo OK"</pre>
|
||||||
|
<p class="result pass">Key was already trusted — ssh-copy-id skipped it, echo OK confirmed passwordless access.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-header"><span class="step-num">2</span> Added reboot to PLEX32 sudoers</div>
|
||||||
|
<pre>sudo visudo -f /etc/sudoers.d/rufusking-nopasswd
|
||||||
|
# Appended: /sbin/reboot</pre>
|
||||||
|
<p class="why">The script runs <code>sudo reboot</code> over SSH — must be passwordless or it hangs.</p>
|
||||||
|
<p class="result pass">Full sudoers line: apt-get update, fail2ban-client status, fail2ban-client status sshd, /sbin/reboot</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-header"><span class="step-num">3</span> First test — containers didn't restart after reboot</div>
|
||||||
|
<pre>docker ps
|
||||||
|
# Result: empty — no containers running</pre>
|
||||||
|
<p class="why">The script ran <code>docker compose down</code> before rebooting, which clears the <code>unless-stopped</code> restart policy. Nothing brought the containers back up.</p>
|
||||||
|
<p class="result fail">Plex was down after reboot — had to manually run <code>docker compose up -d</code>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-header"><span class="step-num">4</span> Fixed — added docker compose up -d after PLEX32 comes back online</div>
|
||||||
|
<pre>ssh ... "cd ~/docker/plex && docker compose up -d"</pre>
|
||||||
|
<p class="result pass">Containers started cleanly after subsequent reboots.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-header"><span class="step-num">5</span> Port check always reported failure despite Plex being up</div>
|
||||||
|
<pre>nc -z -w 5 192.168.150.45 32400; echo "nc exit: $?"
|
||||||
|
# Result: nc exit: 127 (command not found)</pre>
|
||||||
|
<p class="why"><code>nc</code> (netcat) is not installed on NAS08 — the check always returned failure regardless of Plex state.</p>
|
||||||
|
<p class="result fail">Switched to <code>curl</code> HTTP check — confirmed Plex returns HTTP 302 on port 32400.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-header"><span class="step-num">6</span> Replaced flat sleep 60 with polling loop</div>
|
||||||
|
<pre>while [ $PLEX_ELAPSED -lt 90 ]; do
|
||||||
|
HTTP_CODE=$(curl ... http://192.168.150.45:32400/web)
|
||||||
|
[ "$HTTP_CODE" = "302" ] && break
|
||||||
|
sleep 5; PLEX_ELAPSED=$((PLEX_ELAPSED + 5))
|
||||||
|
done</pre>
|
||||||
|
<p class="why">Plex was up within ~10s of containers starting but the script was waiting a full 60s before checking.</p>
|
||||||
|
<p class="result pass">Plex confirmed responding after 10s on final test run.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SECTION 5 — SCRIPT -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title" data-num="5">Script created</div>
|
||||||
|
<div class="script-box">
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">Location (NAS08)</div>
|
||||||
|
<pre>/usr/scripts/nas/plex32_reboot.sh</pre>
|
||||||
|
</div>
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">Log directory</div>
|
||||||
|
<pre>/var/log/plex32-reboot/reboot-YYYYMMDD-HHMMSS.log (90 day retention)</pre>
|
||||||
|
</div>
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">What it does</div>
|
||||||
|
<pre>1. SSH to PLEX32 → docker compose down (graceful container stop)
|
||||||
|
2. SSH to PLEX32 → sudo reboot
|
||||||
|
3. Wait 30s then poll ping every 10s (max 3 min) until PLEX32 responds
|
||||||
|
4. SSH to PLEX32 → docker compose up -d
|
||||||
|
5. Poll curl http://192.168.150.45:32400/web every 5s (max 90s)
|
||||||
|
6. Log success (HTTP 302/200/301) or warning if timeout</pre>
|
||||||
|
</div>
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">OMV Scheduled Job</div>
|
||||||
|
<pre>Tag: PLEX32 Remote Reboot
|
||||||
|
Command: /usr/scripts/nas/plex32_reboot.sh
|
||||||
|
Enabled: OFF — enable manually, run once, disable again</pre>
|
||||||
|
</div>
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">Final test run result</div>
|
||||||
|
<pre>[23:40:39] PLEX32 Remote Reboot — Initiated
|
||||||
|
[23:40:45] Docker containers stopped cleanly.
|
||||||
|
[23:40:45] Reboot command sent. PLEX32 is going down.
|
||||||
|
[23:41:27] PLEX32 is back online (ping responded after ~40s).
|
||||||
|
[23:41:30] Docker containers started.
|
||||||
|
[23:41:40] Plex is responding (HTTP 302) after 10s — reboot successful.</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SECTION 6 — LESSONS LEARNED -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title" data-num="6">Lessons learned</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Lesson</th><th>Detail</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Always set timezone on new installs</td>
|
||||||
|
<td>Debian/Ubuntu default is <code>Etc/UTC</code>. Run <code>sudo timedatectl set-timezone America/New_York</code> immediately after setup. Verify with <code>timedatectl | grep "Time zone"</code>. A 4-hour cron offset is the telltale symptom.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>docker compose down</code> clears restart policy</td>
|
||||||
|
<td>Unlike a system reboot (which respects <code>restart: unless-stopped</code>), an explicit <code>compose down</code> removes containers entirely. Always follow a pre-reboot <code>compose down</code> with a post-boot <code>compose up -d</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>nc</code> is not installed on NAS08</td>
|
||||||
|
<td>Use <code>curl -s -o /dev/null -w "%{http_code}"</code> for HTTP service checks from NAS08. Plex returns HTTP 302 on port 32400 — treat 200, 301, and 302 all as success.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Flat sleep before health checks wastes time</td>
|
||||||
|
<td>Replace <code>sleep N</code> before a readiness check with a polling loop. Check every 5 seconds, break on success, cap with a max timeout. Services often come up well before the sleep expires.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Verify deployed scripts after editing</td>
|
||||||
|
<td>Use <code>grep -n "key_string" /path/to/script</code> to confirm the correct version is on disk before testing. Copy errors and stale files caused multiple confusing test failures in this session.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SECTION 7 — DOCS UPDATED -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title" data-num="7">Documentation updated</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>File</th><th>What changed</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>server_plex32.md</code></td>
|
||||||
|
<td>Added timezone section (was UTC, corrected to America/New_York). Updated sudoers entry to include <code>/sbin/reboot</code>. Updated AI summary with timezone note and NAS08→PLEX32 SSH key entry.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>server_nas08.md</code></td>
|
||||||
|
<td>Added <code>plex32_reboot.sh</code> to OMV Scheduled Jobs table (disabled/manual). Added NAS08 root → PLEX32 SSH key note. Updated AI summary with remote reboot script details.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<span>KingDezigns Home Network</span>
|
||||||
|
<span>Generated 2026-06-21</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
9
PLEX32-Launcher.desktop
Normal file
9
PLEX32-Launcher.desktop
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Version=1.0
|
||||||
|
Type=Application
|
||||||
|
Name=PLEX32 Launcher
|
||||||
|
Comment=Run scripts on PLEX32 remotely
|
||||||
|
Exec=bash -c "gnome-terminal -- bash /home/rufusking/scripts/plex32_launcher.sh"
|
||||||
|
Icon=utilities-terminal
|
||||||
|
Terminal=false
|
||||||
|
Categories=Network;System;
|
||||||
608
PLEX32-Optimization-Summary.html
Normal file
608
PLEX32-Optimization-Summary.html
Normal file
|
|
@ -0,0 +1,608 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>KingDezigns — PLEX32 Optimization Summary</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #f7f6f2;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-alt: #f1f0eb;
|
||||||
|
--border: #e2e0d8;
|
||||||
|
--border-strong: #c8c5ba;
|
||||||
|
--text-primary: #1a1917;
|
||||||
|
--text-secondary: #6b6960;
|
||||||
|
--text-muted: #9b9890;
|
||||||
|
--green-bg: #eaf3de;
|
||||||
|
--green-border: #97c459;
|
||||||
|
--green-text: #2d5a0e;
|
||||||
|
--red-bg: #fcebeb;
|
||||||
|
--red-border: #f09595;
|
||||||
|
--red-text: #7a1f1f;
|
||||||
|
--amber-bg: #faeeda;
|
||||||
|
--amber-border: #fac775;
|
||||||
|
--amber-text: #633806;
|
||||||
|
--coral-bg: #faece7;
|
||||||
|
--coral-border: #f0997b;
|
||||||
|
--coral-text: #4a1b0c;
|
||||||
|
--mono: 'IBM Plex Mono', monospace;
|
||||||
|
--sans: 'IBM Plex Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--sans);
|
||||||
|
font-size: 14px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.7;
|
||||||
|
padding: 2.5rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page { max-width: 860px; margin: 0 auto; }
|
||||||
|
|
||||||
|
header {
|
||||||
|
border-bottom: 1px solid var(--border-strong);
|
||||||
|
padding-bottom: 1.25rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--mono);
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section { margin-bottom: 2.25rem; }
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title::before {
|
||||||
|
content: attr(data-num);
|
||||||
|
background: var(--text-primary);
|
||||||
|
color: var(--bg);
|
||||||
|
font-size: 10px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
p { margin-bottom: 0.75rem; color: var(--text-primary); }
|
||||||
|
p:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
|
.badge-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 1rem; }
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.red { background: var(--red-bg); color: var(--red-text); border-color: var(--red-border); }
|
||||||
|
.badge.green { background: var(--green-bg); color: var(--green-text); border-color: var(--green-border); }
|
||||||
|
.badge.amber { background: var(--amber-bg); color: var(--amber-text); border-color: var(--amber-border); }
|
||||||
|
|
||||||
|
.step {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-header {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-num {
|
||||||
|
background: var(--surface-alt);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: var(--surface-alt);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
white-space: pre;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 12px;
|
||||||
|
background: var(--surface-alt);
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.why {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
.why::before { content: '↳ why: '; font-family: var(--mono); }
|
||||||
|
|
||||||
|
.result { font-size: 12px; margin-top: 0.35rem; padding-left: 2px; }
|
||||||
|
.result.pass { color: var(--green-text); }
|
||||||
|
.result.pass::before { content: '✓ '; font-family: var(--mono); }
|
||||||
|
.result.fail { color: var(--red-text); }
|
||||||
|
.result.fail::before { content: '✗ '; font-family: var(--mono); }
|
||||||
|
.result.neutral { color: var(--text-secondary); }
|
||||||
|
.result.neutral::before { content: '→ '; font-family: var(--mono); }
|
||||||
|
|
||||||
|
.callout { border-radius: 6px; padding: 1rem 1.25rem; margin-bottom: 0.75rem; }
|
||||||
|
.callout.coral { background: var(--coral-bg); border: 1px solid var(--coral-border); }
|
||||||
|
.callout.coral p { color: var(--coral-text); }
|
||||||
|
.callout.coral pre { background: #fdf0eb; border-color: var(--coral-border); }
|
||||||
|
.callout.amber { background: var(--amber-bg); border: 1px solid var(--amber-border); }
|
||||||
|
.callout.amber p { color: var(--amber-text); }
|
||||||
|
.callout.amber pre { background: #fdf5e6; border-color: var(--amber-border); }
|
||||||
|
.callout.amber code { background: #fdf5e6; border-color: var(--amber-border); color: var(--amber-text); }
|
||||||
|
.callout.green { background: var(--green-bg); border: 1px solid var(--green-border); }
|
||||||
|
.callout.green p { color: var(--green-text); }
|
||||||
|
.callout.green pre { background: #d8efc0; border-color: var(--green-border); }
|
||||||
|
|
||||||
|
.clue-label {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
color: var(--amber-text);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.script-box { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
|
||||||
|
.script-row { padding: 0.75rem 1.25rem; border-bottom: 1px solid var(--border); }
|
||||||
|
.script-row:last-child { border-bottom: none; }
|
||||||
|
.script-label { font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-muted); margin-bottom: 6px; }
|
||||||
|
.script-row pre { margin: 0; }
|
||||||
|
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 13px; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
|
||||||
|
th { text-align: left; padding: 8px 14px; font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); background: var(--surface-alt); border-bottom: 1px solid var(--border); font-weight: 500; }
|
||||||
|
td { padding: 9px 14px; border-bottom: 1px solid var(--border); vertical-align: top; line-height: 1.5; }
|
||||||
|
tr:last-child td { border-bottom: none; }
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>KingDezigns — PLEX32 Optimization & Setup Summary</h1>
|
||||||
|
<div class="meta">
|
||||||
|
<span>📅 June 19–20, 2026</span>
|
||||||
|
<span>🖥 PLEX32 — Dell Wyse 5070</span>
|
||||||
|
<span>⚙️ Ubuntu 24.04</span>
|
||||||
|
<span>🐳 Docker Compose</span>
|
||||||
|
<span>☁️ Plex Media Server + Tautulli</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SECTION 1 — OVERVIEW -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title" data-num="1">Overview</div>
|
||||||
|
|
||||||
|
<p>PLEX32 was recently migrated from NAS08 to a dedicated Dell Wyse 5070 (Intel Celeron J4105, 8GB RAM) to support Plex Pass features including Sonic Radio that require x86 CPU capabilities. This session covered a full 7-step optimization of the new server — hardware transcoding, playback tuning, Tautulli setup, automated backup, scheduled maintenance, and subtitle handling.</p>
|
||||||
|
|
||||||
|
<div class="badge-row">
|
||||||
|
<span class="badge green">Hardware Transcoding — Enabled</span>
|
||||||
|
<span class="badge green">Tautulli — Installed</span>
|
||||||
|
<span class="badge green">Backup — Automated</span>
|
||||||
|
<span class="badge green">SSH Keys — Configured</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>All 7 optimization steps completed successfully. PLEX32 is now fully configured and production-ready.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SECTION 2 — STEP 1: HARDWARE TRANSCODING -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title" data-num="2">Step 1 — Hardware transcoding (Intel Quick Sync)</div>
|
||||||
|
|
||||||
|
<p>The J4105 has Intel Quick Sync but the Plex Docker container had no access to <code>/dev/dri</code> and was silently falling back to software transcoding on every stream.</p>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-header"><span class="step-num">1</span> Confirmed iGPU is exposed on the host</div>
|
||||||
|
<pre>ls -la /dev/dri</pre>
|
||||||
|
<p class="result pass">card0 (video, GID 44) and renderD128 (render, GID 993) both present</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-header"><span class="step-num">2</span> Updated docker-compose.yml — added device passthrough and group_add</div>
|
||||||
|
<pre>devices:
|
||||||
|
- /dev/dri:/dev/dri
|
||||||
|
group_add:
|
||||||
|
- "44"
|
||||||
|
- "993"</pre>
|
||||||
|
<p class="why">Container needs both the device node and supplementary group membership to access the GPU</p>
|
||||||
|
<p class="result pass">docker exec confirmed groups 44 and 993 in container process — /dev/dri visible inside container</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-header"><span class="step-num">3</span> Enabled hardware transcoding in Plex UI</div>
|
||||||
|
<pre>Settings → Transcoder → Use hardware acceleration when available ✅
|
||||||
|
Settings → Transcoder → Use hardware-accelerated video encoding ✅</pre>
|
||||||
|
<p class="why">Both were already checked — confirming the settings were attempting HW transcode but failing silently due to missing device access</p>
|
||||||
|
<p class="result pass">Dashboard showed: 4K (HEVC Main 10) (hw) / SD (HEVC) — Transcode (hw) — both sides of pipeline on GPU</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-header"><span class="step-num">4</span> Set transcoder temp directory</div>
|
||||||
|
<pre>Settings → Transcoder → Transcoder temporary directory: /transcode</pre>
|
||||||
|
<p class="why">Was empty — Plex was writing transcode scratch files to uncontrolled location inside container filesystem</p>
|
||||||
|
<p class="result pass">Now maps to /opt/plex/transcode on host via existing volume mount</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SECTION 3 — STEP 2: DIRECT PLAY -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title" data-num="3">Step 2 — Direct Play / Direct Stream tuning</div>
|
||||||
|
|
||||||
|
<p>Library codec analysis revealed 90 HEVC Main 10 files (~75%) and 29 H.264 files (~25%), all with AAC audio. Roku TV (Hisense 55R8BX) was tested — video Direct Streams, audio converts AAC → AC3 for HDMI passthrough.</p>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-header"><span class="step-num">1</span> Codec analysis — video</div>
|
||||||
|
<pre>find "/mnt/plex/media/04 - Videos/Movies" \( -iname "*.mkv" -o -iname "*.mp4" \) | while read -r f; do
|
||||||
|
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,profile \
|
||||||
|
-of csv=p=0 "$f" 2>/dev/null
|
||||||
|
done | sort | uniq -c | sort -rn</pre>
|
||||||
|
<p class="result neutral">82+8 hevc,Main 10 / 29 h264,High — library is 75% HEVC 10-bit</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-header"><span class="step-num">2</span> Codec analysis — audio</div>
|
||||||
|
<pre>find ... | ffprobe ... -select_streams a:0 ... | sort | uniq -c</pre>
|
||||||
|
<p class="result pass">119 aac — 100% AAC audio across entire library. No TrueHD, no DTS. Audio will never force a transcode due to format incompatibility.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-header"><span class="step-num">3</span> Live playback test on Roku</div>
|
||||||
|
<pre>Plex Dashboard → active session while playing on Roku TV</pre>
|
||||||
|
<p class="result pass">Video: 4K (HEVC Main 10) — Direct Stream ✅ / Audio: English (AAC 5.1) → AC3 Transcode (lightweight, expected for Roku HDMI passthrough)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="callout green">
|
||||||
|
<p><strong>Result:</strong> Video never transcodes. Audio does a lightweight AAC→AC3 conversion for Roku surround output. Optimal for this setup.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SECTION 4 — STEP 3 & 4: SONIC RADIO + TAUTULLI -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title" data-num="4">Steps 3 & 4 — Sonic Radio + Tautulli</div>
|
||||||
|
|
||||||
|
<p><strong>Sonic Radio</strong> was confirmed working — the whole reason for migrating to PLEX32. Opening any artist in the Music library and clicking Live Radio plays that artist then branches into similar artists using AI-driven discovery. Requires x86 CPU — confirmed working on J4105.</p>
|
||||||
|
|
||||||
|
<p><strong>Tautulli</strong> was installed as a second service in the existing docker-compose.yml, running on port 8181. Connected to Plex, email notification configured for Plex server down/up alerts using existing Zoho SMTP credentials.</p>
|
||||||
|
|
||||||
|
<div class="script-box">
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">Tautulli added to docker-compose.yml</div>
|
||||||
|
<pre>tautulli:
|
||||||
|
image: lscr.io/linuxserver/tautulli:latest
|
||||||
|
container_name: tautulli
|
||||||
|
environment:
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1001
|
||||||
|
- TZ=America/New_York
|
||||||
|
volumes:
|
||||||
|
- /opt/tautulli/config:/config
|
||||||
|
ports:
|
||||||
|
- 8181:8181
|
||||||
|
restart: unless-stopped</pre>
|
||||||
|
</div>
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">Config directory</div>
|
||||||
|
<pre>/opt/tautulli/config</pre>
|
||||||
|
</div>
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">Web UI</div>
|
||||||
|
<pre>http://192.168.150.45:8181</pre>
|
||||||
|
</div>
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">Notification triggers set</div>
|
||||||
|
<pre>Plex Server Down + Plex Server Back Up → email to rufus.king@kingdezigns.com</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SECTION 5 — STEP 5: BACKUP -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title" data-num="5">Step 5 — Automated config backup</div>
|
||||||
|
|
||||||
|
<p>A backup script was created to archive Plex and Tautulli configs to NAS08 via NFS every 3 days. rsync was attempted first but failed due to NFS directory scanning performance issues and permission constraints from NAS08's all_squash export. The final approach uses tar locally then a single cp to NFS.</p>
|
||||||
|
|
||||||
|
<div class="callout amber">
|
||||||
|
<div class="clue-label">⚠ Key issue — rsync over NFS hung indefinitely</div>
|
||||||
|
<p style="margin-bottom:6px;">rsync hung at "sending incremental file list" because scanning thousands of small Plex metadata files over NFS was extremely slow. Additionally NAS08's all_squash NFS export (anonuid=33/anongid=1001) prevented rufusking on PLEX32 from writing new files to the destination until directory permissions were corrected on NAS08.</p>
|
||||||
|
<pre>sudo chown -R www-data:ncshare /export/kingdezigns-all/Docker/plex/config
|
||||||
|
sudo chmod -R 2770 /export/kingdezigns-all/Docker/plex/config</pre>
|
||||||
|
<p style="margin-top:8px;">Final solution: tar to /tmp locally (no NFS), then single cp to NFS. NFS write speed confirmed at 100 MB/s. Total backup runtime ~10 minutes.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="script-box">
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">Backup script location</div>
|
||||||
|
<pre>/usr/scripts/plex32/plex32_backup.sh</pre>
|
||||||
|
</div>
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">Schedule</div>
|
||||||
|
<pre>0 2 */3 * * /usr/scripts/plex32/plex32_backup.sh # Every 3 days at 2:00 AM</pre>
|
||||||
|
</div>
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">Plex archive destination (NAS08)</div>
|
||||||
|
<pre>/export/kingdezigns-all/Docker/plex/config/backups/plex/
|
||||||
|
plex-config-YYYY-MM-DD.tar.gz (3 copies retained)</pre>
|
||||||
|
</div>
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">Tautulli archive destination (NAS08)</div>
|
||||||
|
<pre>/export/kingdezigns-all/Docker/plex/config/backups/tautulli/
|
||||||
|
tautulli-config-YYYY-MM-DD.tar.gz (3 copies retained)</pre>
|
||||||
|
</div>
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">Excludes from Plex archive</div>
|
||||||
|
<pre>Cache / Logs / Crash Reports</pre>
|
||||||
|
</div>
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">SMTP password file</div>
|
||||||
|
<pre>/etc/plex32-smtp-pass (chmod 600, owned by rufusking)</pre>
|
||||||
|
</div>
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">Restore procedure (run on plex32)</div>
|
||||||
|
<pre>cd ~/docker/plex && docker compose down
|
||||||
|
cp /mnt/plex/config/backups/plex/plex-config-YYYY-MM-DD.tar.gz /tmp/
|
||||||
|
sudo rm -rf /opt/plex/config/Library
|
||||||
|
sudo tar -xzf /tmp/plex-config-YYYY-MM-DD.tar.gz -C /opt/plex
|
||||||
|
sudo chown -R rufusking:ncshare /opt/plex/config
|
||||||
|
cd ~/docker/plex && docker compose up -d
|
||||||
|
docker ps # verify both plex and tautulli show Up
|
||||||
|
# Open http://192.168.150.45:32400/web and confirm library intact
|
||||||
|
rm /tmp/plex-config-YYYY-MM-DD.tar.gz</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SECTION 6 — LAUNCHER + SSH KEYS -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title" data-num="6">Remote launcher + SSH key authentication</div>
|
||||||
|
|
||||||
|
<p>A launcher script was created on KingDezigns001 to remotely execute PLEX32 scripts via SSH with a simple menu. SSH key authentication was configured from KingDezigns001 to all VLAN 50 servers, and from PLEX32 to NAS08, eliminating all password prompts.</p>
|
||||||
|
|
||||||
|
<div class="script-box">
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">Launcher script (KingDezigns001)</div>
|
||||||
|
<pre>~/scripts/plex32_launcher.sh</pre>
|
||||||
|
</div>
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">Desktop shortcut (KingDezigns001)</div>
|
||||||
|
<pre>~/Desktop/PLEX32-Launcher.desktop</pre>
|
||||||
|
</div>
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">Menu options</div>
|
||||||
|
<pre>1) Update Check → /usr/scripts/plex32/plex32_update_check.sh
|
||||||
|
2) Health Report → /usr/scripts/plex32/plex32_health_report.sh
|
||||||
|
3) Plex32 Backup → /usr/scripts/plex32/plex32_backup.sh</pre>
|
||||||
|
</div>
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">SSH keys configured (no password)</div>
|
||||||
|
<pre>KingDezigns001 → PLEX32 (192.168.150.45)
|
||||||
|
KingDezigns001 → NAS08 (192.168.150.35)
|
||||||
|
KingDezigns001 → NAS16 (192.168.150.40)
|
||||||
|
PLEX32 → NAS08 (192.168.150.35) # required for backup disk stats</pre>
|
||||||
|
</div>
|
||||||
|
<div class="script-row">
|
||||||
|
<div class="script-label">sudo without password (plex32) — /etc/sudoers.d/rufusking-nopasswd</div>
|
||||||
|
<pre>rufusking ALL=(ALL) NOPASSWD: /usr/bin/apt-get update, /usr/bin/fail2ban-client status, /usr/bin/fail2ban-client status sshd</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SECTION 7 — STEPS 6 & 7 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title" data-num="7">Steps 6 & 7 — Scheduled maintenance & subtitle handling</div>
|
||||||
|
|
||||||
|
<p><strong>Scheduled maintenance</strong> was already configured to run between 2:00 AM and 5:00 AM. The following library analysis tasks were confirmed enabled as scheduled tasks:</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Task</th><th>Setting</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Generate video preview thumbnails</td><td>As a scheduled task (changed from Never)</td></tr>
|
||||||
|
<tr><td>Generate chapter image thumbnails</td><td>As a scheduled task</td></tr>
|
||||||
|
<tr><td>Analyze audio tracks for loudness</td><td>As a scheduled task</td></tr>
|
||||||
|
<tr><td>Generate intro video markers</td><td>As a scheduled task and when media is added</td></tr>
|
||||||
|
<tr><td>Generate credits video markers</td><td>As a scheduled task and when media is added</td></tr>
|
||||||
|
<tr><td>Analyze audio tracks for sonic features</td><td>As a scheduled task</td></tr>
|
||||||
|
<tr><td>Generate voice activity data</td><td>As a scheduled task (changed from Never)</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin-top:1rem;"><strong>Subtitle handling:</strong> Library contains 1168 SRT (safe, no transcode) and 279 PGS image-based subtitles (burn-in forces transcode if selected). Since subtitles are used occasionally and Quick Sync handles the transcode efficiently, Option B (bulk PGS→SRT conversion) was not pursued. Subtitle Mode confirmed set to <code>Manually selected</code> — subtitles only appear when explicitly chosen, preventing accidental burn-in transcoding.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SECTION 8 — LESSONS LEARNED -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title" data-num="8">Lessons learned</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Lesson</th><th>Detail</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>GPU device passthrough requires group membership</td>
|
||||||
|
<td>Adding /dev/dri to Docker devices is not enough — the container process also needs supplementary groups matching the video (44) and render (993) GIDs on the host. Use group_add in docker-compose.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>rsync over NFS is unreliable for large Plex configs</td>
|
||||||
|
<td>Thousands of small metadata files cause rsync to hang indefinitely building the file list over NFS. Use tar locally then a single cp to NFS instead — much faster and more reliable.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>NAS08 all_squash NFS exports require correct directory ownership</td>
|
||||||
|
<td>Even sudo on PLEX32 cannot create files on NFS mounts with all_squash unless the destination directory on NAS08 is owned by www-data:ncshare with 2770 permissions.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Scripts running as non-root need sudo for apt-get and fail2ban-client</td>
|
||||||
|
<td>Both commands require root access. Grant specific NOPASSWD sudo rights via /etc/sudoers.d/ rather than running the whole script as root.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>SSH keys should be set up on all servers from the start</td>
|
||||||
|
<td>Key authentication eliminates password prompts for both interactive SSH and script-to-script SSH calls (e.g. PLEX32 → NAS08 for disk stats). Generate once per machine, copy to all targets.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Hardware transcoding silently falls back without /dev/dri access</td>
|
||||||
|
<td>Plex showed hardware transcoding as "enabled" in settings but was silently using software transcoding. Always verify with the Dashboard showing (hw) next to the codec during an active transcode.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>AAC audio across the entire library is ideal</td>
|
||||||
|
<td>TrueHD and DTS-HD MA are the common culprits that force audio transcoding on most clients. A library with 100% AAC will never trigger an audio-forced transcode regardless of client.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- SECTION 9 — DOCS TO UPDATE -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title" data-num="9">Network docs — items to update</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Item</th><th>Old value</th><th>New value</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>server_plex32.md — docker-compose.yml</td>
|
||||||
|
<td>No devices or group_add block</td>
|
||||||
|
<td>devices: /dev/dri + group_add: ["44","993"] + Tautulli service added</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>server_plex32.md — Scheduled Jobs</td>
|
||||||
|
<td>2 cron jobs (update check, health report)</td>
|
||||||
|
<td>3 cron jobs — add: <code>0 2 */3 * * /usr/scripts/plex32/plex32_backup.sh</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>server_plex32.md — new scripts section</td>
|
||||||
|
<td>No backup script</td>
|
||||||
|
<td>plex32_backup.sh — tar + NFS copy to NAS08, 3 copies retained, HTML email report</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>server_plex32.md — SSH keys</td>
|
||||||
|
<td>Not documented</td>
|
||||||
|
<td>PLEX32 has SSH key to NAS08 (192.168.150.35) — required for backup disk stats</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>server_plex32.md — sudoers</td>
|
||||||
|
<td>Not documented</td>
|
||||||
|
<td>/etc/sudoers.d/rufusking-nopasswd — apt-get update + fail2ban-client commands</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>server_plex32.md — Tautulli</td>
|
||||||
|
<td>Not present</td>
|
||||||
|
<td>Tautulli running on port 8181, config at /opt/tautulli/config, Plex down/up alerts active</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>network_index.md — KingDezigns001 tools</td>
|
||||||
|
<td>Not documented</td>
|
||||||
|
<td>plex32_launcher.sh at ~/scripts/ + PLEX32-Launcher.desktop on Desktop — SSH keys to all VLAN 50 servers</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>server_nas08.md — NFS export permissions</td>
|
||||||
|
<td>Not documented</td>
|
||||||
|
<td>/export/kingdezigns-all/Docker/plex/config — www-data:ncshare 2770, backups/plex and backups/tautulli subdirs created</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<span>KingDezigns Home Network</span>
|
||||||
|
<span>Generated June 20, 2026</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
471
plex32_backup.sh
Normal file
471
plex32_backup.sh
Normal file
|
|
@ -0,0 +1,471 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# =============================================================
|
||||||
|
# PLEX32 — Config Backup Script
|
||||||
|
# Creates tar archives of Plex and Tautulli configs
|
||||||
|
# Copies to NAS08 via NFS as single files
|
||||||
|
# Keeps last 3 backups, deletes older ones
|
||||||
|
# Sends HTML email report in KingDezigns style
|
||||||
|
# Location: /usr/scripts/plex32/plex32_backup.sh
|
||||||
|
# Schedule: Every 3 days at 2:00 AM (cron)
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# --- Config ---
|
||||||
|
HOSTNAME="plex32"
|
||||||
|
PLEX_SRC="/opt/plex/config"
|
||||||
|
TAUTULLI_SRC="/opt/tautulli/config"
|
||||||
|
NFS_DEST="/mnt/plex/config"
|
||||||
|
TAUTULLI_LOC="backups/tautulli"
|
||||||
|
PLEX_LOC="backups/plex"
|
||||||
|
|
||||||
|
TMP_DIR="/tmp/plex32-backup"
|
||||||
|
LOG_DIR="/var/log/plex32-backup"
|
||||||
|
LOG_FILE="$LOG_DIR/backup-$(date +%Y%m%d).log"
|
||||||
|
RETENTION_COUNT=3
|
||||||
|
COMPOSE_DIR="/home/rufusking/docker/plex"
|
||||||
|
|
||||||
|
DATESTAMP=$(date '+%Y-%m-%d')
|
||||||
|
TIMESTAMP=$(date '+%B %d, %Y at %I:%M %p')
|
||||||
|
|
||||||
|
PLEX_ARCHIVE="plex-config-${DATESTAMP}.tar.gz"
|
||||||
|
TAUTULLI_ARCHIVE="tautulli-config-${DATESTAMP}.tar.gz"
|
||||||
|
|
||||||
|
SMTP_SERVER="smtppro.zoho.com"
|
||||||
|
SMTP_PORT=465
|
||||||
|
SMTP_USER="rufus.king@kingdezigns.com"
|
||||||
|
SMTP_PASS="$(cat /etc/plex32-smtp-pass 2>/dev/null)"
|
||||||
|
EMAIL_TO="rufus.king@kingdezigns.com"
|
||||||
|
EMAIL_FROM="rufus.king@kingdezigns.com"
|
||||||
|
|
||||||
|
# --- Setup ---
|
||||||
|
mkdir -p "$LOG_DIR" "$TMP_DIR"
|
||||||
|
START_TIME=$(date +%s)
|
||||||
|
|
||||||
|
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] PLEX32 backup started"
|
||||||
|
|
||||||
|
# --- Check NFS mount ---
|
||||||
|
if ! mountpoint -q "$NFS_DEST"; then
|
||||||
|
echo "[ERROR] NFS mount not available: $NFS_DEST"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Stop containers ---
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Stopping Plex and Tautulli containers..."
|
||||||
|
cd "$COMPOSE_DIR" && docker compose down
|
||||||
|
STOP_STATUS=$?
|
||||||
|
if [ $STOP_STATUS -ne 0 ]; then
|
||||||
|
echo "[ERROR] Failed to stop containers cleanly"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Tar Plex config ---
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Creating Plex config archive..."
|
||||||
|
PLEX_START=$(date +%s)
|
||||||
|
tar -czf "$TMP_DIR/$PLEX_ARCHIVE" \
|
||||||
|
--exclude="config/Library/Application Support/Plex Media Server/Cache" \
|
||||||
|
--exclude="config/Library/Application Support/Plex Media Server/Logs" \
|
||||||
|
--exclude="config/Library/Application Support/Plex Media Server/Crash Reports" \
|
||||||
|
-C /opt/plex config 2>&1
|
||||||
|
PLEX_TAR_STATUS=$?
|
||||||
|
PLEX_END=$(date +%s)
|
||||||
|
PLEX_DURATION=$((PLEX_END - PLEX_START))
|
||||||
|
|
||||||
|
if [ $PLEX_TAR_STATUS -eq 0 ]; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Plex archive created in ${PLEX_DURATION}s"
|
||||||
|
else
|
||||||
|
echo "[ERROR] Plex tar failed with status $PLEX_TAR_STATUS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Tar Tautulli config ---
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Creating Tautulli config archive..."
|
||||||
|
TAUTULLI_START=$(date +%s)
|
||||||
|
tar -czf "$TMP_DIR/$TAUTULLI_ARCHIVE" \
|
||||||
|
-C /opt/tautulli config 2>&1
|
||||||
|
TAUTULLI_TAR_STATUS=$?
|
||||||
|
TAUTULLI_END=$(date +%s)
|
||||||
|
TAUTULLI_DURATION=$((TAUTULLI_END - TAUTULLI_START))
|
||||||
|
|
||||||
|
if [ $TAUTULLI_TAR_STATUS -eq 0 ]; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tautulli archive created in ${TAUTULLI_DURATION}s"
|
||||||
|
else
|
||||||
|
echo "[ERROR] Tautulli tar failed with status $TAUTULLI_TAR_STATUS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Restart containers ---
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Restarting containers..."
|
||||||
|
cd "$COMPOSE_DIR" && docker compose up -d
|
||||||
|
START_STATUS=$?
|
||||||
|
if [ $START_STATUS -eq 0 ]; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Containers restarted successfully"
|
||||||
|
else
|
||||||
|
echo "[ERROR] Failed to restart containers on first attempt — retrying in 15 seconds..."
|
||||||
|
sleep 15
|
||||||
|
cd "$COMPOSE_DIR" && docker compose up -d
|
||||||
|
START_STATUS=$?
|
||||||
|
if [ $START_STATUS -eq 0 ]; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Containers restarted successfully on retry"
|
||||||
|
else
|
||||||
|
echo "[ERROR] Containers failed to restart after retry — manual intervention required"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Verify containers are actually running ---
|
||||||
|
sleep 5
|
||||||
|
PLEX_RUNNING=$(docker ps --filter "name=plex" --filter "status=running" --format "{{.Names}}" | grep -c "^plex$")
|
||||||
|
TAUTULLI_RUNNING=$(docker ps --filter "name=tautulli" --filter "status=running" --format "{{.Names}}" | grep -c "^tautulli$")
|
||||||
|
|
||||||
|
if [ "$PLEX_RUNNING" -eq 1 ]; then
|
||||||
|
PLEX_CONTAINER_STATUS="✅ RUNNING"
|
||||||
|
PLEX_CONTAINER_COLOR="#166534"
|
||||||
|
PLEX_CONTAINER_BG="#f0fdf4"
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Plex container verified running"
|
||||||
|
else
|
||||||
|
PLEX_CONTAINER_STATUS="❌ NOT RUNNING — MANUAL INTERVENTION REQUIRED"
|
||||||
|
PLEX_CONTAINER_COLOR="#b91c1c"
|
||||||
|
PLEX_CONTAINER_BG="#fff1f2"
|
||||||
|
echo "[ERROR] Plex container is NOT running after backup"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$TAUTULLI_RUNNING" -eq 1 ]; then
|
||||||
|
TAUTULLI_CONTAINER_STATUS="✅ RUNNING"
|
||||||
|
TAUTULLI_CONTAINER_COLOR="#166534"
|
||||||
|
TAUTULLI_CONTAINER_BG="#f0fdf4"
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tautulli container verified running"
|
||||||
|
else
|
||||||
|
TAUTULLI_CONTAINER_STATUS="❌ NOT RUNNING — MANUAL INTERVENTION REQUIRED"
|
||||||
|
TAUTULLI_CONTAINER_COLOR="#b91c1c"
|
||||||
|
TAUTULLI_CONTAINER_BG="#fff1f2"
|
||||||
|
echo "[ERROR] Tautulli container is NOT running after backup"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Copy Plex archive to NAS08 ---
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Copying Plex archive to NAS08..."
|
||||||
|
COPY_PLEX_START=$(date +%s)
|
||||||
|
cp "$TMP_DIR/$PLEX_ARCHIVE" "$NFS_DEST/$PLEX_LOC/"
|
||||||
|
PLEX_COPY_STATUS=$?
|
||||||
|
COPY_PLEX_END=$(date +%s)
|
||||||
|
PLEX_COPY_DURATION=$((COPY_PLEX_END - COPY_PLEX_START))
|
||||||
|
|
||||||
|
if [ $PLEX_COPY_STATUS -eq 0 ]; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Plex archive copied in ${PLEX_COPY_DURATION}s"
|
||||||
|
else
|
||||||
|
echo "[ERROR] Plex archive copy failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Copy Tautulli archive to NAS08 ---
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Copying Tautulli archive to NAS08..."
|
||||||
|
COPY_TAUTULLI_START=$(date +%s)
|
||||||
|
cp "$TMP_DIR/$TAUTULLI_ARCHIVE" "$NFS_DEST/$TAUTULLI_LOC/"
|
||||||
|
TAUTULLI_COPY_STATUS=$?
|
||||||
|
COPY_TAUTULLI_END=$(date +%s)
|
||||||
|
TAUTULLI_COPY_DURATION=$((COPY_TAUTULLI_END - COPY_TAUTULLI_START))
|
||||||
|
|
||||||
|
if [ $TAUTULLI_COPY_STATUS -eq 0 ]; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tautulli archive copied in ${TAUTULLI_COPY_DURATION}s"
|
||||||
|
else
|
||||||
|
echo "[ERROR] Tautulli archive copy failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Rotate old backups — keep last 3 ---
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Rotating old backups..."
|
||||||
|
ls -t "$NFS_DEST/$PLEX_LOC"/plex-config-*.tar.gz 2>/dev/null | tail -n +$((RETENTION_COUNT + 1)) | xargs rm -f
|
||||||
|
ls -t "$NFS_DEST/$TAUTULLI_LOC"/tautulli-config-*.tar.gz 2>/dev/null | tail -n +$((RETENTION_COUNT + 1)) | xargs rm -f
|
||||||
|
|
||||||
|
# --- Cleanup temp files ---
|
||||||
|
rm -f "$TMP_DIR/$PLEX_ARCHIVE" "$TMP_DIR/$TAUTULLI_ARCHIVE"
|
||||||
|
|
||||||
|
# --- Sizes and storage ---
|
||||||
|
PLEX_ARCHIVE_SIZE=$(ls -lh "$NFS_DEST/$PLEX_LOC/$PLEX_ARCHIVE" 2>/dev/null | awk '{print $5}')
|
||||||
|
TAUTULLI_ARCHIVE_SIZE=$(ls -lh "$NFS_DEST/$TAUTULLI_LOC/$TAUTULLI_ARCHIVE" 2>/dev/null | awk '{print $5}')
|
||||||
|
PLEX_BACKUP_COUNT=$(ls "$NFS_DEST/$PLEX_LOC"/plex-config-*.tar.gz 2>/dev/null | wc -l)
|
||||||
|
TAUTULLI_BACKUP_COUNT=$(ls "$NFS_DEST/$TAUTULLI_LOC"/tautulli-config-*.tar.gz 2>/dev/null | wc -l)
|
||||||
|
|
||||||
|
# Get accurate NAS08 disk stats via SSH (avoids NFS dataset quota confusion)
|
||||||
|
NAS08_DISK_INFO=$(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 rufusking@192.168.150.35 \
|
||||||
|
"df -h /export/kingdezigns-all | awk 'NR==2{print \$3\" used / \"\$2\" total / \"\$4\" free\"}'" 2>/dev/null)
|
||||||
|
if [ -z "$NAS08_DISK_INFO" ]; then
|
||||||
|
# Fallback to NFS mount stats if SSH fails
|
||||||
|
NAS08_DISK_INFO=$(df -h "$NFS_DEST" 2>/dev/null | awk 'NR==2{print $3" used / "$2" total / "$4" free"}')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# List of current backups on NAS08
|
||||||
|
PLEX_BACKUP_LIST=$(ls -lh "$NFS_DEST/$PLEX_LOC"/plex-config-*.tar.gz 2>/dev/null | \
|
||||||
|
awk '{print $NF" ("$5")"}' | xargs -I{} basename {} 2>/dev/null | \
|
||||||
|
while read -r line; do echo "$line"; done)
|
||||||
|
TAUTULLI_BACKUP_LIST=$(ls -lh "$NFS_DEST/$TAUTULLI_LOC"/tautulli-config-*.tar.gz 2>/dev/null | \
|
||||||
|
awk '{print $NF" ("$5")"}' | xargs -I{} basename {} 2>/dev/null | \
|
||||||
|
while read -r line; do echo "$line"; done)
|
||||||
|
|
||||||
|
# Build backup list HTML rows
|
||||||
|
PLEX_LIST_ROWS=""
|
||||||
|
while IFS= read -r f; do
|
||||||
|
SIZE=$(ls -lh "$NFS_DEST/$PLEX_LOC/$f" 2>/dev/null | awk '{print $5}')
|
||||||
|
PLEX_LIST_ROWS="${PLEX_LIST_ROWS}<tr><td style='font-size:12px;font-family:monospace;color:#374151;padding:6px 18px;border-top:1px solid #e5e7eb;'>$f</td><td style='font-size:12px;color:#6b7280;padding:6px 18px;border-top:1px solid #e5e7eb;'>$SIZE</td></tr>"
|
||||||
|
done < <(ls "$NFS_DEST/$PLEX_LOC"/plex-config-*.tar.gz 2>/dev/null | xargs -I{} basename {})
|
||||||
|
|
||||||
|
TAUTULLI_LIST_ROWS=""
|
||||||
|
while IFS= read -r f; do
|
||||||
|
SIZE=$(ls -lh "$NFS_DEST/$TAUTULLI_LOC/$f" 2>/dev/null | awk '{print $5}')
|
||||||
|
TAUTULLI_LIST_ROWS="${TAUTULLI_LIST_ROWS}<tr><td style='font-size:12px;font-family:monospace;color:#374151;padding:6px 18px;border-top:1px solid #e5e7eb;'>$f</td><td style='font-size:12px;color:#6b7280;padding:6px 18px;border-top:1px solid #e5e7eb;'>$SIZE</td></tr>"
|
||||||
|
done < <(ls "$NFS_DEST/$TAUTULLI_LOC"/tautulli-config-*.tar.gz 2>/dev/null | xargs -I{} basename {})
|
||||||
|
|
||||||
|
# --- Total duration ---
|
||||||
|
END_TIME=$(date +%s)
|
||||||
|
TOTAL_DURATION=$((END_TIME - START_TIME))
|
||||||
|
TOTAL_MIN=$((TOTAL_DURATION / 60))
|
||||||
|
TOTAL_SEC=$((TOTAL_DURATION % 60))
|
||||||
|
|
||||||
|
# --- Overall status ---
|
||||||
|
PLEX_STATUS=$((PLEX_TAR_STATUS + PLEX_COPY_STATUS))
|
||||||
|
TAUTULLI_STATUS=$((TAUTULLI_TAR_STATUS + TAUTULLI_COPY_STATUS))
|
||||||
|
CONTAINER_STATUS=$((PLEX_RUNNING == 1 && TAUTULLI_RUNNING == 1 ? 0 : 1))
|
||||||
|
|
||||||
|
if [ $PLEX_STATUS -eq 0 ] && [ $TAUTULLI_STATUS -eq 0 ] && [ $CONTAINER_STATUS -eq 0 ]; then
|
||||||
|
OVERALL_STATUS="SUCCESS"
|
||||||
|
STATUS_COLOR="#1a7f4b"
|
||||||
|
BANNER_ICON="✅"
|
||||||
|
BANNER_MSG="Backup completed successfully — Plex and Tautulli configs archived to NAS08"
|
||||||
|
BADGE_COLOR="#1a7f4b"
|
||||||
|
elif [ $PLEX_STATUS -ne 0 ] && [ $TAUTULLI_STATUS -ne 0 ]; then
|
||||||
|
OVERALL_STATUS="FAILED"
|
||||||
|
STATUS_COLOR="#b91c1c"
|
||||||
|
BANNER_ICON="🔴"
|
||||||
|
BANNER_MSG="Backup failed — both archive operations encountered errors"
|
||||||
|
BADGE_COLOR="#b91c1c"
|
||||||
|
else
|
||||||
|
OVERALL_STATUS="PARTIAL"
|
||||||
|
STATUS_COLOR="#b45309"
|
||||||
|
BANNER_ICON="⚠️"
|
||||||
|
BANNER_MSG="Backup partially completed — review individual results below"
|
||||||
|
BADGE_COLOR="#b45309"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Status labels ---
|
||||||
|
if [ $PLEX_STATUS -eq 0 ]; then
|
||||||
|
PLEX_LABEL="✅ SUCCESS"
|
||||||
|
PLEX_ROW_BG="#f0fdf4"
|
||||||
|
PLEX_TEXT_COLOR="#166534"
|
||||||
|
else
|
||||||
|
PLEX_LABEL="❌ FAILED"
|
||||||
|
PLEX_ROW_BG="#fff1f2"
|
||||||
|
PLEX_TEXT_COLOR="#b91c1c"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $TAUTULLI_STATUS -eq 0 ]; then
|
||||||
|
TAUTULLI_LABEL="✅ SUCCESS"
|
||||||
|
TAUTULLI_ROW_BG="#f0fdf4"
|
||||||
|
TAUTULLI_TEXT_COLOR="#166534"
|
||||||
|
else
|
||||||
|
TAUTULLI_LABEL="❌ FAILED"
|
||||||
|
TAUTULLI_ROW_BG="#fff1f2"
|
||||||
|
TAUTULLI_TEXT_COLOR="#b91c1c"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Send email ---
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Sending email report..."
|
||||||
|
|
||||||
|
python3 << PYEOF
|
||||||
|
import smtplib, ssl
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
smtp_server = "$SMTP_SERVER"
|
||||||
|
smtp_port = $SMTP_PORT
|
||||||
|
smtp_user = "$SMTP_USER"
|
||||||
|
smtp_pass = "$SMTP_PASS"
|
||||||
|
email_to = "$EMAIL_TO"
|
||||||
|
email_from = "$EMAIL_FROM"
|
||||||
|
|
||||||
|
subject = "PLEX32 Backup — $OVERALL_STATUS — $DATESTAMP"
|
||||||
|
|
||||||
|
html = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body style="margin:0;padding:20px;background:#f1f5f9;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
|
||||||
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||||||
|
<tr><td align="center">
|
||||||
|
<table width="720" cellspacing="0" cellpadding="0" style="max-width:720px;width:100%;">
|
||||||
|
|
||||||
|
<!-- HEADER -->
|
||||||
|
<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;">Config Backup</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 — Lab/Servers</div>
|
||||||
|
</td>
|
||||||
|
<td align="right">
|
||||||
|
<div style="background:$BADGE_COLOR;color:white;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:700;">$OVERALL_STATUS</div>
|
||||||
|
</td>
|
||||||
|
</tr></table>
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<!-- STATUS BANNER -->
|
||||||
|
<tr><td style="background:$STATUS_COLOR;padding:12px 32px;">
|
||||||
|
<span style="color:white;font-size:14px;font-weight:600;">$BANNER_ICON $BANNER_MSG</span>
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<!-- BODY -->
|
||||||
|
<tr><td style="background:white;border:1px solid #e5e7eb;padding:28px 32px;">
|
||||||
|
|
||||||
|
<!-- STAT CARDS -->
|
||||||
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:4px;">
|
||||||
|
<div style="padding:12px;background:#f9fafb;border-radius:6px;text-align:center;">
|
||||||
|
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Duration</div>
|
||||||
|
<div style="font-size:20px;font-weight:700;color:#111827;margin-top:2px;">${TOTAL_MIN}m ${TOTAL_SEC}s</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="padding:4px;">
|
||||||
|
<div style="padding:12px;background:#f9fafb;border-radius:6px;text-align:center;">
|
||||||
|
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Plex Archive</div>
|
||||||
|
<div style="font-size:20px;font-weight:700;color:#111827;margin-top:2px;">$PLEX_ARCHIVE_SIZE</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="padding:4px;">
|
||||||
|
<div style="padding:12px;background:#f9fafb;border-radius:6px;text-align:center;">
|
||||||
|
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Tautulli Archive</div>
|
||||||
|
<div style="font-size:20px;font-weight:700;color:#111827;margin-top:2px;">$TAUTULLI_ARCHIVE_SIZE</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="padding:4px;">
|
||||||
|
<div style="padding:12px;background:#f9fafb;border-radius:6px;text-align:center;">
|
||||||
|
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Plex Copies</div>
|
||||||
|
<div style="font-size:20px;font-weight:700;color:#111827;margin-top:2px;">$PLEX_BACKUP_COUNT / $RETENTION_COUNT</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- RESULTS TABLE -->
|
||||||
|
<div style="margin-top:24px;background:white;border-radius:10px;border:1px solid #e5e7eb;overflow:hidden;">
|
||||||
|
<div style="font-size:13px;font-weight:700;color:#374151;padding:14px 18px;border-bottom:1px solid #e5e7eb;text-transform:uppercase;letter-spacing:.5px;">📋 Backup Results</div>
|
||||||
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||||||
|
<tr style="background:#f9fafb;">
|
||||||
|
<th style="text-align:left;font-size:11px;color:#6b7280;padding:8px 18px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Component</th>
|
||||||
|
<th style="text-align:left;font-size:11px;color:#6b7280;padding:8px 18px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Archive</th>
|
||||||
|
<th style="text-align:left;font-size:11px;color:#6b7280;padding:8px 18px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Duration</th>
|
||||||
|
<th style="text-align:left;font-size:11px;color:#6b7280;padding:8px 18px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Status</th>
|
||||||
|
</tr>
|
||||||
|
<tr style="background:$PLEX_ROW_BG;">
|
||||||
|
<td style="font-size:13px;font-weight:600;color:#111827;padding:10px 18px;border-top:1px solid #e5e7eb;">🎬 Plex Config</td>
|
||||||
|
<td style="font-size:12px;color:#374151;padding:10px 18px;border-top:1px solid #e5e7eb;font-family:monospace;">$PLEX_ARCHIVE</td>
|
||||||
|
<td style="font-size:13px;color:#374151;padding:10px 18px;border-top:1px solid #e5e7eb;">${PLEX_DURATION}s tar + ${PLEX_COPY_DURATION}s copy</td>
|
||||||
|
<td style="font-size:13px;font-weight:700;padding:10px 18px;border-top:1px solid #e5e7eb;color:$PLEX_TEXT_COLOR;">$PLEX_LABEL</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="background:$TAUTULLI_ROW_BG;">
|
||||||
|
<td style="font-size:13px;font-weight:600;color:#111827;padding:10px 18px;border-top:1px solid #e5e7eb;">📊 Tautulli Config</td>
|
||||||
|
<td style="font-size:12px;color:#374151;padding:10px 18px;border-top:1px solid #e5e7eb;font-family:monospace;">$TAUTULLI_ARCHIVE</td>
|
||||||
|
<td style="font-size:13px;color:#374151;padding:10px 18px;border-top:1px solid #e5e7eb;">${TAUTULLI_DURATION}s tar + ${TAUTULLI_COPY_DURATION}s copy</td>
|
||||||
|
<td style="font-size:13px;font-weight:700;padding:10px 18px;border-top:1px solid #e5e7eb;color:$TAUTULLI_TEXT_COLOR;">$TAUTULLI_LABEL</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CONTAINER STATUS -->
|
||||||
|
<div style="margin-top:20px;background:white;border-radius:10px;border:1px solid #e5e7eb;overflow:hidden;">
|
||||||
|
<div style="font-size:13px;font-weight:700;color:#374151;padding:14px 18px;border-bottom:1px solid #e5e7eb;text-transform:uppercase;letter-spacing:.5px;">🐳 Container Status After Backup</div>
|
||||||
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||||||
|
<tr style="background:$PLEX_CONTAINER_BG;">
|
||||||
|
<td style="font-size:13px;font-weight:600;color:#111827;padding:10px 18px;border-top:1px solid #e5e7eb;">🎬 Plex</td>
|
||||||
|
<td style="font-size:13px;font-weight:700;padding:10px 18px;border-top:1px solid #e5e7eb;color:$PLEX_CONTAINER_COLOR;">$PLEX_CONTAINER_STATUS</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="background:$TAUTULLI_CONTAINER_BG;">
|
||||||
|
<td style="font-size:13px;font-weight:600;color:#111827;padding:10px 18px;border-top:1px solid #e5e7eb;">📊 Tautulli</td>
|
||||||
|
<td style="font-size:13px;font-weight:700;padding:10px 18px;border-top:1px solid #e5e7eb;color:$TAUTULLI_CONTAINER_COLOR;">$TAUTULLI_CONTAINER_STATUS</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CURRENT BACKUPS ON NAS08 -->
|
||||||
|
<div style="margin-top:20px;background:white;border-radius:10px;border:1px solid #e5e7eb;overflow:hidden;">
|
||||||
|
<div style="font-size:13px;font-weight:700;color:#374151;padding:14px 18px;border-bottom:1px solid #e5e7eb;text-transform:uppercase;letter-spacing:.5px;">🗄️ Archives on NAS08</div>
|
||||||
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||||||
|
<tr style="background:#f9fafb;">
|
||||||
|
<th style="text-align:left;font-size:11px;color:#6b7280;padding:8px 18px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;" colspan="2">Plex Backups (max $RETENTION_COUNT kept)</th>
|
||||||
|
</tr>
|
||||||
|
$PLEX_LIST_ROWS
|
||||||
|
<tr style="background:#f9fafb;">
|
||||||
|
<th style="text-align:left;font-size:11px;color:#6b7280;padding:8px 18px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;border-top:2px solid #e5e7eb;" colspan="2">Tautulli Backups (max $RETENTION_COUNT kept)</th>
|
||||||
|
</tr>
|
||||||
|
$TAUTULLI_LIST_ROWS
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DESTINATION INFO -->
|
||||||
|
<div style="margin-top:16px;padding:12px 14px;background:#f8fafc;border-radius:6px;border:1px solid #e5e7eb;">
|
||||||
|
<span style="font-size:12px;font-weight:600;color:#374151;text-transform:uppercase;letter-spacing:.5px;">Backup Destination</span>
|
||||||
|
<span style="margin-left:10px;font-size:13px;color:#374151;">NAS08 (192.168.150.35) — $NAS08_DISK_INFO</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RESTORE INSTRUCTIONS -->
|
||||||
|
<div style="margin-top:16px;background:white;border-radius:10px;border:1px solid #fac775;overflow:hidden;">
|
||||||
|
<div style="font-size:13px;font-weight:700;color:#633806;padding:14px 18px;border-bottom:1px solid #fac775;background:#fffbeb;text-transform:uppercase;letter-spacing:.5px;">💡 How to Restore Plex Config</div>
|
||||||
|
<div style="padding:16px 18px;">
|
||||||
|
|
||||||
|
<div style="font-size:12px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;">Machine: plex32 (192.168.150.45)</div>
|
||||||
|
|
||||||
|
<div style="font-size:12px;color:#6b7280;margin-bottom:4px;margin-top:12px;">Step 1 — Stop Plex and Tautulli</div>
|
||||||
|
<pre style="margin:0;background:#1e293b;color:#e2e8f0;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;">cd ~/docker/plex && docker compose down</pre>
|
||||||
|
|
||||||
|
<div style="font-size:12px;color:#6b7280;margin-bottom:4px;margin-top:12px;">Step 2 — Copy archive from NAS08 to PLEX32</div>
|
||||||
|
<pre style="margin:0;background:#1e293b;color:#e2e8f0;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;">cp /mnt/plex/config/$PLEX_LOC/$PLEX_ARCHIVE /tmp/</pre>
|
||||||
|
|
||||||
|
<div style="font-size:12px;color:#6b7280;margin-bottom:4px;margin-top:12px;">Step 3 — Clear current Plex config</div>
|
||||||
|
<pre style="margin:0;background:#1e293b;color:#e2e8f0;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;">sudo rm -rf /opt/plex/config/Library</pre>
|
||||||
|
|
||||||
|
<div style="font-size:12px;color:#6b7280;margin-bottom:4px;margin-top:12px;">Step 4 — Extract the backup</div>
|
||||||
|
<pre style="margin:0;background:#1e293b;color:#e2e8f0;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;">sudo tar -xzf /tmp/$PLEX_ARCHIVE -C /opt/plex
|
||||||
|
sudo chown -R rufusking:ncshare /opt/plex/config</pre>
|
||||||
|
|
||||||
|
<div style="font-size:12px;color:#6b7280;margin-bottom:4px;margin-top:12px;">Step 5 — Restart containers</div>
|
||||||
|
<pre style="margin:0;background:#1e293b;color:#e2e8f0;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;">cd ~/docker/plex && docker compose up -d</pre>
|
||||||
|
|
||||||
|
<div style="font-size:12px;color:#6b7280;margin-bottom:4px;margin-top:12px;">Step 6 — Verify both containers are running</div>
|
||||||
|
<pre style="margin:0;background:#1e293b;color:#e2e8f0;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;">docker ps</pre>
|
||||||
|
<div style="font-size:12px;color:#6b7280;margin-top:6px;">Both <code style="background:#f1f5f9;padding:1px 5px;border-radius:3px;">plex</code> and <code style="background:#f1f5f9;padding:1px 5px;border-radius:3px;">tautulli</code> should show status <strong>Up</strong>. Then open <code style="background:#f1f5f9;padding:1px 5px;border-radius:3px;">http://192.168.150.45:32400/web</code> and confirm your library and watch history are intact.</div>
|
||||||
|
|
||||||
|
<div style="font-size:12px;color:#6b7280;margin-bottom:4px;margin-top:16px;">Step 7 — Cleanup temp file</div>
|
||||||
|
<pre style="margin:0;background:#1e293b;color:#e2e8f0;padding:10px 14px;border-radius:6px;font-size:12px;line-height:1.6;">rm /tmp/$PLEX_ARCHIVE</pre>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<!-- FOOTER -->
|
||||||
|
<tr><td style="background:#0f172a;border-radius:0 0 10px 10px;padding:16px 32px;text-align:center;">
|
||||||
|
<span style="font-size:11px;color:#64748b;">Config Backup | plex32 | KingDezigns Infrastructure</span>
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
msg = MIMEMultipart("alternative")
|
||||||
|
msg["Subject"] = subject
|
||||||
|
msg["From"] = email_from
|
||||||
|
msg["To"] = email_to
|
||||||
|
msg.attach(MIMEText(html, "html"))
|
||||||
|
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
with smtplib.SMTP_SSL(smtp_server, smtp_port, context=ctx, timeout=15) as s:
|
||||||
|
s.login(smtp_user, smtp_pass)
|
||||||
|
s.sendmail(email_from, [email_to], msg.as_string())
|
||||||
|
print("Email sent successfully")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
EMAIL_STATUS=$?
|
||||||
|
if [ $EMAIL_STATUS -eq 0 ]; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Email sent successfully"
|
||||||
|
else
|
||||||
|
echo "[ERROR] Email send failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Cleanup old logs ---
|
||||||
|
find "$LOG_DIR" -name "backup-*.log" -mtime +90 -delete 2>/dev/null
|
||||||
|
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup complete — Status: $OVERALL_STATUS | Duration: ${TOTAL_MIN}m ${TOTAL_SEC}s"
|
||||||
365
plex32_backup2.sh
Normal file
365
plex32_backup2.sh
Normal file
|
|
@ -0,0 +1,365 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# =============================================================
|
||||||
|
# PLEX32 — Config Backup Script
|
||||||
|
# Creates tar archives of Plex and Tautulli configs
|
||||||
|
# Copies to NAS08 via NFS as single files
|
||||||
|
# Keeps last 3 backups, deletes older ones
|
||||||
|
# Sends HTML email report in KingDezigns style
|
||||||
|
# Location: /usr/scripts/plex32/plex32_backup.sh
|
||||||
|
# Schedule: Every 3 days at 2:00 AM (cron)
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# --- Config ---
|
||||||
|
HOSTNAME="plex32"
|
||||||
|
PLEX_SRC="/opt/plex/config"
|
||||||
|
TAUTULLI_SRC="/opt/tautulli/config"
|
||||||
|
NFS_DEST="/mnt/plex/config"
|
||||||
|
TAUTULLI_LOC="backups/tautulli"
|
||||||
|
PLEX_LOC="backups/plex"
|
||||||
|
|
||||||
|
TMP_DIR="/tmp/plex32-backup"
|
||||||
|
LOG_DIR="/var/log/plex32-backup"
|
||||||
|
LOG_FILE="$LOG_DIR/backup-$(date +%Y%m%d).log"
|
||||||
|
RETENTION_COUNT=3
|
||||||
|
COMPOSE_DIR="/home/rufusking/docker/plex"
|
||||||
|
|
||||||
|
DATESTAMP=$(date '+%Y-%m-%d')
|
||||||
|
TIMESTAMP=$(date '+%B %d, %Y at %I:%M %p')
|
||||||
|
|
||||||
|
PLEX_ARCHIVE="plex-config-${DATESTAMP}.tar.gz"
|
||||||
|
TAUTULLI_ARCHIVE="tautulli-config-${DATESTAMP}.tar.gz"
|
||||||
|
|
||||||
|
SMTP_SERVER="smtppro.zoho.com"
|
||||||
|
SMTP_PORT=465
|
||||||
|
SMTP_USER="rufus.king@kingdezigns.com"
|
||||||
|
SMTP_PASS="$(cat /etc/plex32-smtp-pass 2>/dev/null)"
|
||||||
|
EMAIL_TO="rufus.king@kingdezigns.com"
|
||||||
|
EMAIL_FROM="rufus.king@kingdezigns.com"
|
||||||
|
|
||||||
|
# --- Setup ---
|
||||||
|
mkdir -p "$LOG_DIR" "$TMP_DIR"
|
||||||
|
START_TIME=$(date +%s)
|
||||||
|
|
||||||
|
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] PLEX32 backup started"
|
||||||
|
|
||||||
|
# --- Check NFS mount ---
|
||||||
|
if ! mountpoint -q "$NFS_DEST"; then
|
||||||
|
echo "[ERROR] NFS mount not available: $NFS_DEST"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Stop containers ---
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Stopping Plex and Tautulli containers..."
|
||||||
|
cd "$COMPOSE_DIR" && docker compose down
|
||||||
|
STOP_STATUS=$?
|
||||||
|
if [ $STOP_STATUS -ne 0 ]; then
|
||||||
|
echo "[ERROR] Failed to stop containers cleanly"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Tar Plex config ---
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Creating Plex config archive..."
|
||||||
|
PLEX_START=$(date +%s)
|
||||||
|
tar -czf "$TMP_DIR/$PLEX_ARCHIVE" \
|
||||||
|
--exclude="config/Library/Application Support/Plex Media Server/Cache" \
|
||||||
|
--exclude="config/Library/Application Support/Plex Media Server/Logs" \
|
||||||
|
--exclude="config/Library/Application Support/Plex Media Server/Crash Reports" \
|
||||||
|
-C /opt/plex config 2>&1
|
||||||
|
PLEX_TAR_STATUS=$?
|
||||||
|
PLEX_END=$(date +%s)
|
||||||
|
PLEX_DURATION=$((PLEX_END - PLEX_START))
|
||||||
|
|
||||||
|
if [ $PLEX_TAR_STATUS -eq 0 ]; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Plex archive created in ${PLEX_DURATION}s"
|
||||||
|
else
|
||||||
|
echo "[ERROR] Plex tar failed with status $PLEX_TAR_STATUS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Tar Tautulli config ---
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Creating Tautulli config archive..."
|
||||||
|
TAUTULLI_START=$(date +%s)
|
||||||
|
tar -czf "$TMP_DIR/$TAUTULLI_ARCHIVE" \
|
||||||
|
-C /opt/tautulli config 2>&1
|
||||||
|
TAUTULLI_TAR_STATUS=$?
|
||||||
|
TAUTULLI_END=$(date +%s)
|
||||||
|
TAUTULLI_DURATION=$((TAUTULLI_END - TAUTULLI_START))
|
||||||
|
|
||||||
|
if [ $TAUTULLI_TAR_STATUS -eq 0 ]; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tautulli archive created in ${TAUTULLI_DURATION}s"
|
||||||
|
else
|
||||||
|
echo "[ERROR] Tautulli tar failed with status $TAUTULLI_TAR_STATUS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Restart containers ---
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Restarting containers..."
|
||||||
|
cd "$COMPOSE_DIR" && docker compose up -d
|
||||||
|
START_STATUS=$?
|
||||||
|
if [ $START_STATUS -eq 0 ]; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Containers restarted successfully"
|
||||||
|
else
|
||||||
|
echo "[ERROR] Failed to restart containers — manual intervention required"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Copy Plex archive to NAS08 ---
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Copying Plex archive to NAS08..."
|
||||||
|
COPY_PLEX_START=$(date +%s)
|
||||||
|
cp "$TMP_DIR/$PLEX_ARCHIVE" "$NFS_DEST/$PLEX_LOC/"
|
||||||
|
PLEX_COPY_STATUS=$?
|
||||||
|
COPY_PLEX_END=$(date +%s)
|
||||||
|
PLEX_COPY_DURATION=$((COPY_PLEX_END - COPY_PLEX_START))
|
||||||
|
|
||||||
|
if [ $PLEX_COPY_STATUS -eq 0 ]; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Plex archive copied in ${PLEX_COPY_DURATION}s"
|
||||||
|
else
|
||||||
|
echo "[ERROR] Plex archive copy failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Copy Tautulli archive to NAS08 ---
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Copying Tautulli archive to NAS08..."
|
||||||
|
COPY_TAUTULLI_START=$(date +%s)
|
||||||
|
cp "$TMP_DIR/$TAUTULLI_ARCHIVE" "$NFS_DEST/$TAUTULLI_LOC/"
|
||||||
|
TAUTULLI_COPY_STATUS=$?
|
||||||
|
COPY_TAUTULLI_END=$(date +%s)
|
||||||
|
TAUTULLI_COPY_DURATION=$((COPY_TAUTULLI_END - COPY_TAUTULLI_START))
|
||||||
|
|
||||||
|
if [ $TAUTULLI_COPY_STATUS -eq 0 ]; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tautulli archive copied in ${TAUTULLI_COPY_DURATION}s"
|
||||||
|
else
|
||||||
|
echo "[ERROR] Tautulli archive copy failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Rotate old backups — keep last 3 ---
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Rotating old backups..."
|
||||||
|
ls -t "$NFS_DEST/$PLEX_LOC"/plex-config-*.tar.gz 2>/dev/null | tail -n +$((RETENTION_COUNT + 1)) | xargs rm -f
|
||||||
|
ls -t "$NFS_DEST/$TAUTULLI_LOC"/tautulli-config-*.tar.gz 2>/dev/null | tail -n +$((RETENTION_COUNT + 1)) | xargs rm -f
|
||||||
|
|
||||||
|
# --- Cleanup temp files ---
|
||||||
|
rm -f "$TMP_DIR/$PLEX_ARCHIVE" "$TMP_DIR/$TAUTULLI_ARCHIVE"
|
||||||
|
|
||||||
|
# --- Sizes ---
|
||||||
|
PLEX_ARCHIVE_SIZE=$(ls -lh "$NFS_DEST/$PLEX_LOC/$PLEX_ARCHIVE" 2>/dev/null | awk '{print $5}')
|
||||||
|
TAUTULLI_ARCHIVE_SIZE=$(ls -lh "$NFS_DEST/$TAUTULLI_LOC/$TAUTULLI_ARCHIVE" 2>/dev/null | awk '{print $5}')
|
||||||
|
NFS_FREE=$(df -h "$NFS_DEST" 2>/dev/null | awk 'NR==2{print $4}')
|
||||||
|
PLEX_BACKUP_COUNT=$(ls "$NFS_DEST/$PLEX_LOC"/plex-config-*.tar.gz 2>/dev/null | wc -l)
|
||||||
|
TAUTULLI_BACKUP_COUNT=$(ls "$NFS_DEST/$TAUTULLI_LOC"/tautulli-config-*.tar.gz 2>/dev/null | wc -l)
|
||||||
|
|
||||||
|
# --- Total duration ---
|
||||||
|
END_TIME=$(date +%s)
|
||||||
|
TOTAL_DURATION=$((END_TIME - START_TIME))
|
||||||
|
TOTAL_MIN=$((TOTAL_DURATION / 60))
|
||||||
|
TOTAL_SEC=$((TOTAL_DURATION % 60))
|
||||||
|
|
||||||
|
# --- Overall status ---
|
||||||
|
PLEX_STATUS=$((PLEX_TAR_STATUS + PLEX_COPY_STATUS))
|
||||||
|
TAUTULLI_STATUS=$((TAUTULLI_TAR_STATUS + TAUTULLI_COPY_STATUS))
|
||||||
|
|
||||||
|
if [ $PLEX_STATUS -eq 0 ] && [ $TAUTULLI_STATUS -eq 0 ] && [ $START_STATUS -eq 0 ]; then
|
||||||
|
OVERALL_STATUS="SUCCESS"
|
||||||
|
STATUS_COLOR="#1a7f4b"
|
||||||
|
BANNER_ICON="✅"
|
||||||
|
BANNER_MSG="Backup completed successfully — Plex and Tautulli configs archived to NAS08"
|
||||||
|
BADGE_COLOR="#1a7f4b"
|
||||||
|
elif [ $PLEX_STATUS -ne 0 ] && [ $TAUTULLI_STATUS -ne 0 ]; then
|
||||||
|
OVERALL_STATUS="FAILED"
|
||||||
|
STATUS_COLOR="#b91c1c"
|
||||||
|
BANNER_ICON="🔴"
|
||||||
|
BANNER_MSG="Backup failed — both archive operations encountered errors"
|
||||||
|
BADGE_COLOR="#b91c1c"
|
||||||
|
else
|
||||||
|
OVERALL_STATUS="PARTIAL"
|
||||||
|
STATUS_COLOR="#b45309"
|
||||||
|
BANNER_ICON="⚠️"
|
||||||
|
BANNER_MSG="Backup partially completed — review individual results below"
|
||||||
|
BADGE_COLOR="#b45309"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Status labels ---
|
||||||
|
if [ $PLEX_STATUS -eq 0 ]; then
|
||||||
|
PLEX_LABEL="✅ SUCCESS"
|
||||||
|
PLEX_ROW_BG="#f0fdf4"
|
||||||
|
PLEX_TEXT_COLOR="#166534"
|
||||||
|
else
|
||||||
|
PLEX_LABEL="❌ FAILED"
|
||||||
|
PLEX_ROW_BG="#fff1f2"
|
||||||
|
PLEX_TEXT_COLOR="#b91c1c"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $TAUTULLI_STATUS -eq 0 ]; then
|
||||||
|
TAUTULLI_LABEL="✅ SUCCESS"
|
||||||
|
TAUTULLI_ROW_BG="#f0fdf4"
|
||||||
|
TAUTULLI_TEXT_COLOR="#166534"
|
||||||
|
else
|
||||||
|
TAUTULLI_LABEL="❌ FAILED"
|
||||||
|
TAUTULLI_ROW_BG="#fff1f2"
|
||||||
|
TAUTULLI_TEXT_COLOR="#b91c1c"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $START_STATUS -eq 0 ]; then
|
||||||
|
RESTART_LABEL="✅ RUNNING"
|
||||||
|
RESTART_COLOR="#166534"
|
||||||
|
else
|
||||||
|
RESTART_LABEL="❌ CHECK REQUIRED"
|
||||||
|
RESTART_COLOR="#b91c1c"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Send email ---
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Sending email report..."
|
||||||
|
|
||||||
|
python3 << PYEOF
|
||||||
|
import smtplib, ssl
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
smtp_server = "$SMTP_SERVER"
|
||||||
|
smtp_port = $SMTP_PORT
|
||||||
|
smtp_user = "$SMTP_USER"
|
||||||
|
smtp_pass = "$SMTP_PASS"
|
||||||
|
email_to = "$EMAIL_TO"
|
||||||
|
email_from = "$EMAIL_FROM"
|
||||||
|
|
||||||
|
subject = "PLEX32 Backup — $OVERALL_STATUS — $DATESTAMP"
|
||||||
|
|
||||||
|
html = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body style="margin:0;padding:20px;background:#f1f5f9;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
|
||||||
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||||||
|
<tr><td align="center">
|
||||||
|
<table width="720" cellspacing="0" cellpadding="0" style="max-width:720px;width:100%;">
|
||||||
|
|
||||||
|
<!-- HEADER -->
|
||||||
|
<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;">Config Backup</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 — Lab/Servers</div>
|
||||||
|
</td>
|
||||||
|
<td align="right">
|
||||||
|
<div style="background:$BADGE_COLOR;color:white;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:700;">$OVERALL_STATUS</div>
|
||||||
|
</td>
|
||||||
|
</tr></table>
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<!-- STATUS BANNER -->
|
||||||
|
<tr><td style="background:$STATUS_COLOR;padding:12px 32px;">
|
||||||
|
<span style="color:white;font-size:14px;font-weight:600;">$BANNER_ICON $BANNER_MSG</span>
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<!-- BODY -->
|
||||||
|
<tr><td style="background:white;border:1px solid #e5e7eb;padding:28px 32px;">
|
||||||
|
|
||||||
|
<!-- STAT CARDS -->
|
||||||
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:4px;">
|
||||||
|
<div style="padding:12px;background:#f9fafb;border-radius:6px;text-align:center;">
|
||||||
|
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Duration</div>
|
||||||
|
<div style="font-size:20px;font-weight:700;color:#111827;margin-top:2px;">${TOTAL_MIN}m ${TOTAL_SEC}s</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="padding:4px;">
|
||||||
|
<div style="padding:12px;background:#f9fafb;border-radius:6px;text-align:center;">
|
||||||
|
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Plex Archive</div>
|
||||||
|
<div style="font-size:20px;font-weight:700;color:#111827;margin-top:2px;">$PLEX_ARCHIVE_SIZE</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="padding:4px;">
|
||||||
|
<div style="padding:12px;background:#f9fafb;border-radius:6px;text-align:center;">
|
||||||
|
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Tautulli Archive</div>
|
||||||
|
<div style="font-size:20px;font-weight:700;color:#111827;margin-top:2px;">$TAUTULLI_ARCHIVE_SIZE</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="padding:4px;">
|
||||||
|
<div style="padding:12px;background:#f9fafb;border-radius:6px;text-align:center;">
|
||||||
|
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">NAS08 Free</div>
|
||||||
|
<div style="font-size:20px;font-weight:700;color:#111827;margin-top:2px;">$NFS_FREE</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- RESULTS TABLE -->
|
||||||
|
<div style="margin-top:24px;background:white;border-radius:10px;border:1px solid #e5e7eb;overflow:hidden;">
|
||||||
|
<div style="font-size:13px;font-weight:700;color:#374151;padding:14px 18px;border-bottom:1px solid #e5e7eb;text-transform:uppercase;letter-spacing:.5px;">📋 Backup Results</div>
|
||||||
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||||||
|
<tr style="background:#f9fafb;">
|
||||||
|
<th style="text-align:left;font-size:11px;color:#6b7280;padding:8px 18px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Component</th>
|
||||||
|
<th style="text-align:left;font-size:11px;color:#6b7280;padding:8px 18px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Archive</th>
|
||||||
|
<th style="text-align:left;font-size:11px;color:#6b7280;padding:8px 18px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Copies Kept</th>
|
||||||
|
<th style="text-align:left;font-size:11px;color:#6b7280;padding:8px 18px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Duration</th>
|
||||||
|
<th style="text-align:left;font-size:11px;color:#6b7280;padding:8px 18px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Status</th>
|
||||||
|
</tr>
|
||||||
|
<tr style="background:$PLEX_ROW_BG;">
|
||||||
|
<td style="font-size:13px;font-weight:600;color:#111827;padding:10px 18px;border-top:1px solid #e5e7eb;">🎬 Plex Config</td>
|
||||||
|
<td style="font-size:12px;color:#374151;padding:10px 18px;border-top:1px solid #e5e7eb;font-family:monospace;">$PLEX_ARCHIVE</td>
|
||||||
|
<td style="font-size:13px;color:#374151;padding:10px 18px;border-top:1px solid #e5e7eb;">$PLEX_BACKUP_COUNT / $RETENTION_COUNT</td>
|
||||||
|
<td style="font-size:13px;color:#374151;padding:10px 18px;border-top:1px solid #e5e7eb;">${PLEX_DURATION}s tar + ${PLEX_COPY_DURATION}s copy</td>
|
||||||
|
<td style="font-size:13px;font-weight:700;padding:10px 18px;border-top:1px solid #e5e7eb;color:$PLEX_TEXT_COLOR;">$PLEX_LABEL</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="background:$TAUTULLI_ROW_BG;">
|
||||||
|
<td style="font-size:13px;font-weight:600;color:#111827;padding:10px 18px;border-top:1px solid #e5e7eb;">📊 Tautulli Config</td>
|
||||||
|
<td style="font-size:12px;color:#374151;padding:10px 18px;border-top:1px solid #e5e7eb;font-family:monospace;">$TAUTULLI_ARCHIVE</td>
|
||||||
|
<td style="font-size:13px;color:#374151;padding:10px 18px;border-top:1px solid #e5e7eb;">$TAUTULLI_BACKUP_COUNT / $RETENTION_COUNT</td>
|
||||||
|
<td style="font-size:13px;color:#374151;padding:10px 18px;border-top:1px solid #e5e7eb;">${TAUTULLI_DURATION}s tar + ${TAUTULLI_COPY_DURATION}s copy</td>
|
||||||
|
<td style="font-size:13px;font-weight:700;padding:10px 18px;border-top:1px solid #e5e7eb;color:$TAUTULLI_TEXT_COLOR;">$TAUTULLI_LABEL</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CONTAINER STATUS -->
|
||||||
|
<div style="margin-top:16px;padding:12px 14px;background:#f8fafc;border-radius:6px;border:1px solid #e5e7eb;">
|
||||||
|
<span style="font-size:12px;font-weight:600;color:#374151;text-transform:uppercase;letter-spacing:.5px;">Container Status After Backup</span>
|
||||||
|
<span style="margin-left:10px;font-size:13px;font-weight:700;color:$RESTART_COLOR;">$RESTART_LABEL</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DESTINATION INFO -->
|
||||||
|
<div style="margin-top:10px;padding:12px 14px;background:#f8fafc;border-radius:6px;border:1px solid #e5e7eb;">
|
||||||
|
<span style="font-size:12px;font-weight:600;color:#374151;text-transform:uppercase;letter-spacing:.5px;">Backup Destination</span>
|
||||||
|
<span style="margin-left:10px;font-size:13px;color:#374151;">NAS08 (192.168.150.35) via NFS — $NFS_FREE free</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RESTORE NOTE -->
|
||||||
|
<div style="margin-top:10px;padding:12px 14px;background:#fffbeb;border-radius:6px;border:1px solid #fac775;">
|
||||||
|
<span style="font-size:12px;font-weight:600;color:#633806;text-transform:uppercase;letter-spacing:.5px;">💡 To Restore</span>
|
||||||
|
<span style="margin-left:10px;font-size:13px;color:#633806;">tar -xzf $PLEX_ARCHIVE -C /opt/plex</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<!-- FOOTER -->
|
||||||
|
<tr><td style="background:#0f172a;border-radius:0 0 10px 10px;padding:16px 32px;text-align:center;">
|
||||||
|
<span style="font-size:11px;color:#64748b;">Config Backup | plex32 | KingDezigns Infrastructure</span>
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
msg = MIMEMultipart("alternative")
|
||||||
|
msg["Subject"] = subject
|
||||||
|
msg["From"] = email_from
|
||||||
|
msg["To"] = email_to
|
||||||
|
msg.attach(MIMEText(html, "html"))
|
||||||
|
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
with smtplib.SMTP_SSL(smtp_server, smtp_port, context=ctx, timeout=15) as s:
|
||||||
|
s.login(smtp_user, smtp_pass)
|
||||||
|
s.sendmail(email_from, [email_to], msg.as_string())
|
||||||
|
print("Email sent successfully")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
EMAIL_STATUS=$?
|
||||||
|
if [ $EMAIL_STATUS -eq 0 ]; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Email sent successfully"
|
||||||
|
else
|
||||||
|
echo "[ERROR] Email send failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Cleanup old logs ---
|
||||||
|
find "$LOG_DIR" -name "backup-*.log" -mtime +$RETENTION_DAYS -delete 2>/dev/null
|
||||||
|
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup complete — Status: $OVERALL_STATUS | Duration: ${TOTAL_MIN}m ${TOTAL_SEC}s"
|
||||||
598
plex32_health_report.sh
Normal file
598
plex32_health_report.sh
Normal file
|
|
@ -0,0 +1,598 @@
|
||||||
|
#!/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 "<tr style='background:$row_bg;'>
|
||||||
|
<td style='padding:9px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;'>$icon $label</td>
|
||||||
|
<td style='padding:9px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;text-align:center;'>$size</td>
|
||||||
|
<td style='padding:9px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;text-align:center;'>$used</td>
|
||||||
|
<td style='padding:9px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;text-align:center;'>$avail</td>
|
||||||
|
<td style='padding:9px 14px;font-size:13px;font-weight:700;color:$pct_color;border-bottom:1px solid #f3f4f6;text-align:center;'>$pct</td>
|
||||||
|
</tr>"
|
||||||
|
}
|
||||||
|
|
||||||
|
DISK_ROWS=""
|
||||||
|
DISK_ROWS+=$(build_disk_row "$ROOT_DISK")
|
||||||
|
DISK_ROWS+=$(build_disk_row "$BOOT_DISK")
|
||||||
|
DISK_ROWS+=$(build_disk_row "$NFS_CONFIG")
|
||||||
|
DISK_ROWS+=$(build_disk_row "$NFS_MEDIA")
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# BUILD NFS TABLE HTML
|
||||||
|
# =============================================================
|
||||||
|
build_nfs_row() {
|
||||||
|
local row="$1"
|
||||||
|
local status=$(echo "$row" | cut -d'|' -f1)
|
||||||
|
local label=$(echo "$row" | cut -d'|' -f2)
|
||||||
|
local icon="✅"
|
||||||
|
local row_bg="white"
|
||||||
|
local status_text="Mounted & Accessible"
|
||||||
|
local status_color="#1a7f4b"
|
||||||
|
if [ "$status" = "DOWN" ]; then
|
||||||
|
icon="🚨"
|
||||||
|
row_bg="#fff1f2"
|
||||||
|
status_text="DOWN — Not accessible"
|
||||||
|
status_color="#b91c1c"
|
||||||
|
fi
|
||||||
|
echo "<tr style='background:$row_bg;'>
|
||||||
|
<td style='padding:9px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;'>$icon $label</td>
|
||||||
|
<td style='padding:9px 14px;font-size:13px;font-weight:600;color:$status_color;border-bottom:1px solid #f3f4f6;'>$status_text</td>
|
||||||
|
</tr>"
|
||||||
|
}
|
||||||
|
|
||||||
|
NFS_ROWS=""
|
||||||
|
NFS_ROWS+=$(build_nfs_row "$NFS_CONFIG_STATUS")
|
||||||
|
NFS_ROWS+=$(build_nfs_row "$NFS_TRANSCODE_STATUS")
|
||||||
|
NFS_ROWS+=$(build_nfs_row "$NFS_MEDIA_STATUS")
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# ISSUES SUMMARY HTML
|
||||||
|
# =============================================================
|
||||||
|
ALL_ISSUES=""
|
||||||
|
[ -n "$DISK_ISSUES" ] && ALL_ISSUES+="$DISK_ISSUES"
|
||||||
|
[ -n "$NFS_ISSUES" ] && ALL_ISSUES+="$NFS_ISSUES"
|
||||||
|
[ -n "$MEM_ISSUES" ] && ALL_ISSUES+="$MEM_ISSUES"$'\n'
|
||||||
|
[ -n "$CPU_ISSUES" ] && ALL_ISSUES+="$CPU_ISSUES"$'\n'
|
||||||
|
[ -n "$REBOOT_ISSUES" ] && ALL_ISSUES+="$REBOOT_ISSUES"$'\n'
|
||||||
|
[ -n "$PLEX_CONTAINER_ISSUES" ] && ALL_ISSUES+="$PLEX_CONTAINER_ISSUES"$'\n'
|
||||||
|
[ -n "$SYSTEMD_ISSUES" ] && ALL_ISSUES+="$SYSTEMD_ISSUES"$'\n'
|
||||||
|
[ -n "$F2B_HEALTH_ISSUES" ] && ALL_ISSUES+="$F2B_HEALTH_ISSUES"$'\n'
|
||||||
|
|
||||||
|
ISSUES_HTML=""
|
||||||
|
if [ -n "$ALL_ISSUES" ]; then
|
||||||
|
ISSUES_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;'>⚠ Issues Detected</div>"
|
||||||
|
while IFS= read -r issue; do
|
||||||
|
[ -z "$issue" ] && continue
|
||||||
|
BG="#fffbeb"; BORDER="#b45309"; COL="#92400e"
|
||||||
|
echo "$issue" | grep -q "CRITICAL" && BG="#fff1f2" && BORDER="#b91c1c" && COL="#7f1d1d"
|
||||||
|
ISSUES_HTML+="<div style='margin-bottom:8px;padding:10px 14px;background:$BG;border-left:4px solid $BORDER;border-radius:4px;font-size:13px;color:$COL;'>$issue</div>"
|
||||||
|
done <<< "$ALL_ISSUES"
|
||||||
|
ISSUES_HTML+="</div>"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# REBOOT REQUIRED HTML
|
||||||
|
# =============================================================
|
||||||
|
REBOOT_HTML=""
|
||||||
|
if [ "$REBOOT_STATUS" = "WARNING" ]; then
|
||||||
|
REBOOT_HTML="<div style='margin-top:16px;padding:12px 14px;background:#fffbeb;border-left:4px solid #b45309;border-radius:4px;'>
|
||||||
|
<div style='font-size:11px;font-weight:700;color:#b45309;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px;'>⚠ Reboot Required</div>
|
||||||
|
<div style='font-size:13px;color:#92400e;'>A system reboot is required to complete pending updates.</div>
|
||||||
|
<pre style='margin-top:8px;background:#1e293b;color:#e2e8f0;padding:10px;border-radius:4px;font-size:12px;user-select:all;-webkit-user-select:all;cursor:pointer;'>sudo reboot</pre>
|
||||||
|
<div style='font-size:11px;color:#b45309;margin-top:4px;'>👆 Click command to select — then Ctrl+C</div>
|
||||||
|
</div>"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# JOURNAL ERRORS HTML
|
||||||
|
# =============================================================
|
||||||
|
JOURNAL_HTML=""
|
||||||
|
if [ "$JOURNAL_COUNT" -gt 0 ]; then
|
||||||
|
JOURNAL_ESCAPED=$(echo "$JOURNAL_ERRORS" | sed 's/&/\&/g; s/</\</g; s/>/\>/g')
|
||||||
|
JOURNAL_HTML="<div style='margin-top:24px;'>
|
||||||
|
<div style='font-size:13px;font-weight:700;color:#374151;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px;'>📋 Recent System Errors</div>
|
||||||
|
<pre style='margin:0;background:#1e293b;color:#e2e8f0;padding:14px;border-radius:6px;font-size:11px;line-height:1.6;white-space:pre-wrap;word-break:break-all;'>$JOURNAL_ESCAPED</pre>
|
||||||
|
</div>"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# STATUS ICON HELPER
|
||||||
|
# =============================================================
|
||||||
|
status_icon() {
|
||||||
|
case "$1" in
|
||||||
|
OK) echo "✅" ;;
|
||||||
|
WARNING) echo "⚠" ;;
|
||||||
|
CRITICAL) echo "🚨" ;;
|
||||||
|
*) echo "❓" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
STATUS_ICON_DISK=$(status_icon "$DISK_STATUS")
|
||||||
|
STATUS_ICON_NFS=$(status_icon "$NFS_STATUS")
|
||||||
|
STATUS_ICON_MEM=$(status_icon "$MEM_STATUS")
|
||||||
|
STATUS_ICON_CPU=$(status_icon "$CPU_STATUS")
|
||||||
|
STATUS_ICON_REBOOT=$(status_icon "$REBOOT_STATUS")
|
||||||
|
STATUS_ICON_PLEX=$(status_icon "$PLEX_CONTAINER_STATUS")
|
||||||
|
STATUS_ICON_SYSTEMD=$(status_icon "$SYSTEMD_STATUS")
|
||||||
|
STATUS_ICON_F2B=$(status_icon "$F2B_HEALTH_STATUS")
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# BUILD & SEND EMAIL
|
||||||
|
# =============================================================
|
||||||
|
python3 << PYEOF
|
||||||
|
import smtplib, ssl
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
timestamp = "$TIMESTAMP"
|
||||||
|
status_color = "$STATUS_COLOR"
|
||||||
|
status_badge = "$STATUS_BADGE"
|
||||||
|
status_banner = "$STATUS_BANNER_MSG"
|
||||||
|
subject = "$SUBJECT"
|
||||||
|
from_email = "$EMAIL_FROM"
|
||||||
|
|
||||||
|
uptime_display = "$UPTIME_DISPLAY"
|
||||||
|
load_1 = "$LOAD_1"
|
||||||
|
load_5 = "$LOAD_5"
|
||||||
|
load_15 = "$LOAD_15"
|
||||||
|
cpu_cores = "$CPU_CORES"
|
||||||
|
mem_total = "$MEM_TOTAL_H"
|
||||||
|
mem_used = "$MEM_USED_H"
|
||||||
|
mem_avail = "$MEM_AVAIL_H"
|
||||||
|
mem_pct = "$MEM_PCT"
|
||||||
|
plex_uptime = "$PLEX_UPTIME"
|
||||||
|
|
||||||
|
f2b_running = "$F2B_RUNNING"
|
||||||
|
f2b_bans = "$F2B_BANS"
|
||||||
|
f2b_current = "$F2B_CURRENTLY_BANNED"
|
||||||
|
|
||||||
|
disk_rows = """$DISK_ROWS"""
|
||||||
|
nfs_rows = """$NFS_ROWS"""
|
||||||
|
issues_html = """$ISSUES_HTML"""
|
||||||
|
reboot_html = """$REBOOT_HTML"""
|
||||||
|
journal_html = """$JOURNAL_HTML"""
|
||||||
|
|
||||||
|
icon_disk = "$STATUS_ICON_DISK"
|
||||||
|
icon_nfs = "$STATUS_ICON_NFS"
|
||||||
|
icon_mem = "$STATUS_ICON_MEM"
|
||||||
|
icon_cpu = "$STATUS_ICON_CPU"
|
||||||
|
icon_reboot = "$STATUS_ICON_REBOOT"
|
||||||
|
icon_plex = "$STATUS_ICON_PLEX"
|
||||||
|
icon_systemd = "$STATUS_ICON_SYSTEMD"
|
||||||
|
icon_f2b = "$STATUS_ICON_F2B"
|
||||||
|
|
||||||
|
mem_color = "#b91c1c" if int(mem_pct) >= 95 else "#b45309" if int(mem_pct) >= 85 else "#1a7f4b"
|
||||||
|
load_color = "#b91c1c" if float(load_5) >= float(cpu_cores)*0.9 else "#b45309" if float(load_5) >= float(cpu_cores)*0.7 else "#1a7f4b"
|
||||||
|
|
||||||
|
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;">Health Report</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;">
|
||||||
|
|
||||||
|
<!-- STAT CARDS -->
|
||||||
|
<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;">Uptime</div>
|
||||||
|
<div style="font-size:18px;font-weight:700;color:#111827;margin-top:4px;">{uptime_display}</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;">CPU Load (5m)</div>
|
||||||
|
<div style="font-size:28px;font-weight:700;color:{load_color};margin-top:4px;">{load_5}</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;">Memory Used</div>
|
||||||
|
<div style="font-size:28px;font-weight:700;color:{mem_color};margin-top:4px;">{mem_pct}%</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;">F2B Bans Today</div>
|
||||||
|
<div style="font-size:28px;font-weight:700;color:#111827;margin-top:4px;">{f2b_bans}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{issues_html}
|
||||||
|
{reboot_html}
|
||||||
|
|
||||||
|
<!-- SYSTEM OVERVIEW 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;">🔍 System Overview</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;">System</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>
|
||||||
|
<th style="text-align:left;padding:8px 14px;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;">Detail</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#374151;font-weight:600;border-bottom:1px solid #f3f4f6;">{icon_plex} Plex Container</td>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;">Running</td>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#6b7280;border-bottom:1px solid #f3f4f6;">{plex_uptime}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#374151;font-weight:600;border-bottom:1px solid #f3f4f6;">{icon_f2b} Fail2Ban</td>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;">{f2b_running}</td>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#6b7280;border-bottom:1px solid #f3f4f6;">Currently banned: {f2b_current}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#374151;font-weight:600;border-bottom:1px solid #f3f4f6;">{icon_mem} Memory</td>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;">{mem_used} / {mem_total} used</td>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#6b7280;border-bottom:1px solid #f3f4f6;">{mem_avail} available</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#374151;font-weight:600;border-bottom:1px solid #f3f4f6;">{icon_cpu} CPU Load</td>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;">{load_1} / {load_5} / {load_15}</td>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#6b7280;border-bottom:1px solid #f3f4f6;">{cpu_cores} cores (1m / 5m / 15m avg)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#374151;font-weight:600;border-bottom:1px solid #f3f4f6;">{icon_systemd} Systemd Units</td>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;">No failed units</td>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#6b7280;border-bottom:1px solid #f3f4f6;">All services healthy</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#374151;font-weight:600;">{icon_reboot} Reboot Required</td>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#374151;">{'⚠ Yes' if '$REBOOT_STATUS' == 'WARNING' else 'No'}</td>
|
||||||
|
<td style="padding:9px 14px;font-size:13px;color:#6b7280;">{'Pending kernel/system updates' if '$REBOOT_STATUS' == 'WARNING' else 'System is current'}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DISK USAGE -->
|
||||||
|
<div style="margin-top:24px;">
|
||||||
|
<div style="font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;">💾 Disk Usage</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;">Filesystem</th>
|
||||||
|
<th style="text-align:center;padding:8px 14px;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;">Size</th>
|
||||||
|
<th style="text-align:center;padding:8px 14px;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;">Used</th>
|
||||||
|
<th style="text-align:center;padding:8px 14px;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;">Avail</th>
|
||||||
|
<th style="text-align:center;padding:8px 14px;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;">Use%</th>
|
||||||
|
</tr>
|
||||||
|
{disk_rows}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- NFS MOUNTS -->
|
||||||
|
<div style="margin-top:24px;">
|
||||||
|
<div style="font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;">📁 NFS Mounts (NAS08)</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;">Mount</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>
|
||||||
|
{nfs_rows}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{journal_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;">Health Report | PLEX32 | 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] Health report email sent successfully.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[$TIMESTAMP] Email send failed: {e}")
|
||||||
|
exit(1)
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
echo "[$TIMESTAMP] Health check complete."
|
||||||
88
plex32_launcher.sh
Normal file
88
plex32_launcher.sh
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# =============================================================
|
||||||
|
# PLEX32 Remote Launcher
|
||||||
|
# Run from KingDezigns001 to execute scripts on PLEX32
|
||||||
|
# Location: ~/scripts/plex32_launcher.sh
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
PLEX32_HOST="rufusking@192.168.150.45"
|
||||||
|
|
||||||
|
clear
|
||||||
|
echo "============================================="
|
||||||
|
echo " PLEX32 Remote Script Launcher"
|
||||||
|
echo " KingDezigns Home Network"
|
||||||
|
echo "============================================="
|
||||||
|
echo ""
|
||||||
|
echo " Which script do you want to run?"
|
||||||
|
echo ""
|
||||||
|
echo " 1) Update Check"
|
||||||
|
echo " Checks for OS, Plex, Docker and Fail2Ban updates"
|
||||||
|
echo ""
|
||||||
|
echo " 2) Daily Health Report"
|
||||||
|
echo " Disk, NFS, memory, CPU, container and Fail2Ban status"
|
||||||
|
echo ""
|
||||||
|
echo " 3) Plex32 Backup"
|
||||||
|
echo " Archives Plex and Tautulli configs to NAS08"
|
||||||
|
echo " Note: Plex will be briefly offline during backup"
|
||||||
|
echo ""
|
||||||
|
echo " q) Quit"
|
||||||
|
echo ""
|
||||||
|
echo "============================================="
|
||||||
|
read -rp " Enter choice [1-3 or q]: " CHOICE
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
case "$CHOICE" in
|
||||||
|
1)
|
||||||
|
SCRIPT="/usr/scripts/plex32/plex32_update_check.sh"
|
||||||
|
LABEL="Update Check"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
SCRIPT="/usr/scripts/plex32/plex32_health_report.sh"
|
||||||
|
LABEL="Health Report"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
SCRIPT="/usr/scripts/plex32/plex32_backup.sh"
|
||||||
|
LABEL="Plex32 Backup"
|
||||||
|
echo " ⚠ Plex will be briefly stopped during backup."
|
||||||
|
read -rp " Continue? [y/N]: " CONFIRM
|
||||||
|
echo ""
|
||||||
|
if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
|
||||||
|
echo " Cancelled."
|
||||||
|
echo ""
|
||||||
|
read -rp " Press Enter to close..."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
q|Q)
|
||||||
|
echo " Goodbye."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo " Invalid choice. Exiting."
|
||||||
|
echo ""
|
||||||
|
read -rp " Press Enter to close..."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "============================================="
|
||||||
|
echo " Running: $LABEL on plex32"
|
||||||
|
echo " $(date '+%B %d, %Y at %I:%M %p')"
|
||||||
|
echo "============================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
ssh -t "$PLEX32_HOST" "bash $SCRIPT"
|
||||||
|
SSH_STATUS=$?
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================="
|
||||||
|
if [ $SSH_STATUS -eq 0 ]; then
|
||||||
|
echo " ✅ $LABEL completed successfully"
|
||||||
|
else
|
||||||
|
echo " ⚠ $LABEL finished with status $SSH_STATUS"
|
||||||
|
echo " Check the output above for details"
|
||||||
|
fi
|
||||||
|
echo " $(date '+%B %d, %Y at %I:%M %p')"
|
||||||
|
echo "============================================="
|
||||||
|
echo ""
|
||||||
|
read -rp " Press Enter to close this window..."
|
||||||
115
plex32_reboot.sh
Normal file
115
plex32_reboot.sh
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# =============================================================
|
||||||
|
# plex32_reboot.sh — Remote reboot of PLEX32 from NAS08
|
||||||
|
# Location: /usr/scripts/nas/plex32_reboot.sh
|
||||||
|
# Run via: OMV Scheduled Jobs (disabled — run manually)
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
PLEX32_IP="192.168.150.45"
|
||||||
|
PLEX32_USER="rufusking"
|
||||||
|
SSH_KEY="/root/.ssh/id_ed25519"
|
||||||
|
LOG_DIR="/var/log/plex32-reboot"
|
||||||
|
LOG_FILE="$LOG_DIR/reboot-$(date +%Y%m%d-%H%M%S).log"
|
||||||
|
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Clean up logs older than 90 days ---
|
||||||
|
find "$LOG_DIR" -name "reboot-*.log" -mtime +90 -delete
|
||||||
|
|
||||||
|
log "========================================"
|
||||||
|
log "PLEX32 Remote Reboot — Initiated"
|
||||||
|
log "Target: $PLEX32_USER@$PLEX32_IP"
|
||||||
|
log "========================================"
|
||||||
|
|
||||||
|
# --- Stop Plex and Tautulli containers gracefully before reboot ---
|
||||||
|
log "Stopping Docker containers on PLEX32..."
|
||||||
|
ssh -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes \
|
||||||
|
"$PLEX32_USER@$PLEX32_IP" \
|
||||||
|
"cd ~/docker/plex && docker compose down" >> "$LOG_FILE" 2>&1
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log "Docker containers stopped cleanly."
|
||||||
|
else
|
||||||
|
log "WARNING: docker compose down failed or timed out — proceeding with reboot anyway."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Issue reboot ---
|
||||||
|
log "Issuing reboot command to PLEX32..."
|
||||||
|
ssh -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes \
|
||||||
|
"$PLEX32_USER@$PLEX32_IP" \
|
||||||
|
"sudo reboot" >> "$LOG_FILE" 2>&1
|
||||||
|
|
||||||
|
# SSH will return a non-zero exit code when the connection drops due to reboot — that's expected
|
||||||
|
log "Reboot command sent. PLEX32 is going down."
|
||||||
|
log ""
|
||||||
|
|
||||||
|
# --- Wait for PLEX32 to come back online ---
|
||||||
|
log "Pinging Server -- Waiting for PLEX32 to come back online..."
|
||||||
|
WAIT_MAX=180 # seconds to wait total
|
||||||
|
WAIT_INTERVAL=10
|
||||||
|
ELAPSED=0
|
||||||
|
ONLINE=false
|
||||||
|
|
||||||
|
sleep 30 # Give it time to actually go down before polling
|
||||||
|
|
||||||
|
while [ $ELAPSED -lt $WAIT_MAX ]; do
|
||||||
|
if ping -c 1 -W 2 "$PLEX32_IP" > /dev/null 2>&1; then
|
||||||
|
log "PLEX32 is back online (ping responded after ~$((ELAPSED + 30))s)."
|
||||||
|
ONLINE=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep $WAIT_INTERVAL
|
||||||
|
ELAPSED=$((ELAPSED + WAIT_INTERVAL))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$ONLINE" = false ]; then
|
||||||
|
log "WARNING: PLEX32 did not respond to ping within $((WAIT_MAX + 30)) seconds."
|
||||||
|
log "It may still be booting, or there may be a problem. Check manually."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Start Docker containers on PLEX32 ---
|
||||||
|
log "Starting Docker containers on PLEX32..."
|
||||||
|
ssh -i "$SSH_KEY" -o ConnectTimeout=15 -o BatchMode=yes \
|
||||||
|
"$PLEX32_USER@$PLEX32_IP" \
|
||||||
|
"cd ~/docker/plex && docker compose up -d" >> "$LOG_FILE" 2>&1
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log "Docker containers started."
|
||||||
|
else
|
||||||
|
log "WARNING: docker compose up -d failed — check PLEX32 manually."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Poll for Plex port 32400 to respond ---
|
||||||
|
log "Waiting for Plex (port 32400) to become available..."
|
||||||
|
PLEX_WAIT_MAX=90
|
||||||
|
PLEX_ELAPSED=0
|
||||||
|
PLEX_UP=false
|
||||||
|
|
||||||
|
while [ $PLEX_ELAPSED -lt $PLEX_WAIT_MAX ]; do
|
||||||
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "http://$PLEX32_IP:32400/web")
|
||||||
|
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "301" ] || [ "$HTTP_CODE" = "302" ]; then
|
||||||
|
log "Plex is responding (HTTP $HTTP_CODE) after ${PLEX_ELAPSED}s — reboot successful."
|
||||||
|
PLEX_UP=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
PLEX_ELAPSED=$((PLEX_ELAPSED + 5))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$PLEX_UP" = false ]; then
|
||||||
|
log "NOTE: Plex did not respond within ${PLEX_WAIT_MAX}s. Containers may still be starting."
|
||||||
|
log "Allow another minute then check http://$PLEX32_IP:32400/web manually."
|
||||||
|
fi
|
||||||
|
|
||||||
|
log ""
|
||||||
|
log "========================================"
|
||||||
|
log "Reboot sequence complete."
|
||||||
|
log "Log saved to: $LOG_FILE"
|
||||||
|
log "========================================"
|
||||||
|
|
||||||
|
exit 0
|
||||||
383
plex32_update_check.sh
Normal file
383
plex32_update_check.sh
Normal file
|
|
@ -0,0 +1,383 @@
|
||||||
|
#!/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/&/\&/g; s/</\</g; s/>/\>/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="⚠ 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="🔄 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="✅ 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/&/\&/g; s/</\</g; s/>/\>/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 & 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 | PLEX32 | 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."
|
||||||
Loading…
Add table
Reference in a new issue