diff --git a/CHANGELOG.md b/CHANGELOG.md index 89ee50a..7105df7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ``` diff --git a/docs/audit/preuve-2026-08-13.md b/docs/audit/preuve-2026-08-13.md index 03a49e0..472db39 100644 --- a/docs/audit/preuve-2026-08-13.md +++ b/docs/audit/preuve-2026-08-13.md @@ -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 diff --git a/docs/decisions-architecture.md b/docs/decisions-architecture.md index 57c4fd5..35dcc0f 100644 --- a/docs/decisions-architecture.md +++ b/docs/decisions-architecture.md @@ -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..0–15.x` ; les réseaux de **stockage** en sortent et sont **identiques partout**, en `192.168..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..(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 diff --git a/scripts/prouver.py b/scripts/prouver.py index 644e24e..443a1a4 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -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"]]}, ]