Initial Pantheon infrastructure
This commit is contained in:
@@ -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")
|
||||
Reference in New Issue
Block a user