Set-OPS-Public/scripts/instances.py

191 lines
7.5 KiB
Python
Raw Normal View History

#!/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"
adressage : l'index est borne [0-255] — 10.300.0.0/16 n'est pas un reseau Rien ne bornait `index`. supernet_de(300) rendait « 10.300.0.0/16 » : une CHAINE qui ressemble a un reseau. Elle traverse tout le moteur sans bruit et n'echoue qu'au premier ip_network() qui la lit, tres loin de l'index fautif. La borne est posee A LA SOURCE (valider_index), pas dans un validateur de plan : supernet_de, base3_de et vlan_de y passent toutes, donc aucune ne peut fabriquer une adresse invalide — d'ou qu'on l'appelle : plan, GUI, devis ou test. Elle protege un SECOND plafond, moins visible : a l'index 255 le VLAN vaut 3550+zone, sous les 4094 du 802.1Q. Un index a trois chiffres debordait aussi la. Garde statique ajoutee au controle de federation (P21) : elle nomme le depot fautif au lieu de laisser l'erreur remonter d'une bibliotheque. LE TEST A TROUVE CE QUE LA RELECTURE N'AVAIT PAS VU : valider_index n'attrapait que TypeError et ValueError, or int(float('inf')) leve OverflowError — un infini faisait PLANTER la garde au lieu d'etre refuse. test_underlay_bande_basse.py -> test_adressage_derive.py (il ne parlait plus seulement de la bande basse). 12 cas, dont les refus. Piege de structure au passage : les nouveaux cas, ajoutes apres le bloc `if __name__ == "__main__"`, ne s'executaient pas — le bloc tourne avant que les fonctions suivantes ne soient definies, et le compte affichait « 7 tests » au lieu de 12. Un harnais qui compte ses propres tests doit etre lu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:21:33 -04:00
sys.path.insert(0, str(RACINE / "scripts"))
from inventory_rules import INDEX_MIN, INDEX_MAX # noqa: E402
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 _sites() -> list[dict]:
"""Les depots de SITE et l'index qu'ils declarent dans leur underlay.
POURQUOI ILS ENTRENT DANS LE COMPTE (2026-09-12). Tant que les sites n'avaient pas
d'index, ils ne pouvaient heurter personne : leur adressage etait tape a la main,
hors de la derivation. L'exploitant a decide que SITES et OPS partagent la classe A,
chacun avec son propre index — un site et un locataire peuvent donc desormais
reclamer le meme nombre, et rien ne l'attrapait.
C'est une seconde liste qui suit une premiere. Sa garde s'ecrit le jour meme.
L'index d'un site vit dans son `underlay.yml`, pas dans une `nomenclature.yml` :
c'est la carte de son MATERIEL, et un site n'a pas de categories de zones a la
maniere d'un locataire. Un site sans `index:` declare reste invisible ici — il
n'entre dans le compte que le jour ou il en prend un.
"""
trouves = []
for chemin in sorted(FRERES.glob("SITE-*/underlay.yml")):
try:
d = (yaml.safe_load(chemin.read_text(encoding="utf-8")) or {}).get("underlay") or {}
except (OSError, yaml.YAMLError):
continue
if d.get("index") is None:
continue
trouves.append({"depot": chemin.parent, "index": d["index"]})
return trouves
def decouvrir() -> list[dict]:
actif = _actif()
instances = []
for site in _sites():
instances.append({
"nom": site["depot"].name,
"index": site["index"],
"actif": False,
"federe": True, # un site partage la fabric qu'il fournit
"production": None,
"zones": [],
"vlans": None,
"site": True,
})
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)
adressage : l'index est borne [0-255] — 10.300.0.0/16 n'est pas un reseau Rien ne bornait `index`. supernet_de(300) rendait « 10.300.0.0/16 » : une CHAINE qui ressemble a un reseau. Elle traverse tout le moteur sans bruit et n'echoue qu'au premier ip_network() qui la lit, tres loin de l'index fautif. La borne est posee A LA SOURCE (valider_index), pas dans un validateur de plan : supernet_de, base3_de et vlan_de y passent toutes, donc aucune ne peut fabriquer une adresse invalide — d'ou qu'on l'appelle : plan, GUI, devis ou test. Elle protege un SECOND plafond, moins visible : a l'index 255 le VLAN vaut 3550+zone, sous les 4094 du 802.1Q. Un index a trois chiffres debordait aussi la. Garde statique ajoutee au controle de federation (P21) : elle nomme le depot fautif au lieu de laisser l'erreur remonter d'une bibliotheque. LE TEST A TROUVE CE QUE LA RELECTURE N'AVAIT PAS VU : valider_index n'attrapait que TypeError et ValueError, or int(float('inf')) leve OverflowError — un infini faisait PLANTER la garde au lieu d'etre refuse. test_underlay_bande_basse.py -> test_adressage_derive.py (il ne parlait plus seulement de la bande basse). 12 cas, dont les refus. Piege de structure au passage : les nouveaux cas, ajoutes apres le bloc `if __name__ == "__main__"`, ne s'executaient pas — le bloc tourne avant que les fonctions suivantes ne soient definies, et le compte affichait « 7 tests » au lieu de 12. Un harnais qui compte ses propres tests doit etre lu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:21:33 -04:00
# Un index hors bornes ne produit pas une adresse mais une chaine qui y ressemble.
# Le lever ICI, avec le nom du depot fautif, plutot que de laisser `ip_network()`
# echouer plus tard sur une valeur dont on ne saura plus d'ou elle vient.
hors_bornes = [(i["nom"], i["index"]) for i in instances
if not (INDEX_MIN <= int(i["index"]) <= INDEX_MAX)]
if args.verifier:
adressage : l'index est borne [0-255] — 10.300.0.0/16 n'est pas un reseau Rien ne bornait `index`. supernet_de(300) rendait « 10.300.0.0/16 » : une CHAINE qui ressemble a un reseau. Elle traverse tout le moteur sans bruit et n'echoue qu'au premier ip_network() qui la lit, tres loin de l'index fautif. La borne est posee A LA SOURCE (valider_index), pas dans un validateur de plan : supernet_de, base3_de et vlan_de y passent toutes, donc aucune ne peut fabriquer une adresse invalide — d'ou qu'on l'appelle : plan, GUI, devis ou test. Elle protege un SECOND plafond, moins visible : a l'index 255 le VLAN vaut 3550+zone, sous les 4094 du 802.1Q. Un index a trois chiffres debordait aussi la. Garde statique ajoutee au controle de federation (P21) : elle nomme le depot fautif au lieu de laisser l'erreur remonter d'une bibliotheque. LE TEST A TROUVE CE QUE LA RELECTURE N'AVAIT PAS VU : valider_index n'attrapait que TypeError et ValueError, or int(float('inf')) leve OverflowError — un infini faisait PLANTER la garde au lieu d'etre refuse. test_underlay_bande_basse.py -> test_adressage_derive.py (il ne parlait plus seulement de la bande basse). 12 cas, dont les refus. Piege de structure au passage : les nouveaux cas, ajoutes apres le bloc `if __name__ == "__main__"`, ne s'executaient pas — le bloc tourne avant que les fonctions suivantes ne soient definies, et le compte affichait « 7 tests » au lieu de 12. Un harnais qui compte ses propres tests doit etre lu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:21:33 -04:00
if hors_bornes:
print(f"erreur: index hors bornes [{INDEX_MIN}-{INDEX_MAX}] :", file=sys.stderr)
for nom, idx in sorted(hors_bornes):
print(f" - {nom} : index {idx} — le 2e octet d'une IPv4 ne peut pas le porter",
file=sys.stderr)
return 2
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())