Initial Pantheon infrastructure

This commit is contained in:
2026-08-03 22:56:18 +10:00
commit afc9ee12f8
36 changed files with 4408 additions and 0 deletions
+240
View File
@@ -0,0 +1,240 @@
#!/usr/bin/env python3
import json
import os
import requests
import yaml
from dotenv import load_dotenv
load_dotenv()
TOKEN = os.getenv("CF_API_TOKEN")
ZONE = os.getenv("CF_ZONE_ID")
HEADERS = {
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
}
def dns_key(record_type: str, name: str) -> str:
return f"{record_type}:{name}"
# ------------------------------------------------------------
# Load desired configuration
# ------------------------------------------------------------
with open("dns.yaml", "r") as f:
config = yaml.safe_load(f)
zone = config["zone"]
defaults = config.get("defaults", {})
aliases = config.get("aliases", {})
ignore = set(config.get("ignore", []))
default_type = defaults.get("type", "A")
default_content = defaults.get("content")
default_ttl = defaults.get("ttl", 1)
default_proxied = defaults.get("proxied", False)
desired = {}
for name, record in config.get("records", {}).items():
fqdn = zone if name == "@" else f"{name}.{zone}"
rtype = record.get("type", default_type)
# Only manage A records for now
if rtype != "A":
continue
key = dns_key(rtype, fqdn)
desired[key] = {
"name": key,
"type": rtype,
"content": record.get("content", default_content),
"ttl": record.get("ttl", default_ttl),
"proxied": record.get("proxied", default_proxied),
}
# ------------------------------------------------------------
# Download Cloudflare records
# ------------------------------------------------------------
url = f"https://api.cloudflare.com/client/v4/zones/{ZONE}/dns_records"
response = requests.get(url, headers=HEADERS)
response.raise_for_status()
current = {}
for r in response.json()["result"]:
if r["type"] != "A":
continue
key = f"{r['type']}:{r['name']}"
current[key] = {
"id": r["id"],
"type": r["type"],
"content": r["content"],
"ttl": r["ttl"],
"proxied": r.get("proxied", False),
}
# ------------------------------------------------------------
# Build execution plan
# ------------------------------------------------------------
plan = {
"create": [],
"update": [],
"rename": [],
"delete": [],
}
handled_current = set()
# ------------------------------------------------------------
# Creates / Updates / Renames
# ------------------------------------------------------------
for key, wanted in desired.items():
fqdn = wanted["name"]
short = fqdn.replace(f".{zone}", "")
#
# Existing record?
#
if key in current:
handled_current.add(key)
existing = current[key]
changed = False
for key in ("type", "content", "ttl", "proxied"):
if existing[key] != wanted[key]:
print(f"\nDifference: {fqdn}")
print(f" {key}")
print(f" Current : {existing[key]!r}")
print(f" Wanted : {wanted[key]!r}")
changed = True
if changed:
plan["update"].append({
"name": key,
"id": existing["id"],
"desired": wanted,
})
continue
#
# Alias?
#
old = None
for alias, target in aliases.items():
if target != short:
continue
alias_key = dns_key(wanted["type"], f"{alias}.{zone}")
if alias_key in current:
handled_current.add(alias_key)
plan["rename"].append({
"old": alias_fqdn,
"new": fqdn,
"id": current[alias_key]["id"],
"desired": wanted,
})
old = alias_key
break
if old:
continue
#
# Create
#
plan["create"].append({
"name": key,
"desired": wanted,
})
# ------------------------------------------------------------
# Deletes
# ------------------------------------------------------------
for key, existing in current.items():
_, fqdn = key.split(":", 1)
short = fqdn.replace(f".{zone}", "")
if short in ignore:
continue
if key in handled_current:
continue
if key in desired:
continue
plan["delete"].append({
"name": key,
"id": existing["id"],
})
# ------------------------------------------------------------
# Print
# ------------------------------------------------------------
def heading(title):
print()
print("=" * 70)
print(title)
print("=" * 70)
heading(f"CREATE ({len(plan['create'])})")
for item in sorted(plan["create"], key=lambda x: x["name"]):
print(item["name"])
heading(f"RENAME ({len(plan['rename'])})")
for item in sorted(plan["rename"], key=lambda x: x["old"]):
print(f"{item['old']} -> {item['new']}")
heading(f"UPDATE ({len(plan['update'])})")
for item in sorted(plan["update"], key=lambda x: x["name"]):
print(item["name"])
heading(f"DELETE ({len(plan['delete'])})")
for item in sorted(plan["delete"], key=lambda x: x["name"]):
print(item["name"])
# ------------------------------------------------------------
# Save plan
# ------------------------------------------------------------
with open("plan.json", "w") as f:
json.dump(plan, f, indent=4)
print()
print("Execution plan written to plan.json")