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

View File

@@ -20,3 +20,6 @@ COPY sync.py /sync.py
ENV PYTHONUNBUFFERED=1
CMD ["python", "/sync.py"]
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD [ $(date +%s) -lt $(($(stat -c %Y /tmp/healthy) + 60)) ] || exit 1

View File

@@ -8,6 +8,7 @@ Dieser Container automatisiert die Generierung von DNS-Zonendateien für CoreDNS
* **Ausfallsicherer Betrieb:** Wenn NetBox nicht erreichbar ist oder Fehler zurückgibt, werden die bestehenden Zonendateien beibehalten. Das Schreiben erfolgt atomar, um Korruption bei Abstürzen zu verhindern.
* **Dual-Zone-Unterstützung:** Generiert sowohl Forward- als auch Reverse-(PTR)-Zonen.
* **Intelligenter Fallback:** Konfiguriert automatisch einen Fallback-Nameserver, falls in NetBox keine NS-Einträge definiert sind.
* **Healthcheck:** Überwacht den Sync-Status und ermöglicht Docker-Restarts bei anhaltenden Fehlern.
## Konfiguration
@@ -27,10 +28,12 @@ Die Konfiguration erfolgt vollständig über Umgebungsvariablen.
| Variable | Standardwert | Beschreibung |
| :--- | :--- | :--- |
| `REFRESH_INTERVAL` | `600` | Synchronisationsintervall in Sekunden. |
| `NETBOX_SSL_VERIFY` | `true` | SSL-Zertifikatsprüfung aktivieren (`true`/`false`). Bei selbstsignierten Zertifikaten auf `false` setzen. |
| `OUTPUT_FILE_FWD` | `/zones/db.fwd` | Pfad im Container für die Forward-Zonendatei. |
| `OUTPUT_FILE_REV` | `/zones/db.rev` | Pfad im Container für die Reverse-Zonendatei. |
| `FALLBACK_NS_HOSTNAME`| `ns1` | Hostname, der als NS-Eintrag verwendet wird, falls keiner in NetBox existiert. |
| `FALLBACK_NS_IP` | `127.0.0.1` | IP-Adresse für den Fallback-NS-Glue-Record. |
| `HEALTH_FILE` | `/tmp/healthy` | Pfad zur Statusdatei für den Docker Healthcheck. |
## Verwendung

63
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', [])
@@ -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:
@@ -243,8 +276,24 @@ if __name__ == "__main__":
generate_zone_file_fwd(ipam_data, plugin_records)
generate_zone_file_rev(ipam_data, plugin_records)
except Exception as e:
log(f"CRITICAL ERROR: Sync abgebrochen! Behalte alte Zone-Files. Grund: {e}")
# Reset Errors bei Erfolg
error_counter = 0
touch_health_file()
log(f"Schlafe {REFRESH_INTERVAL} Sekunden...")
time.sleep(REFRESH_INTERVAL)
except Exception as 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)
# 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...")