feat: Add healthcheck, graceful shutdown, and SSL verify option
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 19s

This commit is contained in:
Gemini Bot
2025-12-09 19:19:30 +00:00
parent 61d2fd3b24
commit e656dc6f16
3 changed files with 71 additions and 16 deletions

81
sync.py
View File

@@ -2,6 +2,11 @@ import requests
import os
import time
import sys
import signal
import urllib3
# Suppress insecure request warnings if SSL verification is disabled
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def log(msg):
"""Ungepufferte Ausgabe für Docker Logs"""
@@ -24,6 +29,11 @@ REFRESH_INTERVAL = int(os.getenv("REFRESH_INTERVAL", "600"))
OUTPUT_FILE_FWD = os.getenv("OUTPUT_FILE_FWD", "/zones/db.fwd")
OUTPUT_FILE_REV = os.getenv("OUTPUT_FILE_REV", "/zones/db.rev")
DEFAULT_TTL = os.getenv("DEFAULT_TTL", "3600")
HEALTH_FILE = os.getenv("HEALTH_FILE", "/tmp/healthy")
# SSL Verification
verify_ssl_env = os.getenv("NETBOX_SSL_VERIFY", "true").lower()
VERIFY_SSL = verify_ssl_env in ("true", "1", "yes")
# Fallback Konfiguration (wenn KEIN NS in NetBox gefunden wird)
FALLBACK_NS_HOSTNAME = os.getenv("FALLBACK_NS_HOSTNAME", "ns1")
@@ -31,6 +41,17 @@ FALLBACK_NS_IP = os.getenv("FALLBACK_NS_IP", "127.0.0.1")
HEADERS = {'Authorization': f'Token {TOKEN}', 'Accept': 'application/json'}
# Globale Steuerung
running = True
def signal_handler(signum, frame):
global running
log(f"Signal {signum} empfangen. Beende Graceful...")
running = False
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
def fetch_ipam_data():
"""
Holt alle aktiven IPs mit DNS-Namen aus NetBox.
@@ -39,7 +60,7 @@ def fetch_ipam_data():
url = f"{NETBOX_URL}/api/ipam/ip-addresses/?status=active&dns_name__n=&limit=0"
log(f"Abruf IPAM: {url}")
r = requests.get(url, headers=HEADERS, timeout=10)
r = requests.get(url, headers=HEADERS, timeout=10, verify=VERIFY_SSL)
r.raise_for_status()
return r.json().get('results', [])
@@ -52,7 +73,7 @@ def fetch_plugin_records():
url = f"{NETBOX_URL}/api/plugins/netbox-dns/records/?zone__name={ZONE_NAME}&limit=0"
log(f"Abruf DNS Plugin: {url}")
r = requests.get(url, headers=HEADERS, timeout=10)
r = requests.get(url, headers=HEADERS, timeout=10, verify=VERIFY_SSL)
r.raise_for_status()
data = r.json().get('results', [])
@@ -163,11 +184,11 @@ def generate_zone_file_fwd(ipam_data, plugin_records):
f"$ORIGIN {ZONE_NAME}.",
f"$TTL {DEFAULT_TTL}",
f"@\tIN\tSOA\t{primary_ns}\tadmin.{ZONE_NAME}. (",
f"\t\t{serial}\t; Serial",
"\t\t7200\t\t; Refresh",
"\t\t3600\t\t; Retry",
"\t\t1209600\t\t; Expire",
f"\t\t{DEFAULT_TTL} )\t; Negative Cache TTL",
f"\t\t{serial}\t ; Serial",
"\t\t7200\t\t ; Refresh",
"\t\t3600\t\t ; Retry",
"\t\t1209600\t\t ; Expire",
f"\t\t{DEFAULT_TTL} )\t ; Negative Cache TTL",
""
]
@@ -203,11 +224,11 @@ def generate_zone_file_rev(ipam_data, plugin_records):
f"$ORIGIN {REVERSE_ZONE_NAME}.",
f"$TTL {DEFAULT_TTL}",
f"@\tIN\tSOA\t{primary_ns}\tadmin.{ZONE_NAME}. (",
f"\t\t{serial}\t; Serial",
"\t\t7200\t\t; Refresh",
"\t\t3600\t\t; Retry",
"\t\t1209600\t\t; Expire",
f"\t\t{DEFAULT_TTL} )\t; Negative Cache TTL",
f"\t\t{serial}\t ; Serial",
"\t\t7200\t\t ; Refresh",
"\t\t3600\t\t ; Retry",
"\t\t1209600\t\t ; Expire",
f"\t\t{DEFAULT_TTL} )\t ; Negative Cache TTL",
""
]
@@ -223,6 +244,14 @@ def generate_zone_file_rev(ipam_data, plugin_records):
write_atomic(OUTPUT_FILE_REV, content)
log(f"SUCCESS: Reverse Zone geschrieben ({len(ptr_records)} Records).")
def touch_health_file():
"""Aktualisiert die Health-Datei"""
try:
with open(HEALTH_FILE, 'w') as f:
f.write(str(time.time()))
except Exception as e:
log(f"WARN: Konnte Health-File nicht schreiben: {e}")
if __name__ == "__main__":
if not os.path.exists(os.path.dirname(OUTPUT_FILE_FWD)):
log(f"FATAL: Verzeichnis {os.path.dirname(OUTPUT_FILE_FWD)} fehlt!")
@@ -232,8 +261,12 @@ if __name__ == "__main__":
log(f"--- CoreDNS Sync startet ---")
log(f"NetBox URL: {NETBOX_URL}")
log(f"Zone: {ZONE_NAME}")
log(f"SSL Verify: {VERIFY_SSL}")
while True:
error_counter = 0
MAX_ERRORS = 3
while running:
log(f"\n--- Sync Start: {time.ctime()} ---")
try:
@@ -242,9 +275,25 @@ if __name__ == "__main__":
generate_zone_file_fwd(ipam_data, plugin_records)
generate_zone_file_rev(ipam_data, plugin_records)
# Reset Errors bei Erfolg
error_counter = 0
touch_health_file()
except Exception as e:
log(f"CRITICAL ERROR: Sync abgebrochen! Behalte alte Zone-Files. Grund: {e}")
error_counter += 1
log(f"CRITICAL ERROR ({error_counter}/{MAX_ERRORS}): Sync abgebrochen! Grund: {e}")
if error_counter >= MAX_ERRORS:
log("Too many errors. Exiting to allow Docker restart.")
sys.exit(1)
log(f"Schlafe {REFRESH_INTERVAL} Sekunden...")
time.sleep(REFRESH_INTERVAL)
# Smart Sleep: Check 'running' flag frequently
sleep_steps = 10
step_duration = REFRESH_INTERVAL / sleep_steps
for _ in range(sleep_steps):
if not running: break
time.sleep(step_duration)
log("Exiting...")