mirror of
https://github.com/skoelle/dyndns-updater.git
synced 2026-09-18 00:20:24 +00:00
Add SPEC, PLAN, Dockerfile, compose, env example, app modules, CI workflow
This commit is contained in:
+112
@@ -0,0 +1,112 @@
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from flask import Flask
|
||||
|
||||
from app.config import config
|
||||
from app.fritzbox import FritzBoxError, get_external_ip
|
||||
from app import state, cloudflare, freedns, healthcheck
|
||||
|
||||
log = logging.getLogger("main")
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
_lock = threading.Lock()
|
||||
|
||||
|
||||
def run_cycle(trigger: str = "unknown"):
|
||||
with _lock:
|
||||
log.info("Starte Update-Zyklus (Trigger=%s)", trigger)
|
||||
try:
|
||||
current_ip = get_external_ip(
|
||||
config.fritzbox_host,
|
||||
config.fritzbox_port,
|
||||
config.fritzbox_user,
|
||||
config.fritzbox_password,
|
||||
)
|
||||
except FritzBoxError as exc:
|
||||
log.error("IP-Ermittlung fehlgeschlagen: %s", exc)
|
||||
healthcheck.ping(config.healthcheck_ping_url, status="fail", message=str(exc))
|
||||
return
|
||||
|
||||
last_ip = state.load(config.state_path)
|
||||
|
||||
if current_ip == last_ip:
|
||||
log.info("IP unveraendert (%s) - kein DNS-Update noetig.", current_ip)
|
||||
if trigger == "poll":
|
||||
healthcheck.ping(
|
||||
config.healthcheck_ping_url,
|
||||
status="success",
|
||||
message=f"alive, ip unchanged ({current_ip})",
|
||||
)
|
||||
return
|
||||
|
||||
log.info("IP-Aenderung erkannt: %s -> %s", last_ip, current_ip)
|
||||
|
||||
cf_results = cloudflare.update_records(
|
||||
config.cloudflare_zone_id,
|
||||
config.cloudflare_api_token,
|
||||
config.cloudflare_records,
|
||||
current_ip,
|
||||
)
|
||||
cf_ok = all(v == "ok" for v in cf_results.values())
|
||||
|
||||
freedns_ok = freedns.update(config.freedns_update_url)
|
||||
|
||||
if cf_ok and freedns_ok:
|
||||
state.save(config.state_path, current_ip)
|
||||
healthcheck.ping(
|
||||
config.healthcheck_ping_url,
|
||||
status="success",
|
||||
message=f"IP updated to {current_ip}",
|
||||
)
|
||||
else:
|
||||
failed = {k: v for k, v in cf_results.items() if v != "ok"}
|
||||
log.error(
|
||||
"Update unvollstaendig - state wird NICHT gespeichert. "
|
||||
"Cloudflare-Fehler: %s, FreeDNS ok=%s",
|
||||
failed,
|
||||
freedns_ok,
|
||||
)
|
||||
healthcheck.ping(
|
||||
config.healthcheck_ping_url,
|
||||
status="partial-fail",
|
||||
message=f"partial failure: cf_errors={failed} freedns_ok={freedns_ok}",
|
||||
)
|
||||
|
||||
|
||||
@app.route("/webhook/update", methods=["GET"])
|
||||
def webhook_update():
|
||||
threading.Thread(target=run_cycle, kwargs={"trigger": "webhook"}, daemon=True).start()
|
||||
return {"status": "triggered"}, 202
|
||||
|
||||
|
||||
@app.route("/healthz", methods=["GET"])
|
||||
def healthz():
|
||||
return {"status": "ok"}, 200
|
||||
|
||||
|
||||
def main():
|
||||
config.validate()
|
||||
|
||||
log.info("Initial-Update-Zyklus beim Start...")
|
||||
run_cycle(trigger="startup")
|
||||
|
||||
scheduler = BackgroundScheduler()
|
||||
scheduler.add_job(
|
||||
run_cycle,
|
||||
"interval",
|
||||
minutes=config.poll_interval_minutes,
|
||||
kwargs={"trigger": "poll"},
|
||||
id="fallback-poll",
|
||||
)
|
||||
scheduler.start()
|
||||
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)
|
||||
app.run(host="0.0.0.0", port=config.webhook_port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user