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 <noreply@anthropic.com>
This commit is contained in:
Schulz 2026-02-16 22:28:56 +01:00
parent 75efdcc16d
commit 1b7ebf1b00
11 changed files with 338 additions and 452 deletions

View File

@ -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

View File

@ -1,23 +1,9 @@
# Hetzner DNS API Token # DDNS-Updater Konfiguration
# Erstellen Sie einen Token unter: https://dns.hetzner.com/
HETZNER_API_TOKEN=your_api_token_here
# Domain Name (z.B. example.com) # Update-Intervall (Standard: 5m)
DOMAIN=example.com # Mögliche Werte: 30s, 1m, 5m, 10m, etc.
PERIOD=5m
# DNS Record Name # Log Level
# Verwenden Sie '@' für die Root-Domain oder einen Subdomain-Namen wie 'home' # Mögliche Werte: debug, info, warning, error
RECORD_NAME=@ LOG_LEVEL=info
# 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

4
.gitignore vendored
View File

@ -1,6 +1,10 @@
# Environment variables # Environment variables
.env .env
# DDNS-Updater data
data/
config.json
# Python # Python
__pycache__/ __pycache__/
*.py[cod] *.py[cod]

View File

@ -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/). 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 ## [0.2.0] - 2026-02-16
### Hinzugefügt ### Hinzugefügt

177
CLAUDE.md
View File

@ -4,22 +4,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview ## 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:** **Core Components:**
- `dyndns.py`: Main Python application that handles IP detection and DNS updates - `docker-compose.yml`: Service configuration using qmcgaw/ddns-updater image
- `Dockerfile`: Container configuration for the application - `data/config.json`: DDNS configuration (domains, tokens, providers)
- `docker-compose.yml`: Docker Compose setup for easy deployment - `config.json.example`: Configuration template for Hetzner setup
## Development Commands ## Development Commands
### Docker Operations ### Docker Operations
**Build the container:**
```bash
docker-compose build
```
**Start the service:** **Start the service:**
```bash ```bash
docker-compose up -d docker-compose up -d
@ -40,62 +35,92 @@ docker-compose restart
docker-compose down docker-compose down
``` ```
**Rebuild after code changes:** **Check status:**
```bash ```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 ```bash
# Install dependencies mkdir -p data
pip install -r requirements.txt cp config.json.example data/config.json
# Edit data/config.json with your credentials
# 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
``` ```
**Test IP detection without updating DNS:** **Validate config JSON:**
```python ```bash
from dyndns import HetznerDynDNS cat data/config.json | jq .
client = HetznerDynDNS(api_token, domain, record_name)
current_ip = client.get_current_ip()
print(f"Current IP: {current_ip}")
``` ```
**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 ## Architecture
### Application Flow ### Application Flow
1. **Initialization**: Load configuration from environment variables 1. **Initialization**: Load configuration from `data/config.json`
2. **Zone Lookup**: Auto-detect Hetzner DNS Zone ID if not provided 2. **Main Loop**:
3. **Main Loop**: - Get current public IP (IPv4/IPv6)
- Get current public IP (via api.ipify.org) - Compare with DNS records for each configured domain
- Fetch existing DNS record from Hetzner API - Update DNS if IP has changed
- Compare IPs and update if changed - Wait for configured period (default: 5 minutes)
- Wait for configured interval (default: 5 minutes)
- Repeat - Repeat
### Hetzner DNS API Integration ### Configuration Structure
The application uses Hetzner's DNS API v1 (https://dns.hetzner.com/api/v1): Configuration is in JSON format at `data/config.json`:
- **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 ```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`): **Key Parameters:**
- **Required**: `HETZNER_API_TOKEN`, `DOMAIN` - `provider`: DNS provider name (hetzner, cloudflare, duckdns, etc.)
- **Optional**: `RECORD_NAME` (default: @), `RECORD_TYPE` (default: A), `CHECK_INTERVAL` (default: 300), `LOG_LEVEL` (default: INFO) - `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 ## Versioning Strategy
@ -111,37 +136,67 @@ This project uses a simplified versioning scheme:
## Key Files ## Key Files
- `dyndns.py`: Core application logic (250+ lines) - `docker-compose.yml`: Service definition using qmcgaw/ddns-updater
- `requirements.txt`: Python dependencies - `config.json.example`: Template for Hetzner configuration
- `Dockerfile`: Multi-stage build with non-root user - `data/config.json`: Active configuration (not in git)
- `docker-compose.yml`: Service configuration with restart policy - `.env.example`: Optional environment variables template
- `.env.example`: Configuration template
- `CHANGELOG.md`: Version history - `CHANGELOG.md`: Version history
## Security Notes ## Security Notes
- API token is sensitive - never commit `.env` file - API token is sensitive - never commit `data/config.json` or `.env`
- Container runs as non-root user (UID 1000) - Container runs with minimal privileges
- Minimal Python slim image for reduced attack surface - Only ports 8000 (web UI) exposed
- Use HTTPS reverse proxy for production deployments
- API token should have minimal required permissions (DNS read/write only) - API token should have minimal required permissions (DNS read/write only)
## Troubleshooting ## Troubleshooting
**Check if container is running:** **View Web UI status:**
```bash ```bash
docker-compose ps # Open http://localhost:8000 in browser
``` ```
**View recent logs:** **Check container logs:**
```bash ```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:** **Test API connectivity:**
```bash ```bash
# From host # List zones
curl -H "Auth-API-Token: YOUR_TOKEN" https://dns.hetzner.com/api/v1/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:** **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)

View File

@ -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"]

187
README.md
View File

@ -1,14 +1,18 @@
# DynDNS Docker für Hetzner DNS # 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 ## Features
- Automatische Erkennung von IP-Änderungen - ✅ Automatische Erkennung von IP-Änderungen
- Integration mit Hetzner DNS API - ✅ Integration mit Hetzner DNS API
- Konfigurierbare Check-Intervalle - ✅ Web-UI für Überwachung und Verwaltung (Port 8000)
- Docker-Container für einfaches Deployment - ✅ Konfigurierbare Check-Intervalle
- Umgebungsvariablen für sichere Konfiguration - ✅ 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 ## Voraussetzungen
@ -25,43 +29,125 @@ git clone <repository-url>
cd dyndns-docker cd dyndns-docker
``` ```
### 2. Konfiguration ### 2. Konfiguration erstellen
Kopieren Sie die Beispiel-Konfigurationsdatei und passen Sie sie an: #### a) Umgebungsvariablen (optional)
```bash ```bash
cp .env.example .env cp .env.example .env
``` ```
Bearbeiten Sie die `.env` Datei mit Ihren Daten: Bearbeiten Sie `.env` für optionale Einstellungen:
```env ```env
HETZNER_API_TOKEN=your_api_token_here PERIOD=5m # Update-Intervall
DOMAIN=example.com LOG_LEVEL=info # Log-Level
RECORD_NAME=home
RECORD_TYPE=A
CHECK_INTERVAL=300
``` ```
### 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 ```bash
docker-compose up -d 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 ## 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 ### Umgebungsvariablen
| Variable | Beschreibung | Beispiel | | Variable | Beschreibung | Standard |
|----------|-------------|----------| |----------|-------------|----------|
| `HETZNER_API_TOKEN` | Hetzner DNS API Token | `abc123...` | | `PERIOD` | Update-Intervall | `5m` |
| `ZONE_ID` | Hetzner DNS Zone ID (optional, wird automatisch ermittelt) | `xyz789...` | | `LOG_LEVEL` | Log-Level | `info` |
| `DOMAIN` | Domain-Name | `example.com` |
| `RECORD_NAME` | DNS Record Name | `home` oder `@` für Root | ### Mehrere Domains konfigurieren
| `RECORD_TYPE` | DNS Record Typ | `A` (IPv4) oder `AAAA` (IPv6) |
| `CHECK_INTERVAL` | Prüfintervall in Sekunden | `300` (5 Minuten) | Sie können mehrere Domains in `data/config.json` hinzufügen:
| `LOG_LEVEL` | Log-Level | `INFO`, `DEBUG`, `WARNING` |
```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 ## Verwendung
@ -89,12 +175,49 @@ docker-compose restart
docker-compose down docker-compose down
``` ```
## Hetzner DNS API Token erstellen ### Konfiguration neu laden
1. Melden Sie sich im [Hetzner DNS Console](https://dns.hetzner.com/) an Nach Änderungen an `data/config.json`:
2. Gehen Sie zu "API Tokens"
3. Erstellen Sie einen neuen API Token mit Lese- und Schreibrechten ```bash
4. Kopieren Sie den Token in Ihre `.env` Datei 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 ## Versioning
@ -103,6 +226,12 @@ Dieses Projekt folgt einer vereinfachten Versionierungsstruktur:
- **0.0.1**: Kleine Änderungen (Bugfixes, kleinere Verbesserungen) - **0.0.1**: Kleine Änderungen (Bugfixes, kleinere Verbesserungen)
- **1.x**: Major Releases (nur nach Anweisung) - **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 ## Lizenz
MIT License MIT License

13
config.json.example Normal file
View File

@ -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"
}
]
}

View File

@ -1,20 +1,24 @@
version: '3.8' version: '3.8'
services: services:
dyndns: ddns-updater:
build: . image: qmcgaw/ddns-updater:latest
container_name: hetzner-dyndns container_name: hetzner-dyndns
restart: unless-stopped restart: unless-stopped
env_file: ports:
- .env - "8000:8000"
volumes:
- ./data:/updater/data
environment: environment:
- HETZNER_API_TOKEN=${HETZNER_API_TOKEN} - PERIOD=${PERIOD:-5m}
- DOMAIN=${DOMAIN} - LOG_LEVEL=${LOG_LEVEL:-info}
- RECORD_NAME=${RECORD_NAME:-@} - TZ=Europe/Berlin
- RECORD_TYPE=${RECORD_TYPE:-A} healthcheck:
- ZONE_ID=${ZONE_ID:-} test: ["CMD", "wget", "-qO-", "http://localhost:8000/"]
- CHECK_INTERVAL=${CHECK_INTERVAL:-300} interval: 1m
- LOG_LEVEL=${LOG_LEVEL:-INFO} timeout: 10s
retries: 3
start_period: 30s
logging: logging:
driver: "json-file" driver: "json-file"
options: options:

266
dyndns.py
View File

@ -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()

View File

@ -1 +0,0 @@
requests==2.31.0