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
|
# 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
|
## 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
|
**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 {}
|
ips = _ip_par_hote(data) if data else {}
|
||||||
groupes = {g: sorted(h for h in _hotes_du_groupe(data, g) if h in ips)
|
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 {}
|
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 = {
|
machines = {
|
||||||
h: {"ip": ip,
|
h: {"ip": ip,
|
||||||
|
"etat": "actif" if h in actifs else "planifie",
|
||||||
"groupes": sorted(g for g, membres in groupes.items() if h in membres),
|
"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())
|
"materialisation": {k: v for k, v in sorted((hv.get(h) or {}).items())
|
||||||
if k.startswith("proxmox_")}}
|
if k.startswith("proxmox_")}}
|
||||||
|
|
@ -551,8 +553,10 @@ class Locataire(Ecosysteme):
|
||||||
"administration": {"intrant": intrant, "tunnel": tunnel_admin_de(self.depot),
|
"administration": {"intrant": intrant, "tunnel": tunnel_admin_de(self.depot),
|
||||||
"port_tunnel": (port_vpn_locataire(self.index)
|
"port_tunnel": (port_vpn_locataire(self.index)
|
||||||
if tunnel_admin_de(self.depot) else None),
|
if tunnel_admin_de(self.depot) else None),
|
||||||
"pairs": {n: {k: c.get(k) for k in ("cle_publique", "adresse", "etat")}
|
# Les pairs ENTIERS : `vpn_admin` les reprend tels quels. Des cles
|
||||||
for n, c in sorted(pairs.items()) if isinstance(c, dict)}},
|
# 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,
|
"dns_public": dns_public,
|
||||||
"sauvegarde": {"cle_publique": cle_sauvegarde},
|
"sauvegarde": {"cle_publique": cle_sauvegarde},
|
||||||
"flux": flux,
|
"flux": flux,
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,19 @@ def _plan_de(nom_instance: str) -> dict:
|
||||||
return charger_serveurs(p).get("serveurs") or {}
|
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:
|
def pool_actif() -> str:
|
||||||
"""Pool du tenant ACTIF — ce que le playbook de clonage passe a la creation.
|
"""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:
|
for nom_instance, _prefixe, nomenclature in tenants:
|
||||||
index = int(nomenclature["index"])
|
index = int(nomenclature["index"])
|
||||||
membres, sans_vmid = [], []
|
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()):
|
for nom, srv in sorted(_plan_de(nom_instance).items()):
|
||||||
_, seq = fonction_seq(nom)
|
_, seq = fonction_seq(nom)
|
||||||
derive = deriver_nomenclature(str(srv.get("fonction", "")), seq, nomenclature) or {}
|
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")
|
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:
|
def main() -> int:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
d = Path(tmp)
|
d = Path(tmp)
|
||||||
|
|
@ -619,6 +657,7 @@ def main() -> int:
|
||||||
frontiere_lit_la_face()
|
frontiere_lit_la_face()
|
||||||
decouverte_lit_la_face()
|
decouverte_lit_la_face()
|
||||||
pools_complets()
|
pools_complets()
|
||||||
|
pools_et_tunnels_lisent_la_face()
|
||||||
if ECHECS:
|
if ECHECS:
|
||||||
print(f"\n{len(ECHECS)} echec(s).")
|
print(f"\n{len(ECHECS)} echec(s).")
|
||||||
return 1
|
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 [])}})
|
"pairs": {str(p["nom"]): p for p in (site.get("pairs") or [])}})
|
||||||
for nom_depot, _prefixe, _n in devis_reseau.decouvrir_du_site():
|
for nom_depot, _prefixe, _n in devis_reseau.decouvrir_du_site():
|
||||||
base = RACINE.parent / nom_depot
|
base = RACINE.parent / nom_depot
|
||||||
registre = charger_acces(base / "plan" / "acces.yml")
|
# LA FACE RESEAU D'ABORD (2026-10-05, etape 3) : le locataire publie ses pairs et son
|
||||||
index = charger_nomenclature(base / "plan" / "nomenclature.yml").get("index")
|
# 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
|
# 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
|
# 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.
|
# probleme d'oeuf et de poule, et c'est ce que le premier essai a montre.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue