diff --git a/CHANGELOG.md b/CHANGELOG.md index f73ae76..634d942 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ é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 : vue « Réseau » (devis switch) + bouton Copier.** Nouvel onglet lecture seule qui + affiche le devis `make devis-reseau` (VLANs + SVIs + ACLs), via `GET /api/devis-reseau`, avec + un bouton **Copier** (prêt à coller sur le switch). Un opérateur génère et transmet la config + sans CLI. - **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 diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py index 6ffbe43..d332af1 100644 --- a/scripts/inventory_gui.py +++ b/scripts/inventory_gui.py @@ -20,6 +20,7 @@ from urllib.parse import urlparse import yaml +import devis_reseau from inventory_rules import ( GROUPE_HOTES_ACTIFS, GROUPE_HOTES_PLANIFIES, @@ -916,6 +917,8 @@ HTML = r""" .re-hote { color: var(--teal); font-weight: 700; } .re-type { color: var(--muted); font-size: 11px; } .re-msg { background: var(--surface-3); border-radius: 6px; padding: 4px 8px; margin-top: 3px; font-family: ui-monospace, monospace; font-size: 11.5px; white-space: pre-wrap; word-break: break-word; } + /* Devis réseau (config switch) */ + .devis-reseau { margin: 8px 0 0; padding: 12px 14px; background: var(--surface-3); border-radius: 8px; font-family: ui-monospace, monospace; font-size: 11.5px; line-height: 1.5; white-space: pre; overflow: auto; max-height: calc(100vh - 210px); } @@ -933,6 +936,7 @@ HTML = r""" + @@ -1152,6 +1156,7 @@ HTML = r""" document.getElementById('btn-vue-domaines').classList.toggle('on', v === 'domaines'); document.getElementById('btn-vue-flux').classList.toggle('on', v === 'flux'); document.getElementById('btn-vue-couches').classList.toggle('on', v === 'couches'); + document.getElementById('btn-vue-reseau').classList.toggle('on', v === 'reseau'); dessinerGrilles(); dessinerDetail(); // le panneau droit reflète la sélection de la vue courante majIndicateurSauvegarde(); @@ -1176,6 +1181,7 @@ HTML = r""" if (vuePrincipale === 'domaines') { dessinerDomaines(); return; } if (vuePrincipale === 'flux') { dessinerFlux(); return; } if (vuePrincipale === 'couches') { dessinerCouches(); return; } + if (vuePrincipale === 'reseau') { dessinerReseau(); return; } dessinerServeurs(); } @@ -1191,6 +1197,10 @@ HTML = r"""
tls-cible TLS visé (roadmap)
clair non chiffré (local / edge)
Lecture seule. Éditer les meta/flux.yml des rôles, puis make flux.
`; + } else if (vuePrincipale === 'reseau') { + d.innerHTML = `
Devis réseau
+
VLANs + SVIs (passerelles) + ACLs d'isolation inter-tenant, dérivés des nomenclatures ip-miroir (VLAN = 1000 + index×10 + zone, unique sur le trunk).
+
Lecture seule, régénéré du plan — make devis-reseau. À adapter à ta plateforme (NX-OS/IOS-XE, HSRP, route par défaut vers OPNsense, port de trunk).
`; } else { d.innerHTML = `
Reconstruction
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.
@@ -1215,6 +1225,31 @@ HTML = r""" ${lignes}
`; } + let devisReseauTexte = ''; + async function dessinerReseau() { + const cible = document.getElementById('grilles'); + cible.innerHTML = '
Génération du devis…
'; + try { + const rep = await fetch('/api/devis-reseau', {headers: {'X-Jeton': JETON}}); + const data = await rep.json(); + if (!rep.ok) { cible.innerHTML = '
' + echapper(data.erreur || 'Indisponible.') + '
'; return; } + devisReseauTexte = data.config || ''; + if (!devisReseauTexte) { cible.innerHTML = '
Aucune instance fédérée (schéma ip-miroir) trouvée.
'; return; } + const tenants = (data.tenants || []).map(t => echapper(t.nom) + ' (idx ' + t.index + ')').join(' · '); + cible.innerHTML = `
+
Devis config réseau — switches + ${tenants} +
+
${echapper(devisReseauTexte)}
`; + } catch (e) { + cible.innerHTML = '
Erreur : ' + echapper(e.message) + '
'; + } + } + function copierDevis() { + if (navigator.clipboard) navigator.clipboard.writeText(devisReseauTexte) + .then(() => message('Devis réseau copié — prêt à coller sur le switch.', 'ok'), () => {}); + } + function dessinerCouches() { const cible = document.getElementById('grilles'); if (!couchesRegistre.length) { cible.innerHTML = '
Ordre de déploiement indisponible.
'; return; } @@ -1795,7 +1830,7 @@ HTML = r""" function dessinerDetail() { // Le panneau de droite reflète la sélection de la VUE COURANTE. - if (vuePrincipale === 'flux' || vuePrincipale === 'couches') return legendeLecture(); + if (vuePrincipale === 'flux' || vuePrincipale === 'couches' || vuePrincipale === 'reseau') return legendeLecture(); if (vuePrincipale === 'applications') return inspecteurApplication(); if (vuePrincipale === 'bases') return inspecteurBase(); if (vuePrincipale === 'domaines') return inspecteurDomaine(); @@ -2332,6 +2367,12 @@ class Gestionnaire(BaseHTTPRequestHandler): self.repondre_json(200, inventaire_api(self.inventaire)) elif chemin == "/api/sondes": self.repondre_json(200, sonder_hotes(self.inventaire)) + elif chemin == "/api/devis-reseau": + tenants = devis_reseau.decouvrir() + self.repondre_json(200, { + "config": devis_reseau.generer(tenants) if tenants else "", + "tenants": [{"nom": nom, "index": n["index"]} for nom, _, n in tenants], + }) else: self.repondre_json(404, {"erreur": "Introuvable"}) except Exception as exc: