diff --git a/CHANGELOG.md b/CHANGELOG.md index ab9c714..244ab36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # CHANGELOG — Set-OPS +## 2026-07-23 (suite 7) + +### Ajouté — dialecte de CLI du commutateur (`devis-reseau`) +Constat de l'opérateur : son switch est un **Binardat**, dont la CLI diffère de Cisco sur +deux points que le devis généré ignorait — et deux pièges qui font passer un VLAN mais fuir +un tenant : + +- **Masque d'ACL** — Cisco veut un masque **inversé** (wildcard `0.0.255.255`), Binardat un + masque **normal** (`255.255.0.0`). Le SVI (`ip address … 255.255.255.0`) était déjà normal, + donc valide sur les deux. +- **`remark`** — Binardat n'a pas la commande de commentaire d'ACL de Cisco. Ces lignes + faisaient rejeter le bloc. + +**`scripts/devis_reseau.py`** gagne un **dialecte** (`cisco` par défaut, `binardat`) : +`masque_acl()` choisit wildcard ou masque normal ; `remarque()` omet les `remark` en Binardat. +Réglable par `--dialecte`, par `SETOPS_DIALECTE`, ou `make devis-reseau DIALECTE=binardat`. Le +GUI (lecture seule) suit l'env. Le code public reste **générique** (défaut `cisco`). + ## 2026-07-23 (suite 6) ### Ajouté — plan de recette (le pendant manuel de `make prouver`) diff --git a/Makefile b/Makefile index ec271ea..492bffb 100644 --- a/Makefile +++ b/Makefile @@ -341,8 +341,8 @@ flux: ansible-runtime python3 scripts/resoudre_flux.py nftables .PHONY: devis-reseau -devis-reseau: ansible-runtime ## Devis switch (VLANs/SVIs/ACLs) du reseau converge, derive des nomenclatures - python3 scripts/devis_reseau.py +devis-reseau: ansible-runtime ## Devis switch (VLANs/SVIs/ACLs) du reseau converge, derive des nomenclatures. DIALECTE=cisco|binardat + python3 scripts/devis_reseau.py $(if $(DIALECTE),--dialecte $(DIALECTE),) flux-verifier: python3 scripts/resoudre_flux.py verifier diff --git a/scripts/devis_reseau.py b/scripts/devis_reseau.py index ff8476a..ccf6be3 100644 --- a/scripts/devis_reseau.py +++ b/scripts/devis_reseau.py @@ -11,12 +11,19 @@ Sortie : config Cisco IOS-like (à adapter à la plateforme réelle : NX-OS/IOS- from __future__ import annotations import argparse import ipaddress +import os import re import sys from pathlib import Path import yaml +# Dialecte de CLI du commutateur. Cisco exige un masque INVERSE (wildcard 0.0.255.255) +# dans les ACL ; Binardat exige un masque NORMAL (255.255.0.0). Le SVI (ip address) prend +# un masque normal sur les deux. Reglable via --dialecte ou SETOPS_DIALECTE. +DIALECTES = ("cisco", "binardat") +DIALECTE_DEFAUT = os.environ.get("SETOPS_DIALECTE", "cisco") + RACINE = Path(__file__).resolve().parent.parent DOSSIER_INSTANCES = RACINE.parent @@ -34,6 +41,19 @@ def reseau_wildcard(supernet: str) -> tuple[str, str]: return str(reseau.network_address), str(reseau.hostmask) +def masque_acl(supernet: str, dialecte: str) -> tuple[str, str]: + """Reseau + masque au format ACL du dialecte : wildcard (cisco) ou normal (binardat).""" + reseau = ipaddress.IPv4Network(supernet, strict=False) + if dialecte == "binardat": + return str(reseau.network_address), str(reseau.netmask) + return str(reseau.network_address), str(reseau.hostmask) + + +def remarque(dialecte: str, texte: str) -> list[str]: + """Commentaire d'ACL selon le dialecte. Binardat n'a pas de 'remark' : on l'omet.""" + return [] if dialecte == "binardat" else [f" remark {texte}"] + + def prefixe(nom_dossier: str) -> str: base = re.sub(r"^OPS-", "", nom_dossier) base = re.sub(r"-lab$", "", base) @@ -54,8 +74,9 @@ def decouvrir() -> list[tuple[str, str, dict]]: return tenants -def generer(tenants: list[tuple[str, str, dict]]) -> str: - out = ["configure terminal", "!"] +def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) -> str: + dialecte = dialecte or DIALECTE_DEFAUT + out = ["configure terminal", f"! dialecte CLI : {dialecte}", "!"] out += [ "! ============================================================", "! DEVIS SWITCH — reseau converge multi-tenant (Set-OPS)", @@ -83,18 +104,18 @@ def generer(tenants: list[tuple[str, str, dict]]) -> str: out.append(" no shutdown") out += ["!", "! ----- 3. ACL d'isolation tenant (default-deny inter-tenant) -----"] for nom, pfx, n in tenants: - reseau, wild = reseau_wildcard(supernet_de(n["index"])) + reseau, m = masque_acl(supernet_de(n["index"]), dialecte) out.append(f"ip access-list extended {pfx}{n['index']}-ISOLATION") - out.append(f" remark Intra-tenant {nom} : routage local autorise") - out.append(f" permit ip {reseau} {wild} {reseau} {wild}") + out += remarque(dialecte, f"Intra-tenant {nom} : routage local autorise") + out.append(f" permit ip {reseau} {m} {reseau} {m}") for autre_nom, _, autre in tenants: if autre_nom == nom: continue - a_reseau, a_wild = reseau_wildcard(supernet_de(autre["index"])) - out.append(f" remark Bloquer le tenant {autre_nom}") - out.append(f" deny ip {reseau} {wild} {a_reseau} {a_wild}") - out.append(" remark Reste (Internet / inter-tenant controle) -> passerelle OPNsense") - out.append(f" permit ip {reseau} {wild} any") + a_reseau, a_m = masque_acl(supernet_de(autre["index"]), dialecte) + out += remarque(dialecte, f"Bloquer le tenant {autre_nom}") + out.append(f" deny ip {reseau} {m} {a_reseau} {a_m}") + out += remarque(dialecte, "Reste (Internet / inter-tenant controle) -> passerelle OPNsense") + out.append(f" permit ip {reseau} {m} any") out += ["!", "! ----- 4. Trunk vers Proxmox + inter-switch (a adapter) -----"] vlans = ",".join( str(vlan_de(n["index"], zone)) @@ -109,13 +130,16 @@ def generer(tenants: list[tuple[str, str, dict]]) -> str: def main() -> None: - argparse.ArgumentParser(description=__doc__).parse_args() + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--dialecte", choices=DIALECTES, default=DIALECTE_DEFAUT, + help="CLI du commutateur (defaut : SETOPS_DIALECTE ou 'cisco')") + args = ap.parse_args() tenants = decouvrir() if not tenants: print("Aucune instance 'ip-miroir' federee trouvee (../*/plan/nomenclature.yml).", file=sys.stderr) sys.exit(1) - print(generer(tenants)) + print(generer(tenants, args.dialecte)) if __name__ == "__main__": diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py index 25e9af5..8c596f9 100644 --- a/scripts/inventory_gui.py +++ b/scripts/inventory_gui.py @@ -1257,7 +1257,7 @@ HTML = r""" } else if (vuePrincipale === 'reseau') { d.innerHTML = `
Réseau & fédération
Chaque instance fédérée dérive son adressage de son seed index (VLAN = 1000 + index×10 + zone, unique sur le trunk). La vue liste la flotte, signale toute collision d'index, et génère le devis des switches.
-
Lecture seule, régénéré du plan — make instances / make devis-reseau. Le devis est à adapter à ta plateforme (NX-OS/IOS-XE, HSRP, route par défaut vers OPNsense, port de trunk).
`; +
Lecture seule, régénéré du plan — make instances / make devis-reseau. Masques d'ACL selon le dialecte (SETOPS_DIALECTE=cisco|binardat ou make devis-reseau DIALECTE=binardat). Le reste (HSRP, route par défaut vers OPNsense, port de trunk) reste à adapter à ta plateforme.
`; } else { d.innerHTML = `
Reconstruction
L'ordre part du socle et remonte : chaque couche suppose la précédente debout. Le tri intra-couche vient du graphe des dépendances.