D-80 : un tenant est agnostique de son underlay, a trois cles pres

Formulation de l'exploitant, meilleure que celle du depot. Le commentaire disait
« la fabric reste celle de l'hebergeur, quel que soit le tenant actif » — vrai,
mais centre sur l'HEBERGEUR. Le cadrage juste est centre sur le TENANT : les deux
symlinks composent deux axes INDEPENDANTS (instance = quel tenant, underlay.yml =
sur quelle fabric).

Tout l'adressage derive du seed index : le plan se deplace d'une fabric a l'autre
sans y toucher. Ce qui ne se deplace pas tient en TROIS CLES —
proxmox_clone_noeud, _stockage, _pont. Elles vivent cote tenant (c'est lui qui
choisit ou se poser) mais nomment des objets de l'hebergeur. Trois, pas trente :
c'est ce qui separe « portable » de « theoriquement portable ».

P37 confronte ces trois noms aux listes de proxmox-hebergeur.yml, trouve par
derivation du symlink underlay.yml. Un nom absent est un ecart STATIQUE (D-75) —
sans quoi une faute de frappe ne se decouvre qu'au premier clone, apres quarante
minutes. Eprouvee en negatif sur les trois cles.

Non verifie et dit comme tel : le gabarit (_vmid_modele) est un objet du cluster,
pas une liste declaree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-13 09:47:38 -04:00
parent 671fa1fc60
commit 199783f5fd
4 changed files with 102 additions and 1 deletions

View file

