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>
This commit is contained in:
parent
4de04ec0e1
commit
97670dd27c
3 changed files with 128 additions and 0 deletions
|
|
@ -11,6 +11,11 @@
|
|||
VLANs 1021-1026 / 10.12.x. Chezlepro quitte le bac-à-sable plat 192.168.15/VLAN 15.
|
||||
|
||||
### Ajouté
|
||||
- **`make devis-reseau` : config switch dérivée du plan.** Nouveau `scripts/devis_reseau.py`
|
||||
qui découvre les instances fédérées (`../*/plan/nomenclature.yml`, schéma `ip-miroir`) et
|
||||
émet la config Cisco-like du réseau convergé — **VLANs + SVIs (passerelles) + ACLs d'isolation
|
||||
inter-tenant** — dérivée des nomenclatures, jamais saisie à la main (toujours synchrone avec
|
||||
le plan). One-shot précédemment ; maintenant reproductible.
|
||||
- **GUI : erreurs de déploiement en LANGAGE CLAIR.** Quand une action (créer/vérifier/déployer)
|
||||
échoue, le GUI n'oblige plus à lire le dump Ansible : un encadré résume les tâches en erreur —
|
||||
**étape · hôte · type · message clair** — avec un bouton **« Copier »** (pour transmettre le
|
||||
|
|
|
|||
4
Makefile
4
Makefile
|
|
@ -305,6 +305,10 @@ flux: ansible-runtime
|
|||
python3 scripts/resoudre_flux.py registre
|
||||
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
|
||||
|
||||
flux-verifier:
|
||||
python3 scripts/resoudre_flux.py verifier
|
||||
|
||||
|
|
|
|||
119
scripts/devis_reseau.py
Normal file
119
scripts/devis_reseau.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
#!/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()
|
||||
Loading…
Reference in a new issue