#!/usr/bin/env python3 """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 propriete de `inventory_rules`, pas une place qu'on aurait constatee libre un jour : si `OCTET_ZONE` bougeait sans que la borne suive, la validation continuerait a dire « oui » en laissant l'underlay recouvrir les zones — et personne ne le verrait avant que deux machines ne se disputent une adresse. Le test derive donc sa frontiere de la MEME constante que le code, et verifie les trois cas qui comptent, y compris ceux ou un prefixe FRANCHIT la frontiere (un /19 couvre les octets 0 a 31 : il deborde, meme s'il commence dans la bande basse). """ import sys from pathlib import Path RACINE = Path(__file__).resolve().parent.parent.parent sys.path.insert(0, str(RACINE / "scripts")) import underlay as u # noqa: E402 from inventory_rules import OCTET_ZONE # noqa: E402 SUPERNETS = ["10.11.0.0/16", "10.17.0.0/16"] # deux tenants : index 11 et 17 MIEN = "OPS-Mien" # le tenant dont ce site occupe la bande basse MIEN_INDEX = 11 # UN SITE N'A PAS D'INDEX (add94f2, 2026-08-25). Il ne derive pas : il DECLARE ses # adresses, parce qu'il EST le terrain. Ce qui a remplace l'index, c'est `bande_basse_de` # — porte par CHAQUE reseau, et non par le site : deux reseaux d'un meme site peuvent # occuper la bande basse de deux tenants differents. # # CE TEST A SURVECU TROIS JOURS A CE CHANGEMENT EN ETANT ROUGE (2026-08-25 -> 08-28), et # personne ne l'a vu : le geste quotidien est `make prouver`, qui ne joue pas les tests. # Un test rouge que rien ne lit ne garde plus rien. def _erreurs(sous: str, mien: str | None = MIEN) -> list[str]: reseau = {"nom": "g", "vlan": 10, "sous_reseau": sous} if mien is not None: reseau["bande_basse_de"] = mien site = {"reseaux": [reseau], "tenants": {MIEN: MIEN_INDEX}} return [e for e in u.valider(site, tenants_vlans=set(), tenants_supernets=SUPERNETS) if "deborde" in e or "chevauche le supernet" in e] def test_la_borne_derive_de_la_regle_des_zones(): """La frontiere n'est pas 16 en dur : elle suit `OCTET_ZONE`.""" assert u.PREMIER_OCTET_ZONE == OCTET_ZONE + 1 def test_bande_basse_de_son_propre_supernet_acceptee(): for sous in ("10.11.0.0/24", "10.11.4.0/24", "10.11.5.0/24", "10.11.15.0/24"): assert _erreurs(sous) == [], f"{sous} devrait etre conforme (bande basse)" def test_bande_des_zones_refusee(): for sous in (f"10.11.{OCTET_ZONE + 1}.0/24", "10.11.18.0/24", "10.11.16.0/20"): assert _erreurs(sous), f"{sous} devrait etre REFUSE (bande des zones)" def test_un_prefixe_qui_franchit_la_frontiere_est_refuse(): """Le piege : il COMMENCE dans la bande basse et deborde. Un test qui ne regarderait que le premier octet le laisserait passer.""" for sous in ("10.11.0.0/19", "10.11.0.0/16"): assert _erreurs(sous), f"{sous} couvre au-dela de {OCTET_ZONE} : a refuser" def test_supernet_d_un_autre_site_refuse(): """C'est ce qui empeche deux sites d'etre relies — l'erreur la plus couteuse.""" assert _erreurs("10.17.0.0/24"), "le supernet d'un autre site doit etre refuse" def test_sans_bande_basse_declaree_tout_chevauchement_est_refuse(): """Le defaut est le refus : un reseau qui ne DIT PAS de quel tenant il occupe la bande basse n'a droit a aucun chevauchement, pas meme sur 10.11.x. Ne pas declarer ne peut pas valoir permission.""" assert _erreurs("10.11.0.0/24", mien=None), \ "sans `bande_basse_de`, un chevauchement doit etre refuse" def test_hors_de_tout_supernet_accepte(): """Le stockage (192.168.x, identique partout) et l'heritage 10.0.x avant migration.""" for sous in ("192.168.20.0/24", "192.168.31.0/24", "10.0.0.0/24"): assert _erreurs(sous) == [], f"{sous} est hors des supernets : conforme" # --- 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 def test_un_tenant_declare_introuvable_est_refuse_statiquement(): """`tenants` nomme des dossiers freres : une faute de frappe doit se voir ICI. Sinon elle ne se voit nulle part — le tenant disparait des trois devis du site, qui restent « conformes » sur ce qu'il en reste. Le nom est volontairement absurde : le test ne doit dependre d'aucun dossier reel de la machine qui l'execute. """ erreurs = u._erreurs_tenants_declares({"tenants": ["OPS-NExistePas-9f2c"]}) assert any("aucun dossier frere" in e for e in erreurs), erreurs assert any("aucun nom declare ne correspond" in e for e in erreurs), erreurs def test_sans_la_clef_tenants_rien_n_est_verifie(): """Clef absente = toute la federation : un site unique n'a rien a declarer.""" assert u._erreurs_tenants_declares({"reseaux": []}) == [] assert u._erreurs_tenants_declares(None) == [] def test_un_gabarit_n_est_pas_confronte_aux_dossiers_freres(): """Un modele decrit du MATERIEL : il ne peut pas nommer de tenants reels. La distinction tient a l'appelant — `modeles.py` passe des reperes de tenants explicites, ce qui dit « gabarit ». Sans cette garde, tout modele portant un exemple de `tenants` echouerait chez quiconque n'a pas ce dossier. """ gabarit = {"tenants": ["OPS-NExistePas-9f2c"], "reseaux": [{"nom": "g", "vlan": 10, "sous_reseau": "10.11.0.0/24"}]} erreurs = u.valider(gabarit, tenants_vlans=set(), tenants_supernets=SUPERNETS) assert not any("tenants:" in e for e in erreurs), erreurs 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) adressage derive : bande basse (OCTET_ZONE={OCTET_ZONE}) + bornes de l'index.")