@ -1,5 +1,42 @@
# CHANGELOG — Set-OPS
## 2026-08-13 — D-80 : un tenant est agnostique de son underlay, à trois clés près
Formulation de l'exploitant, meilleure que celle du dépôt. Le commentaire disait *« la
fabric reste celle de l'hébergeur, quel que soit le tenant actif »* — vrai, mais centré
sur l'hébergeur. Le cadrage juste est centré sur le **tenant** : les deux symlinks
composent **deux axes indépendants**.
```
instance -> quel tenant
underlay.yml -> sur quelle fabric
```
Tout l'adressage d'un tenant dérive du seed `index` : son plan se déplace d'une fabric à
l'autre **sans y toucher**. Ce qui ne se déplace pas tient en **trois clés** :
```
proxmox_clone_noeud proxmox_clone_stockage proxmox_clone_pont
```
Elles vivent côté tenant — c'est lui qui choisit où se poser — mais elles **nomment des
objets de l'hébergeur**. Trois, pas trente : c'est ce qui sépare « portable » de
« théoriquement portable ».
### P37 — le placement est confronté à ce que l'hébergeur offre
L'écart est entièrement lisible : le tenant déclare trois noms, l'hébergeur déclare ses
listes dans `proxmox-hebergeur.yml`, trouvé par dérivation du symlink `underlay.yml`. Un
nom absent est un écart **statique** (D-75). Sans cette preuve, une faute de frappe ne se
découvre qu'au premier clone — après quarante minutes de déploiement.
Éprouvée en négatif sur les trois clés : un nœud, un stockage et un pont inexistants sont
refusés, avec la liste de ce qui est réellement offert.
**Non vérifié, et dit comme tel** : le gabarit (`_vmid_modele`) est un objet du cluster,
pas une liste déclarée. Seul le cluster peut dire s'il existe.
## 2026-08-13 — Reconstruction **d'un trait** : 43 groupes, 0 échec, 37 minutes
```

View file

@ -7,7 +7,7 @@
> [`docs/audit/affirmations.md`](affirmations.md).
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
- **Verdict** : ✅ CONFORME (36 OK · 0 echec · 0 saute)
- **Verdict** : ✅ CONFORME (37 OK · 0 echec · 0 saute)
## Preuves
@ -49,6 +49,7 @@
| P34 | Chaque document declare son lecteur | — | ✅ OK | 39 document(s) declarent leur lecteur (16 genere(s) exempte(s)). |
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). |
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (OPS-Chezlepro) : noeud, stockage, pont — tous offerts. |
## Couverture des affirmations ✅ du registre

View file

@ -119,6 +119,7 @@ sont les seules vérifiables.
| **D-77** | **L'underlay d'un site dérive du même `index` que son tenant**, dans la **bande basse** `10.<index>.015.x` ; les réseaux de **stockage** en sortent et sont **identiques partout**, en `192.168.<vlan>.0/24` | tant qu'il n'existait qu'un site, `10.0.x.x` suffisait. Deux sites qui doivent se joindre — reprise mutuelle, sauvegardes croisées, exploitation à distance — **ne peuvent pas** porter les mêmes plages : chaque routeur croit que le réseau est chez lui. Un « numéro de site » a d'abord été proposé : **rejeté**, c'était un SECOND SEED à tenir et à synchroniser, alors que tout dérive déjà d'`index` (D-17). La bande basse est sûre **par la règle, pas par chance** : les zones valent `10.<index>.(15+categorie).0/24`, donc troisième octet ≥ 16 — les octets 0 à 15 ne sont jamais alloués. Les troisièmes et derniers octets de l'underlay actuel sont ainsi **préservés** : seul le deuxième change, et l'invariant `.1` (D-04) survit intact. Le **stockage** en est sorti parce qu'il est jumbo, non routé, et ne quitte jamais son site : il n'a aucun besoin d'être unique, et l'aligner sur le VLAN (`192.168.20.x` ↔ VLAN 20) fait dire son VLAN à l'adresse. Contrepartie assumée : ces réseaux ne pourront jamais traverser un lien inter-sites — sans conséquence, puisque ce qui voyage entre deux sites est l'**état**, par le dépôt de sauvegarde, pas le stockage bloc | `underlay.yml` (`index`), `scripts/underlay.py`, `docs/preparer-un-site-hebergeur.md` | **P23** |
| **D-80** | **Un tenant est agnostique de son underlay.** Les deux symlinks composent deux axes INDEPENDANTS : `instance` designe le tenant, `underlay.yml` la fabric qui l'accueille. Seule exception : une **liaison de placement** de trois cles | le commentaire disait « la fabric reste celle de l'hebergeur, quel que soit le tenant actif » — vrai, mais centre sur l'hebergeur. Le cadrage juste est centre sur le TENANT : tout son adressage derive du seed `index`, donc son plan se deplace d'une fabric a l'autre **sans y toucher**. Ce qui ne se deplace pas, c'est le **placement**`proxmox_clone_noeud`, `_stockage`, `_pont` — qui vit cote tenant parce que c'est lui qui choisit ou se poser, mais qui NOMME des objets de l'hebergeur. Trois cles, pas trente : c'est ce qui separe « portable » de « theoriquement portable ». Le gabarit (`_vmid_modele`) s'y ajoute, mais il n'est pas verifiable statiquement — c'est un objet du cluster, pas une liste declaree | `underlay.py`, `inventories/*/group_vars/proxmox.yml`, `proxmox-hebergeur.yml` | **P37** |
| **D-79** | Le filtrage que l'hyperviseur applique aux VM tourne encore sur **iptables *legacy***. Le passer à `proxmox-firewall` (nftables natif) est une **dette à rembourser**, pas une option — et c'est le même geste qui rend le **pare-feu de VNet** opérant. Décidée le 2026-08-12, **différée après la reconstruction** | mesuré sur `asgard` : `iptables v1.8.9 (legacy)` — pas `(nf_tables)` ; `nftables v1.0.6` installé mais inutilisé par Proxmox ; `proxmox-firewall 0.7.1` **installé et absent des services en cours** ; seul `pve-firewall 5.1.3` tourne. Le pare-feu de VNet est **entièrement expressif** (`type/action/proto/source/dest/dport`, et `policy_forward``ACCEPT,DROP` — donc un vrai default-deny inter-zone) : une règle d'essai a été créée, relue, puis retirée. **Mais il n'est implémenté QUE par le moteur nftables** : posée aujourd'hui, une règle serait acceptée, stockée, visible dans l'interface, et n'appliquerait rien. Trois gains dans le même geste — le moteur rejoint la doctrine (« tout est nftables »), le pare-feu de VNet devient réel, et ses règles **survivent à la reconstruction** là où les groupes par VM doivent être ré-attachés à chaque cycle | `devis_proxmox_fw.py`, `docs/sdn-evpn.md` | P25 *(sur le mécanisme actuel)* |
> **Ce qui n'a PAS été établi, et qu'il faudra éprouver.** `nft list tables` exige root et le

View file

@ -524,6 +524,66 @@ def preuve_diff_vide_toutes_instances() -> tuple[bool, str]:
f"{', '.join(verifiees)} : plan et inventaire applique coincident.")
def preuve_placement_chez_hebergeur() -> tuple[bool, str]:
"""Le placement d'un tenant designe des objets que son hebergeur offre REELLEMENT.
D-80 : un tenant est agnostique de l'underlay — tout son adressage derive du seed.
Il porte pourtant une LIAISON DE PLACEMENT vers l'hebergeur qui l'accueille : sur
quel noeud cloner, sur quel stockage poser le disque, sur quel pont brancher la
carte. Trois valeurs, cote tenant parce que c'est lui qui choisit ou se poser, mais
qui NOMMENT des objets de l'hebergeur.
C'est donc la seule surface qui casse quand un tenant change de fabric — et elle est
entierement lisible : le tenant declare trois noms, l'hebergeur declare ses listes
(`proxmox-hebergeur.yml`, trouve par derivation du symlink `underlay.yml`). Un nom
absent est un ecart STATIQUE. Sans cette preuve, une faute de frappe ne se decouvre
qu'au premier clone — apres quarante minutes de deploiement (D-75).
CE QU'ELLE NE VERIFIE PAS : le GABARIT (`proxmox_clone_vmid_modele`). C'est un objet
du cluster, pas une liste declaree par l'hebergeur : seul le cluster peut dire s'il
existe. `make hote-afficher` et le clone lui-meme s'en chargent.
"""
import yaml as _y
sys.path.insert(0, str(RACINE / "scripts"))
import underlay as underlay_mod
c = underlay_mod.chemin()
if c is None:
return True, "Aucun underlay monte : aucun hebergeur a confronter."
heb = c.resolve().parent / "proxmox-hebergeur.yml"
if not heb.is_file():
return True, f"Aucun {heb.name} chez l'hebergeur monte : rien a confronter."
offert = _y.safe_load(heb.read_text(encoding="utf-8")) or {}
tenant_f = RACINE / INV
tenant_f = tenant_f.parent / "group_vars" / "proxmox.yml"
if not tenant_f.is_file():
return True, "Le tenant monte ne declare aucun placement Proxmox."
place = _y.safe_load(tenant_f.read_text(encoding="utf-8")) or {}
controles = (
("proxmox_clone_noeud", "proxmox_noeuds", "noeud"),
("proxmox_clone_stockage", "proxmox_stockages", "stockage"),
("proxmox_clone_pont", "proxmox_ponts", "pont"),
)
manques, verifies = [], []
for cle_t, cle_h, libelle in controles:
voulu = place.get(cle_t)
dispo = offert.get(cle_h) or []
if voulu is None or not dispo:
continue
verifies.append(libelle)
if str(voulu) not in [str(x) for x in dispo]:
manques.append(f"{libelle} « {voulu} » absent de {cle_h} "
f"({', '.join(str(x) for x in dispo)})")
if manques:
return False, (f"le placement du tenant designe {len(manques)} objet(s) que "
f"l'hebergeur monte n'offre pas : " + " | ".join(manques)
+ " — corriger le placement, ou monter le bon `underlay.yml`.")
return True, (f"placement confronte a l'hebergeur monte ({heb.parent.name}) : "
f"{', '.join(verifies)} — tous offerts.")
def preuve_etat_sauvegarde() -> tuple[bool, str]:
"""Tout hote qui detient de l'etat non regenerable porte `client_backup`.
@ -826,6 +886,8 @@ PREUVES: list[dict] = [
"func": preuve_base_par_consommateur},
{"id": "P36", "titre": "Tout detenteur d'etat porte une sauvegarde", "refs": [],
"func": preuve_etat_sauvegarde},
{"id": "P37", "titre": "Le placement du tenant existe chez son hebergeur", "refs": [],
"func": preuve_placement_chez_hebergeur},
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
"cmds": [[sys.executable, "scripts/verifier_ports.py"]]},
]