nas16-scripts/ddns/ddns_summary.html
2026-07-26 21:21:03 -04:00

406 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KingDezigns — DDNS Updater 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 { 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); display: inline-block; }
.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); }
.badge-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 1rem; }
.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: '↳ note: '; 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.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.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; }
.flow { display: flex; flex-direction: column; gap: 0; }
.flow-row { display: flex; align-items: stretch; gap: 0; }
.flow-node { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 0.65rem 1rem; font-family: var(--mono); font-size: 12px; flex-shrink: 0; display: flex; align-items: center; gap: 8px; }
.flow-node.trigger { border-color: var(--border-strong); background: var(--surface-alt); font-weight: 500; }
.flow-node.action { border-color: var(--green-border); background: var(--green-bg); color: var(--green-text); }
.flow-node.warn { border-color: var(--amber-border); background: var(--amber-bg); color: var(--amber-text); }
.flow-node.fail { border-color: var(--red-border); background: var(--red-bg); color: var(--red-text); }
.flow-node.silent { border-color: var(--border); background: var(--surface-alt); color: var(--text-muted); }
.flow-node.daily { border-color: #a89ccc; background: #f0edf8; color: #3d2d6b; }
.flow-arrow { display: flex; flex-direction: column; align-items: center; padding: 3px 0; color: var(--text-muted); font-family: var(--mono); font-size: 11px; gap: 1px; }
.flow-branch { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; margin: 0.3rem 0; }
.flow-branch-col { display: flex; flex-direction: column; gap: 0.3rem; }
.branch-label { font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); padding: 0 2px; margin-bottom: 2px; }
.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 — DDNS Updater Summary</h1>
<div class="meta">
<span>📅 May 19, 2025</span>
<span>🖥 NAS16 (Raspberry Pi)</span>
<span>⚙️ OpenMediaVault</span>
<span>☁️ DreamHost DNS API</span>
<span>🌐 20 A Records · 4 Domains</span>
</div>
</header>
<!-- ===== SECTION 1 — WHAT WAS BUILT ===================================== -->
<div class="section">
<div class="section-title" data-num="1">What was built</div>
<p>A self-contained Bash script that keeps all KingDezigns DreamHost A records synchronized with the current home WAN IP. It runs every 15 minutes via OMV Scheduled Tasks. It only calls the DreamHost API when the IP actually changes (or during a nightly sanity check), so there is zero risk of being banned for spamming.</p>
<div class="badge-row">
<span class="badge green">20 A records managed</span>
<span class="badge green">4 domains</span>
<span class="badge green">Max 2 API calls per event</span>
<span class="badge green">HTML email notifications</span>
<span class="badge">Every 15 min via cron</span>
<span class="badge">Daily sanity check 00:0000:14</span>
</div>
</div>
<!-- ===== SECTION 2 — LOGIC FLOW MAP ===================================== -->
<div class="section">
<div class="section-title" data-num="2">Logic flow map</div>
<div class="flow">
<div class="flow-node trigger">⏱ Script runs (every 15 min via OMV cron)</div>
<div class="flow-arrow"></div>
<div class="flow-node">🌐 Get current WAN IP from api.ipify.org / ifconfig.me / icanhazip.com</div>
<div class="flow-arrow"></div>
<div class="flow-node">📄 Read cached previous IP from <code>/var/log/ddns/last_wan_ip.txt</code></div>
<div class="flow-arrow"></div>
<div class="flow-node">🕛 Is the time between 00:00 and 00:14? (daily sanity-check window)</div>
<div class="flow-arrow"></div>
<!-- Branch: IP changed vs same -->
<div class="flow-branch">
<div class="flow-branch-col">
<div class="branch-label">IP changed OR daily window</div>
<div class="flow-node action">▶ Query DreamHost — check each A record</div>
<div class="flow-node action">📡 Call dns-remove_record + dns-add_record for each mismatched record</div>
<div class="flow-node">⏳ Wait 35 minutes (random)</div>
<div class="flow-node">🔍 Re-query DreamHost — verify each record</div>
<!-- Sub-branch: all ok vs still failing -->
<div class="flow-branch" style="margin-top:0.3rem;">
<div class="flow-branch-col">
<div class="branch-label">Still wrong</div>
<div class="flow-node warn">🔁 Attempt 2 of 2 — update remaining records</div>
<div class="flow-node">⏳ Wait 35 min · Re-verify</div>
<div class="flow-branch">
<div class="flow-branch-col">
<div class="branch-label">Still failing</div>
<div class="flow-node fail">✗ STOP (max attempts reached)</div>
<div class="flow-node fail">📧 Email: partial failure + manual fix instructions + WAN IP</div>
</div>
<div class="flow-branch-col">
<div class="branch-label">Now correct</div>
<div class="flow-node action">✓ Mark success</div>
<div class="flow-node action">📧 Email: all records updated</div>
</div>
</div>
</div>
<div class="flow-branch-col">
<div class="branch-label">All correct</div>
<div class="flow-node action">✓ All records verified</div>
<div class="flow-node action">📧 Email: success — all records updated</div>
</div>
</div>
<div class="flow-node">💾 Write new WAN IP to cache file</div>
</div>
<div class="flow-branch-col">
<div class="branch-label">IP unchanged + not daily window</div>
<div class="flow-node silent">💾 Overwrite cache with same IP (refreshes timestamp)</div>
<div class="flow-node silent">🔇 Exit silently — no API calls, no email</div>
</div>
</div>
</div>
<div class="callout amber" style="margin-top:1rem;">
<div class="clue-label">⚠ DreamHost API — how updates work</div>
<p>DreamHost has no "update" DNS command. To change an A record you must <strong>remove the old value then add the new one</strong>. The script always fetches the live DreamHost value immediately before each remove/add pair so it never tries to remove a value that isn't there.</p>
</div>
</div>
<!-- ===== SECTION 3 — DOMAINS & RECORDS ================================== -->
<div class="section">
<div class="section-title" data-num="3">Domains &amp; records managed</div>
<table>
<thead><tr><th>Record (DreamHost "record" field)</th><th>Zone / Domain</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>albranch.org</code></td><td>albranch.org</td><td>A (root / @)</td></tr>
<tr><td><code>www.albranch.org</code></td><td>albranch.org</td><td>A</td></tr>
<tr><td><code>indigorhayne.com</code></td><td>indigorhayne.com</td><td>A (root / @)</td></tr>
<tr><td><code>www.indigorhayne.com</code></td><td>indigorhayne.com</td><td>A</td></tr>
<tr><td><code>rufusking.com</code></td><td>rufusking.com</td><td>A (root / @)</td></tr>
<tr><td><code>www.rufusking.com</code></td><td>rufusking.com</td><td>A</td></tr>
<tr><td><code>kingdezigns.com</code></td><td>kingdezigns.com</td><td>A (root / @)</td></tr>
<tr><td><code>www.kingdezigns.com</code></td><td>kingdezigns.com</td><td>A</td></tr>
<tr><td><code>*.web.kingdezigns.com</code></td><td>kingdezigns.com</td><td>A (wildcard)</td></tr>
<tr><td><code>cloud.kingdezigns.com</code></td><td>kingdezigns.com</td><td>A</td></tr>
<tr><td><code>files08.kingdezigns.com</code></td><td>kingdezigns.com</td><td>A</td></tr>
<tr><td><code>files16.kingdezigns.com</code></td><td>kingdezigns.com</td><td>A</td></tr>
<tr><td><code>has.kingdezigns.com</code></td><td>kingdezigns.com</td><td>A</td></tr>
<tr><td><code>nginx.kingdezigns.com</code></td><td>kingdezigns.com</td><td>A</td></tr>
<tr><td><code>omv08.kingdezigns.com</code></td><td>kingdezigns.com</td><td>A</td></tr>
<tr><td><code>omv16.kingdezigns.com</code></td><td>kingdezigns.com</td><td>A</td></tr>
<tr><td><code>pihole.kingdezigns.com</code></td><td>kingdezigns.com</td><td>A</td></tr>
<tr><td><code>plex.kingdezigns.com</code></td><td>kingdezigns.com</td><td>A</td></tr>
<tr><td><code>vault.kingdezigns.com</code></td><td>kingdezigns.com</td><td>A</td></tr>
<tr><td><code>webmin.kingdezigns.com</code></td><td>kingdezigns.com</td><td>A</td></tr>
</tbody>
</table>
</div>
<!-- ===== SECTION 4 — DEPLOYMENT STEPS ================================== -->
<div class="section">
<div class="section-title" data-num="4">Deployment — step by step</div>
<div class="step">
<div class="step-header"><span class="step-num">1</span> Install dependencies</div>
<pre>sudo apt-get update
sudo apt-get install -y curl python3 mailutils</pre>
<p class="why"><code>curl</code> fetches WAN IP and calls the DreamHost API. <code>python3</code> parses the JSON responses. <code>mailutils</code> provides the <code>mail</code> command for sending HTML notifications.</p>
</div>
<div class="step">
<div class="step-header"><span class="step-num">2</span> Copy the script to its permanent location</div>
<pre>sudo cp ddns_update.sh /usr/local/sbin/ddns_update.sh</pre>
<p class="result neutral">This puts it in a root-only system bin directory alongside other admin utilities.</p>
</div>
<div class="step">
<div class="step-header"><span class="step-num">3</span> Set ownership and permissions</div>
<pre>sudo chown root:root /usr/local/sbin/ddns_update.sh
sudo chmod 700 /usr/local/sbin/ddns_update.sh</pre>
<p class="why">The script reads/writes to <code>/var/log/ddns/</code> and calls sendmail — it must run as root. Mode 700 prevents other users from reading the embedded API key.</p>
</div>
<div class="step">
<div class="step-header"><span class="step-num">4</span> Create the log directory</div>
<pre>sudo mkdir -p /var/log/ddns
sudo chmod 700 /var/log/ddns</pre>
<p class="why">The script will create this automatically on first run, but creating it manually with 700 ensures no other user can read the cached IP or logs before then.</p>
</div>
<div class="step">
<div class="step-header"><span class="step-num">5</span> Test a manual run</div>
<pre>sudo /usr/local/sbin/ddns_update.sh</pre>
<p class="why">Run once by hand to verify API connectivity, confirm all 20 records are found, and check that the email arrives at rufus.king@kingdezigns.com. Watch the output — it prints everything it does.</p>
<p class="result neutral">On the very first run there is no cached IP, so it will treat every record as new and perform a full update cycle.</p>
</div>
<div class="step">
<div class="step-header"><span class="step-num">6</span> Verify the log file after test run</div>
<pre>sudo cat /var/log/ddns/ddns_run.log</pre>
<p class="result neutral">You should see the WAN IP detected, DreamHost responses for each record, and "DDNS run complete" at the bottom.</p>
</div>
<div class="step">
<div class="step-header"><span class="step-num">7</span> Add OMV Scheduled Task</div>
<pre>OMV → System → Scheduled Tasks → + (Add)
Enabled: ✓
Minute: */15
Hour: *
Day: *
Month: *
Weekday: *
User: root
Command: /usr/local/sbin/ddns_update.sh</pre>
<p class="why">This fires the script every 15 minutes around the clock. OMV uses standard cron under the hood — you can verify with <code>sudo crontab -l</code> after saving.</p>
<p class="result pass">Script is now live and fully automated.</p>
</div>
</div>
<!-- ===== SECTION 5 — SCRIPT & FILES ===================================== -->
<div class="section">
<div class="section-title" data-num="5">Script &amp; files created</div>
<div class="script-box">
<div class="script-row">
<div class="script-label">Script location</div>
<pre>/usr/local/sbin/ddns_update.sh</pre>
</div>
<div class="script-row">
<div class="script-label">IP cache (previous WAN IP)</div>
<pre>/var/log/ddns/last_wan_ip.txt</pre>
</div>
<div class="script-row">
<div class="script-label">Run log (last 5000 lines, auto-trimmed)</div>
<pre>/var/log/ddns/ddns_run.log</pre>
</div>
<div class="script-row">
<div class="script-label">Notification email</div>
<pre>rufus.king@kingdezigns.com</pre>
</div>
<div class="script-row">
<div class="script-label">Manual run command</div>
<pre>sudo /usr/local/sbin/ddns_update.sh</pre>
</div>
<div class="script-row">
<div class="script-label">Force a fresh update (clears cache)</div>
<pre>sudo rm /var/log/ddns/last_wan_ip.txt &amp;&amp; sudo /usr/local/sbin/ddns_update.sh</pre>
</div>
<div class="script-row">
<div class="script-label">Watch the live log</div>
<pre>sudo tail -f /var/log/ddns/ddns_run.log</pre>
</div>
</div>
</div>
<!-- ===== SECTION 6 — EMAIL BEHAVIOR ===================================== -->
<div class="section">
<div class="section-title" data-num="6">Email notification behavior</div>
<table>
<thead><tr><th>Scenario</th><th>Email sent?</th><th>Subject line</th></tr></thead>
<tbody>
<tr>
<td>IP unchanged, not daily window</td>
<td><span class="badge">No</span></td>
<td>Silent exit — no email</td>
</tr>
<tr>
<td>IP changed, all records updated successfully</td>
<td><span class="badge green">Yes</span></td>
<td>[DDNS] ✅ All Records Updated — &lt;new IP&gt;</td>
</tr>
<tr>
<td>IP changed, some records failed after 2 attempts</td>
<td><span class="badge red">Yes — action needed</span></td>
<td>[DDNS] ⚠️ PARTIAL FAILURE — Manual action needed — &lt;new IP&gt;</td>
</tr>
<tr>
<td>Daily sanity check (00:0000:14), all records match</td>
<td><span class="badge green">Yes</span></td>
<td>[DDNS] ✅ All Records Updated — &lt;IP&gt;</td>
</tr>
<tr>
<td>Daily sanity check, mismatch found &amp; fixed</td>
<td><span class="badge green">Yes</span></td>
<td>[DDNS] ✅ All Records Updated — &lt;new IP&gt;</td>
</tr>
<tr>
<td>WAN IP discovery fails (all 3 sources down)</td>
<td><span class="badge">No</span></td>
<td>Script aborts — will retry next 15-min run</td>
</tr>
</tbody>
</table>
<div class="callout green" style="margin-top:0.75rem;">
<p>All emails are sent as <strong>HTML</strong> using the KingDezigns branded template — same typography and color system as this document. They include a record-by-record results table and, on failure, a direct link to the DreamHost DNS panel with the correct IP pre-noted for manual entry.</p>
</div>
</div>
<!-- ===== SECTION 7 — IMPORTANT NOTES =================================== -->
<div class="section">
<div class="section-title" data-num="7">Important notes &amp; gotchas</div>
<table>
<thead><tr><th>Item</th><th>Detail</th></tr></thead>
<tbody>
<tr>
<td>API key is embedded in the script</td>
<td>The script is chmod 700 / root only. Do not copy or share the file without scrubbing the key first.</td>
</tr>
<tr>
<td>DreamHost has no "update" API command</td>
<td>Every update is a remove + add pair. The script always reads the live DreamHost value first so it never removes the wrong IP.</td>
</tr>
<tr>
<td>Wildcard record <code>*.web.kingdezigns.com</code></td>
<td>DreamHost supports wildcard A records via API. The script passes the literal string <code>*.web.kingdezigns.com</code> to <code>dns-list_records</code> and it will match correctly.</td>
</tr>
<tr>
<td>Mail must be configured on NAS16</td>
<td>Run <code>sudo apt-get install mailutils</code> and ensure your MTA (Postfix or ssmtp) is configured to relay outbound email. Test with: <code>echo "test" | mail -s "test" rufus.king@kingdezigns.com</code></td>
</tr>
<tr>
<td>First run behavior</td>
<td>On the very first run there is no cached IP. The script treats every record as needing an update and performs a full cycle. This is expected and correct.</td>
</tr>
<tr>
<td>Daily window fires once per day</td>
<td>The 00:0000:14 window aligns with the every-15-min schedule — only one run per day falls in that window. No extra logic needed.</td>
</tr>
<tr>
<td>Log rotation</td>
<td>The run log auto-trims itself to 5000 lines on every execution. No logrotate config needed.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer">
<span>KingDezigns Home Network</span>
<span>Generated May 19, 2025 — DDNS Updater build session</span>
</div>
</div>
</body>
</html>