142 lines
6.2 KiB
Python
142 lines
6.2 KiB
Python
|
|
#!/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 <zone> 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())
|