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 # 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) ## 2026-07-23 (suite 6)
### Ajouté — plan de recette (le pendant manuel de `make prouver`) ### 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 python3 scripts/resoudre_flux.py nftables
.PHONY: devis-reseau .PHONY: devis-reseau
devis-reseau: ansible-runtime ## Devis switch (VLANs/SVIs/ACLs) du reseau converge, derive des nomenclatures devis-reseau: ansible-runtime ## Devis switch (VLANs/SVIs/ACLs) du reseau converge, derive des nomenclatures. DIALECTE=cisco|binardat
python3 scripts/devis_reseau.py python3 scripts/devis_reseau.py $(if $(DIALECTE),--dialecte $(DIALECTE),)
flux-verifier: flux-verifier:
python3 scripts/resoudre_flux.py 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 from __future__ import annotations
import argparse import argparse
import ipaddress import ipaddress
import os
import re import re
import sys import sys
from pathlib import Path from pathlib import Path
import yaml 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 RACINE = Path(__file__).resolve().parent.parent
DOSSIER_INSTANCES = RACINE.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) 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: def prefixe(nom_dossier: str) -> str:
base = re.sub(r"^OPS-", "", nom_dossier) base = re.sub(r"^OPS-", "", nom_dossier)
base = re.sub(r"-lab$", "", base) base = re.sub(r"-lab$", "", base)
@ -54,8 +74,9 @@ def decouvrir() -> list[tuple[str, str, dict]]:
return tenants return tenants
def generer(tenants: list[tuple[str, str, dict]]) -> str: def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) -> str:
out = ["configure terminal", "!"] dialecte = dialecte or DIALECTE_DEFAUT
out = ["configure terminal", f"! dialecte CLI : {dialecte}", "!"]
out += [ out += [
"! ============================================================", "! ============================================================",
"! DEVIS SWITCH — reseau converge multi-tenant (Set-OPS)", "! 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.append(" no shutdown")
out += ["!", "! ----- 3. ACL d'isolation tenant (default-deny inter-tenant) -----"] out += ["!", "! ----- 3. ACL d'isolation tenant (default-deny inter-tenant) -----"]
for nom, pfx, n in tenants: 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"ip access-list extended {pfx}{n['index']}-ISOLATION")
out.append(f" remark Intra-tenant {nom} : routage local autorise") out += remarque(dialecte, f"Intra-tenant {nom} : routage local autorise")
out.append(f" permit ip {reseau} {wild} {reseau} {wild}") out.append(f" permit ip {reseau} {m} {reseau} {m}")
for autre_nom, _, autre in tenants: for autre_nom, _, autre in tenants:
if autre_nom == nom: if autre_nom == nom:
continue continue
a_reseau, a_wild = reseau_wildcard(supernet_de(autre["index"])) a_reseau, a_m = masque_acl(supernet_de(autre["index"]), dialecte)
out.append(f" remark Bloquer le tenant {autre_nom}") out += remarque(dialecte, f"Bloquer le tenant {autre_nom}")
out.append(f" deny ip {reseau} {wild} {a_reseau} {a_wild}") out.append(f" deny ip {reseau} {m} {a_reseau} {a_m}")
out.append(" remark Reste (Internet / inter-tenant controle) -> passerelle OPNsense") out += remarque(dialecte, "Reste (Internet / inter-tenant controle) -> passerelle OPNsense")
out.append(f" permit ip {reseau} {wild} any") out.append(f" permit ip {reseau} {m} any")
out += ["!", "! ----- 4. Trunk vers Proxmox + inter-switch (a adapter) -----"] out += ["!", "! ----- 4. Trunk vers Proxmox + inter-switch (a adapter) -----"]
vlans = ",".join( vlans = ",".join(
str(vlan_de(n["index"], zone)) str(vlan_de(n["index"], zone))
@ -109,13 +130,16 @@ def generer(tenants: list[tuple[str, str, dict]]) -> str:
def main() -> None: 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() tenants = decouvrir()
if not tenants: if not tenants:
print("Aucune instance 'ip-miroir' federee trouvee (../*/plan/nomenclature.yml).", print("Aucune instance 'ip-miroir' federee trouvee (../*/plan/nomenclature.yml).",
file=sys.stderr) file=sys.stderr)
sys.exit(1) sys.exit(1)
print(generer(tenants)) print(generer(tenants, args.dialecte))
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1257,7 +1257,7 @@ HTML = r"""<!doctype html>
} else if (vuePrincipale === 'reseau') { } else if (vuePrincipale === 'reseau') {
d.innerHTML = `<div class="detail"><div class="section-tete">Réseau &amp; fédération</div> 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">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 { } else {
d.innerHTML = `<div class="detail"><div class="section-tete">Reconstruction</div> 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> <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>