dns: Technitium-master sync to NetBird managed zone + cleanup
- scripts/dns-sync.py: reconcile Technitium named A-records -> NetBird managed zone via API (cron */10 on dns LXC 107). Single authoring source; kills the manual drift behind the auth/sso/nfs-export saga. - secrets/netbird-pat.yaml: sops-encrypted NetBird API PAT for the sync. - dns.md / 107-dns.md: document the sync model + why forward-to-Technitium was abandoned (NetBird self-IP / nameserver-group quirks). - Cleanup: removed inert Mac secondary; reverted primary AXFR; home-lab-dns -> [192.168.8.2] (1/1 Available); deleted vestigial Proxmox Names group. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
49
scripts/dns-sync.py
Normal file
49
scripts/dns-sync.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Sync Technitium hubris.network A-records -> NetBird managed DNS zone.
|
||||
Technitium is the single authoring source; this reconciles NetBird to match."""
|
||||
import json, urllib.request, urllib.parse, ssl, sys
|
||||
|
||||
NB_API = "https://netbird.hubris.network/api"
|
||||
ZONE_ID = "d7gaad00qfrc73er3n1g"
|
||||
NB_TOKEN = open("/opt/dns-sync/netbird-token").read().strip()
|
||||
TECH = "http://127.0.0.1:5380/api"
|
||||
TECH_PW = open("/opt/technitium/admin_password.txt").read().strip()
|
||||
|
||||
ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
|
||||
|
||||
def jget(url):
|
||||
with urllib.request.urlopen(url, context=ctx, timeout=15) as r: return json.load(r)
|
||||
|
||||
def nb(method, path, data=None):
|
||||
req = urllib.request.Request(NB_API+path, method=method)
|
||||
req.add_header("Authorization", "Token "+NB_TOKEN)
|
||||
body=None
|
||||
if data is not None:
|
||||
req.add_header("Content-Type","application/json"); body=json.dumps(data).encode()
|
||||
with urllib.request.urlopen(req, data=body, context=ctx, timeout=15) as r:
|
||||
t=r.read(); return json.loads(t) if t else {}
|
||||
|
||||
# 1. source of truth: Technitium named A-records
|
||||
tok = jget(f"{TECH}/user/login?user=admin&pass={urllib.parse.quote(TECH_PW)}&includeInfo=false")["token"]
|
||||
recs = jget(f"{TECH}/zones/records/get?token={tok}&zone=hubris.network&domain=hubris.network&listZone=true")["response"]["records"]
|
||||
source = { r["name"]: r["rData"]["ipAddress"]
|
||||
for r in recs
|
||||
if r.get("type")=="A" and not r["name"].startswith("*") and r["name"]!="hubris.network" }
|
||||
|
||||
# 2. current NetBird records
|
||||
cur = { r["name"]: (r["id"], r["content"]) for r in nb("GET", f"/dns/zones/{ZONE_ID}/records") if r.get("type")=="A" }
|
||||
|
||||
# 3. reconcile
|
||||
c=u=d=0; changes=[]
|
||||
for name, ip in source.items():
|
||||
if name not in cur:
|
||||
nb("POST", f"/dns/zones/{ZONE_ID}/records", {"name":name,"type":"A","content":ip,"ttl":300}); c+=1; changes.append(f"+ {name} {ip}")
|
||||
elif cur[name][1] != ip:
|
||||
nb("DELETE", f"/dns/zones/{ZONE_ID}/records/{cur[name][0]}")
|
||||
nb("POST", f"/dns/zones/{ZONE_ID}/records", {"name":name,"type":"A","content":ip,"ttl":300}); u+=1; changes.append(f"~ {name} {cur[name][1]}->{ip}")
|
||||
for name,(rid,ip) in cur.items():
|
||||
if name not in source:
|
||||
nb("DELETE", f"/dns/zones/{ZONE_ID}/records/{rid}"); d+=1; changes.append(f"- {name} {ip}")
|
||||
|
||||
print(f"dns-sync: {len(source)} source records | +{c} ~{u} -{d}")
|
||||
for ch in changes: print(" "+ch)
|
||||
Reference in New Issue
Block a user