Devis switch : dialecte cisco|binardat (masque ACL + remark)

Le switch Binardat differe de Cisco sur deux points que le devis ignorait :
- masque d'ACL normal (255.255.0.0) au lieu du wildcard inverse (0.0.255.255) ;
- pas de commande 'remark' (commentaires d'ACL).

devis_reseau.py gagne un dialecte (defaut cisco, option binardat) : masque_acl()
et remarque() adaptent la sortie. Reglable par --dialecte, SETOPS_DIALECTE, ou
make devis-reseau DIALECTE=binardat. Le GUI suit l'env. Public reste generique.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-07-23 17:15:27 -04:00
parent 58015f56e0
commit 412448ba55
4 changed files with 57 additions and 15 deletions

View file

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

View file

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

View file

@ -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__":

View file

@ -1257,7 +1257,7 @@ HTML = r"""<!doctype html>
} else if (vuePrincipale === 'reseau') {
d.innerHTML = `<div class="detail"><div class="section-tete">Réseau &amp; fédération</div>
<div class="hint-dep">Chaque instance fédérée dérive son adressage de son seed <code>index</code> (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.</div>
<div class="hint-dep" style="margin-top:10px">Lecture seule, régénéré du plan <code>make instances</code> / <code>make devis-reseau</code>. Le devis est à adapter à ta plateforme (NX-OS/IOS-XE, HSRP, route par défaut vers OPNsense, port de trunk).</div></div>`;
<div class="hint-dep" style="margin-top:10px">Lecture seule, régénéré du plan <code>make instances</code> / <code>make devis-reseau</code>. Masques d'ACL selon le dialecte (<code>SETOPS_DIALECTE=cisco|binardat</code> ou <code>make devis-reseau DIALECTE=binardat</code>). Le reste (HSRP, route par défaut vers OPNsense, port de trunk) reste à adapter à ta plateforme.</div></div>`;
} else {
d.innerHTML = `<div class="detail"><div class="section-tete">Reconstruction</div>
<div class="hint-dep">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.</div>