diff --git a/CHANGELOG.md b/CHANGELOG.md index f41e6db..d7fb21c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # CHANGELOG — Set-OPS +## 2026-08-12 — L'index est borné : `10.300.0.0/16` n'est pas un réseau + +Rien ne bornait `index`. `supernet_de(300)` rendait `"10.300.0.0/16"` — **une chaîne qui +ressemble à un réseau**. Elle traverse tout le moteur sans bruit et n'échoue qu'au premier +`ip_network()` qui la lit, très loin de l'index fautif. + +La borne est posée **à la source** (`valider_index`), pas dans un validateur de plan : +toutes les fonctions dérivées y passent — `supernet_de`, `base3_de`, `vlan_de` — donc +aucune ne peut fabriquer une adresse invalide, d'où qu'on l'appelle : plan, GUI, devis ou +test. + +Elle protège **un second plafond, moins visible** : à l'index 255 le VLAN vaut `3550+zone`, +sous les 4094 du 802.1Q. Un index à trois chiffres débordait aussi là. + +Et une garde statique dans le contrôle de fédération (**P21**), qui nomme le dépôt fautif +au lieu de laisser l'erreur remonter d'une bibliothèque. + +### Le test a trouvé ce que la relecture n'avait pas vu + +`valider_index` n'attrapait que `TypeError` et `ValueError`. Or `int(float('inf'))` lève +**`OverflowError`** : un infini flottant passait la garde en la faisant planter au lieu de +la faire refuser. Corrigé — et c'est le cas de test qui l'a levé, pas ma relecture. + +`test_underlay_bande_basse.py` devient `test_adressage_derive.py` : il ne parlait plus +seulement de la bande basse. **12 cas**, dont les refus. + +> **Un piège de structure, au passage.** Les nouveaux cas, ajoutés après le bloc +> `if __name__ == "__main__":`, ne s'exécutaient pas — le bloc tourne avant que les +> fonctions suivantes ne soient définies, et le compte affichait tranquillement « 7 tests » +> au lieu de 12. Un harnais qui compte ses propres tests doit être lu : *sept* était la +> bonne réponse à la mauvaise question. + ## 2026-08-12 — Le décalage de `+10` est retiré : l'index se lit dans l'adresse `supernet_de(index)` rendait `10.(10+index).0.0/16`. **Personne ne savait plus pourquoi** — diff --git a/Makefile b/Makefile index 44104a9..6693f36 100644 --- a/Makefile +++ b/Makefile @@ -211,7 +211,7 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire) python3 scripts/tests/test_inventory_host.py python3 scripts/tests/test_raser.py python3 scripts/tests/test_raser_resultat.py - python3 scripts/tests/test_underlay_bande_basse.py + python3 scripts/tests/test_adressage_derive.py .PHONY: verifier verifier: lint test inventaire-verifier site-verifier flux-verifier syntaxe ## Rejoue les preuves SANS reecrire le rapport (verification rapide) diff --git a/scripts/instances.py b/scripts/instances.py index d8fb540..caf0957 100644 --- a/scripts/instances.py +++ b/scripts/instances.py @@ -30,6 +30,9 @@ RACINE = Path(__file__).resolve().parents[1] FRERES = RACINE.parent LIEN_INSTANCE = RACINE / "instance" +sys.path.insert(0, str(RACINE / "scripts")) +from inventory_rules import INDEX_MIN, INDEX_MAX # noqa: E402 + def _actif() -> Path | None: try: @@ -98,8 +101,19 @@ def main() -> int: instances = decouvrir() coll = collisions(instances) + # 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: + 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) diff --git a/scripts/inventory_rules.py b/scripts/inventory_rules.py index 5e6ec73..da2f640 100644 --- a/scripts/inventory_rules.py +++ b/scripts/inventory_rules.py @@ -472,17 +472,45 @@ def fonction_seq(nom: str) -> tuple[str, int | None]: # plage utile passe de 0-245 a 0-255. OCTET_ZONE = 15 SOCLE_VLAN = 1000 + +# BORNES DE L'INDEX. Le 2e octet d'une IPv4 vaut 0-255 : au-dela, la derivation ne +# produit plus une adresse mais une CHAINE qui y ressemble — `10.300.0.0/16` passe +# silencieusement tant que personne ne la donne a `ip_network()`, et l'erreur remonte +# alors tres loin de sa cause. Depuis le retrait du decalage de +10 (2026-08-12), la +# plage utile est exactement celle de l'octet. +INDEX_MIN, INDEX_MAX = 0, 255 INDEX_BAC_A_SABLE = 9 # index par defaut d'une nomenclature sans seed (jetable) +def valider_index(index) -> int: + """L'index, verifie et rendu en entier. Leve `ValueError` hors bornes. + + Pose a la SOURCE de la derivation plutot que dans un validateur de plan : toutes + les fonctions derivees passent par ici, donc aucune ne peut fabriquer une adresse + invalide, d'ou qu'on l'appelle — plan, GUI, devis ou test. + """ + try: + i = int(index) + except (TypeError, ValueError, OverflowError): + # OverflowError : `int(float('inf'))`. Trouve par le test, pas par relecture — + # un infini flottant n'est ni un entier valide ni une TypeError. + raise ValueError(f"index invalide : {index!r} n'est pas un entier") from None + if not (INDEX_MIN <= i <= INDEX_MAX): + raise ValueError( + f"index {i} hors bornes [{INDEX_MIN}-{INDEX_MAX}] : le 2e octet d'une IPv4 " + f"ne peut pas le porter. La derivation produirait « 10.{i}.0.0/16 », qui " + f"ressemble a un reseau sans en etre un.") + return i + + def supernet_de(index: int) -> str: """/16 du tenant : 10..0.0/16 — l'index se lit dans l'adresse.""" - return f"10.{int(index)}.0.0/16" + return f"10.{valider_index(index)}.0.0/16" def base3_de(index: int, categorie: int) -> str: """Trois premiers octets d'une zone : 10..(15+categorie).""" - return f"10.{int(index)}.{OCTET_ZONE + int(categorie)}" + return f"10.{valider_index(index)}.{OCTET_ZONE + int(categorie)}" def sous_reseau_de(index: int, categorie: int, cidr: int = 24) -> str: @@ -496,8 +524,12 @@ def passerelle_de(index: int, categorie: int) -> str: 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) + """VLAN 802.1Q sur le trunk convergE : 1000 + index x 10 + zone (unique globalement). + + La meme borne y protege un second plafond : a l'index 255 le VLAN vaut 3550+zone, + sous les 4094 du 802.1Q. Sans elle, un index de trois chiffres deborderait aussi la. + """ + return SOCLE_VLAN + valider_index(index) * 10 + int(zone) def deriver_nomenclature(fonction: str, seq: int, nomenclature: dict) -> dict | None: diff --git a/scripts/tests/test_underlay_bande_basse.py b/scripts/tests/test_adressage_derive.py similarity index 65% rename from scripts/tests/test_underlay_bande_basse.py rename to scripts/tests/test_adressage_derive.py index b3cb97b..25031b7 100644 --- a/scripts/tests/test_underlay_bande_basse.py +++ b/scripts/tests/test_adressage_derive.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""D-77 — l'underlay tient dans la BANDE BASSE du supernet de son propre site. +"""Adressage derive : la bande basse de l'underlay (D-77) et les bornes de l'index. Ce que ce test garde, et pourquoi il existe. La regle « underlay dans la bande basse » n'est sure que parce que les zones d'un tenant commencent a `OCTET_ZONE + 1`. C'est une @@ -73,10 +73,55 @@ def test_sans_index_on_retombe_sur_la_regle_stricte(): "sans index, tout chevauchement doit etre refuse" + + +# --- Bornes de l'index (2026-08-12) ----------------------------------------------- +# Sans elles, `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. + +def test_les_bornes_couvrent_exactement_l_octet(): + from inventory_rules import INDEX_MIN, INDEX_MAX + assert (INDEX_MIN, INDEX_MAX) == (0, 255) + + +def test_index_aux_bornes_accepte(): + from inventory_rules import supernet_de + assert supernet_de(0) == "10.0.0.0/16" + assert supernet_de(255) == "10.255.0.0/16" + + +def test_index_hors_bornes_refuse(): + from inventory_rules import supernet_de, base3_de, vlan_de + for fn, args in ((supernet_de, (256,)), (supernet_de, (-1,)), + (base3_de, (300, 1)), (vlan_de, (256, 1))): + try: + fn(*args) + except ValueError: + continue + raise AssertionError(f"{fn.__name__}{args} aurait du etre refuse") + + +def test_index_non_entier_refuse(): + from inventory_rules import supernet_de + for mauvais in ("abc", None, 1.5e400): + try: + supernet_de(mauvais) + except ValueError: + continue + raise AssertionError(f"index {mauvais!r} aurait du etre refuse") + + +def test_le_vlan_reste_sous_le_plafond_802_1q(): + """La meme borne protege un SECOND plafond, moins visible.""" + from inventory_rules import vlan_de, INDEX_MAX + assert vlan_de(INDEX_MAX, 9) < 4094 + + if __name__ == "__main__": n = 0 for nom, fn in sorted(globals().items()): if nom.startswith("test_") and callable(fn): fn() n += 1 - print(f"OK — {n} test(s) D-77 (bande basse, frontiere derivee d'OCTET_ZONE={OCTET_ZONE}).") + print(f"OK — {n} test(s) adressage derive : bande basse (OCTET_ZONE={OCTET_ZONE}) + bornes de l'index.")