Set-OPS-Public/scripts/devis_reseau.py
Daniel Allaire d2345272d4 devis-reseau : noms uniques par tenant + drapeau federe
Deux corrections, revelees quand Chezlepro a eu deux instances (labo index 1 +
prod index 13) decouvertes par le devis :

1. Collision de noms. prefixe() reduisait OPS-Chezlepro ET OPS-Chezlepro-lab au
   meme "CHEZ" (retrait de -lab + troncature 4 lettres) -> deux VLAN "CHEZ-Frontiere"
   et surtout deux ACL "CHEZ-ISOLATION", que le switch aurait FUSIONNEES (config
   cassee). Les noms sont desormais qualifies par l'index (CHEZ1-*, CHEZ13-*,
   TECH2-*) : uniques, et le tenant + son index se lisent dans le nom. Les numeros
   de VLAN encodaient deja l'index ; les noms le font maintenant aussi.

2. Drapeau federe. decouvrir() saute toute nomenclature avec `federe: false`
   (defaut true). Permet d'exclure un bac a sable LOCAL (labo, sans remote, pas sur
   la fabric de production) : on ne provisionne pas ses VLAN sur les switches de
   prod. Le labo genere toujours son propre inventaire.

Verifie : make devis-reseau -> labo (1011-1016) exclu, restent Technolibre
(1021-1026) et Chezlepro prod (1131-1136) ; SVI/ACL coherents (access-group ==
nom d'ACL) ; make verifier rc=0 CONFORME 20/20.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 10:21:34 -04:00

122 lines
5 KiB
Python
Raw 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
# Adressage et VLAN : derives du seul index, source unique dans inventory_rules.
sys.path.insert(0, str(RACINE / "scripts"))
from inventory_rules import passerelle_de, supernet_de, vlan_de # noqa: E402
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 {}
# Un tenant federe = une nomenclature avec un index (l'adressage en decoule).
# `federe: false` exclut un bac a sable local (labo) du reseau converge :
# il ne partage pas la fabric de production, on ne provisionne pas ses VLAN.
if n.get("index") is not None and n.get("categories") and n.get("federe", True):
nom = chemin.parent.parent.name
tenants.append((nom, prefixe(nom), n))
tenants.sort(key=lambda t: t[2]["index"])
return tenants
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}{n['index']}-{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 {passerelle_de(n['index'], zone)} {m}")
out.append(f" ip access-group {pfx}{n['index']}-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(supernet_de(n["index"]))
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}")
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")
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()