From b1460a6ddefa7637cf9e8a2e96592f9c030e609b Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Thu, 23 Jul 2026 11:26:01 -0400 Subject: [PATCH] Multi-instances : make instances (vue + collision) + preuve P21 La bascule d'instance existait deja (make instance-utiliser / instance-courante, repointage du symlink). Manquaient la vue d'ensemble et le filet de securite. - make instances (scripts/instances.py) : liste les instances de la federation (depots freres avec plan/nomenclature.yml), marque l'active (*), montre index, plage VLAN derivee, statut federe/local et production. Lecture seule. - Detection de collision d'index entre instances FEDEREES (memes VLAN/VMID sur le trunk) : le piege exact vecu (prod + labo tous deux a l'index 1) est desormais crie. Mode --verifier -> rc=2 sur collision. - Preuve P21 : cable ce garde-fou dans make prouver / make verifier. No-op quand moins de deux instances federees sont presentes (comme P17 sans SETOPS_MODELES). Valide : make instances liste les 3 instances (labo LOCAL, Technolibre + Chezlepro federees, index 2 et 13) ; detection testee en synthetique (deux federees meme index -> collision ; labo federe:false -> coherent) ; make verifier rc=0 CONFORME 21/21. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 22 +++++ Makefile | 5 ++ docs/audit/README.md | 1 + docs/audit/preuve-2026-07-23.md | 3 +- scripts/instances.py | 137 ++++++++++++++++++++++++++++++++ scripts/prouver.py | 2 + 6 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 scripts/instances.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a18d65..e627ccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # CHANGELOG — Set-OPS +## 2026-07-23 (suite) + +### Ajouté — gestion multi-instances : vue d'ensemble + garde-fou de collision +Le mécanisme de bascule existait déjà (`make instance-utiliser`, `make instance-courante` : +repointer le symlink `instance`). Ce qui manquait : le regard d'ensemble et le filet. + +- **`make instances`** (`scripts/instances.py`) — liste toutes les instances de la + fédération (dépôts frères avec `plan/nomenclature.yml`), marque l'active (`*`), et + montre pour chacune index, plage VLAN dérivée, statut fédéré/local (`federe`) et + production (`setops_production`). Lecture seule. +- **Détection de collision d'index** — signale toute paire d'instances **fédérées** + partageant un `index` (donc mêmes VLAN/VMID sur le trunk convergé). C'est exactement le + piège vécu (Chezlepro-prod et le labo tous deux à l'index 1) : désormais crié, pas + découvert par hasard. Le mode `--verifier` sort en erreur (rc=2) sur collision. +- **Preuve P21** (`make prouver`/`make verifier`) — câble ce garde-fou dans le harnais : + la cohérence de la fédération est vérifiée à chaque passage. No-op quand moins de deux + instances fédérées sont présentes (comme P17 sans `SETOPS_MODELES`). + +Validé : `make instances` liste les 3 instances (labo LOCAL, Technolibre et Chezlepro +fédérées, index 2 et 13) ; détection testée en synthétique (deux fédérées au même index → +collision levée ; labo `federe: false` → cohérent) ; `make verifier` rc=0 **CONFORME 21/21**. + ## 2026-07-23 ### Changé — l'adressage se dérive du seul seed `index` (RUPTURE, mode compact retiré) diff --git a/Makefile b/Makefile index 74c3f85..5fc1113 100644 --- a/Makefile +++ b/Makefile @@ -236,6 +236,11 @@ instance-utiliser: instance-courante: @printf 'instance -> %s\n' "$$(readlink instance 2>/dev/null || echo '(non monté)')" +# Vue d'ensemble : toutes les instances de la fédération, l'active (*), leur index, +# plage VLAN, statut fédéré/prod ; signale les collisions d'index. Lecture seule. +instances: + @python3 scripts/instances.py + config: python3 scripts/config_proxmox.py diff --git a/docs/audit/README.md b/docs/audit/README.md index 01bfea2..f6b88a1 100644 --- a/docs/audit/README.md +++ b/docs/audit/README.md @@ -58,6 +58,7 @@ make prouver | 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 : 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. | +| P21 | Fédération sans collision | aucune paire d'instances **fédérées** ne partage un `index` (mêmes VLAN/VMID sur le trunk). Le garde-fou du multi-instances ; vue avec `make instances`. | > **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 index 76c31da..b9c2525 100644 --- a/docs/audit/preuve-2026-07-23.md +++ b/docs/audit/preuve-2026-07-23.md @@ -7,7 +7,7 @@ > [`docs/audit/affirmations.md`](affirmations.md). - **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml` -- **Verdict** : ✅ CONFORME (20 OK · 0 echec · 0 saute) +- **Verdict** : ✅ CONFORME (21 OK · 0 echec · 0 saute) ## Preuves @@ -33,6 +33,7 @@ | 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. | +| P21 | Federation : aucun index en collision | AFF-001 | ✅ OK | Federation coherente : 2 instance(s) federee(s), aucun index en collision. | ## Couverture des affirmations ✅ du registre diff --git a/scripts/instances.py b/scripts/instances.py new file mode 100644 index 0000000..d8fb540 --- /dev/null +++ b/scripts/instances.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Vue d'ensemble des instances (ecosystemes) de la federation Set-OPS. + +Decouvre les depots freres `../*/plan/nomenclature.yml` et montre, pour chacun : + - nom du depot, + - index (le seed dont derive tout l'adressage), + - ACTIVE (le symlink `instance` pointe-t-il ici ?), + - federe (participe au reseau converge, cf. devis-reseau) — sinon bac a sable local, + - production (setops_production) — pour la prudence au deploiement, + - plage VLAN derivee (1000 + index x 10 + zone). + +Et surtout : SIGNALE toute COLLISION d'index entre instances FEDEREES — deux +instances de meme index ont les memes VLAN et VMID, ce qui casse le reseau converge. +C'est le garde-fou du modele multi-instances. + +Usage : + python3 scripts/instances.py # tableau + python3 scripts/instances.py --verifier # rc=2 si collision d'index entre federees +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import yaml + +RACINE = Path(__file__).resolve().parents[1] +FRERES = RACINE.parent +LIEN_INSTANCE = RACINE / "instance" + + +def _actif() -> Path | None: + try: + return LIEN_INSTANCE.resolve() + except OSError: + return None + + +def _production(depot: Path) -> bool | None: + """Lit setops_production dans les group_vars/all de l'instance (repere de prudence).""" + for inv in sorted((depot / "inventories").glob("*")) if (depot / "inventories").is_dir() else []: + base = inv / "group_vars" / "all" + fichiers = list(base.glob("*.yml")) if base.is_dir() else [] + plat = inv / "group_vars" / "all.yml" + if plat.exists(): + fichiers.append(plat) + for f in fichiers: + try: + d = yaml.safe_load(f.read_text(encoding="utf-8")) or {} + except (OSError, yaml.YAMLError): + continue + if isinstance(d, dict) and "setops_production" in d: + return bool(d["setops_production"]) + return None + + +def decouvrir() -> list[dict]: + actif = _actif() + instances = [] + for chemin in sorted(FRERES.glob("*/plan/nomenclature.yml")): + try: + n = yaml.safe_load(chemin.read_text(encoding="utf-8")) or {} + except (OSError, yaml.YAMLError): + continue + if n.get("index") is None: + continue + depot = chemin.parent.parent + zones = sorted(int(z) for z in (n.get("categories") or {})) + idx = n["index"] + instances.append({ + "nom": depot.name, + "index": idx, + "actif": actif is not None and depot.resolve() == actif, + "federe": bool(n.get("federe", True)), + "production": _production(depot), + "vlans": (1000 + idx * 10 + zones[0], 1000 + idx * 10 + zones[-1]) if zones else None, + }) + instances.sort(key=lambda i: (i["index"], i["nom"])) + return instances + + +def collisions(instances: list[dict]) -> dict[int, list[str]]: + """Index partages par >1 instance FEDEREE -> {index: [noms]}.""" + par_index: dict[int, list[str]] = {} + for i in instances: + if i["federe"]: + par_index.setdefault(i["index"], []).append(i["nom"]) + return {idx: noms for idx, noms in par_index.items() if len(noms) > 1} + + +def main() -> int: + p = argparse.ArgumentParser(description="Vue d'ensemble des instances Set-OPS.") + p.add_argument("--verifier", action="store_true", + help="Ne rien afficher sauf en cas de collision d'index (rc=2).") + args = p.parse_args() + + instances = decouvrir() + coll = collisions(instances) + + if args.verifier: + if coll: + print("erreur: collision d'index entre instances FEDEREES " + "(memes VLAN/VMID) :", file=sys.stderr) + for idx, noms in sorted(coll.items()): + print(f" - index {idx} partage par : {', '.join(noms)}", file=sys.stderr) + return 2 + print(f"Federation coherente : {sum(1 for i in instances if i['federe'])} " + f"instance(s) federee(s), aucun index en collision.") + return 0 + + if not instances: + print("Aucune instance decouverte (depots freres avec plan/nomenclature.yml).") + return 0 + + print(f"{'':1} {'INSTANCE':<22} {'INDEX':>5} {'VLAN':<11} {'FEDERE':<7} {'PROD':<5}") + for i in instances: + marque = "*" if i["actif"] else " " + vlan = f"{i['vlans'][0]}-{i['vlans'][1]}" if i["vlans"] else "—" + fed = "oui" if i["federe"] else "LOCAL" + prod = "oui" if i["production"] else ("non" if i["production"] is False else "?") + print(f"{marque} {i['nom']:<22} {i['index']:>5} {vlan:<11} {fed:<7} {prod:<5}") + print("\n* = instance active (symlink 'instance'). " + "Basculer : make instance-utiliser NOM=") + + if coll: + print("\n⚠ COLLISION d'index entre instances federees :", file=sys.stderr) + for idx, noms in sorted(coll.items()): + print(f" index {idx} : {', '.join(noms)} — memes VLAN/VMID sur le trunk !", + file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/prouver.py b/scripts/prouver.py index e414a39..36c0cb9 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -286,6 +286,8 @@ PREUVES: list[dict] = [ "--tolerer", "nomenclature"]]}, {"id": "P20", "titre": "Adressage 100% derive du seed (aucun stocke)", "refs": ["AFF-001", "AFF-003"], "func": preuve_nomenclature_derivee}, + {"id": "P21", "titre": "Federation : aucun index en collision", "refs": ["AFF-001"], + "cmds": [[sys.executable, "scripts/instances.py", "--verifier"]]}, ]