GUI : le panneau présente les deux devis, et son aide n'est plus périmée

La vue Réseau n'affichait que le devis des commutateurs. Le devis frontière
était totalement absent de l'interface, alors qu'il porte les règles de la
bordure et ses avertissements — dont celui sur « Block private networks »,
invisible dans les règles elles-mêmes.

Ajouté `/api/devis-opnsense` et son bloc d'affichage avec bouton de copie.
Vérifié par HTTP que les deux points d'API servent exactement ce que le CLI
produit : 181 et 125 lignes, identiques au caractère près. Import paresseux
et gardé — ce module lit l'underlay et les inventaires de tous les tenants,
une erreur aurait sinon vidé la vue entière.

Aide corrigée sur trois points périmés : les VLAN tenants annoncés « uniques
sur le trunk » (faux en SDN, aucun n'y circule) ; le dialecte renvoyé à une
variable d'environnement alors que c'est un intrant de la section Fabric ; et
la route par défaut dite « à adapter » alors que la section 5 l'émet.

Elle dit maintenant ce qui reste à nommer à la main : les ports physiques et,
en SDN, le nœud de sortie EVPN.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-03 08:52:22 -04:00
parent 940e319285
commit a85255dc35
2 changed files with 52 additions and 5 deletions

View file

@ -1,5 +1,28 @@
# CHANGELOG — Set-OPS
## 2026-08-03 (suite 2) — le panneau présente les deux devis
La vue *Réseau* n'affichait que le devis des commutateurs : **le devis frontière était
totalement absent de l'interface**, alors qu'il porte les règles de la bordure et ses
avertissements — dont celui sur « Block private networks », invisible dans les règles
elles-mêmes.
Ajouté : `/api/devis-opnsense` et son bloc d'affichage, avec bouton de copie. Vérifié **par
HTTP** que les deux points d'API servent exactement ce que le CLI produit — 181 et 125 lignes,
identiques au caractère près.
L'import est paresseux et gardé : ce module lit l'underlay et les inventaires de tous les
tenants, et une erreur y aurait sinon empêché l'affichage du reste de la vue.
### Corrigé — le texte d'aide était périmé sur trois points
Il annonçait les VLAN tenants « uniques **sur le trunk** » — faux en SDN, où aucun ne circule ;
renvoyait le dialecte à `SETOPS_DIALECTE` alors que c'est un **intrant** de la section
*Fabric* ; et disait que « la route par défaut vers OPNsense reste à adapter » alors que la
section 5 l'émet depuis hier.
Il dit maintenant ce qui reste réellement à nommer à la main : les **ports physiques** et, en
SDN, le **nœud de sortie EVPN**. Rien d'autre — adresses, VLAN et routes se dérivent.
## 2026-08-03 (suite) — le devis frontière rattrape la bascule SDN
Trois affirmations du devis frontière étaient devenues fausses, dont une qui cassait le

View file

@ -1403,8 +1403,9 @@ HTML = r"""<!doctype html>
<div class="hint-dep" style="margin-top:10px">Lecture seule. Éditer les <code>meta/flux.yml</code> des rôles, puis <code>make flux</code>.</div></div>`;
} else if (vuePrincipale === 'reseau') {
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" 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>`;
<div class="hint-dep">Chaque instance fédérée dérive son adressage de son seed <code>index</code> (VLAN/VNI = 1000 + index×10 + zone, unique dans la fédération). La vue liste la flotte, signale toute collision d'index, et génère les <strong>deux devis</strong> : commutateurs et frontière nord/sud.</div>
<div class="hint-dep" style="margin-top:10px">Lecture seule, régénérés du plan <code>make devis-reseau</code> / <code>make devis-opnsense</code>. Le <strong>dialecte de CLI</strong> et le <strong>mode de routage</strong> (commutateur ou SDN EVPN) se règlent dans la section <em>Fabric</em> du panneau « Intrants de base ». En mode SDN, les commutateurs ne portent ni VLAN tenant, ni SVI, ni ACL : le routage et le filtrage inter-zone vivent sur les hyperviseurs.</div>
<div class="hint-dep" style="margin-top:10px">Ce qui reste à nommer à la main : les <strong>ports physiques</strong> (<code>&lt;PORT-VERS-&gt;</code>) et, en SDN, le <strong>nœud de sortie EVPN</strong>. Rien d'autre — adresses, VLAN et routes se dérivent.</div></div>`;
} else {
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>
@ -1430,6 +1431,7 @@ HTML = r"""<!doctype html>
}
let devisReseauTexte = '';
let devisFrontiereTexte = '';
function tableauFlotte(inst, coll, fmodeles, findexPris) {
if (!inst.length) return '<div class="vide" style="padding:14px">Aucune instance découverte (dépôts frères avec plan/nomenclature.yml).</div>';
const banniere = Object.keys(coll || {}).length
@ -1498,11 +1500,12 @@ HTML = r"""<!doctype html>
const cible = document.getElementById('grilles');
cible.innerHTML = '<div class="vide" style="padding:14px">Lecture de la fédération…</div>';
try {
const [ri, rd] = await Promise.all([
const [ri, rd, rf] = await Promise.all([
fetch('/api/instances', {headers: {'X-Jeton': JETON}}),
fetch('/api/devis-reseau', {headers: {'X-Jeton': JETON}}),
fetch('/api/devis-opnsense', {headers: {'X-Jeton': JETON}}),
]);
const di = await ri.json(), data = await rd.json();
const di = await ri.json(), data = await rd.json(), dfr = await rf.json();
if (!rd.ok) { cible.innerHTML = '<div class="vide" style="padding:14px">' + echapper(data.erreur || 'Indisponible.') + '</div>'; return; }
devisReseauTexte = data.config || '';
const flotte = `<div class="section-grille"><div class="section-tete">Flotte d'instances <span class="compte">${(di.instances || []).length}</span></div>${tableauFlotte(di.instances || [], di.collisions || {}, di.modeles || [], di.index_pris || [])}</div>`;
@ -1511,7 +1514,13 @@ HTML = r"""<!doctype html>
<button type="button" class="fantome" style="margin-left:auto" onclick="copierDevis()">Copier</button></div>
<pre class="devis-reseau">${echapper(devisReseauTexte)}</pre></div>`
: '<div class="hint-dep" style="margin-top:12px">Aucune instance fédérée : pas de devis convergé à générer.</div>';
cible.innerHTML = flotte + devis;
devisFrontiereTexte = dfr.config || '';
const frontiere = devisFrontiereTexte
? `<div class="section-grille" style="margin-top:12px"><div class="section-tete">Devis config réseau frontière nord/sud (OPNsense)
<button type="button" class="fantome" style="margin-left:auto" onclick="copierDevisFrontiere()">Copier</button></div>
<pre class="devis-reseau">${echapper(devisFrontiereTexte)}</pre></div>`
: `<div class="hint-dep" style="margin-top:12px">Devis frontière indisponible${dfr.erreur ? ' : ' + echapper(dfr.erreur) : ''}.</div>`;
cible.innerHTML = flotte + devis + frontiere;
} catch (e) {
cible.innerHTML = '<div class="vide" style="padding:14px">Erreur : ' + echapper(e.message) + '</div>';
}
@ -1520,6 +1529,10 @@ HTML = r"""<!doctype html>
if (navigator.clipboard) navigator.clipboard.writeText(devisReseauTexte)
.then(() => message('Devis réseau copié — prêt à coller sur le switch.', 'ok'), () => {});
}
function copierDevisFrontiere() {
if (navigator.clipboard) navigator.clipboard.writeText(devisFrontiereTexte)
.then(() => message('Devis frontière copié.', 'ok'), () => {});
}
function dessinerCouches() {
const cible = document.getElementById('grilles');
@ -2740,6 +2753,17 @@ class Gestionnaire(BaseHTTPRequestHandler):
"config": devis_reseau.generer(tenants) if tenants else "",
"tenants": [{"nom": nom, "index": n["index"]} for nom, _, n in tenants],
})
elif chemin == "/api/devis-opnsense":
# Import paresseux : ce module lit l'underlay et les inventaires de tous
# les tenants ; une erreur ici ne doit pas empecher le reste du panneau.
try:
import devis_opnsense as _dop
tenants = _dop.decouvrir()
self.repondre_json(200, {
"config": _dop.rendre(_dop.construire(tenants)) if tenants else "",
})
except Exception as exc:
self.repondre_json(200, {"config": "", "erreur": str(exc)})
elif chemin == "/api/instances":
import instances as _inst
import instance_creer as _ic