mirror of
https://github.com/skoelle/dyndns-updater.git
synced 2026-09-17 16:10:25 +00:00
some fixes
This commit is contained in:
+2
-2
@@ -7,8 +7,8 @@ FRITZBOX_PASSWORD=change-me
|
|||||||
# Cloudflare
|
# Cloudflare
|
||||||
CLOUDFLARE_API_TOKEN=change-me
|
CLOUDFLARE_API_TOKEN=change-me
|
||||||
CLOUDFLARE_ZONE_ID=change-me
|
CLOUDFLARE_ZONE_ID=change-me
|
||||||
# Kommagetrennte Liste der Subdomains (ohne Proxy!), Anzahl beliebig erweiterbar
|
# Kommagetrennte Subdomain-Teile relativ zur Zone (kein FQDN!), z.B. "sub1" oder "*.home"
|
||||||
CLOUDFLARE_RECORDS=sub1.example.org,sub2.example.org,sub3.example.org
|
CLOUDFLARE_RECORDS=sub1,sub2,sub3
|
||||||
|
|
||||||
# FreeDNS (Fallback/Parallelbetrieb)
|
# FreeDNS (Fallback/Parallelbetrieb)
|
||||||
FREEDNS_UPDATE_URL=https://freedns.afraid.org/dynamic/update.php?TOKEN
|
FREEDNS_UPDATE_URL=https://freedns.afraid.org/dynamic/update.php?TOKEN
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ruff:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- run: pip install ruff
|
||||||
|
- run: ruff check .
|
||||||
|
- run: ruff format --check .
|
||||||
@@ -19,7 +19,7 @@ Details siehe [SPEC.md](SPEC.md) (Architektur/Design) und [PLAN.md](PLAN.md)
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
vim .env # Werte eintragen
|
vim .env # Werte eintragen (CLOUDFLARE_RECORDS: nur Subdomain-Teile, z.B. "sub1,sub2,*.home")
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ die IP bei jedem Trigger selbst ueber die FritzBox TR-064-Schnittstelle.
|
|||||||
## 3. Cloudflare
|
## 3. Cloudflare
|
||||||
|
|
||||||
- Nur A-Records, DNS-only (proxied=false)
|
- Nur A-Records, DNS-only (proxied=false)
|
||||||
- Variable Subdomain-Liste ueber CLOUDFLARE_RECORDS (ENV)
|
- Subdomain-Teile ueber CLOUDFLARE_RECORDS (ENV), relativ zur Zone (z.B. "sub1" oder "*.home", kein FQDN)
|
||||||
- Records pro Lauf per Name aufgeloest, nicht gecacht
|
- Records pro Lauf per Name aufgeloest, nicht gecacht
|
||||||
- Benoetigter Token-Scope: Zone -> DNS -> Edit (Template "Edit zone DNS") + Zone -> Zone -> Read
|
- Benoetigter Token-Scope: Zone -> DNS -> Edit (Template "Edit zone DNS") + Zone -> Zone -> Read
|
||||||
|
|
||||||
|
|||||||
+15
-1
@@ -1,3 +1,9 @@
|
|||||||
|
"""Cloudflare DNS-Update-Modul.
|
||||||
|
|
||||||
|
Erwartet Subdomain-Teile relativ zur Zone (z.B. "sub1" oder "*.home"),
|
||||||
|
keine FQDNs (z.B. NICHT "sub1.example.org").
|
||||||
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -20,12 +26,20 @@ def _headers(api_token: str):
|
|||||||
|
|
||||||
def _find_record_id(zone_id: str, api_token: str, name: str):
|
def _find_record_id(zone_id: str, api_token: str, name: str):
|
||||||
url = f"{API_BASE}/zones/{zone_id}/dns_records"
|
url = f"{API_BASE}/zones/{zone_id}/dns_records"
|
||||||
resp = requests.get(url, headers=_headers(api_token), params={"type": "A", "name": name}, timeout=15)
|
resp = requests.get(
|
||||||
|
url, headers=_headers(api_token), params={"type": "A", "name": name}, timeout=15
|
||||||
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
results = data.get("result", [])
|
results = data.get("result", [])
|
||||||
if not results:
|
if not results:
|
||||||
return None
|
return None
|
||||||
|
if len(results) > 1:
|
||||||
|
log.warning(
|
||||||
|
"Mehrere A-Records fuer '%s' gefunden (%d), nur erster wird aktualisiert.",
|
||||||
|
name,
|
||||||
|
len(results),
|
||||||
|
)
|
||||||
return results[0]["id"]
|
return results[0]["id"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import logging
|
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=os.environ.get("LOG_LEVEL", "INFO"),
|
level=os.environ.get("LOG_LEVEL", "INFO"),
|
||||||
|
|||||||
+3
-2
@@ -11,8 +11,9 @@ class FritzBoxError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_external_ip(host: str, port: int, user: str, password: str, retries: int = 3,
|
def get_external_ip(
|
||||||
backoff_seconds=(2, 5, 10)) -> str:
|
host: str, port: int, user: str, password: str, retries: int = 3, backoff_seconds=(2, 5, 10)
|
||||||
|
) -> str:
|
||||||
last_exc = None
|
last_exc = None
|
||||||
for attempt in range(1, retries + 1):
|
for attempt in range(1, retries + 1):
|
||||||
try:
|
try:
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ def ping(ping_url: str, status: str = "success", message: str = ""):
|
|||||||
|
|
||||||
body = message or status
|
body = message or status
|
||||||
try:
|
try:
|
||||||
requests.post(url, data=body.encode("utf-8"), timeout=10)
|
requests.post(url, data=body.encode("utf-8"), timeout=5)
|
||||||
log.debug("Healthcheck-Ping gesendet (%s): %s", status, message)
|
log.debug("Healthcheck-Ping gesendet (%s): %s", status, message)
|
||||||
except requests.RequestException as exc:
|
except requests.RequestException as exc:
|
||||||
log.warning("Healthcheck-Ping fehlgeschlagen: %s", exc)
|
log.warning("Healthcheck-Ping fehlgeschlagen: %s", exc)
|
||||||
|
|||||||
+10
-4
@@ -1,18 +1,20 @@
|
|||||||
|
import concurrent.futures
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
|
from app import cloudflare, freedns, healthcheck, state
|
||||||
from app.config import config
|
from app.config import config
|
||||||
from app.fritzbox import FritzBoxError, get_external_ip
|
from app.fritzbox import FritzBoxError, get_external_ip
|
||||||
from app import state, cloudflare, freedns, healthcheck
|
|
||||||
|
|
||||||
log = logging.getLogger("main")
|
log = logging.getLogger("main")
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
_lock = threading.Lock()
|
_lock = threading.Lock()
|
||||||
|
_executor = concurrent.futures.ThreadPoolExecutor(max_workers=2)
|
||||||
|
|
||||||
|
|
||||||
def run_cycle(trigger: str = "unknown"):
|
def run_cycle(trigger: str = "unknown"):
|
||||||
@@ -32,7 +34,9 @@ def run_cycle(trigger: str = "unknown"):
|
|||||||
|
|
||||||
last_ip = state.load(config.state_path)
|
last_ip = state.load(config.state_path)
|
||||||
|
|
||||||
if current_ip == last_ip:
|
if last_ip is None:
|
||||||
|
log.info("Erstlauf - IP wird gesetzt: %s", current_ip)
|
||||||
|
elif current_ip == last_ip:
|
||||||
log.info("IP unveraendert (%s) - kein DNS-Update noetig.", current_ip)
|
log.info("IP unveraendert (%s) - kein DNS-Update noetig.", current_ip)
|
||||||
if trigger == "poll":
|
if trigger == "poll":
|
||||||
healthcheck.ping(
|
healthcheck.ping(
|
||||||
@@ -78,7 +82,7 @@ def run_cycle(trigger: str = "unknown"):
|
|||||||
|
|
||||||
@app.route("/webhook/update", methods=["GET"])
|
@app.route("/webhook/update", methods=["GET"])
|
||||||
def webhook_update():
|
def webhook_update():
|
||||||
threading.Thread(target=run_cycle, kwargs={"trigger": "webhook"}, daemon=True).start()
|
_executor.submit(run_cycle, trigger="webhook")
|
||||||
return {"status": "triggered"}, 202
|
return {"status": "triggered"}, 202
|
||||||
|
|
||||||
|
|
||||||
@@ -104,7 +108,9 @@ def main():
|
|||||||
scheduler.start()
|
scheduler.start()
|
||||||
log.info("Fallback-Polling gestartet (alle %d Minuten).", config.poll_interval_minutes)
|
log.info("Fallback-Polling gestartet (alle %d Minuten).", config.poll_interval_minutes)
|
||||||
|
|
||||||
log.info("Webhook-Server startet auf Port %d (GET /webhook/update, kein Auth).", config.webhook_port)
|
log.info(
|
||||||
|
"Webhook-Server startet auf Port %d (GET /webhook/update, kein Auth).", config.webhook_port
|
||||||
|
)
|
||||||
app.run(host="0.0.0.0", port=config.webhook_port)
|
app.run(host="0.0.0.0", port=config.webhook_port)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ def load(state_path: str):
|
|||||||
log.info("Keine vorherige State-Datei gefunden (%s) - Erstlauf.", state_path)
|
log.info("Keine vorherige State-Datei gefunden (%s) - Erstlauf.", state_path)
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
with open(state_path, "r") as f:
|
with open(state_path) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
return data.get("last_ip")
|
return data.get("last_ip")
|
||||||
except (json.JSONDecodeError, OSError) as exc:
|
except (json.JSONDecodeError, OSError) as exc:
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[tool.ruff]
|
||||||
|
target-version = "py312"
|
||||||
|
line-length = 100
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "W", "I", "N", "UP", "B", "C4", "PL", "T20"]
|
||||||
|
ignore = ["E501", "PLR0913", "PLR0917"]
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "double"
|
||||||
|
indent-style = "space"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ruff>=0.6.0
|
||||||
Reference in New Issue
Block a user