#!/usr/bin/env python3 import argparse import ipaddress import json import sys import urllib.parse import urllib.request def request(method, base, path, token, payload=None): data = None headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} if payload is not None: data = json.dumps(payload, separators=(",", ":")).encode("utf-8") req = urllib.request.Request(base.rstrip("/") + path, data=data, headers=headers, method=method) with urllib.request.urlopen(req, timeout=30) as resp: body = json.loads(resp.read().decode("utf-8")) if not body.get("success"): raise SystemExit("FAIL cloudflare_api_error") return body["result"] def main(): parser = argparse.ArgumentParser() parser.add_argument("--api-base", required=True) parser.add_argument("--zone", required=True) parser.add_argument("--public-ip", required=True) parser.add_argument("--apply", action="store_true") parser.add_argument("--output", required=True) args = parser.parse_args() token = sys.stdin.read().strip() if not token: raise SystemExit("FAIL missing_token_stdin") ipaddress.ip_address(args.public_ip) zones = request("GET", args.api_base, "/zones?name=" + urllib.parse.quote(args.zone), token) if len(zones) != 1: raise SystemExit(f"FAIL zone_lookup count={len(zones)}") zone_id = zones[0]["id"] desired = { f"cloud.{args.zone}": {"type": "A", "content": args.public_ip, "proxied": False, "ttl": 300}, f"git.{args.zone}": {"type": "A", "content": args.public_ip, "proxied": False, "ttl": 300}, f"status.{args.zone}": {"type": "A", "content": args.public_ip, "proxied": False, "ttl": 300}, } existing = request("GET", args.api_base, f"/zones/{zone_id}/dns_records?per_page=500", token) by_name_type = {(r["name"], r["type"]): r for r in existing} plan = [] for name, spec in sorted(desired.items()): current = by_name_type.get((name, spec["type"])) if current is None: plan.append({"action": "create", "name": name, **spec}) else: drift = any(current.get(k) != v for k, v in spec.items()) if drift: plan.append({"action": "update", "id": current["id"], "name": name, **spec}) if args.apply: for item in plan: payload = {k: item[k] for k in ("type", "name", "content", "ttl", "proxied")} if item["action"] == "create": request("POST", args.api_base, f"/zones/{zone_id}/dns_records", token, payload) else: request("PUT", args.api_base, f"/zones/{zone_id}/dns_records/{item['id']}", token, payload) result = { "zone": args.zone, "zone_id": zone_id, "public_ip": args.public_ip, "apply": bool(args.apply), "changes": [{k: v for k, v in item.items() if k != "id"} for item in plan], "preserved_record_types": ["MX", "TXT", "SPF", "DKIM", "DMARC", "verification"], } with open(args.output, "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2) f.write("\n") print(f"PASS edge_reconciler apply={bool(args.apply)} changes={len(plan)} zone={args.zone} values=redacted") if __name__ == "__main__": main()