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>
152 lines
6 KiB
Python
152 lines
6 KiB
Python
#!/usr/bin/env python3
|
|
"""Le GUI sait-il editer tout ce que les plans reels contiennent ?
|
|
|
|
Principe 10 : « un operateur humain exploite Set-OPS sans IA — la doc, `make` et
|
|
le GUI suffisent ». Ce principe se dement en silence des qu'un registre accepte un
|
|
champ que l'interface ne sait pas ecrire : l'operateur doit editer le YAML a la
|
|
main, et personne ne s'en apercoit avant de buter dessus.
|
|
|
|
Ce script rend le trou VISIBLE et mesurable. Il croise :
|
|
- les champs REELLEMENT presents dans les plans (instance courante + tous les
|
|
modeles decouverts par scripts/modeles.py) ;
|
|
- `CHAMPS_ECRITS_PAR_GUI` de scripts/inventory_gui.py, qui declare ce que les
|
|
fonctions de sauvegarde ecrivent.
|
|
|
|
Deux sens de verification :
|
|
1. champ dans un plan, absent du GUI -> TROU d'interface (signale) ;
|
|
2. champ declare au GUI, introuvable dans son source -> DERIVE de la
|
|
declaration (signale) — le tableau ment sur ce que le GUI fait.
|
|
|
|
Usage :
|
|
python3 scripts/couverture_gui.py lister
|
|
python3 scripts/couverture_gui.py verifier # rc=2 s'il reste un trou
|
|
python3 scripts/couverture_gui.py verifier --tolerer nomenclature
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
from inventory_gui import CHAMPS_ECRITS_PAR_GUI
|
|
from modeles import decouvrir as decouvrir_modeles
|
|
|
|
RACINE = Path(__file__).resolve().parents[1]
|
|
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
|
SOURCE_GUI = RACINE / "scripts" / "inventory_gui.py"
|
|
|
|
# (fichier du plan, cle racine, nom du registre) — les registres tables-de-tables.
|
|
REGISTRES = [
|
|
("serveurs.yml", "serveurs", "serveurs"),
|
|
("applications.yml", "applications", "applications"),
|
|
("bases-donnees.yml", "bases_donnees", "bases_donnees"),
|
|
("bases-donnees.yml", "serveurs_bd", "serveurs_bd"),
|
|
("domaines.yml", "domaines_publics", "domaines_publics"),
|
|
("nomenclature.yml", "fonctions", "nomenclature"),
|
|
]
|
|
|
|
|
|
def _plans() -> list[Path]:
|
|
"""Dossiers plan/ a inspecter : l'instance courante + tous les modeles."""
|
|
trouves = []
|
|
if (INSTANCE / "plan").is_dir():
|
|
trouves.append(INSTANCE / "plan")
|
|
trouves += [m / "plan" for m in decouvrir_modeles() if (m / "plan").is_dir()]
|
|
return trouves
|
|
|
|
|
|
def champs_utilises() -> dict[str, dict[str, set[str]]]:
|
|
"""{registre: {champ: {origines}}} — champs vus dans les plans reels."""
|
|
vus: dict[str, dict[str, set[str]]] = {r: {} for _, _, r in REGISTRES}
|
|
for plan in _plans():
|
|
etiquette = plan.parent.name
|
|
for fichier, racine_cle, registre in REGISTRES:
|
|
p = plan / fichier
|
|
if not p.is_file():
|
|
continue
|
|
try:
|
|
data = yaml.safe_load(p.read_text(encoding="utf-8")) or {}
|
|
except yaml.YAMLError:
|
|
continue
|
|
entrees = (data or {}).get(racine_cle) or {}
|
|
if not isinstance(entrees, dict):
|
|
continue
|
|
for entree in entrees.values():
|
|
if isinstance(entree, dict):
|
|
for champ in entree:
|
|
vus[registre].setdefault(str(champ), set()).add(etiquette)
|
|
return vus
|
|
|
|
|
|
def declaration_derive() -> list[str]:
|
|
"""Champs declares au GUI mais introuvables dans son source (declaration qui ment)."""
|
|
src = SOURCE_GUI.read_text(encoding="utf-8")
|
|
# On coupe le tableau lui-meme : sa presence ne prouve rien.
|
|
debut = src.find("CHAMPS_ECRITS_PAR_GUI")
|
|
fin = src.find("\n}\n", debut)
|
|
ailleurs = src[:debut] + src[fin:] if debut != -1 and fin != -1 else src
|
|
absents = []
|
|
for registre, champs in CHAMPS_ECRITS_PAR_GUI.items():
|
|
for champ in sorted(champs):
|
|
if champ not in ailleurs:
|
|
absents.append(f"{registre}.{champ}")
|
|
return absents
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Couverture du schema du plan par le GUI.")
|
|
sub = parser.add_subparsers(dest="commande", required=True)
|
|
sub.add_parser("lister", help="Detaille champ par champ.")
|
|
pv = sub.add_parser("verifier", help="Echoue s'il reste un champ non editable.")
|
|
pv.add_argument("--tolerer", action="append", default=[], metavar="REGISTRE",
|
|
help="Registre dont les trous sont connus et assumes (repetable).")
|
|
args = parser.parse_args()
|
|
|
|
utilises = champs_utilises()
|
|
|
|
if args.commande == "lister":
|
|
for registre in sorted(utilises):
|
|
couverts = CHAMPS_ECRITS_PAR_GUI.get(registre, set())
|
|
print(f"\n== {registre} ==")
|
|
for champ in sorted(utilises[registre]):
|
|
etat = "GUI" if champ in couverts else "-- hors GUI --"
|
|
print(f" {champ:<18} {etat:<16} vu dans : "
|
|
f"{', '.join(sorted(utilises[registre][champ]))}")
|
|
return 0
|
|
|
|
tolere = set(args.tolerer)
|
|
trous: list[str] = []
|
|
for registre, champs in utilises.items():
|
|
if registre in tolere:
|
|
continue
|
|
couverts = CHAMPS_ECRITS_PAR_GUI.get(registre, set())
|
|
for champ in sorted(set(champs) - couverts):
|
|
trous.append(f"{registre}.{champ} (vu dans : "
|
|
f"{', '.join(sorted(champs[champ]))})")
|
|
|
|
derive = declaration_derive()
|
|
|
|
if trous:
|
|
print(f"erreur: {len(trous)} champ(s) present(s) dans un plan reel que le GUI "
|
|
f"ne sait pas ecrire :", file=sys.stderr)
|
|
for t in trous:
|
|
print(f" - {t}", file=sys.stderr)
|
|
if derive:
|
|
print(f"erreur: {len(derive)} champ(s) declare(s) dans CHAMPS_ECRITS_PAR_GUI mais "
|
|
f"introuvable(s) dans le source du GUI : {', '.join(derive)}", file=sys.stderr)
|
|
|
|
if trous or derive:
|
|
return 2
|
|
total = sum(len(c) for c in utilises.values())
|
|
print(f"GUI : les {total} champ(s) des plans reels sont editables "
|
|
f"({len(_plans())} plan(s) inspecte(s))"
|
|
+ (f", registres toleres : {', '.join(sorted(tolere))}." if tolere else "."))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|