dns-traefik-sync/dns_sync.py

119 lines
3.3 KiB
Python

#!/usr/bin/env python3
import time
import re
import subprocess
import logging
import os
import json
from pathlib import Path
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s'
)
log = logging.getLogger(__name__)
TRAEFIK_API = os.environ.get('TRAEFIK_API', 'http://<TARGET_IP>:8080')
DNS_SERVER = os.environ.get('DNS_SERVER', '<DNS_SERVER_IP>')
DNS_ZONE = os.environ.get('DNS_ZONE', '<DNS_ZONE>')
TARGET_IP = os.environ.get('TARGET_IP', '<TARGET_IP>')
TTL = int(os.environ.get('TTL', '300'))
POLL_INTERVAL = int(os.environ.get('POLL_INTERVAL', '60'))
KEYTAB = os.environ.get('KEYTAB', '/run/secrets/dns-updater-keytab')
KRB_PRINCIPAL = os.environ.get('KRB_PRINCIPAL', 'dns-updater@<REALM>')
STATE_FILE = os.environ.get('STATE_FILE', '/tmp/dns_state.json')
def kinit():
result = subprocess.run(
['kinit', '-kt', KEYTAB, KRB_PRINCIPAL],
capture_output=True, text=True
)
if result.returncode != 0:
raise RuntimeError(f"kinit failed: {result.stderr}")
log.info("Kerberos ticket obtained")
def get_traefik_hostnames():
import urllib.request
import urllib.error
try:
with urllib.request.urlopen(f"{TRAEFIK_API}/api/http/routers", timeout=10) as r:
routers = json.loads(r.read())
except urllib.error.URLError as e:
log.error(f"Failed to reach Traefik API: {e}")
return set()
hostnames = set()
for router in routers:
if router.get('provider') != 'docker':
continue
rule = router.get('rule', '')
for match in re.finditer(r'Host\(`([^`]+)`\)', rule):
host = match.group(1)
if host.endswith(DNS_ZONE):
hostnames.add(host)
return hostnames
def nsupdate(commands):
input_data = f"gss-tsig\n{commands}\n"
result = subprocess.run(
['nsupdate', '-g'],
input=input_data, capture_output=True, text=True
)
if result.returncode != 0:
raise RuntimeError(f"nsupdate failed: {result.stderr}")
def add_record(hostname):
log.info(f"Adding {hostname} -> {TARGET_IP}")
nsupdate(f"""server {DNS_SERVER}
zone {DNS_ZONE}
update add {hostname}. {TTL} A {TARGET_IP}
send""")
def remove_record(hostname):
log.info(f"Removing {hostname}")
nsupdate(f"""server {DNS_SERVER}
zone {DNS_ZONE}
update delete {hostname}. A
send""")
def load_state():
try:
return set(json.loads(Path(STATE_FILE).read_text()))
except Exception:
return set()
def save_state(hostnames):
Path(STATE_FILE).write_text(json.dumps(list(hostnames)))
def sync():
kinit()
current = get_traefik_hostnames()
previous = load_state()
to_add = current - previous
to_remove = previous - current
for h in to_add:
try:
add_record(h)
except Exception as e:
log.error(f"Failed to add {h}: {e}")
current.discard(h)
for h in to_remove:
try:
remove_record(h)
except Exception as e:
log.error(f"Failed to remove {h}: {e}")
save_state(current)
if __name__ == '__main__':
log.info("dns-traefik-sync starting")
while True:
try:
sync()
except Exception as e:
log.error(f"Sync failed: {e}")
time.sleep(POLL_INTERVAL)