119 lines
3.3 KiB
Python
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"{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)
|