2026-07-23 11:26:01 -04:00
|
|
|
#!/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
|
|
|
|
|
|
2026-07-23 11:26:01 -04:00
|
|
|
|
|
|
|
|
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)
|
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)]
|
2026-07-23 11:26:01 -04:00
|
|
|
|
|
|
|
|
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
|
2026-07-23 11:26:01 -04:00
|
|
|
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())
|