feat: Add healthcheck, graceful shutdown, and SSL verify option
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 19s
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 19s
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
81
sync.py
81
sync.py
@@ -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...")
|
||||
|
||||
Reference in New Issue
Block a user