some fixes

This commit is contained in:
2026-08-07 10:04:31 +02:00
parent 6dd541b798
commit c7e575ff9b
12 changed files with 65 additions and 14 deletions
+2 -2
View File
@@ -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
+18
View File
@@ -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 .
+1 -1
View File
@@ -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
``` ```
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+11
View File
@@ -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"
+1
View File
@@ -0,0 +1 @@
ruff>=0.6.0