From 36a882b125fa94847cd4ae3a9c87b3db273523fa Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Thu, 23 Jul 2026 02:58:15 -0400 Subject: [PATCH] Adressage derive du seul seed index (rupture, mode compact retire) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Principe : les valeurs de configuration se derivent des intrants, elles ne se reecrivent pas a la main. La nomenclature dupliquait ce qu'index determine deja (supernet, sous-reseaux, passerelles, VLAN). Corrige en rupture nette. - inventory_rules : source unique de derivation — supernet_de, base3_de, sous_reseau_de, passerelle_de, vlan_de. Modele 6 zones encode une fois (2e octet = 10+index, 3e octet zone = 15+categorie, VLAN = 1000+index*10+zone). deriver_nomenclature ne lit plus aucun adressage stocke ; mode compact supprime. - devis_reseau : importe ces helpers (fin de la duplication) ; decouvre les tenants sur `index` present (filtre vmid_schema retire). - GUI : `index` devient un INTRANT (section Reseau). Il vit dans la nomenclature (plan reseau uniforme, contrairement aux intrants des modeles heterogenes) et le GUI l'ecrit chirurgicalement (une ligne, sans reformater). Le miroir JS derive le VLAN du seed (fin de la lecture de c.vlan stocke). - socle public : nomenclature au format maigre. Preuve P20 (preuve_nomenclature_derivee) : aucune nomenclature ne stocke d'adressage — garde-fou permanent, teste en negatif. Valide : DIFF VIDE sur les 3 instances (la derivation reproduit exactement l'adressage stocke), 7 modeles valident, devis_reseau genere les memes VLAN (1011-1016 derives), make verifier rc=0 CONFORME 20/20, node --check du GUI OK. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 33 +++++++++ docs/audit/README.md | 3 +- docs/audit/preuve-2026-07-23.md | 56 +++++++++++++++ exemples/modeles/socle/plan/nomenclature.yml | 25 ++++--- scripts/devis_reseau.py | 17 ++--- scripts/inventory_gui.py | 28 +++++++- scripts/inventory_rules.py | 73 ++++++++++++++------ scripts/prouver.py | 37 ++++++++++ 8 files changed, 228 insertions(+), 44 deletions(-) create mode 100644 docs/audit/preuve-2026-07-23.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f3dbeac..9a18d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # CHANGELOG — Set-OPS +## 2026-07-23 + +### Changé — l'adressage se dérive du seul seed `index` (RUPTURE, mode compact retiré) +Principe posé par l'utilisateur : les valeurs de configuration doivent se dériver des +intrants, pas se réécrire à la main. La nomenclature dupliquait ce que `index` détermine +déjà (supernet, sous-réseaux, passerelles, VLAN). C'est corrigé, en rupture nette. + +- **`inventory_rules`** : nouvelles fonctions de dérivation, **source unique** — + `supernet_de`, `base3_de`, `sous_reseau_de`, `passerelle_de`, `vlan_de`. Le modèle à + 6 zones est encodé une fois : 2ᵉ octet = `10+index`, 3ᵉ octet de zone = `15+catégorie`, + VLAN trunk = `1000+index×10+zone`. `deriver_nomenclature` ne lit plus **aucun** adressage + stocké ; le mode `compact` est supprimé (tout est ip-miroir dérivé). +- **`devis_reseau`** importe ces helpers (plus de duplication) ; découverte des tenants sur + `index` présent (le filtre `vmid_schema` disparaît). +- **Les 10 nomenclatures** (3 instances + 7 modèles) passent au **format maigre** : `index`, + `cidr_hote`, `reservations`, libellés de zones et `fonctions` seulement. Supprimés : + `supernet`, `vmid_schema`, et par zone `sous_reseau`/`passerelle`/`vlan`. `presence-web`, + encore en `compact`, gagne `index: 1`. +- **GUI** : `index` devient un **intrant** (section « Réseau » du panneau Intrants). Il vit + dans la nomenclature (le plan réseau, uniforme partout — contrairement aux intrants des + modèles, hétérogènes) et le GUI l'écrit **chirurgicalement** (une ligne, sans reformater + le fichier). Le miroir JS dérive le VLAN du seed (fin de la lecture de `c.vlan` stocké). + +### Ajouté +- **Preuve P20** (`preuve_nomenclature_derivee`) : aucune nomenclature ne stocke d'adressage + — garde-fou permanent contre une rechute vers l'écriture manuelle. Testée en négatif + (une rechute simulée est bien rejetée). + +Validé : **DIFF VIDE sur les 3 instances** (la dérivation reproduit exactement l'adressage +qui était stocké), les **7 modèles valident**, `devis_reseau` génère les mêmes VLAN +(1011-1016 dérivés), `make verifier` rc=0 **CONFORME 20/20**, `node --check` du GUI OK, +aller-retour d'écriture de `index` : une seule ligne touchée. + ## 2026-07-22 (suite) ### Ajouté — trois preuves qui ferment les angles morts du harnais diff --git a/docs/audit/README.md b/docs/audit/README.md index d5b7a3a..01bfea2 100644 --- a/docs/audit/README.md +++ b/docs/audit/README.md @@ -56,7 +56,8 @@ make prouver | P16 | Inventaire Ansible (voûte) | `ansible-inventory --list` — sauté sans mot de passe Vault. | | P17 | **Tous** les modèles d'instance | chaque modèle découvert valide (pas seulement `socle`). `SETOPS_MODELES=../Set-OPS-Modeles` inclut les modèles assemblés privés. | | P18 | Gabarit de voûte complet | `vault.yml.example` couvre **exactement** les secrets que le plan exige (rôles actifs + bases + group_vars). | -| P19 | GUI couvre le plan | tout champ présent dans un plan réel est éditable par le GUI (nomenclature tolérée : trou connu). | +| P19 | GUI couvre le plan | tout champ présent dans un plan réel est éditable par le GUI (nomenclature tolérée : le seed `index` est désormais un intrant ; l'adressage est dérivé, donc rien à éditer). | +| P20 | Adressage dérivé du seed | aucune nomenclature ne **stocke** d'adressage (supernet, sous-réseau, passerelle, VLAN) : tout se dérive du seul `index`. Attrape toute rechute vers l'écriture manuelle. | > **P17, P18, P19 ferment les angles morts du harnais** : il ne vérifiait qu'*une* instance > et le seul modèle `socle`. P17 aurait attrapé l'hôte fantôme d'`integral` ; P18, les neuf diff --git a/docs/audit/preuve-2026-07-23.md b/docs/audit/preuve-2026-07-23.md new file mode 100644 index 0000000..76c31da --- /dev/null +++ b/docs/audit/preuve-2026-07-23.md @@ -0,0 +1,56 @@ +# Preuve de conformite — Set-OPS — 2026-07-23 + +> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer +> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ; +> aucune validation n'est reimplementee ici. Voir le mode d'emploi : +> [`docs/audit/README.md`](README.md), et le registre trace : +> [`docs/audit/affirmations.md`](affirmations.md). + +- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml` +- **Verdict** : ✅ CONFORME (20 OK · 0 echec · 0 saute) + +## Preuves + +| # | Preuve | Affirmations | Statut | Detail | +|---|---|---|---|---| +| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | | +| P02 | Tests unitaires (inventory_host) | — | ✅ OK | 4 tests passes. | +| P03 | Diff-vide du plan (inventaire genere) | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | DIFF VIDE : le plan reproduit exactement l'inventaire actuel. Bascule possible. | +| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | | +| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | | +| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. | +| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check). | +| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 30 groupes classes, aucun cycle, aucune arete en arriere. | +| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 29 rôles, 63 flux, schéma + matrice OK. | +| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). | +| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | playbook: playbooks/proxmox/cloner_vm_debian.yml | +| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. | +| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. | +| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. | +| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. | +| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 14 hotes, 31 groupes (inventaire dechiffre et parse). | +| P17 | Tous les modeles d'instance valident | AFF-022, AFF-099 | ✅ OK | Les 7 modele(s) decouvert(s) valident. | +| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 23 secret(s) exige(s), tous presents. | +| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 27 champ(s) des plans reels sont editables (8 plan(s) inspecte(s)), registres toleres : nomenclature. | +| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 8 nomenclature(s) : adressage 100% derive du seed index. | + +## Couverture des affirmations ✅ du registre + +Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus. +Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005 +`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite +d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ; +elles restent hors du harnais recurrent (rien d'executable a rejouer). + +## Declarations d'intention (⚪ invérifiables localement — assumees) + +Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees** +comme declarations d'intention, non comme preuves : + +- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement + contre une flotte vivante. +- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles. +- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee. +- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume. + +_Rapport genere le 2026-07-23._ diff --git a/exemples/modeles/socle/plan/nomenclature.yml b/exemples/modeles/socle/plan/nomenclature.yml index db4f25e..3ebef10 100644 --- a/exemples/modeles/socle/plan/nomenclature.yml +++ b/exemples/modeles/socle/plan/nomenclature.yml @@ -1,17 +1,22 @@ --- -# MODÈLE « socle » — infrastructure de base (4 VM) : edge, DNS, PKI, magasin mail. -# Zones/VLAN alignés sur integral (un socle peut grandir vers un écosystème complet). -# index : ⚠️ REMPLACER par l'index unique du tenant (préfixe VMID + supernet 10.(10+index)). +# Nomenclature = MODELE de l'ecosysteme (zones + placement des fonctions). +# L'ADRESSAGE (supernet, sous-reseaux, passerelles, VLAN, VMID) N'EST PAS ecrit ici : +# il se DERIVE du seul seed `index` (scripts/inventory_rules : supernet_de, base3_de, +# passerelle_de, vlan_de). Modele a 6 zones : 10.(10+index).(15+zone).0/24, VLAN +# 1000+index*10+zone. Editer `index` via le panneau Intrants du GUI. index: 1 -supernet: 10.11.0.0/16 -vmid_schema: ip-miroir cidr_hote: 24 -reservations: { passerelle: 1, reserve_min: 2, reserve_max: 9 } +reservations: + passerelle: 1 + reserve_min: 2 + reserve_max: 9 +# Zones de securite (un /24 + VLAN chacune) — seuls les libelles sont du modele. categories: - 1: { libelle: Frontiere, vlan: 16, sous_reseau: 10.11.16.0/24, passerelle: 10.11.16.1 } - 4: { libelle: Services-infra, vlan: 19, sous_reseau: 10.11.19.0/24, passerelle: 10.11.19.1 } + 1: { libelle: Frontiere } + 4: { libelle: Services-infra } +# Placement : quelle fonction dans quelle zone (categorie) et son rang (service). fonctions: - infra-edge: { categorie: 1, service: 1 } infra-dns: { categorie: 4, service: 1 } - infra-pki: { categorie: 4, service: 2 } + infra-edge: { categorie: 1, service: 1 } infra-mail: { categorie: 4, service: 3 } + infra-pki: { categorie: 4, service: 2 } diff --git a/scripts/devis_reseau.py b/scripts/devis_reseau.py index 8808d44..079ece2 100644 --- a/scripts/devis_reseau.py +++ b/scripts/devis_reseau.py @@ -20,6 +20,10 @@ import yaml RACINE = Path(__file__).resolve().parent.parent DOSSIER_INSTANCES = RACINE.parent +# Adressage et VLAN : derives du seul index, source unique dans inventory_rules. +sys.path.insert(0, str(RACINE / "scripts")) +from inventory_rules import passerelle_de, supernet_de, vlan_de # noqa: E402 + def masque(cidr: int) -> str: return str(ipaddress.IPv4Network(f"0.0.0.0/{cidr}").netmask) @@ -40,17 +44,14 @@ def decouvrir() -> list[tuple[str, str, dict]]: tenants = [] for chemin in sorted(DOSSIER_INSTANCES.glob("*/plan/nomenclature.yml")): n = yaml.safe_load(chemin.read_text(encoding="utf-8")) or {} - if n.get("vmid_schema") == "ip-miroir" and n.get("index") is not None: + # Un tenant federe = une nomenclature avec un index (l'adressage en decoule). + if n.get("index") is not None and n.get("categories"): nom = chemin.parent.parent.name tenants.append((nom, prefixe(nom), n)) tenants.sort(key=lambda t: t[2]["index"]) return tenants -def vlan_de(index: int, zone: int) -> int: - return 1000 + index * 10 + zone - - def generer(tenants: list[tuple[str, str, dict]]) -> str: out = ["configure terminal", "!"] out += [ @@ -75,19 +76,19 @@ def generer(tenants: list[tuple[str, str, dict]]) -> str: c = n["categories"][zone] out.append(f"interface Vlan{vlan_de(n['index'], zone)}") out.append(f" description {nom}-{c['libelle']}") - out.append(f" ip address {c['passerelle']} {m}") + out.append(f" ip address {passerelle_de(n['index'], zone)} {m}") out.append(f" ip access-group {pfx}-ISOLATION in") out.append(" no shutdown") out += ["!", "! ----- 3. ACL d'isolation tenant (default-deny inter-tenant) -----"] for nom, pfx, n in tenants: - reseau, wild = reseau_wildcard(n["supernet"]) + reseau, wild = reseau_wildcard(supernet_de(n["index"])) out.append(f"ip access-list extended {pfx}-ISOLATION") out.append(f" remark Intra-tenant {nom} : routage local autorise") out.append(f" permit ip {reseau} {wild} {reseau} {wild}") for autre_nom, _, autre in tenants: if autre_nom == nom: continue - a_reseau, a_wild = reseau_wildcard(autre["supernet"]) + a_reseau, a_wild = reseau_wildcard(supernet_de(autre["index"])) out.append(f" remark Bloquer le tenant {autre_nom}") out.append(f" deny ip {reseau} {wild} {a_reseau} {a_wild}") out.append(" remark Reste (Internet / inter-tenant controle) -> passerelle OPNsense") diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py index 39afa71..3fd62dd 100644 --- a/scripts/inventory_gui.py +++ b/scripts/inventory_gui.py @@ -82,10 +82,15 @@ FICHIER_SERVEURS = INSTANCE / "plan/serveurs.yml" # n'y entrent jamais. INTRANTS_IDENTITE = INVENTAIRE_DEFAUT.parent / "group_vars/all/10-intrants.yml" INTRANTS_PROXMOX = INVENTAIRE_MODELE.parent / "group_vars/proxmox.yml" -FICHIERS_INTRANTS = {"identite": INTRANTS_IDENTITE, "proxmox": INTRANTS_PROXMOX} +# Le seed reseau (`index`) vit dans la nomenclature (le plan reseau) — d'ou tout +# l'adressage se derive. Le panneau Intrants le lit/ecrit la, chirurgicalement, sans +# reformater le fichier (cf. _ecrire_index_nomenclature). +FICHIERS_INTRANTS = {"identite": INTRANTS_IDENTITE, "proxmox": INTRANTS_PROXMOX, + "reseau": FICHIER_NOMENCLATURE} # (variable, fichier, classe, section, libelle, type) INTRANTS_SCHEMA = [ + ("index", "reseau", "constante", "Réseau", "Index d'instance dans la fédération (seed : dérive supernet 10.(10+index), VLAN 1000+index×10+zone, VMID)", "int"), ("domaine_interne", "identite", "constante", "Identité", "Domaine DNS interne", "str"), ("fuseau_horaire", "identite", "defaut", "Identité", "Fuseau horaire", "str"), ("organisation", "identite", "defaut", "Identité", "Organisation (annuaire LDAP, certificats)", "str"), @@ -498,6 +503,22 @@ def ecrire_intrants(payload: dict) -> None: INTRANTS_PROXMOX, fusion, "# Parametres Proxmox non sensibles (les secrets vont dans proxmox.vault.yml).\n" "# Cles d'intrants editees par le panneau « Intrants de base » du GUI.\n---\n") + if "reseau" in par_fichier and "index" in par_fichier["reseau"]: + _ecrire_index_nomenclature(int(par_fichier["reseau"]["index"])) + + +def _ecrire_index_nomenclature(index: int) -> None: + """Met a jour `index:` dans plan/nomenclature.yml SANS reformater le reste. + + Le fichier garde ses commentaires et son formatage compact (l'adressage se derive + de ce seul seed). Remplacement de la ligne existante, ou insertion en tete a defaut. + """ + path = FICHIER_NOMENCLATURE + texte = path.read_text(encoding="utf-8") if path.exists() else "---\n" + nouveau, remplace = re.subn(r"(?m)^index:.*$", f"index: {index}", texte) + if not remplace: + nouveau = re.sub(r"(?m)^(---\n)", rf"\g<1>index: {index}\n", texte, count=1) or texte + path.write_text(nouveau, encoding="utf-8") def ecrire_bases(path: Path, registre: dict) -> None: @@ -1952,7 +1973,10 @@ HTML = r""" if (!d) return null; const c = (nomenclature.categories || {})[d.categorie]; if (!c) return null; - return {categorie: d.categorie, service: d.service, libelle: c.libelle, vlan: c.vlan, sous_reseau: c.sous_reseau, passerelle: c.passerelle}; + // VLAN derive du seed index (miroir de inventory_rules.vlan_de) : 1000 + index*10 + zone. + const idx = (nomenclature.index != null) ? nomenclature.index : 9; + const vlan = 1000 + idx * 10 + Number(d.categorie); + return {categorie: d.categorie, service: d.service, libelle: c.libelle, vlan}; } function echapper(valeur) { return String(valeur).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); diff --git a/scripts/inventory_rules.py b/scripts/inventory_rules.py index ae8f31b..d8b8b3a 100644 --- a/scripts/inventory_rules.py +++ b/scripts/inventory_rules.py @@ -376,10 +376,55 @@ def fonction_seq(nom: str) -> tuple[str, int | None]: return str(nom or ""), None +# --- Derivation de l'ADRESSAGE depuis le seul seed `index` -------------------- +# +# Principe : TOUT l'adressage (supernet, sous-reseaux, passerelles, VLAN, VMID) se +# DERIVE de l'index d'instance + du numero de zone. Rien n'est ecrit a la main dans +# la nomenclature (elle ne garde que le MODELE : libelles de zones + placement des +# fonctions). Source unique, ci-dessous, partagee par la generation d'inventaire et +# le devis reseau (scripts/devis_reseau.py). +# +# Conventions du modele a 6 zones (reference integral) : +# - 2e octet du supernet = OCTET_TENANT + index (index 1 -> 10.11.0.0/16) +# - 3e octet de la zone = OCTET_ZONE + categorie (zone 1 -> .16, dans .16.0/20) +# - VLAN sur le trunk convergE = SOCLE_VLAN + index x 10 + zone (4 chiffres, unique) +# - VMID ip-miroir = VLAN(4) . octet-hote(3) . sequence(2) = 9 chiffres mnemotechniques +OCTET_TENANT = 10 +OCTET_ZONE = 15 +SOCLE_VLAN = 1000 +INDEX_BAC_A_SABLE = 9 # index par defaut d'une nomenclature sans seed (jetable) + + +def supernet_de(index: int) -> str: + """/16 du tenant : 10.(10+index).0.0/16.""" + return f"10.{OCTET_TENANT + int(index)}.0.0/16" + + +def base3_de(index: int, categorie: int) -> str: + """Trois premiers octets d'une zone : 10.(10+index).(15+categorie).""" + return f"10.{OCTET_TENANT + int(index)}.{OCTET_ZONE + int(categorie)}" + + +def sous_reseau_de(index: int, categorie: int, cidr: int = 24) -> str: + """/24 d'une zone : 10.(10+index).(15+categorie).0/24.""" + return f"{base3_de(index, categorie)}.0/{int(cidr)}" + + +def passerelle_de(index: int, categorie: int) -> str: + """Passerelle d'une zone (SVI) : premier hote du /24.""" + return f"{base3_de(index, categorie)}.1" + + +def vlan_de(index: int, zone: int) -> int: + """VLAN 802.1Q sur le trunk convergE : 1000 + index x 10 + zone (unique globalement).""" + return SOCLE_VLAN + int(index) * 10 + int(zone) + + def deriver_nomenclature(fonction: str, seq: int, nomenclature: dict) -> dict | None: """Derive VMID / VLAN / IP / passerelle / CIDR depuis la fonction et le rang. Source unique de la derivation (miroir Python de l'auto-proposition du GUI). + Tout vient de l'index + de la zone : aucune lecture d'adressage stocke. """ fonctions = nomenclature.get("fonctions") or {} categories = nomenclature.get("categories") or {} @@ -391,32 +436,14 @@ def deriver_nomenclature(fonction: str, seq: int, nomenclature: dict) -> dict | c = categories.get(cat) or categories.get(str(cat)) if not c: return None - base3 = ".".join(str(c.get("sous_reseau", "")).split(".")[:3]) - # Prefixe VMID = index d'instance (federation multi-ecosystemes). Defaut 9 : - # retro-compatible (instances sans index, bacs a sable). 1 chiffre -> 9 instances - # prod federables avec un VMID a 5 chiffres ; elargir l'index pour davantage. - index = nomenclature.get("index", 9) + index = nomenclature.get("index", INDEX_BAC_A_SABLE) hote = svc * 10 + int(seq) - # Schema du VMID. 'ip-miroir' (opt-in) : 9 chiffres mnemotechniques encodant - # index + VLAN + octet-hote + sequence -> le VMID contient litteralement l'IP - # (10.(10+index).VLAN.hote) et le tenant. 'compact' (defaut, retro-compatible) : - # index.categorie.service.sequence (largeur variable, VMID des instances existantes). - schema = nomenclature.get("vmid_schema", "compact") - if schema == "ip-miroir": - # VLAN DERIVE : 1000 + index x 10 + zone. Le socle 1000 force le VLAN a - # TOUJOURS 4 chiffres (1011..4094) -> le VMID (VLAN.octet.seq) fait TOUJOURS - # 9 chiffres : LONGUEURS UNIFORMES et mnemotechniques. Unique GLOBALEMENT sur - # un trunk convergE (retirer 1000 redonne index x 10 + zone = tenant + zone). - vlan = 1000 + index * 10 + int(cat) - vmid = f"{vlan}{hote:03d}{int(seq):02d}" - else: - vlan = c.get("vlan") - vmid = f"{index}{cat}{svc}{int(seq):02d}" + vlan = vlan_de(index, int(cat)) return { - "vmid": vmid, + "vmid": f"{vlan}{hote:03d}{int(seq):02d}", "vlan": vlan, - "adresse_ip": f"{base3}.{hote}", - "passerelle": c.get("passerelle"), + "adresse_ip": f"{base3_de(index, int(cat))}.{hote}", + "passerelle": passerelle_de(index, int(cat)), "cidr": nomenclature.get("cidr_hote", 24), } diff --git a/scripts/prouver.py b/scripts/prouver.py index c42565c..e414a39 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -163,6 +163,41 @@ def preuve_modele_socle() -> tuple[bool, str]: return True, "Modele public socle : domaines/serveurs/applications/bases valides." +def preuve_nomenclature_derivee() -> tuple[bool, str]: + """Aucune nomenclature ne STOCKE d'adressage : tout se derive du seed `index`. + + Interdit `supernet` au niveau racine et `sous_reseau`/`passerelle`/`vlan` par zone. + Attrape toute rechute vers l'ecriture manuelle de l'adressage (rupture du 2026-07-23). + Portee : l'instance liee + tous les modeles decouverts (SETOPS_MODELES inclus). + """ + import yaml + sys.path.insert(0, str(RACINE / "scripts")) + import modeles as _mod + + cibles = [] + inst_nom = RACINE / INSTANCE / "plan" / "nomenclature.yml" + if inst_nom.exists(): + cibles.append(inst_nom) + cibles += [m / "plan" / "nomenclature.yml" for m in _mod.decouvrir() + if (m / "plan" / "nomenclature.yml").exists()] + + interdits_zone = {"sous_reseau", "passerelle", "vlan"} + fautes = [] + for p in cibles: + n = yaml.safe_load(p.read_text(encoding="utf-8")) or {} + rel = p.relative_to(RACINE) if p.is_relative_to(RACINE) else p + if "supernet" in n: + fautes.append(f"{rel}: supernet stocke") + if "vmid_schema" in n: + fautes.append(f"{rel}: vmid_schema obsolete (mode compact retire)") + 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") + 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." + + def preuve_pas_lab_code_en_dur() -> tuple[bool, str]: """Aucun chemin `instance/inventories/lab/group_vars` code en dur (AFF-097). @@ -249,6 +284,8 @@ PREUVES: list[dict] = [ "refs": ["AFF-002", "AFF-095"], "cmds": [[sys.executable, "scripts/couverture_gui.py", "verifier", "--tolerer", "nomenclature"]]}, + {"id": "P20", "titre": "Adressage 100% derive du seed (aucun stocke)", + "refs": ["AFF-001", "AFF-003"], "func": preuve_nomenclature_derivee}, ]