#!/usr/bin/env python3 """Les hyperviseurs routent-ils toutes les zones que le SITE declare ? Ne corrige rien. POURQUOI CE CONTROLE EXISTE (2026-09-10). Un hyperviseur atteint les zones du site par des routes SPECIFIQUES posees sur `vmbr0`, et non par sa route par defaut — celle-ci est GELEE (D-57) et vise le routeur du site. Le commentaire qui les accompagne, ecrit le 2026-08-26, dit pourquoi : Aller et retour empruntent la MEME patte — pf cree son etat sur une seule interface, et une reponse revenant par une autre serait jetee. CE QUE CA A COUTE. Le site a gagne deux zones — `site-sauvegarde` (35) et `site-supervision` (36) — et la liste des routes ne les a pas suivies. Elle s'arretait a 34. Consequence mesuree : la supervision du site ne pouvait PAS collecter les hyperviseurs, parce que la reponse partait vers un routeur qui ne connait pas 10.0.36.0/24. Le symptome ne ressemblait pas a une route manquante — il ressemblait a un pare-feu, puis a un probleme de reseau chez l'exploitant. Une liste qui doit suivre une autre liste finit toujours par prendre du retard. La seule question est de savoir si quelqu'un s'en apercevra avant la panne. CE QU'ON COMPARE, ET DANS LES DEUX SENS : 1. les zones DECLAREES par `underlay.reseaux` (la verite du site) ; 2. les routes DECLAREES dans `/etc/network/interfaces` (ce qui survit au redemarrage) ; 3. les routes VIVANTES dans la table du noyau (ce qui vaut maintenant). Une route vivante mais non declaree disparait au prochain redemarrage — c'est le cas le plus traitre, parce que tout fonctionne jusqu'a la maintenance suivante. Ce n'est pas une preuve du harnais : elle exigerait le cluster, que `make prouver` ne suppose pas joignable. C'est un controle a la demande, comme `make gabarit-etat`. Usage : python3 scripts/routes_fabric_etat.py # code de sortie 0 si conforme """ from __future__ import annotations import re import subprocess import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import underlay as underlay_mod # noqa: E402 # La patte de la frontiere sur le plan d'administration : le prochain saut de ces routes. # DERIVEE, jamais ecrite : c'est l'hote `frontiere` du reseau `grappe-controle`. RESEAU_PROCHAIN_SAUT = "grappe-controle" def _lire(hote: str, commande: str) -> str: r = subprocess.run( ["ssh", "-o", "ConnectTimeout=8", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", f"ansible@{hote}", commande], capture_output=True, text=True, timeout=60) return r.stdout if r.returncode == 0 else "" def main() -> int: u = underlay_mod.charger() if not u: print("Aucun underlay monte : rien a comparer.") return 0 hotes = underlay_mod.hotes(u) # 1. Les zones du site, telles que la carte les declare. zones = sorted({str(r["sous_reseau"]) for r in underlay_mod.reseaux(u) if str(r.get("nom", "")).startswith("site-") and r.get("sous_reseau")}) if not zones: print("Le site ne declare aucune zone : rien a router.") return 0 saut = next((str(h["ip"]) for h in hotes if h.get("role") == "frontiere" and str(h.get("reseau")) == RESEAU_PROCHAIN_SAUT and h.get("ip")), "") if not saut: print(f"Aucune patte `frontiere` sur `{RESEAU_PROCHAIN_SAUT}` : " f"le prochain saut ne se derive pas.") return 1 # Les hyperviseurs, par leur adresse d'administration — la seule que le poste route. noeuds = {} for h in hotes: if h.get("role") != "hyperviseur" or not h.get("ip"): continue if str(h.get("reseau")) != RESEAU_PROCHAIN_SAUT: continue noeuds.setdefault(str(h["nom"]), str(h["ip"])) if not noeuds: print("Aucun hyperviseur sur le plan d'administration : rien a interroger.") return 0 print(f"Zones declarees par le site : {len(zones)} — prochain saut {saut}") ecarts: list[str] = [] for nom, ip in sorted(noeuds.items()): conf = _lire(ip, "cat /etc/network/interfaces") table = _lire(ip, "ip route show") if not conf and not table: ecarts.append(f"{nom} ({ip}) : injoignable — le controle ne peut rien dire") print(f" {nom:<9} INJOIGNABLE") continue declarees = set(re.findall(r"ip route replace (\d+\.\d+\.\d+\.\d+/\d+) via " + re.escape(saut), conf)) vivantes = set(re.findall(r"^(\d+\.\d+\.\d+\.\d+/\d+) via " + re.escape(saut), table, re.M)) manque_decl = [z for z in zones if z not in declarees] manque_vive = [z for z in zones if z not in vivantes] # Le cas le plus traitre : vivante mais non declaree — elle part au redemarrage. ephemeres = sorted(vivantes - declarees) etat = "=" if not (manque_decl or manque_vive or ephemeres) else "≠" print(f" {nom:<9} declarees {len(declarees)}/{len(zones)} " f"vivantes {len(vivantes)}/{len(zones)} {etat}") for z in manque_decl: ecarts.append(f"{nom} : {z} n'est pas declaree dans `/etc/network/interfaces` " f"— elle ne survivra pas au redemarrage") for z in manque_vive: ecarts.append(f"{nom} : {z} absente de la table du noyau — les reponses vers " f"cette zone partent par la route par defaut et se perdent") for z in ephemeres: ecarts.append(f"{nom} : {z} est VIVANTE mais non declaree — elle disparaitra " f"au prochain redemarrage, et tout marchera jusque-la") if ecarts: print("\nECART — la fabric ne route pas ce que le site declare :") for e in ecarts: print(f" - {e}") print(f"\n Remede, sur le noeud concerne :") print(f" ip route replace via {saut} dev vmbr0 # tout de suite") print(f" puis la meme ligne en `post-up` dans /etc/network/interfaces # pour apres") print(" L'ordre importe peu ; faire les DEUX importe.") return 1 print("\nConforme — chaque hyperviseur route toutes les zones du site, " "declarees ET vivantes.") return 0 if __name__ == "__main__": raise SystemExit(main())