Ctrl K
ComunidadGuías…

deploy/dns-drift-fix.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Gardien de dérive DNS (drift-fix) — timer quotidien côté VPS.

Si l'IP publique actuelle du VPS diffère de celle en mémoire, ou si des
records A surveillés ne pointent plus vers l'IP publique, re-synchronise
via l'API Cloudflare et notifie Vektor (webhook selfheal, rapport "dit").
Sans /opt/caddy/cf.env : exit 0 silencieux (fonctionnalité non armée).
State : /var/lib/dns-drift/state.json (dernière IP vue).
"""
import importlib.machinery
import importlib.util
import json
import subprocess
import sys
import time
import urllib.request
from pathlib import Path

CF_ENV = Path("/opt/caddy/cf.env")
STATE_DIR = Path("/var/lib/dns-drift")
STATE_FILE = STATE_DIR / "state.json"
LOG = Path("/var/log/dns-drift.log")
_loader = importlib.machinery.SourceFileLoader("dns_failover", "/usr/local/bin/dns-failover")
_spec = importlib.util.spec_from_loader("dns_failover", _loader)
_dns = importlib.util.module_from_spec(_spec)
_loader.exec_module(_dns)
API, HOSTNAMES, ZONE_NAME = _dns.API, _dns.HOSTNAMES, _dns.ZONE_NAME
cf_request, load_creds, zone_records = _dns.cf_request, _dns.load_creds, _dns.zone_records


def log(msg: str) -> None:
    try:
        with open(LOG, "a") as fh:
            fh.write(f"{time.strftime('%F %T')} {msg}\n")
    except OSError:
        pass


def public_ip() -> str:
    for host in ("https://api.ipify.org", "https://ifconfig.me/ip"):
        try:
            with urllib.request.urlopen(host, timeout=10) as resp:
                ip = resp.read().decode().strip()
            if ip:
                return ip
        except Exception:
            continue
    raise SystemExit("impossible de déterminer l'IP publique")


def notify_vektor(title: str, fingerprint: str, status: str) -> None:
    ip = subprocess.run(
        ["docker", "inspect", "vektor-api", "--format",
         "{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}"],
        capture_output=True, text=True, timeout=10, check=False,
    ).stdout.split()
    secret = ""
    for line in Path("/opt/vektor/.env").read_text().splitlines():
        if line.startswith("VEKTOR_SELFHEAL_SECRET="):
            secret = line.split("=", 1)[1].strip()
    if not ip or not secret:
        return
    payload = json.dumps({
        "alerts": [{"fingerprint": fingerprint, "title": title, "status": status,
                    "labels": {"severity": "warning"}}],
    }).encode()
    req = urllib.request.Request(
        f"http://{ip[0]}:8000/api/webhook/grafana", data=payload,
        headers={"Content-Type": "application/json", "X-Vektor-Selfheal": secret},
        method="POST",
    )
    try:
        urllib.request.urlopen(req, timeout=30)
    except Exception as exc:
        log(f"notify_vektor KO: {exc.__class__.__name__}: {exc}")


def main() -> int:
    if not CF_ENV.exists():
        return 0  # non armé : no-op silencieux pour le timer
    token, zone = load_creds()
    current = public_ip()
    last = ""
    try:
        last = json.loads(STATE_FILE.read_text()).get("last_ip", "")
    except (OSError, ValueError):
        pass

    records = zone_records(token, zone)
    drifted = [
        r for r in records
        if r["type"] == "A" and r["name"] != ZONE_NAME
        and r["name"].endswith(f".{ZONE_NAME}")
        and r["name"].split(".")[0] in HOSTNAMES
        and r["content"] != current
    ]

    if not drifted and (last == current or not last):
        STATE_DIR.mkdir(parents=True, exist_ok=True, mode=0o700)
        STATE_FILE.write_text(json.dumps({"last_ip": current}))
        return 0

    fixed = 0
    for r in drifted:
        cf_request("PATCH", f"/zones/{zone}/dns_records/{r['id']}", token,
                   {"content": current})
        log(f"{r['name']}: {r['content']} -> {current}")
        fixed += 1

    if last and last != current:
        log(f"rotation IP publique détectée : {last} -> {current} ({fixed} records corrigés)")
        notify_vektor(
            f"IP publique VPS changée ({last} -> {current}) ; DNS re-synchronisé ({fixed} records)",
            "dns-drift-ip-rotation", "firing")
    elif fixed:
        log(f"{fixed} record(s) A dérivé(s) corrigé(s) vers {current}")
        notify_vektor(
            f"{fixed} record(s) DNS dérivé(s) re-pointé(s) vers {current} par dns-drift-fix",
            "dns-drift-correction", "firing")

    STATE_DIR.mkdir(parents=True, exist_ok=True, mode=0o700)
    STATE_FILE.write_text(json.dumps({"last_ip": current}))
    return 0


if __name__ == "__main__":
    sys.exit(main())