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>
This commit is contained in:
Daniel Allaire 2026-08-12 16:21:33 -04:00
parent 5ace6bdb95
commit 8b4ad276ca
5 changed files with 130 additions and 7 deletions

View file

@ -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**

View file

@ -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)

View file

@ -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)

View file

@ -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.<index>.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.<index>.(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:

View file

@ -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.")