placement : le devis validait un pont que la flotte n'utilise pas
Some checks are pending
verifier / verifier (push) Waiting to run
Some checks are pending
verifier / verifier (push) Waiting to run
Remarque de l'exploitant en preparant patient 0 : « le pont ne me semble pas approprie du tout, depuis qu'on cree des VNets pour des tenants ». Juste, et plus grave que cosmetique. `make placement-plan` confrontait `proxmox_clone_pont` (vmbr1) au cluster. Ce n'est PAS la que les VM de la flotte atterrissent : `instancier` pose dans chaque hote le pont DERIVE de sa zone (le VNet du tenant), et `make creer-vm` le passe au clone en ecrasant ce defaut. vmbr1 n'est que le repli des clones MANUELS, hors plan. Le devis mesurait donc un objet qui ne sert pas, et ignorait celui qui sert. SUR PATIENT 0 : avant, « pont vmbr1 existe -> CONFORME ». Apres, « reseaux VM : t29appl, t29donn, t29fron, t29serv INTROUVABLE — passer `make sdn-appliquer` AVANT de creer les VM ». Aucun de ses quatre VNets n'existe sur le cluster : le devis d'avant-vol declarait conforme un tenant dont les VM n'auraient eu nulle part ou naitre. D-80 avait pourtant ete corrigee le 2026-08-13 — la liaison de placement est noeud, stockage et gabarit, le pont se derive. Le devis continuait de compter quatre objets et de nommer le mauvais : une doctrine corrigee dans un document ne se propage pas toute seule dans le code qui l'applique. MESURE MAINTENANT : en `sdn`, les VNets derives confrontes a /cluster/sdn/vnets ; en `switch`, les ponts du noeud retenu. Avec le geste correctif quand il en manque. NON-REGRESSION sur l'ecosysteme de reference : ses six VNets existent, conforme, code 0. Quatre tests (Cluster simule, aucun reseau touche), harnais 38/38. Au passage, dans patient 0 : le commentaire annoncait « les QUATRE valeurs qui rattachent un tenant a une fabric » — trois, et le pont n'en est pas. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
254268d5f2
commit
61488777ee
3 changed files with 152 additions and 23 deletions
46
CHANGELOG.md
46
CHANGELOG.md
|
|
@ -1,5 +1,51 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-20 — Le devis d'avant-vol validait le mauvais réseau
|
||||
|
||||
Remarque de l'exploitant, en préparant patient 0 : *« le pont ne me semble pas approprié
|
||||
du tout, depuis qu'on crée des VNets pour des tenants. »* Il avait raison, et le défaut
|
||||
était plus grave que cosmétique.
|
||||
|
||||
`make placement-plan` confrontait `proxmox_clone_pont` — `vmbr1` — au cluster. Or ce
|
||||
n'est **pas** là que les VM de la flotte atterrissent : `instancier` pose dans chaque hôte
|
||||
le pont **dérivé** de sa zone (le VNet du tenant), et `make creer-vm` le passe au clone en
|
||||
écrasant ce défaut. `vmbr1` n'est que le repli des clones **manuels**, hors plan.
|
||||
|
||||
Le devis mesurait donc un objet qui ne sert pas, et ne mesurait pas celui qui sert.
|
||||
|
||||
### Ce que ça donnait sur patient 0
|
||||
|
||||
```
|
||||
avant : pont vmbr1 existe → CONFORME
|
||||
après : reseaux VM t29appl, t29donn, t29fron, t29serv INTROUVABLE
|
||||
-> VNet(s) absent(s) : ... — passer `make sdn-appliquer` AVANT de creer les VM
|
||||
```
|
||||
|
||||
Aucun des quatre VNets de patient 0 n'existe sur le cluster. Le devis d'avant-vol disait
|
||||
« conforme » à un tenant dont les VM n'auraient eu nulle part où naître.
|
||||
|
||||
D-80 avait pourtant été corrigée le 13 août : la liaison de placement est **nœud, stockage
|
||||
et gabarit** — le pont se dérive. Le devis, lui, continuait de compter quatre objets et de
|
||||
nommer le mauvais. Une doctrine corrigée dans un document ne se propage pas toute seule
|
||||
dans le code qui l'applique.
|
||||
|
||||
### Ce qu'il mesure maintenant
|
||||
|
||||
Les réseaux **où les VM atterriront** : en `sdn`, les VNets dérivés confrontés à
|
||||
`/cluster/sdn/vnets` ; en `switch`, les ponts du nœud retenu. Avec, quand il en manque, le
|
||||
geste exact qui répare.
|
||||
|
||||
Non-régression vérifiée sur l'écosystème de référence : ses six VNets existent, verdict
|
||||
conforme, code de sortie 0. Quatre tests, Cluster simulé, aucun réseau touché.
|
||||
|
||||
> **Le vert le plus dangereux est celui qui porte sur un objet voisin du bon.** Ici tout
|
||||
> était vrai — `vmbr1` existe bel et bien — et la conclusion était fausse. C'est la
|
||||
> quatrième fois cette semaine : la frontière qui poliçait les tenants d'un autre site,
|
||||
> P03 qui comparait à l'inventaire d'une autre instance, le catalogue qui décrivait un
|
||||
> moteur d'il y a quatre mois, et maintenant un devis qui contrôle un pont que la flotte
|
||||
> n'utilise pas.
|
||||
|
||||
|
||||
## 2026-08-20 — Le devis d'avant-vol mourait au lieu de parler
|
||||
|
||||
Soir de reconstruction, VPN pas encore monté. `make placement-plan` — le devis qu'on lance
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import yaml
|
|||
RACINE = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
|
||||
import underlay as underlay_mod # noqa: E402
|
||||
from proxmox_api import Cluster # noqa: E402
|
||||
|
||||
INVENTAIRES = ("principal", "production", "lab")
|
||||
|
|
@ -87,9 +88,36 @@ def _liste(api: Cluster, chemin: str) -> list:
|
|||
return rep
|
||||
|
||||
|
||||
def constater(api: Cluster, place: dict) -> list[dict]:
|
||||
def _ponts_derives(base: Path | None) -> set[str]:
|
||||
"""Les reseaux ou les VM de CE tenant atterriront reellement.
|
||||
|
||||
Ce ne sont pas les valeurs du plan : `instancier` pose dans chaque hote le pont
|
||||
DERIVE de sa zone (`proxmox_pont` = le VNet du tenant), et `make creer-vm` le passe au
|
||||
clone en ecrasant le defaut. C'est donc la seule chose a confronter au cluster.
|
||||
"""
|
||||
ponts: set[str] = set()
|
||||
if base is None:
|
||||
return ponts
|
||||
for f in sorted(base.glob("inventories/*/hosts.yml")):
|
||||
data = yaml.safe_load(f.read_text(encoding="utf-8")) or {}
|
||||
|
||||
def marcher(noeud):
|
||||
if not isinstance(noeud, dict):
|
||||
return
|
||||
for hote in (noeud.get("hosts") or {}).values():
|
||||
if isinstance(hote, dict) and hote.get("proxmox_pont"):
|
||||
ponts.add(str(hote["proxmox_pont"]))
|
||||
for enfant in (noeud.get("children") or {}).values():
|
||||
marcher(enfant or {})
|
||||
marcher((data.get("all") or {}))
|
||||
return ponts
|
||||
|
||||
|
||||
def constater(api: Cluster, place: dict, base: Path | None = None,
|
||||
routage: str = "sdn") -> list[dict]:
|
||||
"""Confronte chaque valeur au cluster. Renvoie une ligne par objet."""
|
||||
lignes: list[dict] = []
|
||||
place = {**place, "_instance": base}
|
||||
|
||||
noeuds = [n.get("node") for n in _liste(api, "/nodes")]
|
||||
voulu = place.get("proxmox_clone_noeud")
|
||||
|
|
@ -108,17 +136,42 @@ def constater(api: Cluster, place: dict) -> list[dict]:
|
|||
"note": ("" if voulu not in stockages or "images" in stockages[voulu]
|
||||
else f"present mais n'accepte pas `images` ({stockages[voulu]})")})
|
||||
|
||||
# Le pont doit exister SUR LE NOEUD retenu : un pont partiel est un piege, la VM ne
|
||||
# demarre que sur certains noeuds et l'erreur ne le dit pas.
|
||||
n = place.get("proxmox_clone_noeud")
|
||||
ponts: list[str] = []
|
||||
if n in noeuds:
|
||||
ponts = [i.get("iface") for i in _liste(api, f"/nodes/{n}/network")
|
||||
if str(i.get("type")) in ("bridge", "OVSBridge")]
|
||||
voulu = place.get("proxmox_clone_pont")
|
||||
lignes.append({"objet": "pont", "voulu": voulu,
|
||||
"existe": voulu in ponts, "offre": ponts,
|
||||
"note": "" if n in noeuds else "noeud introuvable : pont non verifiable"})
|
||||
# LE RESEAU OU LES VM ATTERRISSENT — pas `proxmox_clone_pont`.
|
||||
#
|
||||
# Mesure du 2026-08-20, remarque de l'exploitant : « le pont ne me semble pas approprie
|
||||
# depuis qu'on cree des VNets par tenant ». Il avait raison, et le defaut etait pire
|
||||
# que cosmetique. Ce devis validait `vmbr1` — le REPLI des clones manuels — pendant que
|
||||
# le point d'attache reel des VM de la flotte n'etait controle par personne :
|
||||
# `creer-vm` lit le pont DERIVE de chaque hote (`SETOPS_PONT`, le VNet de sa zone) et
|
||||
# ecrase le defaut. Resultat : « CONFORME » sur un tenant dont aucun VNet n'existait.
|
||||
#
|
||||
# D-80 corrigee le 2026-08-13 dit deja que la liaison de placement est nœud, stockage
|
||||
# et gabarit — le pont se derive. Le devis, lui, continuait de compter quatre objets.
|
||||
ponts_derives = sorted(_ponts_derives(place.get("_instance")))
|
||||
if routage == "sdn":
|
||||
vnets = {v.get("vnet") for v in _liste(api, "/cluster/sdn/vnets")}
|
||||
manquants = [p for p in ponts_derives if p not in vnets]
|
||||
lignes.append({
|
||||
"objet": "reseaux VM", "voulu": ", ".join(ponts_derives) or "(aucun)",
|
||||
"existe": bool(ponts_derives) and not manquants,
|
||||
"offre": sorted(vnets),
|
||||
"note": ("" if not manquants else
|
||||
f"VNet(s) absent(s) du cluster : {', '.join(manquants)} — "
|
||||
f"passer `make sdn-appliquer` AVANT de creer les VM")})
|
||||
else:
|
||||
n = place.get("proxmox_clone_noeud")
|
||||
ponts: list[str] = []
|
||||
if n in noeuds:
|
||||
ponts = [i.get("iface") for i in _liste(api, f"/nodes/{n}/network")
|
||||
if str(i.get("type")) in ("bridge", "OVSBridge")]
|
||||
manquants = [p for p in ponts_derives if p not in ponts]
|
||||
lignes.append({
|
||||
"objet": "reseaux VM", "voulu": ", ".join(ponts_derives) or "(aucun)",
|
||||
"existe": bool(ponts_derives) and not manquants,
|
||||
"offre": ponts,
|
||||
"note": ("noeud introuvable : ponts non verifiables" if n not in noeuds else
|
||||
"" if not manquants else
|
||||
f"pont(s) absent(s) sur {n} : {', '.join(manquants)}")})
|
||||
|
||||
# Le gabarit : celui que P37 ne peut PAS voir. Presence, et surtout `template=1` —
|
||||
# cloner une VM ordinaire marcherait, et produirait quatorze copies d'une machine
|
||||
|
|
@ -142,20 +195,20 @@ def afficher(lignes: list[dict], source: Path | None, depot: Path) -> bool:
|
|||
# du lien, pas celui du tenant. Un devis doit nommer ce qu'il a mesure.
|
||||
tenant = source.parents[3].resolve().name if source else "?"
|
||||
print(f"Devis du placement — tenant « {tenant} » sur l'hebergeur « {depot.name} »\n")
|
||||
print(f" {'OBJET':<10} {'DEMANDE':<22} {'VERDICT'}")
|
||||
print(f" {'OBJET':<12} {'DEMANDE':<34} {'VERDICT'}")
|
||||
ok = True
|
||||
for l in lignes:
|
||||
verdict = "existe" if l["existe"] else "INTROUVABLE"
|
||||
ok = ok and l["existe"]
|
||||
print(f" {l['objet']:<10} {str(l['voulu']):<22} {verdict}")
|
||||
print(f" {l['objet']:<12} {str(l['voulu']):<34} {verdict}")
|
||||
if l.get("note"):
|
||||
print(f" {'':<10} {'':<22} -> {l['note']}")
|
||||
print(f" {'':<12} {'':<34} -> {l['note']}")
|
||||
if not l["existe"] and l["offre"]:
|
||||
offert = ", ".join(str(x) for x in l["offre"][:6])
|
||||
print(f" {'':<10} {'':<22} offert : {offert}")
|
||||
print("\n" + ("CONFORME : les quatre objets de placement existent sur ce cluster."
|
||||
if ok else
|
||||
"ECART : le tenant designe des objets que ce cluster n'offre pas."))
|
||||
print(f" {'':<12} {'':<34} offert : {offert}")
|
||||
print("\n" + ("CONFORME : le noeud, le stockage, le gabarit et les reseaux des VM "
|
||||
"existent sur ce cluster." if ok else
|
||||
"ECART : le tenant a besoin d'objets que ce cluster n'offre pas."))
|
||||
return ok
|
||||
|
||||
|
||||
|
|
@ -174,7 +227,9 @@ def main() -> int:
|
|||
print(f"Cluster injoignable : {e}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
lignes = constater(api, place)
|
||||
base = source.parents[3] if source else None
|
||||
routage = underlay_mod.routage_tenants(underlay_mod.charger())
|
||||
lignes = constater(api, place, base, routage)
|
||||
if a.verifier:
|
||||
mauvais = [l for l in lignes if not l["existe"]]
|
||||
if mauvais:
|
||||
|
|
@ -182,8 +237,8 @@ def main() -> int:
|
|||
print(f"placement : {l['objet']} « {l['voulu']} » introuvable"
|
||||
+ (f" — {l['note']}" if l.get("note") else ""), file=sys.stderr)
|
||||
return 1
|
||||
print(f"Placement conforme : noeud, stockage, pont et gabarit existent sur "
|
||||
f"« {depot.name} ».")
|
||||
print(f"Placement conforme : noeud, stockage, gabarit et reseaux des VM "
|
||||
f"existent sur « {depot.name} ».")
|
||||
return 0
|
||||
return 0 if afficher(lignes, source, depot) else 1
|
||||
|
||||
|
|
|
|||
|
|
@ -69,11 +69,39 @@ def test_une_reponse_normale_traverse_la_garde():
|
|||
"""La garde ne doit pas gener le cas nominal : quatre lignes de constat."""
|
||||
api = _ClusterSimule([]) # cluster joignable, mais qui n'offre rien
|
||||
lignes = dp.constater(api, PLACEMENT)
|
||||
assert [l["objet"] for l in lignes] == ["noeud", "stockage", "pont", "gabarit"]
|
||||
assert [l["objet"] for l in lignes] == ["noeud", "stockage", "reseaux VM", "gabarit"]
|
||||
assert all(l["existe"] is False for l in lignes), \
|
||||
"un cluster vide n'offre aucun des quatre objets"
|
||||
|
||||
|
||||
def test_le_reseau_mesure_est_celui_ou_les_VM_atterrissent(tmp=None):
|
||||
"""Le pont du plan n'est PAS ce qu'on verifie — c'est le VNet derive de chaque hote.
|
||||
|
||||
Le defaut du 2026-08-20 : le devis validait `proxmox_clone_pont` (vmbr1), qui n'est
|
||||
que le repli des clones manuels, et rendait CONFORME sur un tenant dont AUCUN VNet
|
||||
n'existait. `make creer-vm` passe le pont derive par hote et ecrase ce defaut.
|
||||
"""
|
||||
import tempfile
|
||||
import yaml as _yaml
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
base = Path(d)
|
||||
inv = base / "inventories" / "production"
|
||||
inv.mkdir(parents=True)
|
||||
(inv / "hosts.yml").write_text(_yaml.safe_dump({
|
||||
"all": {"children": {"hotes_actifs": {"hosts": {
|
||||
"a-01": {"proxmox_pont": "t29fron"},
|
||||
"b-01": {"proxmox_pont": "t29serv"}}}}}}), encoding="utf-8")
|
||||
|
||||
api = _ClusterSimule([{"vnet": "t29fron"}]) # un seul des deux existe
|
||||
ligne = [l for l in dp.constater(api, PLACEMENT, base, "sdn")
|
||||
if l["objet"] == "reseaux VM"][0]
|
||||
assert ligne["existe"] is False, "un VNet manquant doit faire echouer le devis"
|
||||
assert "t29serv" in ligne["note"], ligne["note"]
|
||||
assert "sdn-appliquer" in ligne["note"], "le geste correctif doit etre nomme"
|
||||
assert "vmbr" not in str(ligne["voulu"]), \
|
||||
"le devis ne doit plus presenter le pont des clones manuels"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
||||
for t in tests:
|
||||
|
|
|
|||
Loading…
Reference in a new issue