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
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
CMD ["python", "/sync.py"]
|
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.
|
* **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.
|
* **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.
|
* **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
|
## Konfiguration
|
||||||
|
|
||||||
@@ -27,10 +28,12 @@ Die Konfiguration erfolgt vollständig über Umgebungsvariablen.
|
|||||||
| Variable | Standardwert | Beschreibung |
|
| Variable | Standardwert | Beschreibung |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| `REFRESH_INTERVAL` | `600` | Synchronisationsintervall in Sekunden. |
|
| `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_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. |
|
| `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_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. |
|
| `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
|
## Verwendung
|
||||||
|
|
||||||
|
|||||||
63
sync.py
63
sync.py
@@ -2,6 +2,11 @@ import requests
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
|
import signal
|
||||||
|
import urllib3
|
||||||
|
|
||||||
|
# Suppress insecure request warnings if SSL verification is disabled
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
def log(msg):
|
def log(msg):
|
||||||
"""Ungepufferte Ausgabe für Docker Logs"""
|
"""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_FWD = os.getenv("OUTPUT_FILE_FWD", "/zones/db.fwd")
|
||||||
OUTPUT_FILE_REV = os.getenv("OUTPUT_FILE_REV", "/zones/db.rev")
|
OUTPUT_FILE_REV = os.getenv("OUTPUT_FILE_REV", "/zones/db.rev")
|
||||||
DEFAULT_TTL = os.getenv("DEFAULT_TTL", "3600")
|
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 Konfiguration (wenn KEIN NS in NetBox gefunden wird)
|
||||||
FALLBACK_NS_HOSTNAME = os.getenv("FALLBACK_NS_HOSTNAME", "ns1")
|
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'}
|
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():
|
def fetch_ipam_data():
|
||||||
"""
|
"""
|
||||||
Holt alle aktiven IPs mit DNS-Namen aus NetBox.
|
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"
|
url = f"{NETBOX_URL}/api/ipam/ip-addresses/?status=active&dns_name__n=&limit=0"
|
||||||
log(f"Abruf IPAM: {url}")
|
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()
|
r.raise_for_status()
|
||||||
return r.json().get('results', [])
|
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"
|
url = f"{NETBOX_URL}/api/plugins/netbox-dns/records/?zone__name={ZONE_NAME}&limit=0"
|
||||||
log(f"Abruf DNS Plugin: {url}")
|
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()
|
r.raise_for_status()
|
||||||
data = r.json().get('results', [])
|
data = r.json().get('results', [])
|
||||||
|
|
||||||
@@ -223,6 +244,14 @@ def generate_zone_file_rev(ipam_data, plugin_records):
|
|||||||
write_atomic(OUTPUT_FILE_REV, content)
|
write_atomic(OUTPUT_FILE_REV, content)
|
||||||
log(f"SUCCESS: Reverse Zone geschrieben ({len(ptr_records)} Records).")
|
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 __name__ == "__main__":
|
||||||
if not os.path.exists(os.path.dirname(OUTPUT_FILE_FWD)):
|
if not os.path.exists(os.path.dirname(OUTPUT_FILE_FWD)):
|
||||||
log(f"FATAL: Verzeichnis {os.path.dirname(OUTPUT_FILE_FWD)} fehlt!")
|
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"--- CoreDNS Sync startet ---")
|
||||||
log(f"NetBox URL: {NETBOX_URL}")
|
log(f"NetBox URL: {NETBOX_URL}")
|
||||||
log(f"Zone: {ZONE_NAME}")
|
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()} ---")
|
log(f"\n--- Sync Start: {time.ctime()} ---")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -243,8 +276,24 @@ if __name__ == "__main__":
|
|||||||
generate_zone_file_fwd(ipam_data, plugin_records)
|
generate_zone_file_fwd(ipam_data, plugin_records)
|
||||||
generate_zone_file_rev(ipam_data, plugin_records)
|
generate_zone_file_rev(ipam_data, plugin_records)
|
||||||
|
|
||||||
except Exception as e:
|
# Reset Errors bei Erfolg
|
||||||
log(f"CRITICAL ERROR: Sync abgebrochen! Behalte alte Zone-Files. Grund: {e}")
|
error_counter = 0
|
||||||
|
touch_health_file()
|
||||||
|
|
||||||
log(f"Schlafe {REFRESH_INTERVAL} Sekunden...")
|
except Exception as e:
|
||||||
time.sleep(REFRESH_INTERVAL)
|
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...")
|
||||||
|
|||||||
Reference in New Issue
Block a user