Set-OPS-Public/scripts/routes_fabric_etat.py
Daniel Allaire 5abf20102d le site surveille enfin sa fabric
La supervision du site voyait ses sept VM et rien d autre. Au depart, depuis
site-mon-01, les trois hyperviseurs, les neuf pattes de la frontiere et sa
PROPRE passerelle par defaut rendaient tous 100 pourcent de perte au ping.

  16 hotes UP sur 16       dont 9 pattes de frontiere en controle ACTIF
  11 cibles Prometheus     dont 3 hyperviseurs, job fabric separe

LE PARTAGE. Les hyperviseurs portent node_exporter - D-48 l autorise - et
exposent 1005 unites systemd avec leur etat, soit ce que la sonde sante
mesurait, plus la charge et le disque. Ils n entrent PAS dans le socle :
leur appliquer serveur_durci reecrirait le pare-feu, le SSH et les sysctl
de la machine qui tient tout le reste. La frontiere, elle, n accueille
aucun agent : controle actif, une entree par PATTE, parce qu une interface
eteinte coupe une zone pendant que les autres vont bien.

ON TIRE, ON NE POUSSE PAS. J avais propose du passif et il avait ete
valide ; la mesure a dit non. Un hyperviseur envoie vers un routeur qui ne
connait pas les reseaux du site, et sa route par defaut est GELEE (D-57).
Le porteur de sante y expirait en 20 s.

LE VRAI DEFAUT ETAIT UNE LISTE QUI N A PAS SUIVI. Le mecanisme de routage
existait deja sur vmbr0, avec un commentaire du 2026-08-26 tenant
exactement le raisonnement qu on venait de refaire. Sa liste s arretait a
10.0.34.0/24 quand le site en declare six : les zones sauvegarde et
supervision sont nees, les routes n ont pas suivi. Le symptome ne
ressemblait pas a une route manquante - il ressemblait a un pare-feu, puis
a un probleme de reseau chez l exploitant.

make routes-fabric-etat compare desormais TROIS choses : zones declarees,
routes declarees dans /etc/network/interfaces, routes vivantes dans le
noyau. Le cas le plus traitre est vivante mais non declaree : tout
fonctionne, la supervision est verte, et la panne attend la prochaine
maintenance. Eprouvee dans les deux sens.

Deux defauts trouves en construisant. bifrost-2 est un nom RESERVE, pas un
boitier - l underlay le disait en prose, illisible par le moteur ; il porte
desormais etat: reserve. Et un service passif n existe que pour un hote qui
peut POUSSER : l appartenance a un groupe sert deux choses qui ne
coincident pas toujours, a qui l on deploie et de qui l on attend un
rapport.

Les commutateurs restent dehors, choix de l exploitant, coherent avec D-48.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 18:50:43 -04:00

141 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())