contexte : etape 3, le site decouvre ses locataires par leur face reseau

decouvrir_du_site part de underlay.tenants et prend index et zones dans
la face publiee. Les sept sorties du site sont identiques a HEAD, a
l'octet pres ; frontiere-plan ne voit rien a faire.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-10-05 13:10:39 -04:00
parent 013c1b5279
commit d0a6002b87
5 changed files with 97 additions and 7 deletions

View file

@ -1,5 +1,36 @@
# CHANGELOG — Set-OPS
## 2026-10-05 (94) — Étape 3 : le site découvre ses locataires par leur face réseau
**Le chemin** (`docs/conception-contextes.md` §6), étape 3. La découverte des locataires
(`devis_reseau.decouvrir_du_site`) alimente tous les devis du site : frontière, Proxmox, SDN,
pools, commutateur, et l'inventaire du site. Elle lisait la nomenclature de chaque locataire.
**Fait** :
- relevé de ce que le site lit d'une nomenclature : **cinq champs** (`index`, `categories`,
`reservations`, `cidr_hote`, `federe`) ; la face réseau les publie (`nomenclature`) ;
- `decouvrir_du_site` part des noms que le site déclare (`underlay.tenants`) et prend index
et zones dans la face publiée de chacun. Ordre, filtre `federe`, avertissement pour un nom
sans dépôt et refus d'un site sans locataire inchangés. Sans face, la nomenclature, et elle
le dit ;
- faces republiées chez les deux locataires.
**Le critère** : les sept sorties des consommateurs du site sont **identiques octet pour
octet** à celles de la version de HEAD, lancée à part : commutateur (8,8 Ko), frontière
(167 Ko), Proxmox (63 Ko), SDN (ses deux formats), pools, inventaire du site (81 Ko).
`make frontiere-plan` contre la frontière réelle : rien à faire.
**Ce qui lit encore la nomenclature des locataires**, nommé : la découverte de la
**fédération** (`decouvrir`), qui sert sur le poste à garder les collisions d'index (P21) et à
lister les instances — légitime, ce n'est pas le site — et que `decouvrir_du_site` appelle
encore pour son repli ; `vpn_admin.py` (index pour le tunnel) ; `devis_proxmox_pools`
(nommer les VM, avec le plan). Ils partiront avec la matérialisation et la fin du repli.
**Éprouvé** :
- `test_contexte.py`, 103 contrôles : un index altéré dans la face publiée est celui que le
site découvre (17, puis 99) — la découverte lit bien la face.
- `make verifier` conforme, 92/92.
## 2026-10-05 (93) — Étape 3 : la frontière lit la face réseau
**Le chemin** (`docs/conception-contextes.md` §6), étape 3. Quatrième consommateur du site, le

View file

@ -182,8 +182,8 @@ les publie désormais (`sources_declarees`). La frontière, en trois temps : les
(P87), les entrées publiques (P88), l'administration (P89) et les sorties (P90) sont faites.
**L'étape 2 est terminée** (2026-10-05). Étape 3 : l'instancier lit la fiche déposée par le site, et
l'inventaire d'un locataire se génère sans son site, à l'octet près (P91). Le locataire publie sa face
réseau (P92) ; les comptes de sauvegarde, le DNS public, le pare-feu Proxmox et la frontière du site
la lisent.
réseau (P92) ; les comptes de sauvegarde, le DNS public, le pare-feu Proxmox, la frontière et la
découverte des locataires du site la lisent.
Méthodes du contrat : `site.fiche_pour(locataire)`, `locataire.face_reseau()`. Une classe
n'ouvre jamais les fichiers de l'autre ; une preuve vérifiera la règle.

View file

@ -540,6 +540,11 @@ class Locataire(Ecosysteme):
"sources": sources,
"index": self.index,
"zones": zones,
# Ce que le site lit de sa nomenclature (decouverte, frontiere, SDN, commutateur) :
# ces cinq champs, et rien d'autre (releve du 2026-10-05).
"nomenclature": {k: self.nomenclature()[k] for k in
("index", "categories", "reservations", "cidr_hote", "federe")
if k in self.nomenclature()},
"machines": machines,
"groupes": groupes,
"administration": {"intrant": intrant, "tunnel": tunnel_admin_de(self.depot),

View file

@ -619,6 +619,20 @@ def decouvrir() -> list[tuple[str, str, dict]]:
return tenants
def _nomenclature_publiee(nom: str) -> dict | None:
"""Les champs de nomenclature que ce locataire publie dans sa face reseau, ou None."""
try:
import contexte
face = contexte.Locataire(DOSSIER_INSTANCES / nom).face_publiee()
except Exception:
face = None
if face is None or not face.get("nomenclature"):
print(f"note : {nom} n'a pas publie sa nomenclature dans sa face reseau — lecture de "
f"son plan (transition).", file=sys.stderr)
return None
return dict(face["nomenclature"])
def decouvrir_du_site() -> list[tuple[str, str, dict]]:
"""Les tenants que CE SITE heberge — `decouvrir()` restreint par `underlay.tenants`.
@ -645,12 +659,23 @@ def decouvrir_du_site() -> list[tuple[str, str, dict]]:
portes = underlay_mod.tenants_du_site(underlay_mod.charger())
if portes is None:
return tenants
connus = {nom for nom, _pfx, _n in tenants}
# LA FACE RESEAU D'ABORD (2026-10-05, docs/conception-contextes.md §6, etape 3). Les noms
# viennent du site (`underlay.tenants`) ; l'index et les zones, de ce que chaque locataire
# PUBLIE (`face-reseau.yml`, champ `nomenclature`) — plus de sa nomenclature. Sans face
# publiee (transition), sa nomenclature, et on le dit.
par_nom = {nom: (pfx, n) for nom, pfx, n in tenants}
retenus = []
for nom in portes:
if nom not in connus:
print(f"ATTENTION : `tenants` de l'underlay nomme « {nom} », "
f"qu'aucun dossier frere ne fournit.", file=sys.stderr)
retenus = [t for t in tenants if t[0] in set(portes)]
n = _nomenclature_publiee(nom)
if n is None:
if nom not in par_nom:
print(f"ATTENTION : `tenants` de l'underlay nomme « {nom} », "
f"qu'aucun dossier frere ne fournit.", file=sys.stderr)
continue
n = par_nom[nom][1]
if n.get("index") is not None and n.get("categories") and n.get("federe", True):
retenus.append((nom, prefixe(nom), n))
retenus.sort(key=lambda t: t[2]["index"])
if not retenus:
raise SystemExit("Aucun tenant heberge sur ce site : `underlay.tenants` n'en "
"retient aucun de connu.")

View file

@ -551,6 +551,34 @@ def frontiere_lit_la_face() -> None:
f"la frontiere lit la face publiee : {g} {port}/tcp sur le WAN, {avant} regle(s), 0 une fois le verdict bascule")
def decouverte_lit_la_face() -> None:
"""La decouverte du site lit l'index que le locataire publie, pas sa nomenclature."""
import devis_reseau
import yaml as _yaml
couples = C.couples_hebergeur()
if not couples:
print("SAUTE aucun site frere : aucune decouverte a confronter")
return
site, loc = couples[0]
chemin = loc.depot / C.NOM_FICHIER_FACE_RESEAU
original = chemin.read_text(encoding="utf-8")
def index_decouvert():
with C._monter(site):
return next((n["index"] for nom, _p, n in devis_reseau.decouvrir_du_site() if nom == loc.nom), None)
avant = index_decouvert()
try:
f = _yaml.safe_load(original)
f["nomenclature"]["index"] = 99
chemin.write_text(_yaml.safe_dump(f, allow_unicode=True, sort_keys=False), encoding="utf-8")
apres = index_decouvert()
finally:
chemin.write_text(original, encoding="utf-8")
verifier(avant == loc.index and apres == 99,
f"la decouverte du site lit l'index publie : {avant}, puis 99 une fois la face alteree")
def main() -> int:
with tempfile.TemporaryDirectory() as tmp:
d = Path(tmp)
@ -570,6 +598,7 @@ def main() -> int:
face_publiee()
proxmox_lit_la_face()
frontiere_lit_la_face()
decouverte_lit_la_face()
if ECHECS:
print(f"\n{len(ECHECS)} echec(s).")
return 1