Set-OPS-Public/scripts/devis_mtu.py
Daniel Allaire c18debc25e
Some checks are pending
verifier / verifier (push) Waiting to run
resolution d'instance : une seule, partagee — au lieu de neuf copies
Cinq jours, cinq defauts, tous de la meme famille : « quelle instance, quel inventaire ? »
Neuf modules portaient chacun leur reponse.

- 18 aout : P03 comparait chaque instance a l'inventaire d'une AUTRE ;
- 19 aout : verifier_ports codait `principal/` en dur ; verifier_intrants et
  _frontiere_absente lisaient le symlink au lieu de la variable ;
- 20 aout : devis_placement rendait un verdict juste sur le mauvais tenant ;
- 22 aout : P35, puis P36 — la dixieme, trouvee par la preuve elle-meme.

Aucune n'etait une faute d'inattention : chacune avait ete ecrite de bonne foi, a un
moment ou le besoin semblait local. C'est le mode de panne de la duplication — pas
l'erreur, mais la DERIVE, invisible depuis l'interieur d'un fichier.

LA RESOLUTION UNIQUE. `inventory_rules` porte instance_courante(), inventaire_de(),
dossier_inventaire() et plan_de(). Trois niveaux de repli, dont le TROISIEME manquait a la
moitie des copies : un hosts.yml existant, puis un REPERTOIRE existant (instance neuve —
c'est ce qui faisait echouer `make instancier` sur le modele public), puis le defaut.
Vingt-huit modules y sont branches.

CE QUI REND CE REFACTOR SUR : avant de toucher quoi que ce soit, chaque module a ete
interroge sur ce qu'il resolvait, pour les DEUX ecosystemes. Apres refactor, meme mesure :
17 modules x 2 instances, diff VIDE. Aucune resolution n'a change — prouve, pas suppose.

P41 echoue des qu'un module reintroduit une copie. Eprouvee en negatif : une copie
replacee dans genome.py est signalee avec son numero de ligne. Trois exemptions nommees :
instances.py et inventory_gui.py manipulent le SYMLINK lui-meme (bascule d'instance), et
devis_opnsense lit deliberement quelle instance est ACTIVE. Elles parlent du lien, pas de
la resolution.

make verifier 41 OK, 0 echec, 0 saute ; make ci idem ; lint vert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:19:55 -04:00

96 lines
4 KiB
Python

#!/usr/bin/env python3
"""Devis du MTU : l'invite porte-t-il le MTU de la zone a laquelle il est attache ?
Le MTU de la ZONE est pose sur l'hyperviseur par `make sdn-appliquer`. Rien ne garantit
que l'INVITE le porte : sa carte doit l'heriter du pont (`mtu=1` chez Proxmox), sans quoi
il nait a 1500 quelle que soit la zone.
Quand les deux divergent, la panne est sournoise : la connexion s'etablit, les petites
requetes passent, les grosses reponses restent suspendues. Elle depend alors entierement de
la decouverte de MTU de chemin — donc de messages ICMP que n'importe quel pare-feu peut
avaler en silence.
ET ELLE EST INVISIBLE TANT QUE LA FLOTTE TIENT SUR UN SEUL HYPERVISEUR : deux VM du meme
noeud communiquent par le pont local, sans encapsulation. Mesure du 2026-08-10 : zones
`t11`/`t17` a 1450, les 14 invites a 1500, et aucun symptome — les 14 etaient sur `asgard`.
Le jour ou on repartit la flotte, tout casse d'un coup et rien ne dit pourquoi.
Le MTU attendu n'est pas saisi : il est lu dans `devis_sdn.py`, la source qui configure les
zones. Le rattachement hote -> zone se derive du pont porte par l'inventaire (`t17serv` est
un VNet de la zone `t17`).
Aucun acces reseau ici — c'est le playbook qui releve (D-23).
make mtu-mesurer
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from pathlib import Path
from inventory_rules import instance_courante # noqa: E402
RACINE = Path(__file__).resolve().parent.parent
def zones_attendues() -> dict[str, int]:
"""MTU attendu par VNet, lu a la source qui configure les zones."""
r = subprocess.run([sys.executable, str(RACINE / "scripts" / "devis_sdn.py"), "--json"],
cwd=RACINE, capture_output=True, text=True)
if r.returncode != 0:
raise SystemExit("Le devis SDN ne se genere pas :\n" + r.stderr.strip()[:300])
par_vnet: dict[str, int] = {}
for z in (json.loads(r.stdout).get("zones") or []):
for v in (z.get("vnets") or []):
par_vnet[str(v.get("vnet"))] = int(z.get("mtu") or 0)
return par_vnet
def analyser(releve: dict, attendus: dict[str, int]) -> tuple[list[str], list[str]]:
ecarts: list[str] = []
lignes: list[str] = []
invites = releve.get("invites") or {}
ponts = releve.get("ponts") or {}
for hote in sorted(invites):
vu = int(invites[hote] or 0)
pont = str(ponts.get(hote) or "")
attendu = attendus.get(pont)
if attendu is None:
# Hors SDN (pont classique) : le devis n'a pas d'attendu derive, il se tait
# plutot que d'inventer un seuil.
lignes.append(f" -- {hote:<16} {vu:>5} pont {pont or '(inconnu)'} — hors zone SDN")
continue
ok = vu == attendu
lignes.append(f" {'ok ' if ok else 'ECART'} {hote:<16} {vu:>5} attendu {attendu} "
f"(zone du pont {pont})")
if not ok:
ecarts.append(f"{hote} : carte a {vu}, zone {pont} a {attendu}")
return ecarts, lignes
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--releve", default=None)
a = ap.parse_args(argv)
chemin = Path(a.releve) if a.releve else instance_courante() / "devis-mtu.json"
if not chemin.is_file():
raise SystemExit(f"Aucun releve : {chemin}\nLancer d'abord `make mtu-mesurer`.")
ecarts, lignes = analyser(json.loads(chemin.read_text()), zones_attendues())
print("Devis du MTU — carte de l'invite contre MTU de sa zone\n")
print("\n".join(lignes))
if ecarts:
print(f"\nECART : {len(ecarts)} invite(s) ne portent pas le MTU de leur zone.")
for e in ecarts:
print(" -", e)
print("\n Corriger : `mtu: 1` sur la carte (herite du pont) — deja pose par\n"
" `cloner_vm_debian.yml`. Une VM existante doit redemarrer pour le prendre.")
return 1
print("\nCONFORME : chaque invite porte le MTU de sa zone.")
return 0
if __name__ == "__main__":
sys.exit(main())