Some checks are pending
verifier / verifier (push) Waiting to run
Cinq jours, cinq defauts, tous de la meme famille : « quelle instance, quel inventaire ? » Neuf modules portaient chacun leur reponse. - 18 aout : P03 comparait chaque instance a l'inventaire d'une AUTRE ; - 19 aout : verifier_ports codait `principal/` en dur ; verifier_intrants et _frontiere_absente lisaient le symlink au lieu de la variable ; - 20 aout : devis_placement rendait un verdict juste sur le mauvais tenant ; - 22 aout : P35, puis P36 — la dixieme, trouvee par la preuve elle-meme. Aucune n'etait une faute d'inattention : chacune avait ete ecrite de bonne foi, a un moment ou le besoin semblait local. C'est le mode de panne de la duplication — pas l'erreur, mais la DERIVE, invisible depuis l'interieur d'un fichier. LA RESOLUTION UNIQUE. `inventory_rules` porte instance_courante(), inventaire_de(), dossier_inventaire() et plan_de(). Trois niveaux de repli, dont le TROISIEME manquait a la moitie des copies : un hosts.yml existant, puis un REPERTOIRE existant (instance neuve — c'est ce qui faisait echouer `make instancier` sur le modele public), puis le defaut. Vingt-huit modules y sont branches. CE QUI REND CE REFACTOR SUR : avant de toucher quoi que ce soit, chaque module a ete interroge sur ce qu'il resolvait, pour les DEUX ecosystemes. Apres refactor, meme mesure : 17 modules x 2 instances, diff VIDE. Aucune resolution n'a change — prouve, pas suppose. P41 echoue des qu'un module reintroduit une copie. Eprouvee en negatif : une copie replacee dans genome.py est signalee avec son numero de ligne. Trois exemptions nommees : instances.py et inventory_gui.py manipulent le SYMLINK lui-meme (bascule d'instance), et devis_opnsense lit deliberement quelle instance est ACTIVE. Elles parlent du lien, pas de la resolution. make verifier 41 OK, 0 echec, 0 saute ; make ci idem ; lint vert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
154 lines
6 KiB
Python
154 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
|
|
|
|
from inventory_rules import instance_courante, instance_courante # noqa: E402
|
|
|
|
RACINE = Path(__file__).resolve().parents[1]
|
|
INSTANCE = instance_courante()
|
|
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())
|