adressage : le site est une flotte, et P20 le regarde enfin

L'exploitant a demande si l'adressage derive ne valait pas pour toute flotte, site compris.
Oui — decision du 12 septembre, index 37 pour le site. J'avais affirme le contraire a
l'entree (11) du CHANGELOG, en recopiant un commentaire perime de huit jours au lieu de
mesurer. Un commentaire perime se propage : celui-la s'etait aussi loge dans la docstring de
ConsoleSite.

P20 s'appelle « Adressage 100% derive du seed (aucun stocke) » et sa portee etait
« l'instance liee + les modeles » : elle ne regardait jamais le site, qui stockait
sous_reseau et passerelle pour ses sept zones. Elle juge desormais les deux natures d'un
underlay et refuse une entree qui ne declare pas la sienne — on ne peut pas juger ce qu'on
ne sait pas lire. Controle negatif joue : une zone remise a 10.36.31.0/24 est refusee, en
nommant l'index dont elle aurait du descendre.

Le loader derive sous_reseau et passerelle a la lecture, pour que rien ne change chez les
consommateurs. Mesure : 14 valeurs retirees du fichier, zero ecart sur ce qu'ils lisent, et
make underlay, devis-sdn et devis-reseau passent.

Valide : make test a 0 echec, P20 verte sur 2 nomenclatures et 7 zones de site. P02 et P60
restent, pour les raisons deja consignees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-09-20 22:46:43 -04:00
parent cd0f94a50b
commit d461492623
4 changed files with 152 additions and 6 deletions

View file

@ -1,5 +1,62 @@
# CHANGELOG — Set-OPS
## 2026-09-20 (12) — Une décision appliquée à moitié se croit tenue
**83 preuves, `make test` à 0 échec.** L'exploitant a demandé : « n'avions-nous pas décidé
d'un plan d'adressage dérivé pour chaque flotte, qu'elle soit tenant ou site ? » Oui. La
décision est écrite dans l'underlay du site, datée du 12 septembre :
> *« sites et locataires se partagent la classe A, chacun avec SON index. Le site prend
> 37, le locataire garde 17. `make instances` les compte ensemble depuis ce jour. »*
### Ce que l'entrée (11) affirmait, et qui était faux
Elle disait qu'un site *« déclare ses adresses, il n'a aucun seed dont les faire
descendre »*. C'était la recopie d'un commentaire en tête de `SITE-Chezlepro/plan/serveurs.yml`
— **périmé depuis le jour où le site a reçu son index**, et lu au lieu d'être mesuré.
Un commentaire périmé se propage : celui-ci s'est retrouvé le même jour dans la docstring
de `ConsoleSite` et dans une entrée de CHANGELOG. C'est exactement ce que
`CARTE-DU-SITE.md` décrit chez un locataire — **inerte et crédible** : le moteur ne le lit
pas, donc rien ne le corrige ; un humain le lit, et le croit.
### La mesure
Les sept zones du site suivaient `10.<index>.<vlan>.0/24` **à la lettre** — et étaient
pourtant **écrites** : `sous_reseau` et `passerelle` stockés pour chacune. C'est
précisément ce que P20 interdit à un locataire, sous le titre *« Adressage 100 % dérivé du
seed (aucun stocké) »*. Et la portée de P20 était *« l'instance liée + les modèles »* :
**elle ne regardait jamais le site**.
Les valeurs concordaient parce qu'un humain les avait tapées juste, ce jour-là. Rien ne le
garantissait : une zone écrite `10.36.x` serait passée sans un mot.
### Deux natures dans une seule liste
`reseaux:` mélangeait ce qui peut dériver et ce qui ne le peut pas. Chaque entrée déclare
maintenant sa nature :
| `nature: fabric` | grappe, iSCSI, Ceph, transit, VXLAN, management | adressage dicté par les participants d'un lien physique (D-02, D-04) — il reste **écrit** |
| `nature: site` | les sept zones du site | elles **descendent de son index**, et ne stockent plus rien |
Le loader dérive `sous_reseau` et `passerelle` à la lecture, pour que les consommateurs ne
voient aucune différence. **Quatorze valeurs stockées ont disparu du fichier, et ce que
les consommateurs lisent est identique — écart mesuré : zéro.** `make underlay`,
`make devis-sdn` et `make devis-reseau` passent.
P20 juge désormais les deux natures, et refuse une entrée qui ne déclare pas la sienne :
on ne peut pas juger ce qu'on ne sait pas lire. Contrôle négatif joué : une zone remise à
`10.36.31.0/24` est refusée, en nommant l'index dont elle aurait dû descendre.
### Ce qui reste écrit, et qui est une dette nommée
Les **machines** du site gardent leurs `ip`, `vmid` et `noeud` dans son plan. Ce n'est plus
une doctrine — « le site est le terrain, il n'a rien dont faire descendre » — puisque le
seed existe maintenant pour elles aussi. C'est une dette, et la nommer est le minimum :
*un objectif qu'on abandonne sans le dire devient un objectif qu'on croit atteint.*
**P02 et P60 restent**, pour les raisons déjà consignées.
## 2026-09-20 (11) — Deux fonctions qui doivent rendre la même forme finissent par ne plus la rendre
**83 preuves, `make test` à 0 échec, 8 tests de rendu.** La console avait deux

View file

@ -774,9 +774,12 @@ class ConsoleLocataire(Console):
class ConsoleSite(Console):
"""Elle MATERIALISE : cree et detruit des VM, pose le reseau, avec la voute du site.
SON PLAN EST DECLARE, PAS DERIVE. Un site est le terrain : il ecrit `ip`, `vmid` et
`noeud`, parce qu'il n'a aucun seed dont les faire descendre. Son inventaire n'est
pas un fichier mais un SCRIPT.
SES ZONES DERIVENT, SES MACHINES SONT ENCORE ECRITES. Un site a son propre index
depuis le 2026-09-12 — le 37 pour celui-ci — et ses zones en descendent comme celles
d'un locataire descendent du sien (P20 le garde). Ce qui reste ecrit dans son plan,
ce sont les `ip`, `vmid` et `noeud` de ses MACHINES : une dette nommee, pas une
doctrine. La docstring disait le contraire : elle recopiait un commentaire perime de
huit jours au lieu de mesurer. Son inventaire, lui, n'est pas un fichier mais un SCRIPT.
ELLE A UN PLAN, ET ON LE LUI SERT. La charge du site servait cinq registres vides
« pour ne pas fabriquer un faux plan de tenant ». Le site n'a pas de faux plan : il a

View file

@ -946,9 +946,48 @@ def preuve_nomenclature_derivee() -> tuple[bool, str]:
for z, c in (n.get("categories") or {}).items():
for k in interdits_zone & set(c or {}):
fautes.append(f"{rel}: categorie {z}.{k} stocke")
# ── ET LE SITE, qui est une flotte comme une autre (2026-09-20) ────────────────
#
# CETTE PREUVE NE REGARDAIT QUE « l'instance liee + les modeles ». Le site a recu son
# propre index le 2026-09-12 — decision ecrite dans son underlay : « sites et
# locataires se partagent la classe A, chacun avec SON index » — et son adressage a
# continue d'etre ECRIT : `sous_reseau` et `passerelle` pour chacune de ses sept
# zones, exactement ce que les lignes ci-dessus interdisent a un locataire.
#
# Les valeurs concordaient parce qu'un humain les avait tapees juste. Rien ne le
# garantissait : une zone ecrite `10.36.x` serait passee sans un mot. Une decision
# appliquee a moitie est plus dangereuse qu'une decision non prise — on la croit tenue.
#
# LES DEUX NATURES NE SE JUGENT PAS PAREIL. Un lien de fabric PHYSIQUE (grappe, iSCSI,
# Ceph, transit, VXLAN) tient son adressage des participants du lien, pas d'un seed :
# D-02 et D-04 le disent, et il reste ecrit. Seules les zones du site derivent.
import underlay as _u
carte = _u.chemin()
zones_site = 0
if carte:
brut = (yaml.safe_load(carte.read_text(encoding="utf-8")) or {}).get("underlay") or {}
index = brut.get("index")
for reseau in brut.get("reseaux") or []:
nature = reseau.get("nature")
nom = reseau.get("nom", "?")
if nature not in ("site", "fabric"):
fautes.append(f"{carte.name}: {nom} ne declare pas sa nature "
f"(site|fabric) — on ne peut pas juger ce qu'on ne sait pas lire")
continue
if nature != "site":
continue
zones_site += 1
for champ in ("sous_reseau", "passerelle"):
if champ in reseau:
fautes.append(f"{carte.name}: zone {nom}.{champ} stocke alors "
f"qu'elle derive de l'index {index}")
if reseau.get("vlan") is None:
fautes.append(f"{carte.name}: zone {nom} sans vlan — rien dont deriver")
if fautes:
return False, f"{len(fautes)} champ(s) d'adressage stockes : " + " | ".join(fautes[:4])
return True, f"{len(cibles)} nomenclature(s) : adressage 100% derive du seed index."
return True, (f"{len(cibles)} nomenclature(s) et {zones_site} zone(s) de site : "
f"adressage 100% derive du seed index.")
def preuve_pas_lab_code_en_dur() -> tuple[bool, str]:

View file

@ -88,13 +88,60 @@ def chemin() -> Path | None:
return p if p.exists() else None
# --- LES ZONES DU SITE DERIVENT DE SON INDEX, COMME CELLES DE N'IMPORTE QUELLE FLOTTE ---
#
# LA DECISION EST DU 2026-09-12, SON APPLICATION DU 2026-09-20. Ce jour-la, l'exploitant a
# tranche : « sites et locataires se partagent la classe A, chacun avec SON index ». Le
# site a pris 37. Mais son `underlay.yml` a continue d'ECRIRE `sous_reseau` et
# `passerelle` pour chacune de ses sept zones — exactement ce que P20 interdit a un
# locataire, sous le titre « Adressage 100% derive du seed (aucun stocke) ».
#
# Les valeurs concordaient, parce qu'un humain les avait tapees juste ce jour-la. Rien ne
# le garantissait : une zone ecrite `10.36.x` serait passee sans un mot, P20 ne regardant
# que « l'instance liee + les modeles ». Une decision appliquee a moitie est plus
# dangereuse qu'une decision non prise : on la croit tenue.
#
# DEUX NATURES DANS UNE SEULE LISTE, et c'est ce qui brouillait la lecture :
#
# nature: fabric les liens PHYSIQUES — grappe, iSCSI, Ceph, transit, VXLAN. Leur
# adressage est dicte par les participants du lien (D-02, D-04) : il
# ne descend d'aucun seed, et il reste donc ECRIT.
# nature: site les zones du site lui-meme. Elles descendent de son index, comme
# celles d'un tenant descendent du sien.
#
# CE QUI SE DERIVE, ET RIEN D'AUTRE : `sous_reseau` = `10.<index>.<vlan>.0/24` et
# `passerelle` = `.1` de ce reseau. Les deux valeurs etaient deja deductibles de deux
# champs presents — c'est le propre d'une valeur stockee pour rien.
def _resoudre_reseaux(underlay: dict | None) -> dict | None:
"""Rend l'underlay avec ses zones de site RESOLUES. Idempotent.
Une zone qui porte deja ses valeurs les garde : la garde (P20) refusera qu'elle les
porte, mais ce module ne doit rien casser sur un site pas encore converti.
"""
if not underlay:
return underlay
index = underlay.get("index")
if index is None:
return underlay
for reseau in underlay.get("reseaux") or []:
if reseau.get("nature") != "site":
continue
vlan = reseau.get("vlan")
if vlan is None:
continue
reseau.setdefault("sous_reseau", f"10.{index}.{vlan}.0/24")
reseau.setdefault("passerelle", f"10.{index}.{vlan}.1")
return underlay
def charger() -> dict | None:
"""Le bloc `underlay` du fichier, ou None si absent/vide."""
"""Le bloc `underlay` du fichier, RESOLU, ou None si absent/vide."""
c = chemin()
if not c:
return None
data = yaml.safe_load(c.read_text(encoding="utf-8")) or {}
return data.get("underlay") or None
return _resoudre_reseaux(data.get("underlay") or None)
# --- LE PLAN DU SITE, a cote de sa carte ---------------------------------------