Files
2026-08-10 22:43:06 +02:00

121 lines
3.8 KiB
Python

# Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
# Licensed under the MIT License. See LICENSE file in project root for details.
import concurrent.futures
import logging
import threading
from apscheduler.schedulers.background import BackgroundScheduler
from flask import Flask
from app import cloudflare, freedns, healthcheck, notify, state
from app.config import config
from app.fritzbox import FritzBoxError, get_external_ip
log = logging.getLogger("main")
app = Flask(__name__)
_lock = threading.Lock()
_executor = concurrent.futures.ThreadPoolExecutor(max_workers=2)
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 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)
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}",
)
notify.send_ip_changed_email(last_ip, 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():
_executor.submit(run_cycle, trigger="webhook")
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 8090 (GET /webhook/update, kein Auth).")
logging.getLogger("werkzeug").setLevel(logging.ERROR)
app.run(host="0.0.0.0", port=8090)
if __name__ == "__main__":
main()