diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..beadee7 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# .env Example Configuration +# Copy this file to .env and adjust the values + +# --- Required --- +NETBOX_URL=http://netbox.example.com +NETBOX_TOKEN=0123456789abcdef0123456789abcdef01234567 + +# --- Optional (Defaults shown) --- +# ZONE_NAME=klenzel.net +# REVERSE_ZONE_NAME=172.in-addr.arpa +# REFRESH_INTERVAL=600 +# FALLBACK_NS_HOSTNAME=fks-01-cl-cdns +# FALLBACK_NS_IP=172.25.16.152 diff --git a/Dockerfile b/Dockerfile index e3ad3fa..62839a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,17 @@ FROM python:3.11-alpine -# Standard-Environment-Variablen (können beim Start überschrieben werden) -ENV NETBOX_URL="http://172.30.242.99" \ - NETBOX_TOKEN="0b740520aeef964cfd2ca34b82a472a271d51649" \ +# Defaults für nicht-sensitive Konfiguration +ENV REFRESH_INTERVAL=600 \ ZONE_NAME="klenzel.net" \ REVERSE_ZONE_NAME="172.in-addr.arpa" \ - REFRESH_INTERVAL=600 \ OUTPUT_FILE_FWD="/zones/db.klenzel.net" \ OUTPUT_FILE_REV="/zones/db.reverse.arpa" \ DEFAULT_TTL=3600 \ FALLBACK_NS_HOSTNAME="fks-01-cl-cdns" \ FALLBACK_NS_IP="172.25.16.152" +# Hinweis: NETBOX_URL und NETBOX_TOKEN müssen zur Laufzeit übergeben werden! + RUN pip install requests COPY sync.py /sync.py @@ -19,4 +19,4 @@ COPY sync.py /sync.py # Logs ungepuffert ausgeben ENV PYTHONUNBUFFERED=1 -CMD ["python", "/sync.py"] \ No newline at end of file +CMD ["python", "/sync.py"] diff --git a/README.md b/README.md index 53a45da..7e6c020 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,73 @@ # CoreDNS NetBox Sync -Dieser Container synchronisiert DNS-Records aus [NetBox](https://github.com/netbox-community/netbox) in lokale Zonefiles, die von [CoreDNS](https://coredns.io/) (oder BIND) genutzt werden können. +This container automates the generation of DNS zone files for CoreDNS (or BIND) by synchronizing data from [NetBox](https://github.com/netbox-community/netbox). It fetches IPAM data (active IPs with DNS names) and DNS Plugin records. ## Features -- **Automatische Synchronisation:** Holt alle 10 Minuten (konfigurierbar) Daten aus NetBox. -- **Forward & Reverse Zones:** Erstellt A/AAAA und PTR Records. -- **Ausfallsicherheit:** Wenn NetBox nicht erreichbar ist oder die API Fehler wirft, werden **keine** leeren Dateien geschrieben. Die alten Zonefiles bleiben erhalten, um `NXDOMAIN` Antworten zu verhindern. -- **Fallback NS:** Konfigurierbarer Fallback-Nameserver, falls in NetBox keine NS-Records für die Zone definiert sind. +* **Automated Synchronization:** Periodically fetches data from NetBox (Default: 10 minutes). +* **Fail-Safe Operation:** If NetBox is unreachable or returns errors, the existing zone files are preserved to prevent `NXDOMAIN` issues. +* **Dual Zone Support:** Generates both Forward and Reverse (PTR) zones. +* **Intelligent Fallback:** Automatically configures a fallback Nameserver if no NS records are defined in NetBox. -## Installation & Nutzung +## Configuration -### 1. Image bauen +Configuration is handled entirely via environment variables. -```bash -docker build -t local/dns-sync . -``` +### Required Variables -### 2. Container starten +| Variable | Description | +| :--- | :--- | +| `NETBOX_URL` | The full URL to the NetBox instance (e.g., `http://netbox.local`). | +| `NETBOX_TOKEN` | The API Token for authentication (Read-Only permissions are sufficient). | -Da die Standardwerte im Dockerfile hinterlegt sind, reicht dein bisheriger Befehl völlig aus, solange sich an der Konfiguration nichts geändert hat: +### Optional Variables -```bash -docker run -d \ - --name klzDNS-worker \ - --restart unless-stopped \ - --net=container:klzDNS-coredns \ - -v klzDNS-data:/zones \ - local/dns-sync -``` - -### 3. Konfiguration anpassen (Optional) - -Möchtest du Werte ändern (z.B. URL, Token oder Interval), kannst du diese als Umgebungsvariablen (`-e`) übergeben: - -```bash -docker run -d \ - --name klzDNS-worker \ - --restart unless-stopped \ - --net=container:klzDNS-coredns \ - -v klzDNS-data:/zones \ - -e NETBOX_URL="http://deine-netbox-url" \ - -e NETBOX_TOKEN="dein-neuer-token" \ - -e REFRESH_INTERVAL=300 \ - local/dns-sync -``` - -Alternativ kannst du eine `.env` Datei erstellen: - -```ini -# .env Datei -NETBOX_URL=http://192.168.1.50 -REFRESH_INTERVAL=60 -``` - -Und diese einbinden: - -```bash -docker run -d ... --env-file .env local/dns-sync -``` - -## Verfügbare Variablen - -| Variable | Standardwert | Beschreibung | +| Variable | Default | Description | | :--- | :--- | :--- | -| `NETBOX_URL` | `http://172.30.242.99` | URL zur NetBox Instanz | -| `NETBOX_TOKEN` | `0b74...` | API Token (Read-Only reicht) | -| `ZONE_NAME` | `klenzel.net` | Die zu verwaltende DNS-Zone | -| `REVERSE_ZONE_NAME` | `172.in-addr.arpa` | Reverse Lookup Zone | -| `REFRESH_INTERVAL` | `600` | Sync-Intervall in Sekunden | -| `OUTPUT_FILE_FWD` | `/zones/db.klenzel.net` | Pfad zur Forward Zone im Container | -| `OUTPUT_FILE_REV` | `/zones/db.reverse.arpa` | Pfad zur Reverse Zone im Container | -| `FALLBACK_NS_HOSTNAME`| `fks-01-cl-cdns` | Hostname des NS, falls keiner in NetBox definiert ist | -| `FALLBACK_NS_IP` | `172.25.16.152` | IP des Fallback NS (für Glue Record) | +| `REFRESH_INTERVAL` | `600` | Synchronization interval in seconds. | +| `ZONE_NAME` | `klenzel.net` | The DNS zone name to manage. | +| `REVERSE_ZONE_NAME` | `172.in-addr.arpa` | The reverse lookup zone name. | +| `OUTPUT_FILE_FWD` | `/zones/db.klenzel.net` | Path inside the container for the forward zone file. | +| `OUTPUT_FILE_REV` | `/zones/db.reverse.arpa` | Path inside the container for the reverse zone file. | +| `FALLBACK_NS_HOSTNAME`| `fks-01-cl-cdns` | Hostname used for NS record if none exist in NetBox. | +| `FALLBACK_NS_IP` | `172.25.16.152` | IP address for the fallback NS glue record. | + +## Usage + +### Docker + +1. **Build the image:** + ```bash + docker build -t local/dns-sync . + ``` + +2. **Run with environment variables:** + ```bash + docker run -d \ + --name klzDNS-worker \ + --restart unless-stopped \ + --net=container:klzDNS-coredns \ + -v klzDNS-data:/zones \ + -e NETBOX_URL="http://172.30.242.99" \ + -e NETBOX_TOKEN="your-secret-token" \ + local/dns-sync + ``` + +### Using a `.env` file + +1. Create a `.env` file based on the example: + ```bash + cp .env.example .env + # Edit .env and add your credentials + ``` + +2. Run the container referencing the file: + ```bash + docker run -d \ + --name klzDNS-worker \ + --restart unless-stopped \ + --net=container:klzDNS-coredns \ + -v klzDNS-data:/zones \ + --env-file .env \ + local/dns-sync + ``` \ No newline at end of file diff --git a/sync.py b/sync.py index 1c7749a..5c9071f 100644 --- a/sync.py +++ b/sync.py @@ -3,17 +3,24 @@ import os import time import sys -# --- KONFIGURATION (Via Environment Variables) --- -# Falls keine ENV-Vars gesetzt sind, werden die alten Hardcoded-Werte als Fallback genutzt. -NETBOX_URL = os.getenv("NETBOX_URL", "http://172.30.242.99") -TOKEN = os.getenv("NETBOX_TOKEN", "0b740520aeef964cfd2ca34b82a472a271d51649") +def log(msg): + """Ungepufferte Ausgabe für Docker Logs""" + print(msg, flush=True) +# --- KONFIGURATION --- +# Pflichtfelder - Abbruch wenn nicht gesetzt +NETBOX_URL = os.getenv("NETBOX_URL") +TOKEN = os.getenv("NETBOX_TOKEN") + +if not NETBOX_URL or not TOKEN: + log("FATAL ERROR: Environment variables NETBOX_URL and NETBOX_TOKEN are required.") + log("Please provide them via -e flag or .env file.") + sys.exit(1) + +# Optionale Konfiguration mit Defaults ZONE_NAME = os.getenv("ZONE_NAME", "klenzel.net") REVERSE_ZONE_NAME = os.getenv("REVERSE_ZONE_NAME", "172.in-addr.arpa") - -# Interval in Sekunden REFRESH_INTERVAL = int(os.getenv("REFRESH_INTERVAL", "600")) - OUTPUT_FILE_FWD = os.getenv("OUTPUT_FILE_FWD", "/zones/db.klenzel.net") OUTPUT_FILE_REV = os.getenv("OUTPUT_FILE_REV", "/zones/db.reverse.arpa") DEFAULT_TTL = os.getenv("DEFAULT_TTL", "3600") @@ -21,18 +28,13 @@ DEFAULT_TTL = os.getenv("DEFAULT_TTL", "3600") # Fallback Konfiguration (wenn KEIN NS in NetBox gefunden wird) FALLBACK_NS_HOSTNAME = os.getenv("FALLBACK_NS_HOSTNAME", "fks-01-cl-cdns") FALLBACK_NS_IP = os.getenv("FALLBACK_NS_IP", "172.25.16.152") -# --------------------- HEADERS = {'Authorization': f'Token {TOKEN}', 'Accept': 'application/json'} -def log(msg): - """Ungepufferte Ausgabe für Docker Logs""" - print(msg, flush=True) - def fetch_ipam_data(): """ Holt alle aktiven IPs mit DNS-Namen aus NetBox. - Wirft eine Exception, wenn NetBox nicht erreichbar ist (kein try/except hier!). + Wirft eine Exception, wenn NetBox nicht erreichbar ist. """ url = f"{NETBOX_URL}/api/ipam/ip-addresses/?status=active&dns_name__n=&limit=0" log(f"Abruf IPAM: {url}") @@ -114,7 +116,6 @@ def get_ns_config(plugin_records, all_records): def generate_zone_file_fwd(ipam_data, plugin_records): """Erstellt die Forward-Zone""" - # Daten aufbereiten ipam_records = [] for ip in ipam_data: dns_name = ip.get('dns_name', '') @@ -133,7 +134,6 @@ def generate_zone_file_fwd(ipam_data, plugin_records): all_records = plugin_records + ipam_records - # Header-Logik primary_ns, extra_headers = get_ns_config(plugin_records, all_records) serial = int(time.time()) @@ -152,7 +152,6 @@ def generate_zone_file_fwd(ipam_data, plugin_records): header.extend(extra_headers) header.append("") - # Schreiben with open(OUTPUT_FILE_FWD, "w") as f: f.write("\n".join(header)) f.write("\n; --- Records ---\n") @@ -207,11 +206,10 @@ def generate_zone_file_rev(ipam_data, plugin_records): log(f"SUCCESS: Reverse Zone geschrieben ({len(ptr_records)} Records).") if __name__ == "__main__": - # Kurzer Check ob Zielordner existiert if not os.path.exists(os.path.dirname(OUTPUT_FILE_FWD)): log(f"FATAL: Verzeichnis {os.path.dirname(OUTPUT_FILE_FWD)} fehlt!") - time.sleep(30) - exit(1) + time.sleep(5) + sys.exit(1) log(f"--- CoreDNS Sync startet ---") log(f"NetBox URL: {NETBOX_URL}") @@ -221,22 +219,14 @@ if __name__ == "__main__": log(f"\n--- Sync Start: {time.ctime()} ---") try: - # Schritt 1: Daten holen (wirft Exception bei Fehler) - # Wir holen BEIDE Datensätze, bevor wir irgendetwas schreiben. - # Wenn einer fehlschlägt, bricht der ganze Block ab. ipam_data = fetch_ipam_data() plugin_records = fetch_plugin_records() - # Schritt 2: Zonen schreiben - # Wird nur erreicht, wenn fetch_ipam_data UND fetch_plugin_records erfolgreich waren generate_zone_file_fwd(ipam_data, plugin_records) generate_zone_file_rev(ipam_data, plugin_records) except Exception as e: - # Sicherheits-Logik: Bei JEDEM Fehler (Netzwerk, HTTP 500, Parse-Error) - # fangen wir hier ab und schreiben KEINE Dateien. - # Die alten Dateien bleiben erhalten -> CoreDNS liefert weiter die alten Daten (kein NXDOMAIN). log(f"CRITICAL ERROR: Sync abgebrochen! Behalte alte Zone-Files. Grund: {e}") log(f"Schlafe {REFRESH_INTERVAL} Sekunden...") - time.sleep(REFRESH_INTERVAL) \ No newline at end of file + time.sleep(REFRESH_INTERVAL)