From 1b7ebf1b00b496c40cc9c37e9c54e27a4a47f17b Mon Sep 17 00:00:00 2001 From: Schulz Date: Mon, 16 Feb 2026 22:28:56 +0100 Subject: [PATCH] Migrate to qmcgaw/ddns-updater v0.3.0 BREAKING CHANGE: Replace custom implementation with universal DDNS client - Replace custom Python implementation with qmcgaw/ddns-updater - Add Web UI for monitoring on port 8000 - Add support for 50+ DNS providers (not just Hetzner) - Add multi-domain support via JSON configuration - Add config.json.example for Hetzner setup - Update documentation with Web UI and new setup process - Remove custom Python code, Dockerfile, requirements.txt - Simplify deployment with established, production-ready solution Benefits: - Professional Web UI for status monitoring - Active community support - Flexibility for future provider changes - Well-tested, production-ready solution Co-Authored-By: Claude Sonnet 4.5 --- .dockerignore | 34 ------ .env.example | 28 ++--- .gitignore | 4 + CHANGELOG.md | 25 +++++ CLAUDE.md | 177 +++++++++++++++++++---------- Dockerfile | 29 ----- README.md | 187 ++++++++++++++++++++++++++----- config.json.example | 13 +++ docker-compose.yml | 26 +++-- dyndns.py | 266 -------------------------------------------- requirements.txt | 1 - 11 files changed, 338 insertions(+), 452 deletions(-) delete mode 100644 .dockerignore delete mode 100644 Dockerfile create mode 100644 config.json.example delete mode 100644 dyndns.py delete mode 100644 requirements.txt diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index fe36d33..0000000 --- a/.dockerignore +++ /dev/null @@ -1,34 +0,0 @@ -# Git -.git -.gitignore - -# Environment -.env -.env.* -!.env.example - -# Python -__pycache__ -*.py[cod] -*$py.class -*.so -.Python -venv/ -env/ - -# IDE -.vscode -.idea -*.swp -*.swo - -# Documentation -README.md -CHANGELOG.md -CLAUDE.md - -# Docker -docker-compose.yml -docker-compose.override.yml -Dockerfile -.dockerignore diff --git a/.env.example b/.env.example index 76e95f7..22568a2 100644 --- a/.env.example +++ b/.env.example @@ -1,23 +1,9 @@ -# Hetzner DNS API Token -# Erstellen Sie einen Token unter: https://dns.hetzner.com/ -HETZNER_API_TOKEN=your_api_token_here +# DDNS-Updater Konfiguration -# Domain Name (z.B. example.com) -DOMAIN=example.com +# Update-Intervall (Standard: 5m) +# Mögliche Werte: 30s, 1m, 5m, 10m, etc. +PERIOD=5m -# DNS Record Name -# Verwenden Sie '@' für die Root-Domain oder einen Subdomain-Namen wie 'home' -RECORD_NAME=@ - -# DNS Record Typ -# A für IPv4, AAAA für IPv6 -RECORD_TYPE=A - -# Optional: Zone ID (wird automatisch ermittelt wenn leer) -ZONE_ID= - -# Prüfintervall in Sekunden (Standard: 300 = 5 Minuten) -CHECK_INTERVAL=300 - -# Log Level (DEBUG, INFO, WARNING, ERROR) -LOG_LEVEL=INFO +# Log Level +# Mögliche Werte: debug, info, warning, error +LOG_LEVEL=info diff --git a/.gitignore b/.gitignore index 60d9264..e2dd0f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ # Environment variables .env +# DDNS-Updater data +data/ +config.json + # Python __pycache__/ *.py[cod] diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dede74..fa65efd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,31 @@ Alle wichtigen Änderungen an diesem Projekt werden in dieser Datei dokumentiert Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/). +## [0.3.0] - 2026-02-16 + +### Geändert +- **BREAKING**: Migration von eigener Implementierung zu **qmcgaw/ddns-updater** +- Neue Konfiguration via JSON (`data/config.json`) statt Umgebungsvariablen +- Vereinfachtes Setup mit etabliertem, universellen DDNS-Client + +### Hinzugefügt +- Web-UI für Überwachung und Verwaltung (Port 8000) +- Unterstützung für 50+ DNS-Provider (nicht nur Hetzner) +- Multi-Domain-Support in einer Konfiguration +- Beispiel-Konfiguration (`config.json.example`) +- Verbesserte Dokumentation mit Web-UI Hinweisen + +### Entfernt +- Eigene Python-Implementierung (`dyndns.py`) +- Custom Dockerfile und requirements.txt +- .dockerignore (nicht mehr benötigt) + +### Vorteile +- Etablierte, gut getestete Lösung +- Aktive Community und Support +- Flexibilität für zukünftige Provider-Wechsel +- Professionelle Web-UI zur Statusüberwachung + ## [0.2.0] - 2026-02-16 ### Hinzugefügt diff --git a/CLAUDE.md b/CLAUDE.md index 08943df..883e90d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,22 +4,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -This is a Docker-based Dynamic DNS client for the Hetzner DNS Console. It automatically detects IP changes and updates DNS records via the Hetzner DNS API. +This is a Docker-based Dynamic DNS client for Hetzner DNS (and 50+ other providers) using **[qmcgaw/ddns-updater](https://github.com/qdm12/ddns-updater)** - a universal, production-ready DDNS solution with web UI. **Core Components:** -- `dyndns.py`: Main Python application that handles IP detection and DNS updates -- `Dockerfile`: Container configuration for the application -- `docker-compose.yml`: Docker Compose setup for easy deployment +- `docker-compose.yml`: Service configuration using qmcgaw/ddns-updater image +- `data/config.json`: DDNS configuration (domains, tokens, providers) +- `config.json.example`: Configuration template for Hetzner setup ## Development Commands ### Docker Operations -**Build the container:** -```bash -docker-compose build -``` - **Start the service:** ```bash docker-compose up -d @@ -40,62 +35,92 @@ docker-compose restart docker-compose down ``` -**Rebuild after code changes:** +**Check status:** ```bash -docker-compose up -d --build +docker-compose ps ``` -### Local Development +### Configuration Management -**Run Python script directly (requires Python 3.11+):** +**Create initial config:** ```bash -# Install dependencies -pip install -r requirements.txt - -# Set environment variables or use .env file -export HETZNER_API_TOKEN="your_token" -export DOMAIN="example.com" -export RECORD_NAME="@" - -# Run script -python dyndns.py +mkdir -p data +cp config.json.example data/config.json +# Edit data/config.json with your credentials ``` -**Test IP detection without updating DNS:** -```python -from dyndns import HetznerDynDNS -client = HetznerDynDNS(api_token, domain, record_name) -current_ip = client.get_current_ip() -print(f"Current IP: {current_ip}") +**Validate config JSON:** +```bash +cat data/config.json | jq . ``` +**Get Hetzner Zone ID via API:** +```bash +curl -H "Auth-API-Token: YOUR_TOKEN" https://dns.hetzner.com/api/v1/zones +``` + +### Web UI + +**Access Web UI:** +Open browser to `http://localhost:8000` + +The Web UI shows: +- Current IP status +- Last update timestamp +- Update history +- Errors and warnings +- Per-domain status + ## Architecture ### Application Flow -1. **Initialization**: Load configuration from environment variables -2. **Zone Lookup**: Auto-detect Hetzner DNS Zone ID if not provided -3. **Main Loop**: - - Get current public IP (via api.ipify.org) - - Fetch existing DNS record from Hetzner API - - Compare IPs and update if changed - - Wait for configured interval (default: 5 minutes) +1. **Initialization**: Load configuration from `data/config.json` +2. **Main Loop**: + - Get current public IP (IPv4/IPv6) + - Compare with DNS records for each configured domain + - Update DNS if IP has changed + - Wait for configured period (default: 5 minutes) - Repeat -### Hetzner DNS API Integration +### Configuration Structure -The application uses Hetzner's DNS API v1 (https://dns.hetzner.com/api/v1): -- **Authentication**: Via `Auth-API-Token` header -- **GET /zones**: Find zone ID for domain -- **GET /records**: List DNS records for zone -- **POST /records**: Create new DNS record -- **PUT /records/{id}**: Update existing DNS record +Configuration is in JSON format at `data/config.json`: -### Configuration +```json +{ + "settings": [ + { + "provider": "hetzner", + "zone_identifier": "zone_id", + "domain": "example.com", + "host": "@", + "ttl": 60, + "token": "api_token", + "ip_version": "ipv4" + } + ] +} +``` -All configuration is via environment variables (see `.env.example`): -- **Required**: `HETZNER_API_TOKEN`, `DOMAIN` -- **Optional**: `RECORD_NAME` (default: @), `RECORD_TYPE` (default: A), `CHECK_INTERVAL` (default: 300), `LOG_LEVEL` (default: INFO) +**Key Parameters:** +- `provider`: DNS provider name (hetzner, cloudflare, duckdns, etc.) +- `zone_identifier`: Hetzner DNS Zone ID +- `domain`: Base domain name +- `host`: Subdomain or `@` for root +- `ttl`: Time-to-live in seconds +- `token`: Hetzner API token with DNS edit permissions +- `ip_version`: `ipv4`, `ipv6`, or `ipv4 or ipv6` + +### Multi-Domain Support + +Add multiple entries in the `settings` array to manage multiple domains or subdomains. + +### Environment Variables + +Optional variables in `.env`: +- `PERIOD`: Update check interval (default: 5m) +- `LOG_LEVEL`: Logging verbosity (debug, info, warning, error) ## Versioning Strategy @@ -111,37 +136,67 @@ This project uses a simplified versioning scheme: ## Key Files -- `dyndns.py`: Core application logic (250+ lines) -- `requirements.txt`: Python dependencies -- `Dockerfile`: Multi-stage build with non-root user -- `docker-compose.yml`: Service configuration with restart policy -- `.env.example`: Configuration template +- `docker-compose.yml`: Service definition using qmcgaw/ddns-updater +- `config.json.example`: Template for Hetzner configuration +- `data/config.json`: Active configuration (not in git) +- `.env.example`: Optional environment variables template - `CHANGELOG.md`: Version history ## Security Notes -- API token is sensitive - never commit `.env` file -- Container runs as non-root user (UID 1000) -- Minimal Python slim image for reduced attack surface +- API token is sensitive - never commit `data/config.json` or `.env` +- Container runs with minimal privileges +- Only ports 8000 (web UI) exposed +- Use HTTPS reverse proxy for production deployments - API token should have minimal required permissions (DNS read/write only) ## Troubleshooting -**Check if container is running:** +**View Web UI status:** ```bash -docker-compose ps +# Open http://localhost:8000 in browser ``` -**View recent logs:** +**Check container logs:** ```bash -docker-compose logs --tail=50 +docker-compose logs --tail=50 -f +``` + +**Validate configuration:** +```bash +# Check JSON syntax +cat data/config.json | jq . ``` **Test API connectivity:** ```bash -# From host +# List zones curl -H "Auth-API-Token: YOUR_TOKEN" https://dns.hetzner.com/api/v1/zones + +# List records for a zone +curl -H "Auth-API-Token: YOUR_TOKEN" "https://dns.hetzner.com/api/v1/records?zone_id=ZONE_ID" ``` **Debug mode:** -Set `LOG_LEVEL=DEBUG` in `.env` and restart container. +Set `LOG_LEVEL=debug` in `.env` and restart container. + +**Container won't start:** +- Check `docker-compose logs` +- Verify `data/config.json` exists and is valid JSON +- Check port 8000 is not in use + +## Provider Migration + +To switch from Hetzner to another provider: +1. Check [supported providers](https://github.com/qdm12/ddns-updater#providers) +2. Update `provider` field in `data/config.json` +3. Adjust provider-specific fields +4. Restart: `docker-compose restart` + +No code changes needed - just configuration! + +## Upstream Documentation + +- [DDNS-Updater GitHub](https://github.com/qdm12/ddns-updater) +- [Hetzner Configuration Guide](https://github.com/qdm12/ddns-updater/blob/master/docs/hetzner.md) +- [All Supported Providers](https://github.com/qdm12/ddns-updater#providers) diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 94a0a1f..0000000 --- a/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -FROM python:3.11-slim - -# Set working directory -WORKDIR /app - -# Copy requirements first for better caching -COPY requirements.txt . - -# Install dependencies -RUN pip install --no-cache-dir -r requirements.txt - -# Copy application -COPY dyndns.py . - -# Make script executable -RUN chmod +x dyndns.py - -# Run as non-root user -RUN useradd -m -u 1000 dyndns && \ - chown -R dyndns:dyndns /app - -USER dyndns - -# Health check -HEALTHCHECK --interval=5m --timeout=10s --start-period=30s --retries=3 \ - CMD python -c "import sys; sys.exit(0)" - -# Run the application -CMD ["python", "-u", "dyndns.py"] diff --git a/README.md b/README.md index f22c5af..1636c71 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,18 @@ # DynDNS Docker für Hetzner DNS -Ein Docker-basiertes Dynamic DNS System für die Hetzner DNS Console. +Ein Docker-basiertes Dynamic DNS System für die Hetzner DNS Console mit **[qmcgaw/ddns-updater](https://github.com/qdm12/ddns-updater)** - einem universellen DDNS-Client, der 50+ Provider unterstützt. ## Features -- Automatische Erkennung von IP-Änderungen -- Integration mit Hetzner DNS API -- Konfigurierbare Check-Intervalle -- Docker-Container für einfaches Deployment -- Umgebungsvariablen für sichere Konfiguration +- ✅ Automatische Erkennung von IP-Änderungen +- ✅ Integration mit Hetzner DNS API +- ✅ Web-UI für Überwachung und Verwaltung (Port 8000) +- ✅ Konfigurierbare Check-Intervalle +- ✅ Unterstützt 50+ DNS-Provider (nicht nur Hetzner) +- ✅ Multi-Domain-Support +- ✅ IPv4 und IPv6 Unterstützung +- ✅ Health Check für Container-Monitoring +- ✅ Automatische Restart-Policy ## Voraussetzungen @@ -25,43 +29,125 @@ git clone cd dyndns-docker ``` -### 2. Konfiguration +### 2. Konfiguration erstellen -Kopieren Sie die Beispiel-Konfigurationsdatei und passen Sie sie an: +#### a) Umgebungsvariablen (optional) ```bash cp .env.example .env ``` -Bearbeiten Sie die `.env` Datei mit Ihren Daten: - +Bearbeiten Sie `.env` für optionale Einstellungen: ```env -HETZNER_API_TOKEN=your_api_token_here -DOMAIN=example.com -RECORD_NAME=home -RECORD_TYPE=A -CHECK_INTERVAL=300 +PERIOD=5m # Update-Intervall +LOG_LEVEL=info # Log-Level ``` -### 3. Container starten +#### b) DDNS-Konfiguration (erforderlich) + +Erstellen Sie `data/config.json` basierend auf der Vorlage: + +```bash +mkdir -p data +cp config.json.example data/config.json +``` + +Bearbeiten Sie `data/config.json`: + +```json +{ + "settings": [ + { + "provider": "hetzner", + "zone_identifier": "your_zone_id_here", + "domain": "example.com", + "host": "@", + "ttl": 60, + "token": "your_hetzner_api_token_here", + "ip_version": "ipv4" + } + ] +} +``` + +### 3. Hetzner API Token und Zone ID ermitteln + +#### API Token erstellen: +1. Melden Sie sich im [Hetzner DNS Console](https://dns.hetzner.com/) an +2. Gehen Sie zu **"API Tokens"** +3. Klicken Sie auf **"Create access token"** +4. Geben Sie einen Namen ein (z.B. "DynDNS") +5. Wählen Sie **Read & Write** Rechte +6. Kopieren Sie den generierten Token + +#### Zone ID finden: +1. Gehen Sie zu Ihrer Domain-Übersicht +2. Die **Zone ID** finden Sie in der URL oder im Domain-Detail +3. Alternativ: API-Aufruf `curl -H "Auth-API-Token: YOUR_TOKEN" https://dns.hetzner.com/api/v1/zones` + +### 4. Container starten ```bash docker-compose up -d ``` +### 5. Web-UI aufrufen + +Öffnen Sie im Browser: **http://localhost:8000** + +Hier sehen Sie: +- ✅ Aktuellen IP-Status +- ✅ Letztes Update +- ✅ Update-Historie +- ✅ Fehler und Warnungen + ## Konfiguration +### Domain-Konfiguration + +| Parameter | Beschreibung | Beispiel | +|-----------|-------------|----------| +| `provider` | DNS-Provider Name | `hetzner` | +| `zone_identifier` | Hetzner Zone ID | `abc123...` | +| `domain` | Domain-Name | `example.com` | +| `host` | Subdomain oder `@` für Root | `@`, `home`, `*.example.com` | +| `ttl` | Time-to-Live in Sekunden | `60` | +| `token` | Hetzner API Token | `xyz789...` | +| `ip_version` | IP-Version | `ipv4`, `ipv6`, `ipv4 or ipv6` | + ### Umgebungsvariablen -| Variable | Beschreibung | Beispiel | +| Variable | Beschreibung | Standard | |----------|-------------|----------| -| `HETZNER_API_TOKEN` | Hetzner DNS API Token | `abc123...` | -| `ZONE_ID` | Hetzner DNS Zone ID (optional, wird automatisch ermittelt) | `xyz789...` | -| `DOMAIN` | Domain-Name | `example.com` | -| `RECORD_NAME` | DNS Record Name | `home` oder `@` für Root | -| `RECORD_TYPE` | DNS Record Typ | `A` (IPv4) oder `AAAA` (IPv6) | -| `CHECK_INTERVAL` | Prüfintervall in Sekunden | `300` (5 Minuten) | -| `LOG_LEVEL` | Log-Level | `INFO`, `DEBUG`, `WARNING` | +| `PERIOD` | Update-Intervall | `5m` | +| `LOG_LEVEL` | Log-Level | `info` | + +### Mehrere Domains konfigurieren + +Sie können mehrere Domains in `data/config.json` hinzufügen: + +```json +{ + "settings": [ + { + "provider": "hetzner", + "zone_identifier": "zone1_id", + "domain": "example.com", + "host": "@", + "token": "token1", + "ip_version": "ipv4" + }, + { + "provider": "hetzner", + "zone_identifier": "zone2_id", + "domain": "example.org", + "host": "home", + "token": "token2", + "ip_version": "ipv4" + } + ] +} +``` ## Verwendung @@ -89,12 +175,49 @@ docker-compose restart docker-compose down ``` -## Hetzner DNS API Token erstellen +### Konfiguration neu laden -1. Melden Sie sich im [Hetzner DNS Console](https://dns.hetzner.com/) an -2. Gehen Sie zu "API Tokens" -3. Erstellen Sie einen neuen API Token mit Lese- und Schreibrechten -4. Kopieren Sie den Token in Ihre `.env` Datei +Nach Änderungen an `data/config.json`: + +```bash +docker-compose restart +``` + +## Weitere unterstützte Provider + +Dieser Setup nutzt **qmcgaw/ddns-updater**, der auch diese Provider unterstützt: + +- Cloudflare +- Google Domains +- DuckDNS +- No-IP +- Namecheap +- GoDaddy +- **und 40+ weitere** + +Um einen anderen Provider zu nutzen, passen Sie einfach die `provider`-Konfiguration in `data/config.json` an. Siehe [Dokumentation](https://github.com/qdm12/ddns-updater) für Details. + +## Troubleshooting + +### Container startet nicht +```bash +docker-compose logs +``` + +### Web-UI nicht erreichbar +- Prüfen Sie, ob Port 8000 verfügbar ist +- Prüfen Sie Firewall-Einstellungen + +### DNS-Update schlägt fehl +- Prüfen Sie den API Token +- Prüfen Sie die Zone ID +- Prüfen Sie die Logs: `docker-compose logs -f` + +### Konfiguration testen +```bash +# Prüfen Sie die config.json Syntax +cat data/config.json | jq . +``` ## Versioning @@ -103,6 +226,12 @@ Dieses Projekt folgt einer vereinfachten Versionierungsstruktur: - **0.0.1**: Kleine Änderungen (Bugfixes, kleinere Verbesserungen) - **1.x**: Major Releases (nur nach Anweisung) +## Links + +- [DDNS-Updater GitHub](https://github.com/qdm12/ddns-updater) +- [Hetzner DNS Console](https://dns.hetzner.com/) +- [Hetzner DNS API Dokumentation](https://dns.hetzner.com/api-docs) + ## Lizenz MIT License diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..b23bb2d --- /dev/null +++ b/config.json.example @@ -0,0 +1,13 @@ +{ + "settings": [ + { + "provider": "hetzner", + "zone_identifier": "your_zone_id_here", + "domain": "example.com", + "host": "@", + "ttl": 60, + "token": "your_hetzner_api_token_here", + "ip_version": "ipv4" + } + ] +} diff --git a/docker-compose.yml b/docker-compose.yml index e27e593..04308ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,24 @@ version: '3.8' services: - dyndns: - build: . + ddns-updater: + image: qmcgaw/ddns-updater:latest container_name: hetzner-dyndns restart: unless-stopped - env_file: - - .env + ports: + - "8000:8000" + volumes: + - ./data:/updater/data environment: - - HETZNER_API_TOKEN=${HETZNER_API_TOKEN} - - DOMAIN=${DOMAIN} - - RECORD_NAME=${RECORD_NAME:-@} - - RECORD_TYPE=${RECORD_TYPE:-A} - - ZONE_ID=${ZONE_ID:-} - - CHECK_INTERVAL=${CHECK_INTERVAL:-300} - - LOG_LEVEL=${LOG_LEVEL:-INFO} + - PERIOD=${PERIOD:-5m} + - LOG_LEVEL=${LOG_LEVEL:-info} + - TZ=Europe/Berlin + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:8000/"] + interval: 1m + timeout: 10s + retries: 3 + start_period: 30s logging: driver: "json-file" options: diff --git a/dyndns.py b/dyndns.py deleted file mode 100644 index e698726..0000000 --- a/dyndns.py +++ /dev/null @@ -1,266 +0,0 @@ -#!/usr/bin/env python3 -""" -DynDNS Client for Hetzner DNS API -Automatically updates DNS records when IP address changes -""" - -import os -import sys -import time -import logging -import requests -from typing import Optional, Dict, Any - - -class HetznerDynDNS: - """Hetzner DNS API client for dynamic DNS updates""" - - BASE_URL = "https://dns.hetzner.com/api/v1" - - def __init__( - self, - api_token: str, - domain: str, - record_name: str, - record_type: str = "A", - zone_id: Optional[str] = None - ): - """ - Initialize Hetzner DynDNS client - - Args: - api_token: Hetzner DNS API token - domain: Domain name (e.g., example.com) - record_name: DNS record name (e.g., 'home' or '@' for root) - record_type: Record type ('A' for IPv4, 'AAAA' for IPv6) - zone_id: Optional zone ID (will be auto-detected if not provided) - """ - self.api_token = api_token - self.domain = domain - self.record_name = record_name if record_name != "@" else "" - self.record_type = record_type - self.zone_id = zone_id - self.headers = { - "Auth-API-Token": api_token, - "Content-Type": "application/json" - } - - if not self.zone_id: - self.zone_id = self._get_zone_id() - - def _make_request( - self, - method: str, - endpoint: str, - data: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: - """Make HTTP request to Hetzner API""" - url = f"{self.BASE_URL}/{endpoint}" - - try: - if method == "GET": - response = requests.get(url, headers=self.headers) - elif method == "POST": - response = requests.post(url, headers=self.headers, json=data) - elif method == "PUT": - response = requests.put(url, headers=self.headers, json=data) - else: - raise ValueError(f"Unsupported HTTP method: {method}") - - response.raise_for_status() - return response.json() - except requests.exceptions.RequestException as e: - logging.error(f"API request failed: {e}") - raise - - def _get_zone_id(self) -> str: - """Get zone ID for the domain""" - logging.info(f"Looking up zone ID for domain: {self.domain}") - - try: - response = self._make_request("GET", "zones") - zones = response.get("zones", []) - - for zone in zones: - if zone.get("name") == self.domain: - zone_id = zone.get("id") - logging.info(f"Found zone ID: {zone_id}") - return zone_id - - raise ValueError(f"Zone not found for domain: {self.domain}") - except Exception as e: - logging.error(f"Failed to get zone ID: {e}") - raise - - def get_current_ip(self) -> str: - """Get current public IP address""" - try: - if self.record_type == "A": - # IPv4 - response = requests.get("https://api.ipify.org", timeout=10) - else: - # IPv6 - response = requests.get("https://api6.ipify.org", timeout=10) - - response.raise_for_status() - ip = response.text.strip() - logging.debug(f"Current IP ({self.record_type}): {ip}") - return ip - except requests.exceptions.RequestException as e: - logging.error(f"Failed to get current IP: {e}") - raise - - def get_dns_record(self) -> Optional[Dict[str, Any]]: - """Get existing DNS record""" - try: - response = self._make_request("GET", f"records?zone_id={self.zone_id}") - records = response.get("records", []) - - for record in records: - if (record.get("type") == self.record_type and - record.get("name") == self.record_name): - logging.debug(f"Found existing record: {record}") - return record - - logging.info(f"No existing record found for {self.record_name}.{self.domain}") - return None - except Exception as e: - logging.error(f"Failed to get DNS record: {e}") - raise - - def create_dns_record(self, ip: str) -> Dict[str, Any]: - """Create new DNS record""" - logging.info(f"Creating new DNS record: {self.record_name}.{self.domain} -> {ip}") - - data = { - "zone_id": self.zone_id, - "type": self.record_type, - "name": self.record_name, - "value": ip, - "ttl": 60 - } - - try: - response = self._make_request("POST", "records", data) - logging.info(f"DNS record created successfully") - return response - except Exception as e: - logging.error(f"Failed to create DNS record: {e}") - raise - - def update_dns_record(self, record_id: str, ip: str) -> Dict[str, Any]: - """Update existing DNS record""" - logging.info(f"Updating DNS record: {self.record_name}.{self.domain} -> {ip}") - - data = { - "zone_id": self.zone_id, - "type": self.record_type, - "name": self.record_name, - "value": ip, - "ttl": 60 - } - - try: - response = self._make_request("PUT", f"records/{record_id}", data) - logging.info(f"DNS record updated successfully") - return response - except Exception as e: - logging.error(f"Failed to update DNS record: {e}") - raise - - def update(self) -> bool: - """ - Update DNS record if IP has changed - - Returns: - True if record was updated, False if no update was needed - """ - try: - current_ip = self.get_current_ip() - existing_record = self.get_dns_record() - - if existing_record: - if existing_record.get("value") == current_ip: - logging.info(f"IP unchanged ({current_ip}), no update needed") - return False - else: - logging.info(f"IP changed: {existing_record.get('value')} -> {current_ip}") - self.update_dns_record(existing_record.get("id"), current_ip) - return True - else: - logging.info(f"No existing record, creating new one with IP: {current_ip}") - self.create_dns_record(current_ip) - return True - - except Exception as e: - logging.error(f"Update failed: {e}") - return False - - -def setup_logging(log_level: str = "INFO") -> None: - """Configure logging""" - logging.basicConfig( - level=getattr(logging, log_level.upper()), - format='%(asctime)s - %(levelname)s - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' - ) - - -def main(): - """Main entry point""" - # Load configuration from environment variables - api_token = os.getenv("HETZNER_API_TOKEN") - domain = os.getenv("DOMAIN") - record_name = os.getenv("RECORD_NAME", "@") - record_type = os.getenv("RECORD_TYPE", "A") - zone_id = os.getenv("ZONE_ID") - check_interval = int(os.getenv("CHECK_INTERVAL", "300")) - log_level = os.getenv("LOG_LEVEL", "INFO") - - # Setup logging - setup_logging(log_level) - - # Validate required configuration - if not api_token: - logging.error("HETZNER_API_TOKEN environment variable is required") - sys.exit(1) - - if not domain: - logging.error("DOMAIN environment variable is required") - sys.exit(1) - - logging.info("Starting Hetzner DynDNS client") - logging.info(f"Domain: {domain}") - logging.info(f"Record: {record_name if record_name else '@'}.{domain}") - logging.info(f"Type: {record_type}") - logging.info(f"Check interval: {check_interval} seconds") - - # Initialize client - try: - client = HetznerDynDNS( - api_token=api_token, - domain=domain, - record_name=record_name, - record_type=record_type, - zone_id=zone_id - ) - except Exception as e: - logging.error(f"Failed to initialize client: {e}") - sys.exit(1) - - # Main loop - while True: - try: - client.update() - except KeyboardInterrupt: - logging.info("Shutting down...") - break - except Exception as e: - logging.error(f"Unexpected error: {e}") - - # Wait before next check - time.sleep(check_interval) - - -if __name__ == "__main__": - main() diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2c24336..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -requests==2.31.0