diff --git a/docs/multi-instances.md b/docs/multi-instances.md index 50d54e0..db92dac 100644 --- a/docs/multi-instances.md +++ b/docs/multi-instances.md @@ -33,19 +33,43 @@ Elle possède **tout ce qui lui est propre** : ``` Isolation **totale** entre instances : plan, inventaire, **voûte**, **identité** -(`domaine_interne`), réseau (`supernet`) et **cible Proxmox** distincts. Une instance -ne peut pas toucher l'infra d'une autre. +(`domaine_interne`), réseau (seed `index` → adressage dérivé distinct) et **cible +Proxmox** distincts. Une instance ne peut pas toucher l'infra d'une autre. -## Choisir l'instance active +## Gérer la flotte -Le moteur lit la variable `SETOPS_INSTANCE` (par défaut le symlink `instance`). +Une seule instance est **active** à la fois — celle que pointe le symlink `instance`. +Toutes les commandes `make` (instancier, deployer, creer-vm, prouver…) visent l'active. + +**Voir la flotte** — qui existe, laquelle est active (`★`), leur index, plage VLAN, +statut fédéré/local et production ; signale toute **collision d'index** : + +```bash +make instances +``` +``` +★ OPS-Chezlepro 13 1131-1136 fédérée prod + OPS-Technolibre 2 1021-1026 fédérée + OPS-Chezlepro-lab 1 1011-1016 local +``` + +**Basculer l'active** — le symlink, avec garde-fous (le dossier existe, `instance` est +bien un symlink) : ```bash make instance-courante # quelle instance est montée ? -make instance-utiliser NOM=OPS-Chezlepro # bascule sur la production -make instance-utiliser NOM=OPS-Chezlepro-lab # bascule sur le bac à sable +make instance-utiliser NOM=OPS-Technolibre # bascule (l'inventaire suit le lien) ``` +Rien à « recharger » : l'inventaire vit **dans** le dépôt de l'instance, il suit le lien. +Le GUI (onglet **Réseau**) montre la même flotte et les collisions ; la bascule reste au +CLI (chirurgie de symlink, mal placée dans une interface web). + +**Deux réflexes.** (1) Avant tout déploiement, `make instance-courante` : la seule vraie +façon de se tromper est de déployer sur la mauvaise flotte (la colonne `prod` est là pour +ça). (2) Un **index unique** par instance fédérée — `make instances` le crie, la preuve +**P21** le refuse dans le harnais, mais choisis-le distinct dès la création. + Sans toucher au symlink (utile en CI ou pour du parallèle) : ```bash @@ -57,6 +81,29 @@ make deployer SETOPS_INVENTAIRE=../OPS-ClientX/inventories/principal/hosts.yml H L'inventaire est détecté de façon **rétro-compatible** : `principal` > `production` > `lab` (les anciennes instances à double inventaire continuent de marcher). +## Comment le moteur découvre les instances + +**Par convention de disposition, pas par un registre.** Il n'y a ni base de données ni +fichier qui liste les instances. `scripts/instances.py` et `scripts/devis_reseau.py` font +simplement : + +```python +FRERES = RACINE.parent # le dossier qui CONTIENT Set-OPS-public +FRERES.glob("*/plan/nomenclature.yml") # tout dépôt frère ayant un plan +``` + +Une instance **est** donc un dossier : (1) **frère** du moteur (`../OPS-Chezlepro`, +`../OPS-Technolibre`…), (2) portant un **`plan/nomenclature.yml`**, (3) avec un **`index`**. +De là : **active** = ce que résout le symlink `instance` ; **fédérée** = `index` présent +*et* `federe ≠ false`. Les **modèles** (`Set-OPS-Modeles/integral/…`) sont un cran plus +profond — le glob ne les attrape pas, volontairement. + +Conséquence : « inscrire » une instance = la déposer à côté des autres. Rien à éditer, +aucune synchronisation, donc aucune dérive « inscrite mais absente ». Le revers — la flotte +étant « ce qui est là » — est qu'un simple dossier frère (le bac à sable) y apparaît : +d'où le drapeau `federe: false` pour l'écarter du réseau convergé, et `make instances` + +P21 pour garder un regard et un filet. + ## `setops_production` — un attribut, pas une catégorie Le déploiement réel est permis sur **toute instance** (un bac à sable déploie sur *son* @@ -71,7 +118,8 @@ sable met `false` et affiche « bac à sable ». structure ci-dessus. Le plus simple : copier un modèle de `exemples/modeles/` ou l'instance bac à sable comme point de départ. 2. Régler l'identité (`10-intrants.yml` : domaine **distinct**), le réseau - (`nomenclature.yml` : `supernet` **distinct**) et la cible Proxmox (`proxmox.yml`). + (`nomenclature.yml` : **`index` distinct** — tout l'adressage en dérive, cf. plus bas) + et la cible Proxmox (`proxmox.yml`). 3. Créer la voûte unique depuis le gabarit (cf. [`config-proxmox.md`](config-proxmox.md)) : `cp exemples/vault.exemple.yml …/group_vars/all/vault.yml` puis `ansible-vault encrypt`. 4. `make instance-utiliser NOM=OPS-ClientX` puis `make instancier-appliquer`, @@ -88,29 +136,39 @@ moteur, isolé. C'est la base concrète de la **portabilité des tenants**. > (NetBox, AWX, un coffre type Vault HashiCorp) **à adopter aux seuils**, pas à > réimplémenter dans le moteur. Cf. [`positionnement.md`](positionnement.md). -## Adressage fédéré (index d'instance) +## Adressage fédéré : tout dérive du seed `index` -Pour que plusieurs écosystèmes **coexistent et puissent s'interconnecter** sans -collision, chaque instance de production reçoit un **index** (déclaré en tête de -`plan/nomenclature.yml` : `index: N`). Il pilote : +Pour que plusieurs écosystèmes **coexistent** sur une même fabric sans collision, chaque +instance reçoit un **`index`** (unique champ d'adressage de `plan/nomenclature.yml` : +`index: N`). **Rien d'autre n'est écrit à la main** — la nomenclature ne garde que le +*modèle* (libellés de zones + placement des fonctions) ; supernet, sous-réseaux, +passerelles, VLAN et VMID se **dérivent** (`scripts/inventory_rules` : `supernet_de`, +`base3_de`, `passerelle_de`, `vlan_de`). Changer `index` rederive tout le réseau — et la +preuve **P20** interdit tout adressage stocké. -- le **supernet** : `10.(10 + index).0.0/16` (zéro chevauchement) ; -- le **préfixe VMID** : `index·catégorie·service·séq` (ex. `21101`) au lieu du `9` fixe. +Ce que `index` dérive (modèle à 6 zones) : -| Instance | index | Supernet | VMID | -| --- | --- | --- | --- | -| Chezlepro | 1 | `10.11.0.0/16` | `1CSNN` | -| Technolibre | 2 | `10.12.0.0/16` | `2CSNN` | -| … | N | `10.(10+N).0.0/16` | `N·CSNN` | -| Bacs à sable | *(aucun)* | `172.19.x` ad-hoc | `9CSNN` (défaut) | +| Élément | Formule | Exemple (index 13) | +| --- | --- | --- | +| Supernet | `10.(10+index).0.0/16` | `10.23.0.0/16` | +| Sous-réseau de zone | `10.(10+index).(15+zone).0/24` | `10.23.16.0/24` (Frontière) | +| Passerelle | `…(15+zone).1` | `10.23.16.1` | +| VLAN sur le trunk | `1000 + index×10 + zone` | `1131`-`1136` | +| VMID (ip-miroir) | `VLAN · octet-hôte · séq` (9 chiffres) | `113102101` | -Sans `index` déclaré, on retombe sur `9CSNN` (rétro-compatible) : c'est ce qu'utilisent -les **bacs à sable**, qui vivent hors fédération (plages jetables `172.19.x`) et n'ont -pas vocation à s'interconnecter. +Les VLAN de deux index différents ne peuvent **mathématiquement pas** se chevaucher +(l'espacement ×10 laisse de la marge). Deux instances de **même** index, elles, ont les +mêmes VLAN/VMID : c'est la collision que `make instances` et **P21** attrapent. -Plafond : un index à 1 chiffre = **9 écosystèmes** prod fédérés (VMID à 5 chiffres) ; -élargir l'index pour davantage. Au-delà d'une poignée d'instances co-localisées, confier -l'allocation à un **IPAM** (NetBox) plutôt qu'au moteur — cf. `positionnement.md`. +**Bacs à sable.** Un bac à sable local (labo) a lui aussi un `index`, mais on le marque +`federe: false` : il est alors **exclu du réseau convergé** (devis, `make instances` le +montre « local »). Il garde son adressage dérivé et reste déployable sur *son* infra. + +**Plafond théorique : 245 écosystèmes fédérés** (index 1 à 245), borné par l'IPv4 +`10.(10+index)` (2ᵉ octet 11→255). Le VLAN (≤ 4094) autorise jusqu'à 308, le VMID bien +plus — c'est donc l'adressage IP qui plafonne. Au-delà d'une poignée d'instances +co-localisées, confier l'allocation à un **IPAM** (NetBox) plutôt qu'au moteur — cf. +[`positionnement.md`](positionnement.md). ## Voir aussi - [`meta-classe.md`](meta-classe.md) — le plan d'une instance instancie sa flotte. diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py index 3fd62dd..034449b 100644 --- a/scripts/inventory_gui.py +++ b/scripts/inventory_gui.py @@ -1255,9 +1255,9 @@ HTML = r"""
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).
`; + d.innerHTML = `
Réseau & fédération
+
Chaque instance fédérée dérive son adressage de son seed index (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.
+
Lecture seule, régénéré du plan — make instances / make devis-reseau. Le devis est à 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.
@@ -1283,21 +1283,42 @@ HTML = r""" } let devisReseauTexte = ''; + function tableauFlotte(inst, coll) { + if (!inst.length) return '
Aucune instance découverte (dépôts frères avec plan/nomenclature.yml).
'; + const banniere = Object.keys(coll || {}).length + ? `
⚠ COLLISION d'index entre instances fédérées : ${Object.entries(coll).map(([i, n]) => 'index ' + i + ' → ' + n.map(echapper).join(', ')).join(' ; ')} — mêmes VLAN/VMID sur le trunk.
` + : ''; + const lignes = inst.map(i => { + const vlan = i.vlans ? (i.vlans[0] + '-' + i.vlans[1]) : '—'; + const fed = i.federe ? 'fédérée' : 'local'; + const prod = i.production ? 'prod' : ''; + return ` + ${i.actif ? '★ ' : ''}${echapper(i.nom)} + ${i.index} + ${vlan} + ${fed} ${prod}`; + }).join(''); + return `${banniere}${lignes}
InstanceIndexVLANStatut
+
★ = active (symlink instance). Basculer en CLI : make instance-utiliser NOM=<dépôt>.
`; + } async function dessinerReseau() { const cible = document.getElementById('grilles'); - cible.innerHTML = '
Génération du devis…
'; + cible.innerHTML = '
Lecture de la fédération…
'; 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; } + const [ri, rd] = await Promise.all([ + fetch('/api/instances', {headers: {'X-Jeton': JETON}}), + fetch('/api/devis-reseau', {headers: {'X-Jeton': JETON}}), + ]); + const di = await ri.json(), data = await rd.json(); + if (!rd.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)}
`; + const flotte = `
Flotte d'instances ${(di.instances || []).length}
${tableauFlotte(di.instances || [], di.collisions || {})}
`; + const devis = devisReseauTexte + ? `
Devis config réseau — switches (instances fédérées) +
+
${echapper(devisReseauTexte)}
` + : '
Aucune instance fédérée : pas de devis convergé à générer.
'; + cible.innerHTML = flotte + devis; } catch (e) { cible.innerHTML = '
Erreur : ' + echapper(e.message) + '
'; } @@ -2495,6 +2516,13 @@ 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/instances": + import instances as _inst + liste = _inst.decouvrir() + self.repondre_json(200, { + "instances": liste, + "collisions": {str(k): v for k, v in _inst.collisions(liste).items()}, + }) else: self.repondre_json(404, {"erreur": "Introuvable"}) except Exception as exc: