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:
parent
2e9b31bef7
commit
40af2d1ec6
5 changed files with 116 additions and 4 deletions
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue