GUI : bascule d'instance (vraiment multi-instance)
Demande explicite et repetee de l'operateur : gerer les instances DEPUIS le GUI. - Inventaire resolu DYNAMIQUEMENT : le serveur figeait l'inventaire au demarrage (args.inventaire.resolve()). Il est desormais relu depuis le symlink instance/ a chaque requete (propriete Gestionnaire.inventaire) -> la bascule prend effet sans redemarrer. Les FICHIER_* de plan suivaient deja le lien. SETOPS_INVENTAIRE force encore un inventaire fixe (CI). - POST /api/instance-utiliser + basculer_instance(nom) : repointe le symlink avec les garde-fous de make instance-utiliser + validation STRICTE (nom doit etre une instance decouverte -> pas de traversee de chemin ; ../etc refuse, teste). - Vue Reseau : bouton « Activer » par instance dans la table de flotte. Confirme (avertissement renforce si production), bascule, recharge -> toutes les vues et les deploiements visent la nouvelle instance. L'edition de federe reste au CLI (rarement change) ; la bascule est CLI ET GUI. docs/carte-set-ops.md : la ligne Multi-instance du tableau des mecanismes documente la decouverte (glob des dossiers freres, aucun registre) et le garde-fou P21. Valide : node --check ; bascule testee de bout en bout (repoint -> inventaire dynamique suit -> traversee refusee -> restauration) ; make verifier rc=0 CONFORME 21/21. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
dbc78f69ec
commit
1ad7b48483
3 changed files with 84 additions and 6 deletions
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -1,5 +1,31 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-07-23 (suite 2)
|
||||
|
||||
### Ajouté — bascule d'instance depuis le GUI (vraiment multi-instance)
|
||||
Demande explicite et répétée de l'opérateur : gérer les instances **depuis le GUI**, pas
|
||||
seulement en CLI. Le plan de contrôle reste maison, mais cette capacité y entre.
|
||||
|
||||
- **Inventaire résolu dynamiquement.** Le serveur GUI figeait l'inventaire au démarrage
|
||||
(`args.inventaire.resolve()`). Il est désormais relu **à chaque requête** depuis le
|
||||
symlink `instance/` (propriété `Gestionnaire.inventaire`) — la bascule prend effet sans
|
||||
redémarrer. Les chemins de plan (`FICHIER_*`) étaient déjà relatifs au symlink et suivent
|
||||
de même. `SETOPS_INVENTAIRE` force encore un inventaire fixe (CI).
|
||||
- **`POST /api/instance-utiliser`** + `basculer_instance(nom)` — repointe le symlink avec
|
||||
les garde-fous de `make instance-utiliser`, plus une **validation stricte** : `nom` doit
|
||||
être une instance **découverte** (dossier frère), ce qui interdit toute traversée de
|
||||
chemin (testé : `../etc` refusé).
|
||||
- **GUI, vue Réseau** — la table de flotte gagne un bouton **« Activer »** par instance
|
||||
(l'active affiche « active »). Il confirme (avec avertissement renforcé si l'instance est
|
||||
en **production**), bascule, puis recharge la page : toutes les vues et les déploiements
|
||||
visent la nouvelle instance.
|
||||
|
||||
L'édition du drapeau `federe` reste au CLI (rarement changé). La bascule, elle, est
|
||||
maintenant CLI **et** GUI.
|
||||
|
||||
Validé : `node --check` du GUI ; bascule testée de bout en bout (repoint → l'inventaire
|
||||
dynamique suit → traversée refusée → restauration) ; `make verifier` rc=0 CONFORME 21/21.
|
||||
|
||||
## 2026-07-23 (suite)
|
||||
|
||||
### Ajouté — gestion multi-instances : vue d'ensemble + garde-fou de collision
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ Ce que je re-découvre sinon. **Consulter avant de concevoir un nouveau mécanis
|
|||
| Ordonnancement socle-first | socle/durci avant les `client_*` | `serveur_debian`/`serveur_durci` d'abord (posent `/etc/hosts` via `hosts_statiques`) | — |
|
||||
| Sûreté check-mode | dry-run fiable | `when: not ansible_check_mode` sur les tâches de service + handlers | — |
|
||||
| Voûte au déploiement | secret jamais en clair | `ANSIBLE_VAULT_PASSWORD_FILE` / `~/.config/setops-vault-pass` ; déréférencé par `lookup('vars', <nom>)` | — |
|
||||
| Multi-instance | un dépôt par écosystème | symlink `instance/` | `multi-instances.md` |
|
||||
| Multi-instance | un dépôt par écosystème ; l'active = symlink `instance/`, les autres **découvertes par convention** (dossiers frères, aucun registre) | active : symlink `instance/` ; découverte : `scripts/instances.py` / `devis_reseau.py` (glob `../*/plan/nomenclature.yml` avec `index`) ; garde-fou collision : preuve **P21** | `multi-instances.md` |
|
||||
| Exposition → edge | app expose un FQDN public servi par un edge | `plan/domaines.yml` + `expose` (applications) | `bindings-conception.md` §4 |
|
||||
|
||||
> ⚠️ **Deux directions de binding, assumées** : `app→app` côté app (instancier),
|
||||
|
|
|
|||
|
|
@ -1292,14 +1292,33 @@ HTML = r"""<!doctype html>
|
|||
const vlan = i.vlans ? (i.vlans[0] + '-' + i.vlans[1]) : '—';
|
||||
const fed = i.federe ? '<span class="chip">fédérée</span>' : '<span class="chip" style="opacity:.6">local</span>';
|
||||
const prod = i.production ? '<span class="chip chi-tls-requis">prod</span>' : '';
|
||||
const action = i.actif
|
||||
? '<span class="chip chi-tls-requis">active</span>'
|
||||
: `<button type="button" class="fantome" onclick="basculerInstance('${echapper(i.nom)}', ${i.production ? 'true' : 'false'})">Activer</button>`;
|
||||
return `<tr>
|
||||
<td class="mono">${i.actif ? '★ ' : ''}${echapper(i.nom)}</td>
|
||||
<td class="mono">${i.index}</td>
|
||||
<td class="mono">${vlan}</td>
|
||||
<td>${fed} ${prod}</td></tr>`;
|
||||
<td>${fed} ${prod}</td>
|
||||
<td>${action}</td></tr>`;
|
||||
}).join('');
|
||||
return `${banniere}<table class="tbl-lecture"><thead><tr><th>Instance</th><th>Index</th><th>VLAN</th><th>Statut</th></tr></thead><tbody>${lignes}</tbody></table>
|
||||
<div class="hint-dep" style="margin-top:6px">★ = active (symlink <code>instance</code>). Basculer en CLI : <code>make instance-utiliser NOM=<dépôt></code>.</div>`;
|
||||
return `${banniere}<table class="tbl-lecture"><thead><tr><th>Instance</th><th>Index</th><th>VLAN</th><th>Statut</th><th>Active</th></tr></thead><tbody>${lignes}</tbody></table>
|
||||
<div class="hint-dep" style="margin-top:6px">★ = active (symlink <code>instance</code>). « Activer » bascule toutes les vues et les déploiements vers cette instance.</div>`;
|
||||
}
|
||||
async function basculerInstance(nom, estProd) {
|
||||
const avert = estProd
|
||||
? `\\n\\n⚠ « ${nom} » est une instance de PRODUCTION. Déployer y touchera l'infra réelle.`
|
||||
: '';
|
||||
if (!confirm(`Basculer l'instance active vers « ${nom} » ?\\n\\nToutes les vues et les déploiements viseront alors « ${nom} ».${avert}`)) return;
|
||||
try {
|
||||
const rep = await fetch('/api/instance-utiliser', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json', 'X-Jeton': JETON},
|
||||
body: JSON.stringify({nom})});
|
||||
const data = await rep.json();
|
||||
if (!rep.ok) { message(data.erreur || 'Bascule refusée.', 'erreur'); return; }
|
||||
message('Instance active : ' + nom + ' — rechargement…', 'ok');
|
||||
setTimeout(() => location.reload(), 500);
|
||||
} catch (e) { message('Erreur : ' + e.message, 'erreur'); }
|
||||
}
|
||||
async function dessinerReseau() {
|
||||
const cible = document.getElementById('grilles');
|
||||
|
|
@ -2375,8 +2394,39 @@ HTML = r"""<!doctype html>
|
|||
"""
|
||||
|
||||
|
||||
def basculer_instance(nom: str) -> str:
|
||||
"""Repointe le symlink `instance/` vers un depot frere. Renvoie la nouvelle cible.
|
||||
|
||||
Memes garde-fous que `make instance-utiliser`, plus une validation STRICTE : `nom`
|
||||
doit etre une instance DECOUVERTE (dossier frere avec plan/nomenclature.yml), ce
|
||||
qui interdit toute traversee de chemin (pas de '/', '..', cible arbitraire).
|
||||
"""
|
||||
import instances as _inst
|
||||
connues = {i["nom"] for i in _inst.decouvrir()}
|
||||
if nom not in connues:
|
||||
raise ValueError(f"Instance inconnue : {nom!r} (attendu : {', '.join(sorted(connues)) or 'aucune'}).")
|
||||
cible = RACINE.parent / nom
|
||||
if not cible.is_dir():
|
||||
raise ValueError(f"Dossier introuvable : ../{nom}")
|
||||
lien = RACINE / "instance"
|
||||
if lien.exists() and not lien.is_symlink():
|
||||
raise ValueError("'instance' existe et n'est pas un symlink — refus.")
|
||||
if lien.is_symlink() or lien.exists():
|
||||
lien.unlink()
|
||||
lien.symlink_to(f"../{nom}")
|
||||
return os.readlink(lien)
|
||||
|
||||
|
||||
class Gestionnaire(BaseHTTPRequestHandler):
|
||||
inventaire: Path = INVENTAIRE_DEFAUT
|
||||
# Inventaire de l'instance ACTIVE, resolu DYNAMIQUEMENT depuis le symlink
|
||||
# `instance/` a chaque acces : ainsi la bascule d'instance (POST
|
||||
# /api/instance-utiliser) prend effet sans redemarrer le serveur. Les chemins
|
||||
# de plan (FICHIER_*) sont deja relatifs au symlink et suivent de meme.
|
||||
# SETOPS_INVENTAIRE force un inventaire fixe (CI / usage explicite).
|
||||
@property
|
||||
def inventaire(self) -> Path:
|
||||
force = os.environ.get("SETOPS_INVENTAIRE")
|
||||
return Path(force) if force else _inventaire(INSTANCE, "principal", "production")
|
||||
|
||||
def log_message(self, *args) -> None: # silence par defaut
|
||||
pass
|
||||
|
|
@ -2565,6 +2615,9 @@ class Gestionnaire(BaseHTTPRequestHandler):
|
|||
elif chemin == "/api/intrants":
|
||||
ecrire_intrants(donnees)
|
||||
self.repondre_json(200, inventaire_api(self.inventaire))
|
||||
elif chemin == "/api/instance-utiliser":
|
||||
cible = basculer_instance(str(donnees.get("nom", "")).strip())
|
||||
self.repondre_json(200, {"ok": True, "cible": cible})
|
||||
elif chemin == "/api/instancier":
|
||||
# Cliquer « Appliquer le plan » EST l'intention explicite : on force
|
||||
# (le diff-refus est un garde-fou CLI ; ici le clic le confirme). git = filet.
|
||||
|
|
@ -2594,7 +2647,6 @@ def main() -> int:
|
|||
parser.add_argument("--port", type=int, default=8765)
|
||||
args = parser.parse_args()
|
||||
|
||||
Gestionnaire.inventaire = args.inventaire.resolve()
|
||||
serveur = ThreadingHTTPServer((args.hote, args.port), Gestionnaire)
|
||||
print(f"Interface inventaire: http://{args.hote}:{args.port}/")
|
||||
print(f"Inventaire: {Gestionnaire.inventaire}")
|
||||
|
|
|
|||
Loading…
Reference in a new issue