Ctrl K
ComunidadGuías…

deploy/patch-caddy-failover.py

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Patch failover du Caddyfile VPS (inode-safe, idempotent, restaurable).

- Insère un bloc handle_errors 502/503/504 dans le snippet (access_log) :
  les 27 sites qui l'importent servent alors une page de garde statique
  (fichier /config/maintenance/index.html monté dans le conteneur) sur
  erreur d'infrastructure, SANS masquer l'état réel (status 502 conservé
  pour que Kuma/les moniteurs continuent de voir DOWN et alertent Vektor).
- Ajoute un site racine mayoraz-net.ch (page de maintenance file_server)
  uniquement si le domaine se résout en DNS.
- Idempotent : réexécution sans effet si déjà patché.
- Écriture in-place (open(w)+fsync) : le Caddyfile est un bind-mount
  FICHIER dans le conteneur — JAMAIS de rename/os.replace (piège inode
  du 03/10 matin).
- La validation `caddy validate` et le reload sont orchestrés par
  l'appelant ; en cas de validate KO, restaurer avec --restore <backup>.

Usage :
  python3 patch-caddy-failover.py                     # applique
  python3 patch-caddy-failover.py --restore <backup>  # restaure in-place
"""
import os
import socket
import sys
from pathlib import Path

CADDYFILE = Path("/opt/caddy/Caddyfile")

HANDLE_ERRORS = [
    "\thandle_errors 502 503 504 {",
    "\t\trewrite * /index.html",
    "\t\tfile_server {",
    "\t\t\troot /config/maintenance",
    "\t\t}",
    "\t}",
]

APEX = [
    "",
    "mayoraz-net.ch {",
    "\timport access_log",
    "\timport comp",
    "\ttls internal",
    "\troot * /config/maintenance",
    "\tfile_server",
    "}",
]


def write_in_place(path: Path, content: str) -> None:
    """Bind-mount fichier : truncate+write = même inode. Jamais de rename."""
    with open(path, "w") as fh:
        fh.write(content)
        fh.flush()
        os.fsync(fh.fileno())


def main() -> int:
    if len(sys.argv) >= 3 and sys.argv[1] == "--restore":
        backup = Path(sys.argv[2])
        write_in_place(CADDYFILE, backup.read_text())
        print(f"RESTAURE: {backup} -> {CADDYFILE} (in-place, inode préservée)")
        return 0

    text = CADDYFILE.read_text()
    changed = []

    if "handle_errors" in text:
        print("handle_errors: déjà présent, insertion ignorée")
    else:
        lines = text.split("\n")
        start = next(
            (i for i, ln in enumerate(lines) if ln.strip() == "(access_log) {"),
            None,
        )
        if start is None:
            print("ERREUR: snippet (access_log) introuvable — aucun changement")
            return 1
        depth = 0
        end = None
        for i in range(start, len(lines)):
            depth += lines[i].count("{") - lines[i].count("}")
            if depth == 0:
                end = i
                break
        if end is None:
            print("ERREUR: accolades déséquilibrées dans (access_log)")
            return 1
        lines[end:end] = HANDLE_ERRORS
        text = "\n".join(lines)
        changed.append(f"handle_errors 502/503/504 inséré dans (access_log) avant la ligne {end + 1}")

    if "\nmayoraz-net.ch {" in text or text.startswith("mayoraz-net.ch {"):
        print("site racine: déjà présent")
    else:
        try:
            ip = socket.gethostbyname("mayoraz-net.ch")
        except OSError:
            print("site racine: IGNORÉ (mayoraz-net.ch ne se résout pas en DNS)")
        else:
            text = text.rstrip("\n") + "\n" + "\n".join(APEX) + "\n"
            changed.append(f"site racine mayoraz-net.ch ajouté (DNS -> {ip})")

    if not changed:
        print("aucun changement")
        return 0

    write_in_place(CADDYFILE, text)
    for item in changed:
        print("OK:", item)
    return 0


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