Set-OPS-Public/scripts/modeles.py
Daniel Allaire 7256486c9c modèles : l'underlay devient modélisable, et P17 le valide
Un modèle décrivait un tenant — ses services, ses zones, ses bases. Tous les
hébergeurs n'ont pas le même matériel : l'infrastructure physique mérite le
même traitement.

Le modèle public gagne un underlay volontairement minimal (un commutateur,
pas de fabric de stockage séparée), point de départ honnête d'un petit
hébergeur. Les montages plus riches sont d'autres modèles, conformément à la
doctrine : un générique public, les étoffés en privé.

Le modèle contient désormais deux moitiés qui ne vont pas au même endroit :
`plan/` et `inventories/` chez le tenant, `underlay.yml` chez l'hébergeur.

`modeles.py verifier` le valide (P17), facultativement et sur sa cohérence
INTERNE seulement — pas contre les tenants fédérés réels, un modèle étant un
gabarit et non un site déployé. Il a fallu rendre paramétrables deux
hypothèses du validateur, qui lisait la nomenclature de l'instance active et
globait les dépôts frères ; comportement par défaut inchangé.

Cinq cas de rejet exercés : VLAN empiétant sur la plage tenant, passerelle au
mauvais dernier octet (lue dans la nomenclature du modèle), routeur inconnu,
sortie hors du lien, port déclaré deux fois.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:11:18 -04:00

149 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""Valide TOUS les modeles d'instance, pas seulement le socle public.
Un modele est un dossier portant un `plan/` (serveurs, applications, bases,
domaines, nomenclature). Ce script rejoue sur chacun les MEMES validateurs que
sur une instance reelle — il ne reimplemente aucune regle.
Pourquoi : les modeles assembles vivent dans un depot separe (actif commercial).
Rien ne les verifiait, et `integral` a porte pendant des semaines une application
posee sur un hote jamais declare. Un modele defectueux se propage a chaque
locataire cree depuis lui.
Decouverte des modeles :
- `exemples/modeles/*` du depot (le socle public) ;
- plus les chemins de la variable d'environnement SETOPS_MODELES
(separes par ':'). Un chemin peut designer un modele OU un dossier de
modeles. Absent = ignore en silence (le depot prive n'est pas toujours la).
Usage :
python3 scripts/modeles.py lister
python3 scripts/modeles.py verifier
SETOPS_MODELES=../Set-OPS-Modeles python3 scripts/modeles.py verifier
"""
from __future__ import annotations
import argparse
import os
import subprocess
import sys
from pathlib import Path
RACINE = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(RACINE / "scripts"))
MODELES_DEPOT = RACINE / "exemples" / "modeles"
VALIDATEURS = ("domaines", "serveurs", "applications", "bases_donnees")
def _est_modele(p: Path) -> bool:
return p.is_dir() and (p / "plan").is_dir()
def decouvrir() -> list[Path]:
"""Modeles du depot + ceux designes par SETOPS_MODELES. Tries, dedoublonnes."""
trouves: list[Path] = []
if MODELES_DEPOT.is_dir():
trouves += [p for p in sorted(MODELES_DEPOT.iterdir()) if _est_modele(p)]
for brut in (os.environ.get("SETOPS_MODELES") or "").split(":"):
brut = brut.strip()
if not brut:
continue
chemin = Path(brut)
if not chemin.is_absolute():
chemin = (RACINE / chemin).resolve()
if not chemin.is_dir():
continue
if _est_modele(chemin):
trouves.append(chemin)
else:
trouves += [p for p in sorted(chemin.iterdir()) if _est_modele(p)]
vus: dict[str, Path] = {}
for p in trouves:
vus.setdefault(str(p.resolve()), p)
return list(vus.values())
def verifier_underlay(modele: Path) -> list[str]:
"""Valide l'underlay du modele, s'il en porte un. Liste vide sinon.
Un modele d'underlay decrit le MATERIEL d'un hebergeur : tous n'ont pas les memes
equipements, d'ou l'interet d'en avoir plusieurs. On le valide pour sa coherence
INTERNE (VLAN sous la plage tenant, sous-reseaux disjoints, passerelle dans son
reseau, routeur declare, ports non dupliques, dernier octet partage) — mais PAS
contre les tenants federes reels : un modele est un gabarit, pas un site deploye.
D'ou les listes de tenants vides, et la nomenclature du modele plutot que celle de
l'instance active.
"""
fichier = modele / "underlay.yml"
if not fichier.is_file():
return []
import underlay as underlay_mod
u = underlay_mod.charger_depuis(fichier)
if u is None:
return ["underlay: fichier present mais bloc `underlay:` absent ou vide"]
erreurs = underlay_mod.valider(
u, tenants_vlans=set(), tenants_supernets=[],
plan_nomenclature=modele / "plan" / "nomenclature.yml",
)
return [f"underlay: {e}" for e in erreurs]
def verifier_un(modele: Path) -> list[str]:
"""Rejoue les validateurs de registres sur un modele, + son underlay s'il en a un."""
erreurs: list[str] = []
env = {**os.environ, "SETOPS_INSTANCE": str(modele)}
env.pop("SETOPS_INVENTAIRE", None)
for nom in VALIDATEURS:
r = subprocess.run([sys.executable, f"scripts/{nom}.py", "verifier"],
cwd=RACINE, env=env, capture_output=True, text=True)
if r.returncode != 0:
lignes = (r.stdout + r.stderr).strip().splitlines()
erreurs.append(f"{nom}: {lignes[-1] if lignes else 'rc!=0'}")
erreurs += verifier_underlay(modele)
return erreurs
def main() -> int:
parser = argparse.ArgumentParser(description="Validation des modeles d'instance Set-OPS.")
sub = parser.add_subparsers(dest="commande", required=True)
sub.add_parser("lister", help="Liste les modeles decouverts.")
sub.add_parser("verifier", help="Valide les registres de chaque modele.")
args = parser.parse_args()
modeles = decouvrir()
if args.commande == "lister":
if not modeles:
print("Aucun modele decouvert.")
return 0
for m in modeles:
try:
rel = m.relative_to(RACINE)
except ValueError:
rel = m
print(f"{m.name} ({rel})")
return 0
if not modeles:
print("Aucun modele decouvert : rien a verifier.")
return 0
en_faute = 0
for m in modeles:
erreurs = verifier_un(m)
if erreurs:
en_faute += 1
print(f"MODELE {m.name} : INVALIDE", file=sys.stderr)
for e in erreurs:
print(f" - {e}", file=sys.stderr)
else:
print(f"MODELE {m.name} : valide")
if en_faute:
print(f"erreur: {en_faute} modele(s) invalide(s) sur {len(modeles)}.", file=sys.stderr)
return 2
print(f"Les {len(modeles)} modele(s) decouvert(s) valident.")
return 0
if __name__ == "__main__":
raise SystemExit(main())