Set-OPS-Public/scripts/instances.py
Daniel Allaire b1460a6dde 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 <noreply@anthropic.com>
2026-07-23 11:26:01 -04:00

137 lines
5 KiB
Python

#!/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=<depot>")
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())