Le harnais ne verifiait qu'UNE instance et le seul modele socle. Tout ce qui vit a cote du moteur echappait au controle. Trois preuves ferment ces angles morts : - P17 (scripts/modeles.py) : TOUS les modeles valident, pas seulement socle. SETOPS_MODELES=../Set-OPS-Modeles inclut les modeles assembles prives. A trouve 6 modeles invalides sur 7 (corriges dans Set-OPS-Modeles). - P18 (scripts/voute.py) : le gabarit vault.yml.example couvre EXACTEMENT les secrets que le plan exige (bases + roles actifs + group_vars). Ne dechiffre jamais la vraie voute : compare des noms. - P19 (scripts/couverture_gui.py) : tout champ present dans un plan reel est editable par le GUI. A trouve applications.websocket (comble). Nomenclature toleree (trou connu). GUI : - champ « Liens (bindings) » dans l'inspecteur d'application : role -> cible en listes deroulantes, les roles proposes = ceux que le role porteur accepte (meta/liens.yml). Comble un manque : les bindings ne se declaraient qu'en editant le YAML a la main. - champ « WebSocket » (Collabora). - CHAMPS_ECRITS_PAR_GUI : declaration de ce que le GUI sait ecrire, verifiee par P19. Garde-fou de fond : valider_applications refuse une application posee sur un hote non declare (l'hote fantome exact qu'integral portait). Cable partout + POST du GUI. liens_acceptes()/catalogue_liens() dans inventory_rules : source unique partagee par le validateur, le GUI et instancier.py (dont la copie locale est retiree). Valide : make verifier rc=0, CONFORME 19/19, ansible-lint 0 echec, 7 modeles valident, DIFF VIDE, node --check du GUI OK. Piece justificative : docs/audit/preuve-2026-07-22.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
172 lines
6.8 KiB
Python
172 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Lit et valide le registre des applications Set-OPS (instance/plan/applications.yml)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
from inventory_gui import charger_yaml, liste_hotes
|
|
from inventory_rules import (
|
|
bases_de_application,
|
|
chaine_connexion,
|
|
charger_applications,
|
|
charger_bases_donnees,
|
|
charger_domaines,
|
|
charger_serveurs,
|
|
valider_applications,
|
|
)
|
|
|
|
RACINE = Path(__file__).resolve().parents[1]
|
|
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
|
FICHIER = INSTANCE / "plan/applications.yml"
|
|
FICHIER_BASES = INSTANCE / "plan/bases-donnees.yml"
|
|
FICHIER_DOMAINES = INSTANCE / "plan/domaines.yml"
|
|
FICHIER_SERVEURS = INSTANCE / "plan/serveurs.yml"
|
|
def _inventaire(instance: Path, *noms: str) -> Path:
|
|
forced = os.environ.get("SETOPS_INVENTAIRE")
|
|
if forced:
|
|
return Path(forced)
|
|
for nom in noms:
|
|
p = instance / "inventories" / nom / "hosts.yml"
|
|
if p.exists():
|
|
return p
|
|
return instance / "inventories" / noms[0] / "hosts.yml"
|
|
|
|
|
|
INVENTAIRE = _inventaire(INSTANCE, "principal", "production")
|
|
|
|
# Groupes 'serveurs_*' qui sont des capacites de SOCLE (sur toutes les VM),
|
|
# pas des applications a part entiere.
|
|
GROUPES_SOCLE = {"serveur_debian", "serveur_durci"}
|
|
|
|
|
|
def _liste(valeur: str) -> list:
|
|
return [v.strip() for v in (valeur or "").split(",") if v.strip()]
|
|
|
|
|
|
def ecrire(registre: dict) -> None:
|
|
entete = (
|
|
"# Registre des applications Set-OPS (application = entite pivot).\n"
|
|
"# Edite par make inventaire-ui ou scripts/applications.py.\n"
|
|
"# Une VM peut porter plusieurs applications ; une base se lie a une application.\n"
|
|
"---\n"
|
|
)
|
|
with FICHIER.open("w", encoding="utf-8") as fichier:
|
|
fichier.write(entete)
|
|
yaml.safe_dump({"applications": registre.get("applications", {}) or {}},
|
|
fichier, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
|
|
|
|
def lister(registre: dict, bases: dict) -> None:
|
|
apps = registre.get("applications", {})
|
|
if not apps:
|
|
print("Aucune application declaree.")
|
|
return
|
|
for nom, app in apps.items():
|
|
extras = []
|
|
if app.get("port"):
|
|
extras.append(f"port {app['port']}")
|
|
if app.get("requiert"):
|
|
extras.append(f"requiert {', '.join(app['requiert'])}")
|
|
if app.get("expose"):
|
|
extras.append(f"expose {', '.join(app['expose'])}")
|
|
suffixe = (" {" + " ; ".join(extras) + "}") if extras else ""
|
|
print(f"{nom} [groupe {app.get('groupe', '?')}, hote {app.get('hote', '?')}]{suffixe}")
|
|
for entree in bases_de_application(bases, application=nom,
|
|
groupe=app.get("groupe"), hote=app.get("hote")):
|
|
base = entree["base"]
|
|
portee = base.get("portee", "groupe")
|
|
print(f" DSN ({portee}/{base.get('usage', '-')}): {chaine_connexion(base, entree['serveur'])}")
|
|
|
|
|
|
def bootstrap(registre_existant: dict) -> dict:
|
|
"""(Re)genere les applications depuis les appartenances de groupes de l'inventaire.
|
|
|
|
Un groupe 'serveurs_*' (hors socle) sur un hote = une application. Les entrees
|
|
existantes correspondant a un couple (groupe, hote) toujours present sont
|
|
preservees telles quelles (port / expose / requiert ...).
|
|
"""
|
|
existant = registre_existant.get("applications") or {}
|
|
par_cle = {(a.get("groupe"), a.get("hote")): (cle, a) for cle, a in existant.items()}
|
|
apps: dict = {}
|
|
for h in liste_hotes(charger_yaml(INVENTAIRE)):
|
|
hote = h.get("nom")
|
|
if not hote:
|
|
continue
|
|
for groupe in sorted(h.get("groupes", [])):
|
|
if not groupe.startswith("serveurs_") or groupe in GROUPES_SOCLE:
|
|
continue
|
|
garde = par_cle.get((groupe, hote))
|
|
if garde:
|
|
apps[garde[0]] = garde[1]
|
|
continue
|
|
base = groupe[len("serveurs_"):]
|
|
app_id = base if base not in apps else f"{base}-{hote}"
|
|
apps[app_id] = {"groupe": groupe, "hote": hote}
|
|
return {"applications": apps}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Registre des applications Set-OPS.")
|
|
sub = parser.add_subparsers(dest="commande", required=True)
|
|
sub.add_parser("lister", help="Affiche les applications et leurs DSN resolus.")
|
|
sub.add_parser("verifier", help="Valide la coherence du registre.")
|
|
sub.add_parser("bootstrap", help="(Re)genere applications.yml depuis les groupes de l'inventaire.")
|
|
|
|
pa = sub.add_parser("ajouter", help="Ajoute ou met a jour une application.")
|
|
pa.add_argument("--id", required=True)
|
|
pa.add_argument("--groupe", required=True)
|
|
pa.add_argument("--hote", required=True)
|
|
pa.add_argument("--port", type=int)
|
|
pa.add_argument("--requiert", default="", help="Applications dont elle depend (separees par virgule).")
|
|
pa.add_argument("--expose", default="", help="FQDN publics qui la publient (separes par virgule).")
|
|
|
|
pr = sub.add_parser("retirer", help="Retire une application.")
|
|
pr.add_argument("--id", required=True)
|
|
|
|
args = parser.parse_args()
|
|
try:
|
|
registre = charger_applications(FICHIER)
|
|
domaines = charger_domaines(FICHIER_DOMAINES)
|
|
serveurs = charger_serveurs(FICHIER_SERVEURS)
|
|
if args.commande == "lister":
|
|
lister(registre, charger_bases_donnees(FICHIER_BASES))
|
|
elif args.commande == "verifier":
|
|
valider_applications(registre, domaines, serveurs)
|
|
print("Registre des applications valide.")
|
|
elif args.commande == "bootstrap":
|
|
registre = bootstrap(registre)
|
|
valider_applications(registre, domaines, serveurs)
|
|
ecrire(registre)
|
|
print(f"instance/plan/applications.yml genere depuis l'inventaire ({len(registre['applications'])} applications).")
|
|
elif args.commande == "ajouter":
|
|
app = {"groupe": args.groupe, "hote": args.hote}
|
|
if args.port is not None:
|
|
app["port"] = args.port
|
|
if _liste(args.requiert):
|
|
app["requiert"] = _liste(args.requiert)
|
|
if _liste(args.expose):
|
|
app["expose"] = _liste(args.expose)
|
|
registre["applications"][args.id] = app
|
|
valider_applications(registre, domaines, serveurs)
|
|
ecrire(registre)
|
|
print(f"Application '{args.id}' enregistree.")
|
|
elif args.commande == "retirer":
|
|
registre["applications"].pop(args.id, None)
|
|
valider_applications(registre, domaines, serveurs)
|
|
ecrire(registre)
|
|
print(f"Application '{args.id}' retiree.")
|
|
except Exception as exc:
|
|
print(f"erreur: {exc}", file=sys.stderr)
|
|
return 2
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|