Set-OPS-Public/scripts/devis_reseau.py
Daniel Allaire 97670dd27c make devis-reseau : config switch derivee du plan
scripts/devis_reseau.py decouvre les instances federees (ip-miroir) et emet
VLANs + SVIs + ACLs d'isolation inter-tenant, derives des nomenclatures.
Plus jamais saisi a la main. Eprouve sur Chezlepro + Technolibre.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:50:18 -04:00

119 lines
4.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Devis de configuration switch (VLANs + SVIs + ACLs) du réseau convergé.
DÉRIVÉ des nomenclatures `ip-miroir` de toutes les instances fédérées (dossiers
frères `../*/plan/nomenclature.yml` ayant `vmid_schema: ip-miroir` + un `index`).
Rien n'est saisi à la main : le devis reste synchrone avec le plan.
VLAN = 1000 + index×10 + zone (4 chiffres, unique globalement sur le trunk convergé).
Sortie : config Cisco IOS-like (à adapter à la plateforme réelle : NX-OS/IOS-XE, HSRP, VRF).
"""
from __future__ import annotations
import argparse
import ipaddress
import re
import sys
from pathlib import Path
import yaml
RACINE = Path(__file__).resolve().parent.parent
DOSSIER_INSTANCES = RACINE.parent
def masque(cidr: int) -> str:
return str(ipaddress.IPv4Network(f"0.0.0.0/{cidr}").netmask)
def reseau_wildcard(supernet: str) -> tuple[str, str]:
reseau = ipaddress.IPv4Network(supernet, strict=False)
return str(reseau.network_address), str(reseau.hostmask)
def prefixe(nom_dossier: str) -> str:
base = re.sub(r"^OPS-", "", nom_dossier)
base = re.sub(r"-lab$", "", base)
return (re.sub(r"[^A-Za-z0-9]", "", base).upper() or "T")[:4]
def decouvrir() -> list[tuple[str, str, dict]]:
tenants = []
for chemin in sorted(DOSSIER_INSTANCES.glob("*/plan/nomenclature.yml")):
n = yaml.safe_load(chemin.read_text(encoding="utf-8")) or {}
if n.get("vmid_schema") == "ip-miroir" and n.get("index") is not None:
nom = chemin.parent.parent.name
tenants.append((nom, prefixe(nom), n))
tenants.sort(key=lambda t: t[2]["index"])
return tenants
def vlan_de(index: int, zone: int) -> int:
return 1000 + index * 10 + zone
def generer(tenants: list[tuple[str, str, dict]]) -> str:
out = ["configure terminal", "!"]
out += [
"! ============================================================",
"! DEVIS SWITCH — reseau converge multi-tenant (Set-OPS)",
"! Genere par scripts/devis_reseau.py depuis les nomenclatures.",
"! VLAN = 1000 + index*10 + zone (unique globalement sur le trunk).",
"! ============================================================",
"!",
"! ----- 1. VLANs -----",
]
for nom, pfx, n in tenants:
out.append(f"! {nom} (index {n['index']})")
for zone in sorted(n["categories"]):
c = n["categories"][zone]
out.append(f"vlan {vlan_de(n['index'], zone)}")
out.append(f" name {pfx}-{c['libelle']}")
out += ["!", "! ----- 2. Interfaces de routage (SVI = passerelle des hotes) -----"]
for nom, pfx, n in tenants:
m = masque(int(n.get("cidr_hote", 24)))
for zone in sorted(n["categories"]):
c = n["categories"][zone]
out.append(f"interface Vlan{vlan_de(n['index'], zone)}")
out.append(f" description {nom}-{c['libelle']}")
out.append(f" ip address {c['passerelle']} {m}")
out.append(f" ip access-group {pfx}-ISOLATION in")
out.append(" no shutdown")
out += ["!", "! ----- 3. ACL d'isolation tenant (default-deny inter-tenant) -----"]
for nom, pfx, n in tenants:
reseau, wild = reseau_wildcard(n["supernet"])
out.append(f"ip access-list extended {pfx}-ISOLATION")
out.append(f" remark Intra-tenant {nom} : routage local autorise")
out.append(f" permit ip {reseau} {wild} {reseau} {wild}")
for autre_nom, _, autre in tenants:
if autre_nom == nom:
continue
a_reseau, a_wild = reseau_wildcard(autre["supernet"])
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")
out += ["!", "! ----- 4. Trunk vers Proxmox + inter-switch (a adapter) -----"]
vlans = ",".join(
str(vlan_de(n["index"], zone))
for _, _, n in tenants
for zone in sorted(n["categories"])
)
out.append("interface <PORT-VERS-PROXMOX>")
out.append(" switchport mode trunk")
out.append(f" switchport trunk allowed vlan add {vlans}")
out += ["!", "end", "write memory"]
return "\n".join(out)
def main() -> None:
argparse.ArgumentParser(description=__doc__).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))
if __name__ == "__main__":
main()