diff --git a/.env.example b/.env.example index beadee7..013bc8b 100644 --- a/.env.example +++ b/.env.example @@ -5,9 +5,15 @@ 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 +# --- Configuration (Defaults shown are generic) --- +ZONE_NAME=example.com +REVERSE_ZONE_NAME=1.168.192.in-addr.arpa +REFRESH_INTERVAL=600 + +# Paths inside the container +OUTPUT_FILE_FWD=/zones/db.fwd +OUTPUT_FILE_REV=/zones/db.rev + +# Fallback Nameserver (used if no NS records found in NetBox) +FALLBACK_NS_HOSTNAME=ns1 +FALLBACK_NS_IP=127.0.0.1 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 62839a7..fd9d5c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,13 +2,13 @@ FROM python:3.11-alpine # Defaults für nicht-sensitive Konfiguration ENV REFRESH_INTERVAL=600 \ - ZONE_NAME="klenzel.net" \ - REVERSE_ZONE_NAME="172.in-addr.arpa" \ - OUTPUT_FILE_FWD="/zones/db.klenzel.net" \ - OUTPUT_FILE_REV="/zones/db.reverse.arpa" \ + ZONE_NAME="example.com" \ + REVERSE_ZONE_NAME="1.168.192.in-addr.arpa" \ + OUTPUT_FILE_FWD="/zones/db.fwd" \ + OUTPUT_FILE_REV="/zones/db.rev" \ DEFAULT_TTL=3600 \ - FALLBACK_NS_HOSTNAME="fks-01-cl-cdns" \ - FALLBACK_NS_IP="172.25.16.152" + FALLBACK_NS_HOSTNAME="ns1" \ + FALLBACK_NS_IP="127.0.0.1" # Hinweis: NETBOX_URL und NETBOX_TOKEN müssen zur Laufzeit übergeben werden! diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..279a27f --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,64 @@ +# CoreDNS NetBox Sync + +This project 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 entries to maintain up-to-date Forward and Reverse zones. + +## Project Overview + +* **Core Logic:** A Python script (`sync.py`) runs in a continuous loop, fetching data from the NetBox API. +* **Output:** Generates standard DNS zone files compatible with CoreDNS and BIND. +* **Resilience:** If the NetBox API is unreachable, the script preserves existing zone files to prevent DNS outages (NXDOMAIN). +* **Environment:** designed to run as a Docker container, sharing the generated zone files via a volume with the CoreDNS container. + +## Key Files + +* `sync.py`: The main application logic. Handles API authentication, data fetching, data formatting, and file writing. +* `Dockerfile`: Defines the minimal Python 3.11 Alpine-based image for running the script. +* `.env.example`: Template for required environment variables. +* `README.md`: Official project documentation (German). + +## Configuration + +Configuration is handled entirely via environment variables. + +| Variable | Required | Default | Description | +| :--- | :---: | :--- | :--- | +| `NETBOX_URL` | Yes | - | Full URL to NetBox (e.g., `http://netbox.local`). | +| `NETBOX_TOKEN` | Yes | - | API Token (Read-only sufficient). | +| `ZONE_NAME` | No | `klenzel.net` | The DNS zone to manage. | +| `REVERSE_ZONE_NAME` | No | `172.in-addr.arpa` | The reverse lookup zone. | +| `REFRESH_INTERVAL` | No | `600` | Sync interval in seconds. | +| `OUTPUT_FILE_FWD` | No | `/zones/db.klenzel.net` | Path for the forward zone file. | +| `OUTPUT_FILE_REV` | No | `/zones/db.reverse.arpa` | Path for the reverse zone file. | +| `FALLBACK_NS_HOSTNAME`| No | `fks-01-cl-cdns` | Fallback NS hostname if none in NetBox. | +| `FALLBACK_NS_IP` | No | `172.25.16.152` | Fallback NS IP for glue record. | + +## Development & Usage + +### Building the Image + +```bash +docker build -t local/dns-sync . +``` + +### Running Locally (for testing) + +1. Create a `.env` file with your NetBox credentials. +2. Run the container: + +```bash +docker run -d \ + --name dns-sync-test \ + --env-file .env \ + -v $(pwd)/zones:/zones \ + local/dns-sync +``` + +*(Ensure the `./zones` directory exists locally before running)* + +### Logic Details + +* **IPAM Fetch:** Queries `/api/ipam/ip-addresses/?status=active&dns_name__n=&limit=0`. +* **Plugin Fetch:** Queries `/api/plugins/netbox-dns/records/?zone__name={ZONE_NAME}&limit=0`. +* **Nameserver Logic:** + * If NetBox has NS records for the zone, the first one is used as the Primary SOA. + * If **no** NS records exist, it falls back to `FALLBACK_NS_HOSTNAME` and creates a Glue Record (A record) for it to ensure the zone is valid. diff --git a/README.md b/README.md index 259164a..1096674 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ Dieser Container automatisiert die Generierung von DNS-Zonendateien für CoreDNS ## Funktionen -* **Automatische Synchronisation:** Ruft periodisch Daten von NetBox ab (Standard: alle 10 Minuten). -* **Ausfallsicherer Betrieb:** Wenn NetBox nicht erreichbar ist oder Fehler zurückgibt, werden die bestehenden Zonendateien beibehalten, um `NXDOMAIN`-Probleme zu vermeiden. +* **Automatische Synchronisation:** Ruft periodisch Daten von NetBox ab. +* **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. @@ -19,55 +19,54 @@ Die Konfiguration erfolgt vollständig über Umgebungsvariablen. | :--- | :--- | | `NETBOX_URL` | Die vollständige URL zur NetBox-Instanz (z.B. `http://netbox.local`). | | `NETBOX_TOKEN` | Das API-Token zur Authentifizierung (Nur-Lese-Berechtigungen sind ausreichend). | +| `ZONE_NAME` | Der Name der zu verwaltenden DNS-Zone (z.B. `example.com`). | +| `REVERSE_ZONE_NAME` | Der Name der Reverse-Lookup-Zone (z.B. `1.168.192.in-addr.arpa`). | ### Optionale Variablen | Variable | Standardwert | Beschreibung | | :--- | :--- | :--- | | `REFRESH_INTERVAL` | `600` | Synchronisationsintervall in Sekunden. | -| `ZONE_NAME` | `klenzel.net` | Der Name der zu verwaltenden DNS-Zone. | -| `REVERSE_ZONE_NAME` | `172.in-addr.arpa` | Der Name der Reverse-Lookup-Zone. | -| `OUTPUT_FILE_FWD` | `/zones/db.klenzel.net` | Pfad im Container für die Forward-Zonendatei. | -| `OUTPUT_FILE_REV` | `/zones/db.reverse.arpa` | Pfad im Container für die Reverse-Zonendatei. | -| `FALLBACK_NS_HOSTNAME`| `fks-01-cl-cdns` | Hostname, der als NS-Eintrag verwendet wird, falls keiner in NetBox existiert. | -| `FALLBACK_NS_IP` | `172.25.16.152` | IP-Adresse für den Fallback-NS-Glue-Record. | +| `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. | ## Verwendung -### Docker +### Docker (CLI Beispiel) -1. **Image bauen:** - ```bash - docker build -t local/dns-sync . - ``` +Starten Sie den Container mit folgendem Befehl. Passen Sie die Werte entsprechend Ihrer Umgebung an. -2. **Mit Umgebungsvariablen starten:** - ```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="dein-geheimes-token" \ - local/dns-sync - ``` +```bash +docker run -d \ + --name netbox-dns-sync \ + --restart unless-stopped \ + --network host \ + -v ./zones:/zones \ + -e NETBOX_URL="http://netbox.example.com" \ + -e NETBOX_TOKEN="0123456789abcdef0123456789abcdef01234567" \ + -e ZONE_NAME="example.com" \ + -e REVERSE_ZONE_NAME="1.168.192.in-addr.arpa" \ + -e OUTPUT_FILE_FWD="/zones/db.example.com" \ + -e OUTPUT_FILE_REV="/zones/db.192.168.1" \ + -e FALLBACK_NS_HOSTNAME="ns1" \ + -e FALLBACK_NS_IP="192.168.1.5" \ + -e REFRESH_INTERVAL="300" \ + local/dns-sync +``` + +*(Hinweis: Der Pfad `./zones` muss auf dem Host existieren und schreibbar sein)* ### Verwendung einer `.env`-Datei -1. Erstellen Sie eine `.env`-Datei basierend auf dem Beispiel: - ```bash - cp .env.example .env - # Bearbeiten Sie die .env und fügen Sie Ihre Zugangsdaten hinzu - ``` +1. Erstellen Sie eine `.env`-Datei basierend auf dem Beispiel (`.env.example`). +2. Starten Sie den Container: -2. Starten Sie den Container unter Verweis auf die Datei: ```bash docker run -d \ - --name klzDNS-worker \ - --restart unless-stopped \ - --net=container:klzDNS-coredns \ - -v klzDNS-data:/zones \ + --name netbox-dns-sync \ --env-file .env \ + -v ./zones:/zones \ local/dns-sync - ``` + ``` \ No newline at end of file diff --git a/sync.py b/sync.py index 5c9071f..ccb5963 100644 --- a/sync.py +++ b/sync.py @@ -18,16 +18,16 @@ if not NETBOX_URL or not TOKEN: 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") +ZONE_NAME = os.getenv("ZONE_NAME", "example.com") +REVERSE_ZONE_NAME = os.getenv("REVERSE_ZONE_NAME", "1.168.192.in-addr.arpa") 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") +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") # 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") +FALLBACK_NS_HOSTNAME = os.getenv("FALLBACK_NS_HOSTNAME", "ns1") +FALLBACK_NS_IP = os.getenv("FALLBACK_NS_IP", "127.0.0.1") HEADERS = {'Authorization': f'Token {TOKEN}', 'Accept': 'application/json'} @@ -113,6 +113,28 @@ def get_ns_config(plugin_records, all_records): return primary_ns, extra_header_lines +def write_atomic(filepath, content): + """Schreibt Datei atomar: Erst in .tmp, dann move/replace.""" + tmp_path = filepath + ".tmp" + try: + # Verzeichnis sicherstellen + os.makedirs(os.path.dirname(filepath), exist_ok=True) + + with open(tmp_path, "w") as f: + f.write(content) + f.flush() + os.fsync(f.fileno()) + os.replace(tmp_path, filepath) + return True + except Exception as e: + log(f"ERROR beim Schreiben von {filepath}: {e}") + if os.path.exists(tmp_path): + try: + os.remove(tmp_path) + except: + pass + raise e + def generate_zone_file_fwd(ipam_data, plugin_records): """Erstellt die Forward-Zone""" @@ -152,11 +174,10 @@ def generate_zone_file_fwd(ipam_data, plugin_records): header.extend(extra_headers) header.append("") - with open(OUTPUT_FILE_FWD, "w") as f: - f.write("\n".join(header)) - f.write("\n; --- Records ---\n") - f.write("\n".join(all_records)) - f.write("\n") + content = "\n".join(header) + "\n; --- Records --- +" + "\n".join(all_records) + "\n" + + write_atomic(OUTPUT_FILE_FWD, content) log(f"SUCCESS: Forward Zone geschrieben ({len(all_records)} Records).") def generate_zone_file_rev(ipam_data, plugin_records): @@ -198,11 +219,10 @@ def generate_zone_file_rev(ipam_data, plugin_records): header.append("") - with open(OUTPUT_FILE_REV, "w") as f: - f.write("\n".join(header)) - f.write("\n; --- PTR Records ---\n") - f.write("\n".join(ptr_records)) - f.write("\n") + content = "\n".join(header) + "\n; --- PTR Records --- +" + "\n".join(ptr_records) + "\n" + + write_atomic(OUTPUT_FILE_REV, content) log(f"SUCCESS: Reverse Zone geschrieben ({len(ptr_records)} Records).") if __name__ == "__main__": @@ -229,4 +249,4 @@ if __name__ == "__main__": log(f"CRITICAL ERROR: Sync abgebrochen! Behalte alte Zone-Files. Grund: {e}") log(f"Schlafe {REFRESH_INTERVAL} Sekunden...") - time.sleep(REFRESH_INTERVAL) + time.sleep(REFRESH_INTERVAL) \ No newline at end of file