contexte : etape 3, les pools et les tunnels lisent la face reseau

devis_proxmox_pools et vpn_admin prennent machines, VMID, etat, pairs et
index dans la face publiee. Sorties identiques a un worktree complet de
la version precedente ; temoins d'alteration verts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-10-05 14:59:29 -04:00
parent 2e9b31bef7
commit 40af2d1ec6
5 changed files with 116 additions and 4 deletions

View file

@ -1,5 +1,35 @@
# CHANGELOG — Set-OPS
## 2026-10-05 (96) — Étape 3 : les pools et les tunnels d'administration lisent la face réseau
**Le chemin** (`docs/conception-contextes.md` §6), étape 3, matérialisation, premiers temps :
les deux devis du site qui lisaient encore le plan des locataires.
**Fait** :
- la face publie l'**état** de chaque machine (actif ou planifié, lu dans l'inventaire) et
les **pairs WireGuard entiers** (`vpn_admin` les reprend tels quels ; des clés publiques et
des adresses, rien de secret) ; faces republiées ;
- `devis_proxmox_pools.py` prend dans la face le nom, le VMID et l'état de chaque machine, au
lieu de relire `plan/serveurs.yml` et de re-dériver les VMID ;
- `vpn_admin.py` prend dans la face les pairs et l'index, au lieu de `plan/acces.yml` et de la
nomenclature. Sans face, l'ancienne lecture, et chacun le dit.
**Le critère, comparé comme il faut** (leçon de (95)) : un `git worktree` à `2e9b31b`, arbre
complet, même environnement. Identiques octet pour octet : le plan des tunnels
(`tunnels()` et `plan_vpn()`), les pools, la frontière, Proxmox, le SDN, le commutateur,
l'inventaire du site.
**Témoins qui doivent bouger** (`test_contexte.py`, 107 contrôles) : un VMID altéré dans la
face est repris par le devis des pools ; des pairs retirés de la face retirent le tunnel du
plan (1 pair, puis 0).
**Validation** : `make verifier` conforme, 92/92.
**Reste de la matérialisation** : les outils du poste qui agissent sur le locataire monté
(placement, configuration Proxmox, clonage, rasage) lisent son inventaire. Décider qui
matérialise — le poste sur l'inventaire du locataire, ou le site sur sa face — est une
question pour l'exploitant.
## 2026-10-05 (95) — Une régression publiée : le devis des pools vide, et une comparaison qui ne testait rien
**Ce qui s'est passé.** (94) a fait passer la découverte des locataires par la face réseau, qui

View file

@ -449,8 +449,10 @@ class Locataire(Ecosysteme):
ips = _ip_par_hote(data) if data else {}
groupes = {g: sorted(h for h in _hotes_du_groupe(data, g) if h in ips)
for g in sorted(_enfants(data)) if est_groupe_operationnel(g)} if data else {}
actifs = set(_hotes_du_groupe(data, "hotes_actifs")) if data else set()
machines = {
h: {"ip": ip,
"etat": "actif" if h in actifs else "planifie",
"groupes": sorted(g for g, membres in groupes.items() if h in membres),
"materialisation": {k: v for k, v in sorted((hv.get(h) or {}).items())
if k.startswith("proxmox_")}}
@ -551,8 +553,10 @@ class Locataire(Ecosysteme):
"administration": {"intrant": intrant, "tunnel": tunnel_admin_de(self.depot),
"port_tunnel": (port_vpn_locataire(self.index)
if tunnel_admin_de(self.depot) else None),
"pairs": {n: {k: c.get(k) for k in ("cle_publique", "adresse", "etat")}
for n, c in sorted(pairs.items()) if isinstance(c, dict)}},
# Les pairs ENTIERS : `vpn_admin` les reprend tels quels. Des cles
# PUBLIQUES et des adresses — rien de secret.
"pairs": {n: dict(c) for n, c in sorted(pairs.items())
if isinstance(c, dict)}},
"dns_public": dns_public,
"sauvegarde": {"cle_publique": cle_sauvegarde},
"flux": flux,

View file

@ -59,6 +59,19 @@ def _plan_de(nom_instance: str) -> dict:
return charger_serveurs(p).get("serveurs") or {}
def _face_publiee(nom: str) -> dict | None:
"""La face reseau que ce locataire a publiee, ou None (et on le dit)."""
try:
import contexte
face = contexte.Locataire(DOSSIER_INSTANCES / nom).face_publiee()
except Exception:
face = None
if face is None:
print(f"note : {nom} n'a pas publie sa face reseau — lecture de son plan (transition).",
file=sys.stderr)
return face
def pool_actif() -> str:
"""Pool du tenant ACTIF — ce que le playbook de clonage passe a la creation.
@ -82,6 +95,19 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
for nom_instance, _prefixe, nomenclature in tenants:
index = int(nomenclature["index"])
membres, sans_vmid = [], []
# LA FACE RESEAU D'ABORD (2026-10-05, docs/conception-contextes.md §6, etape 3) : le
# locataire publie ses machines, leur VMID et leur etat ; le devis ne lit plus son plan.
face = _face_publiee(nom_instance)
if face is not None:
for nom, m in sorted((face.get("machines") or {}).items()):
vmid = (m.get("materialisation") or {}).get("proxmox_vmid")
if vmid is None:
sans_vmid.append(nom)
continue
membres.append({"nom": nom, "vmid": int(vmid), "etat": str(m.get("etat", "planifie"))})
blocs.append({"pool": pool_de(nom_instance, index), "tenant": nom_instance,
"index": index, "membres": membres, "sans_vmid": sans_vmid})
continue
for nom, srv in sorted(_plan_de(nom_instance).items()):
_, seq = fonction_seq(nom)
derive = deriver_nomenclature(str(srv.get("fonction", "")), seq, nomenclature) or {}

View file

@ -598,6 +598,44 @@ def pools_complets() -> None:
f"pool {loc.nom} : {len(b.get('membres') or [])} VM sur {attendu} publiees, aucune sans VMID")
def pools_et_tunnels_lisent_la_face() -> None:
"""Les pools et les tunnels d'administration lisent la face publiee : l'alterer les change."""
import devis_proxmox_pools
import devis_reseau
import vpn_admin
import yaml as _yaml
couples = C.couples_hebergeur()
if not couples:
print("SAUTE aucun site frere : ni pool ni tunnel a confronter")
return
site = couples[0][0]
loc = next((l for _s, l in couples if (l.face_publiee() or {}).get("administration", {}).get("pairs")),
couples[0][1])
chemin = loc.depot / C.NOM_FICHIER_FACE_RESEAU
original = chemin.read_text(encoding="utf-8")
def releve():
with C._monter(site):
pool = next(b for b in devis_proxmox_pools.construire(devis_reseau.decouvrir_du_site())["pools"]
if b["tenant"] == loc.nom)
tun = next((x for x in vpn_admin.tunnels(True) if x.get("portee") == loc.nom), {})
return sorted(m["vmid"] for m in pool["membres"]), sorted(tun.get("pairs") or {})
vmids, pairs = releve()
try:
f = _yaml.safe_load(original)
premier = sorted(f["machines"])[0]
f["machines"][premier]["materialisation"]["proxmox_vmid"] = 999999999
f["administration"]["pairs"] = {}
chemin.write_text(_yaml.safe_dump(f, allow_unicode=True, sort_keys=False), encoding="utf-8")
vmids2, pairs2 = releve()
finally:
chemin.write_text(original, encoding="utf-8")
verifier(999999999 in vmids2 and 999999999 not in vmids, "le devis des pools lit la face publiee (VMID altere repris)")
verifier(bool(pairs) and not pairs2,
f"les tunnels d'administration lisent la face publiee ({len(pairs)} pair(s), puis 0)")
def main() -> int:
with tempfile.TemporaryDirectory() as tmp:
d = Path(tmp)
@ -619,6 +657,7 @@ def main() -> int:
frontiere_lit_la_face()
decouverte_lit_la_face()
pools_complets()
pools_et_tunnels_lisent_la_face()
if ECHECS:
print(f"\n{len(ECHECS)} echec(s).")
return 1

View file

@ -62,8 +62,21 @@ def tunnels(inclure_vides: bool = False) -> list[dict]:
"pairs": {str(p["nom"]): p for p in (site.get("pairs") or [])}})
for nom_depot, _prefixe, _n in devis_reseau.decouvrir_du_site():
base = RACINE.parent / nom_depot
registre = charger_acces(base / "plan" / "acces.yml")
index = charger_nomenclature(base / "plan" / "nomenclature.yml").get("index")
# LA FACE RESEAU D'ABORD (2026-10-05, etape 3) : le locataire publie ses pairs et son
# index ; le site ne lit plus son `plan/acces.yml` ni sa nomenclature.
try:
import contexte
face = contexte.Locataire(base).face_publiee()
except Exception:
face = None
if face is not None:
registre = {"acces_admin_vpn": dict((face.get("administration") or {}).get("pairs") or {})}
index = face.get("index")
else:
print(f"note : {nom_depot} n'a pas publie sa face reseau — lecture de son plan "
f"(transition).", file=sys.stderr)
registre = charger_acces(base / "plan" / "acces.yml")
index = charger_nomenclature(base / "plan" / "nomenclature.yml").get("index")
# UN LOCATAIRE SANS PAIR N'A PAS DE TUNNEL — sauf quand on vient justement lui en
# proposer un (`pair-nouveau`) : refuser la` aurait fait de la premiere cle un
# probleme d'oeuf et de poule, et c'est ce que le premier essai a montre.