Set-OPS-Public/scripts/instance_creer.py
Daniel Allaire d8474d1c9f Créer une instance depuis un modèle (CLI + GUI)
Le moteur reste independant des instances ET des modeles assembles (prives) :
il ne contient que le modele public socle ; les modeles assembles sont
DECOUVERTS au runtime via SETOPS_MODELES (depot prive de l'operateur), jamais
embarques dans le code public.

- scripts/instance_creer.py : copie un modele (exemples/modeles/* + SETOPS_MODELES)
  vers un depot frere ../<nom>, y fixe l'index, et refuse un nom existant, un
  modele inconnu, ou un index en collision avec une instance federee (verifie
  AVANT toute copie). .git et hosts.genere.yml non copies.
- make instance-creer NOM=.. MODELE=.. [INDEX=N] ; make instance-modeles.
- GUI (vue Reseau) : formulaire « Creer une instance depuis un modele » sous la
  flotte. POST /api/instance-creer ; /api/instances renvoie aussi modeles +
  index_pris. Ne bascule pas l'active.

Corrige : gabarit de voute du labo complete (P18 a echoue en passant l'active
sur le labo, dont le vault.yml.example etait reste a 17 cles ; aligne sur 23).

Valide : garde-fous de creation testes en isolement (collision refusee avant
copie, ecrasement refuse, modele inconnu refuse, aucune pollution) ; node --check ;
make verifier rc=0 CONFORME 21/21 (active = labo).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:13:36 -04:00

120 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Crée un dépôt d'instance frère depuis un modèle (socle ou modèle assemblé).
Une instance = un dossier frère du moteur (`../OPS-<nom>`) avec un `plan/`. Ce script
copie un modèle vers ce dossier, y fixe éventuellement l'`index` (le seed dont dérive
tout l'adressage) en refusant une collision avec une instance fédérée existante, et
imprime la marche à suivre. Il ne bascule PAS le symlink (geste explicite) ni ne
`git init` (au choix de l'opérateur).
Modèles disponibles : `exemples/modeles/*` du dépôt + ceux de `SETOPS_MODELES`
(cf. scripts/modeles.py).
Usage :
python3 scripts/instance_creer.py --lister-modeles
python3 scripts/instance_creer.py --nom OPS-ClientX --modele socle --index 4
"""
from __future__ import annotations
import argparse
import os
import shutil
import sys
from pathlib import Path
RACINE = Path(__file__).resolve().parents[1]
FRERES = RACINE.parent
sys.path.insert(0, str(RACINE / "scripts"))
import instances as _instances # noqa: E402
import modeles as _modeles # noqa: E402
def modeles_disponibles() -> dict[str, Path]:
"""{nom: chemin} des modèles découverts (dépôt + SETOPS_MODELES)."""
return {m.name: m for m in _modeles.decouvrir()}
def index_federes_utilises() -> set[int]:
"""Index déjà pris par des instances FÉDÉRÉES (une collision casserait le trunk)."""
return {i["index"] for i in _instances.decouvrir() if i["federe"]}
def _fixer_index(nomenclature: Path, index: int) -> None:
"""Écrit `index: N` dans la nomenclature sans reformater le reste (chirurgical)."""
import re
texte = nomenclature.read_text(encoding="utf-8")
nouveau, n = re.subn(r"(?m)^index:.*$", f"index: {index}", texte)
if not n:
nouveau = re.sub(r"(?m)^(---\n)", rf"\g<1>index: {index}\n", texte, count=1) or texte
nomenclature.write_text(nouveau, encoding="utf-8")
def creer(nom: str, modele: str, index: int | None = None) -> Path:
"""Crée l'instance ../<nom> depuis <modele>. Renvoie le chemin créé."""
if not nom or "/" in nom or nom.startswith("."):
raise ValueError(f"Nom d'instance invalide : {nom!r} (dossier frère simple attendu, ex. OPS-ClientX).")
dispo = modeles_disponibles()
if modele not in dispo:
raise ValueError(f"Modèle inconnu : {modele!r}. Disponibles : {', '.join(sorted(dispo)) or 'aucun'}.")
cible = FRERES / nom
if cible.exists():
raise ValueError(f"Existe déjà : {cible} — refus (ne rien écraser).")
if index is not None and index in index_federes_utilises():
raise ValueError(f"Index {index} déjà utilisé par une instance fédérée — collision "
f"(mêmes VLAN/VMID). Choisir un index libre (cf. make instances).")
shutil.copytree(dispo[modele], cible, ignore=shutil.ignore_patterns(".git", "hosts.genere.yml"))
if index is not None:
nom_yml = cible / "plan" / "nomenclature.yml"
if nom_yml.is_file():
_fixer_index(nom_yml, index)
return cible
def _suite(nom: str, index: int | None) -> None:
print(f"\nInstance créée : ../{nom}")
print("Prochaines étapes :")
if index is None:
print(f" 1. Fixer un index UNIQUE dans ../{nom}/plan/nomenclature.yml (cf. make instances).")
print(f" 2. Régler l'identité (../{nom}/inventories/*/group_vars/all/10-intrants.yml :")
print(" domaine, organisation, realm) et la cible Proxmox (proxmox.yml).")
print(f" 3. Créer la voûte depuis le gabarit (…/group_vars/all/vault.yml.example) puis ansible-vault encrypt.")
print(f" 4. make instance-utiliser NOM={nom} && make instancier-appliquer FORCE=1")
print(f" (Optionnel : cd ../{nom} && git init, pour versionner l'instance.)")
def main() -> int:
p = argparse.ArgumentParser(description="Crée une instance Set-OPS depuis un modèle.")
p.add_argument("--lister-modeles", action="store_true", help="Liste les modèles disponibles.")
p.add_argument("--nom", help="Nom du dépôt d'instance frère (ex. OPS-ClientX).")
p.add_argument("--modele", help="Modèle de départ (ex. socle, integral).")
p.add_argument("--index", type=int, help="Index (seed d'adressage) ; refuse une collision fédérée.")
args = p.parse_args()
if args.lister_modeles:
dispo = modeles_disponibles()
if not dispo:
print("Aucun modèle découvert.")
return 0
pris = index_federes_utilises()
for nom in sorted(dispo):
print(f" {nom}")
print(f"\nIndex fédérés déjà pris : {sorted(pris) or 'aucun'} (choisir un index libre).")
return 0
if not args.nom or not args.modele:
print("Usage : make instance-creer NOM=OPS-ClientX MODELE=socle [INDEX=N]", file=sys.stderr)
print(" (make instance-modeles pour la liste des modèles)", file=sys.stderr)
return 2
try:
creer(args.nom, args.modele, args.index)
except (ValueError, OSError) as exc:
print(f"erreur: {exc}", file=sys.stderr)
return 2
_suite(args.nom, args.index)
return 0
if __name__ == "__main__":
raise SystemExit(main())