Files
dyndns-updater/app/main.py
T

113 lines
3.4 KiB
Python

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