Set-OPS-Public/scripts/routes_fabric_etat.py

142 lines
6.2 KiB
Python
Raw Permalink Normal View History

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
#!/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())