la console dit sa portee : un site n affichait aucune de ses machines
Servie par le runner d un SITE, la console montrait zero serveur sans une erreur : serveur_ops retire le lien instance sur un hebergeur, charger_yaml rend un inventaire vide sur un fichier absent, et la page dessinait ce vide comme un plan vide. La portee se DERIVE des deux symlinks (instance = je configure, underlay = je materialise), jamais d un reglage declare. Une console de site sert desormais son inventaire dynamique ; POUVOIR_REQUIS exige un pouvoir pour chaque route POST et le refus dit pourquoi ; la page nomme la console. P81 refuse une portee sans source d inventaire, un site sans machines, et toute route qui echapperait a la table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a5152a5162
commit
e46fa198fd
7 changed files with 366 additions and 7 deletions
|
|
@ -188,7 +188,7 @@ Si `ansible-lint` n’est pas disponible, le signaler clairement. Ne pas invente
|
|||
## Écrire, puis relire (D-68)
|
||||
|
||||
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
|
||||
C'est aussi ce que font les 80 preuves de `make prouver` : elles lisent le dépôt, sans le
|
||||
C'est aussi ce que font les 81 preuves de `make prouver` : elles lisent le dépôt, sans le
|
||||
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
|
||||
dépôt annonce.**
|
||||
|
||||
|
|
|
|||
55
CHANGELOG.md
55
CHANGELOG.md
|
|
@ -1,5 +1,60 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-16 (3) — La console affichait un ecosysteme vide, et c'etait le site
|
||||
|
||||
Servie par le runner d'un SITE, la console d'exploitation montrait **zero serveur, zero
|
||||
application, zero base** — sans une erreur, sans un avertissement. Le site a sept
|
||||
machines.
|
||||
|
||||
Trois faits corrects, mis bout a bout, produisaient un mensonge :
|
||||
|
||||
serveur_ops RETIRE le lien `instance` sur un runner d'hebergeur
|
||||
(juste : un lien perime vers un tenant serait pire)
|
||||
charger_yaml rend {"all": {"children": {}}} sur un fichier absent
|
||||
(juste : une console sans plan ne doit pas tomber)
|
||||
la page dessine ce vide comme un plan vide
|
||||
(faux : l'inventaire d'un site est un SCRIPT, pas un hosts.yml)
|
||||
|
||||
C'est la forme exacte que **P79** garde partout ailleurs — *une derivation qui ne trouve
|
||||
rien ne se distingue pas d'une derivation qui n'a rien a trouver* — sous son pire visage :
|
||||
celui d'un ecosysteme qu'on croit sans machines.
|
||||
|
||||
### La portee se DERIVE, elle ne se declare pas
|
||||
|
||||
Un `serveur_ops_gui_mode: site|tenant` au plan aurait ete une SECONDE liste, qui prend du
|
||||
retard des qu'on reconfigure un runner sans y penser. Les deux symlinks, eux, SONT le
|
||||
pouvoir — et `roles/serveur_ops` exige deja de savoir lequel il est :
|
||||
|
||||
instance/ -> je configure CET ecosysteme (j'ai sa voute)
|
||||
underlay.yml -> je materialise sur CETTE fabric (j'ai celle du site)
|
||||
|
||||
les deux -> poste du mainteneur instance seul -> console de locataire
|
||||
underlay seul -> console de SITE aucun -> rien a piloter, et elle le dit
|
||||
|
||||
Meme coupure que les trois portees de `docs/responsabilites-locataire-hebergeur.md` :
|
||||
calculer, configurer, materialiser.
|
||||
|
||||
### Ce que ca change
|
||||
|
||||
Une console de SITE sert desormais l'inventaire **dynamique** de ses machines
|
||||
(`site_inventaire.inventaire()`, importe — pas un sous-processus par page). Ses registres
|
||||
de plan partent vides **avec leur raison** : on ne fabrique pas un faux plan de tenant
|
||||
pour remplir un ecran.
|
||||
|
||||
`POUVOIR_REQUIS` exige un pouvoir pour CHAQUE route POST, et le refus **dit pourquoi** :
|
||||
« interdit » sans raison envoie chercher une panne la ou il n'y en a pas. La garde est au
|
||||
serveur ; le bouton grise n'est qu'une politesse.
|
||||
|
||||
Et la page **nomme** la console. Trois consoles identiques a trois URL differentes etaient
|
||||
un piege pour qui en ouvre deux.
|
||||
|
||||
### P81
|
||||
|
||||
Elle refuse une portee sans source d'inventaire, un site dont l'inventaire ne rend aucune
|
||||
machine, et **toute route POST absente de la table** — c'est la garde anti-derive : une
|
||||
route d'ecriture ajoutee demain s'executerait sinon sur une console qui n'en a pas le
|
||||
pouvoir, et personne ne le verrait avant l'incident.
|
||||
|
||||
## 2026-09-16 (2) — Les responsabilites, deduites des pouvoirs
|
||||
|
||||
Un partage de responsabilites se redige d'habitude en distribuant des devoirs. Celui-ci
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||
|
||||
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
|
||||
- **Verdict** : ✅ CONFORME (79 OK · 0 echec · 1 saute)
|
||||
- **Verdict** : ✅ CONFORME (80 OK · 0 echec · 1 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
|
||||
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 13 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 13. |
|
||||
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
|
||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (80 preuves, 68 roles, 41 groupes). |
|
||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (81 preuves, 68 roles, 41 groupes). |
|
||||
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
|
||||
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
|
||||
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `392da4d` (publie le 2026-09-14). |
|
||||
|
|
@ -93,6 +93,7 @@
|
|||
| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). |
|
||||
| P79 | Replis silencieux : une derivation vide ne passe pas pour un succes | — | ✅ OK | 6 ecosysteme(s) (instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, OPS-Patient0, SITE-Chezlepro) : pattes, edges, certificats, rechargemen |
|
||||
| P80 | Remise au client : inscrite, nommee, et son second temps a l'heure | — | ✅ OK | Aucun ecosysteme remis a un client : rien a tenir. |
|
||||
| P81 | La console dit sa portee, et ne sert pas un inventaire vide en silence | — | ✅ OK | Portee `poste` derivee des symlinks, source d'inventaire `instance`, et 14 route(s) POST exigent toutes un pouvoir. |
|
||||
|
||||
## Couverture des affirmations ✅ du registre
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ make placement-plan # chaque VM est-elle là où le plan la met
|
|||
|
||||
## Le trou qu'il comble
|
||||
|
||||
`scripts/prouver.py` porte 80 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||
`scripts/prouver.py` porte 81 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
|
||||
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
|
||||
propriétaire, que rien n'est codé en dur.
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ Nommer un point ouvert vaut mieux qu'une ligne rassurante sans mécanisme derri
|
|||
Aucune de ces responsabilités n'est laissée à la parole :
|
||||
|
||||
```
|
||||
make prouver le dépôt est-il cohérent avec lui-même (80 preuves, zéro réseau)
|
||||
make prouver le dépôt est-il cohérent avec lui-même (81 preuves, zéro réseau)
|
||||
make remise-verifier le second temps de la remise est-il fait, ou en retard ? (P80)
|
||||
make certificats-plan ce que le disque porte contre ce que la mémoire sert
|
||||
make expositions-plan chaque service publié répond-il, et depuis où
|
||||
|
|
|
|||
|
|
@ -110,6 +110,171 @@ def _depot_hebergeur() -> Path | None:
|
|||
return FICHIER_UNDERLAY.resolve().parent
|
||||
|
||||
|
||||
# --- CE QUE CETTE CONSOLE PEUT, DERIVE DE CE QUE SA MACHINE PORTE --------------------
|
||||
#
|
||||
# LE DEFAUT QUI A RENDU CECI NECESSAIRE (2026-09-16). Servie par le runner d'un SITE,
|
||||
# cette console affichait ZERO serveur, ZERO application, ZERO base — sans une erreur.
|
||||
# `serveur_ops` RETIRE le lien `instance` sur un runner d'hebergeur (a juste titre : un
|
||||
# lien perime vers un tenant serait pire), `charger_yaml` rend `{"all": {"children": {}}}`
|
||||
# sur un fichier absent, et la page dessinait ce vide comme un plan vide.
|
||||
#
|
||||
# C'est la forme exacte que P79 garde partout ailleurs : une derivation qui ne trouve rien
|
||||
# ne se distingue pas d'une derivation qui n'a rien a trouver. Ici elle avait le pire
|
||||
# visage possible — celui d'un ecosysteme qu'on croit sans machines.
|
||||
#
|
||||
# ON DERIVE, ON NE DECLARE PAS. Un `serveur_ops_gui_mode: site|tenant` au plan serait une
|
||||
# SECONDE liste, et une liste qui suit une autre prend du retard : il suffirait de
|
||||
# reconfigurer un runner sans y penser. Les deux symlinks, eux, SONT le pouvoir :
|
||||
#
|
||||
# instance/ -> je configure CET ecosysteme (j'ai sa voute)
|
||||
# underlay.yml -> je materialise sur CETTE fabric (j'ai celle du site)
|
||||
#
|
||||
# C'est la meme coupure que `roles/serveur_ops` exige deja dans son assertion, et que
|
||||
# `docs/responsabilites-locataire-hebergeur.md` nomme : calculer, configurer, materialiser.
|
||||
|
||||
def contexte() -> dict:
|
||||
"""La portee de cette console, et la raison de chaque interdit.
|
||||
|
||||
Quatre cas, dont deux seulement se rencontrent sur une machine deployee :
|
||||
|
||||
poste les deux liens — l'atelier du mainteneur, rien n'est retire
|
||||
tenant `instance/` seul — il configure, il ne materialise pas
|
||||
site `underlay.yml` seul — il materialise, il n'entre chez personne
|
||||
orphelin aucun des deux — il ne pilote rien, et c'est ce qu'il doit dire
|
||||
"""
|
||||
a_instance = INSTANCE.exists()
|
||||
a_fabric = FICHIER_UNDERLAY.exists()
|
||||
depot_site = _depot_hebergeur()
|
||||
ecosysteme = INSTANCE.resolve().name if a_instance else None
|
||||
fabric = depot_site.name if depot_site else None
|
||||
|
||||
portee = ("poste" if a_instance and a_fabric else
|
||||
"tenant" if a_instance else
|
||||
"site" if a_fabric else "orphelin")
|
||||
|
||||
peut = {
|
||||
# Configurer et editer le plan supposent un ecosysteme monte ET sa voute.
|
||||
"configurer": a_instance,
|
||||
"editer_le_plan": a_instance,
|
||||
# Materialiser suppose de savoir sur quelle fabric poser la VM.
|
||||
"materialiser": a_fabric,
|
||||
# Les devis de fabric (commutateurs, frontiere, SDN, pools) appartiennent a
|
||||
# l'hebergeur. Un locataire les LIT — ils restent affiches — mais la copie qu'il
|
||||
# porte est un miroir, et le dire vaut mieux que le laisser croire.
|
||||
"fabric": a_fabric,
|
||||
}
|
||||
raison = {
|
||||
"configurer": ("Cette console materialise le terrain ; elle n'a la voute d'aucun "
|
||||
"locataire, donc elle ne configure aucun de leurs services."),
|
||||
"editer_le_plan": ("Aucun ecosysteme n'est monte ici : il n'y a pas de plan a "
|
||||
"editer. L'inventaire de cette machine est DYNAMIQUE."),
|
||||
"materialiser": ("Cette console configure un ecosysteme ; elle ne sait pas sur "
|
||||
"quelle fabric poser une VM, et c'est voulu : exploiter n'est "
|
||||
"pas engendrer."),
|
||||
"fabric": ("La fabric appartient a l'hebergeur. Ce qui s'affiche ici vient d'une "
|
||||
"copie locale, et une copie avait deja diverge."),
|
||||
}
|
||||
titres = {
|
||||
"poste": f"Poste du mainteneur — {ecosysteme} sur {fabric}",
|
||||
"tenant": f"Console du locataire — {ecosysteme}",
|
||||
"site": f"Console du SITE — {fabric}",
|
||||
"orphelin": "Console sans ecosysteme — rien a piloter",
|
||||
}
|
||||
return {
|
||||
"portee": portee,
|
||||
"titre": titres[portee],
|
||||
"ecosysteme": ecosysteme,
|
||||
"fabric": fabric,
|
||||
"peut": peut,
|
||||
"raison": raison,
|
||||
# QUELLE SOURCE D'INVENTAIRE, et c'est le coeur du correctif : un site n'a pas de
|
||||
# `hosts.yml`, son inventaire est un SCRIPT.
|
||||
"inventaire": ("site" if portee == "site" else
|
||||
"instance" if a_instance else None),
|
||||
}
|
||||
|
||||
|
||||
def inventaire_du_site() -> dict:
|
||||
"""Les machines du SITE, mises dans la forme d'un `hosts.yml`.
|
||||
|
||||
L'inventaire d'un site est un SCRIPT (`site_inventaire.py`) : on l'EXECUTE — en
|
||||
l'important, sans sous-processus a chaque page — et l'on traduit son resultat dans la
|
||||
forme que le reste de ce fichier sait lire. Juger sa source reviendrait a relire le
|
||||
raisonnement au lieu du resultat.
|
||||
"""
|
||||
import site_inventaire
|
||||
brut = site_inventaire.inventaire() or {}
|
||||
hostvars = ((brut.get("_meta") or {}).get("hostvars") or {})
|
||||
enfants_site: dict = {}
|
||||
for groupe, valeur in brut.items():
|
||||
if groupe == "_meta" or not isinstance(valeur, dict):
|
||||
continue
|
||||
hotes = valeur.get("hosts") or []
|
||||
if not hotes:
|
||||
continue
|
||||
enfants_site[groupe] = {"hosts": {h: dict(hostvars.get(h) or {}) for h in hotes}}
|
||||
return {"all": {"children": enfants_site}}
|
||||
|
||||
|
||||
def inventaire_api_du_site(ctx: dict) -> dict:
|
||||
"""La charge utile d'une console de SITE : ses machines, et AUCUN plan a editer.
|
||||
|
||||
On ne fabrique pas un faux plan de tenant pour remplir l'ecran. Les registres partent
|
||||
vides, et le contexte dit POURQUOI — un vide explique n'est plus un vide silencieux.
|
||||
"""
|
||||
data = inventaire_du_site()
|
||||
return {
|
||||
"flux": flux_api(),
|
||||
"couches": couches_api(),
|
||||
"inventaire": f"{ctx['fabric']} (inventaire dynamique)",
|
||||
"production": True,
|
||||
"groupes": groupes_disponibles(data),
|
||||
"dependances": charger_dependances(FICHIER_DEPENDANCES),
|
||||
"nomenclature": {},
|
||||
"chaine": roles_des_groupes(),
|
||||
"bases": {},
|
||||
"applications": {},
|
||||
"domaines": {},
|
||||
"serveurs": {},
|
||||
"hotes": liste_hotes(data),
|
||||
"intrants": None,
|
||||
"integrations_disponibles": integrations_disponibles(),
|
||||
"integrations_universelles": integrations_universelles(RACINE),
|
||||
"liens_acceptes": catalogue_liens(RACINE),
|
||||
"schema": schema_du_plan(),
|
||||
"contexte": ctx,
|
||||
}
|
||||
|
||||
|
||||
def sonder_donnees(data: dict) -> dict:
|
||||
"""Meme sonde que `sonder_hotes`, sur un inventaire DEJA charge (le site)."""
|
||||
cibles = [(h.get("nom"), h.get("adresse_ip")) for h in liste_hotes(data)]
|
||||
with ThreadPoolExecutor(max_workers=16) as executeur:
|
||||
resultats = executeur.map(lambda c: (c[0], _joignable(c[1])), cibles)
|
||||
return {nom: ok for nom, ok in resultats if nom}
|
||||
|
||||
|
||||
# CE QUE CHAQUE POINT D'ECRITURE EXIGE. Toute route POST y figure — P81 refuse qu'une
|
||||
# route nouvelle echappe a la garde en silence, parce qu'une garde qui suit une liste
|
||||
# prend du retard sur elle.
|
||||
POUVOIR_REQUIS = {
|
||||
"/api/inventaire": "editer_le_plan",
|
||||
"/api/bases": "editer_le_plan",
|
||||
"/api/applications": "editer_le_plan",
|
||||
"/api/serveurs": "editer_le_plan",
|
||||
"/api/nomenclature": "editer_le_plan",
|
||||
"/api/domaines": "editer_le_plan",
|
||||
"/api/intrants": "editer_le_plan",
|
||||
"/api/instancier": "editer_le_plan",
|
||||
"/api/instance-creer": "editer_le_plan",
|
||||
"/api/instance-utiliser": "editer_le_plan",
|
||||
"/api/verifier": "configurer",
|
||||
"/api/deployer": "configurer",
|
||||
"/api/pousser": "configurer",
|
||||
"/api/creer": "materialiser",
|
||||
}
|
||||
|
||||
|
||||
def _fichier_frontiere() -> Path:
|
||||
"""Les intrants de la frontiere — a la RACINE du depot de site depuis le 2026-08-22.
|
||||
|
||||
|
|
@ -1569,6 +1734,8 @@ HTML = r"""<!doctype html>
|
|||
<header>
|
||||
<div class="marque"><span class="pastille"></span><div class="titre"><h1>Set-OPS</h1><span class="slogan">Votre artisan numérique</span></div></div>
|
||||
<span class="chemin" id="chemin"></span>
|
||||
<span id="portee" style="font-size:11.5px;font-weight:800;letter-spacing:.02em;padding:3px 9px;border-radius:999px;white-space:nowrap"
|
||||
title="Ce que cette console peut faire — dérivé de ce que sa machine porte, pas d'un réglage"></span>
|
||||
<div class="grandit"></div>
|
||||
<div class="resume" id="resume"></div>
|
||||
<div class="barre" title="Raccourcis clavier — 1-3 : changer de vue · j/k : naviguer les serveurs · v : vérifier · d : déployer · / : filtrer · Ctrl+S : sauvegarder la vue">
|
||||
|
|
@ -1717,9 +1884,48 @@ HTML = r"""<!doctype html>
|
|||
setTimeout(() => { if (t.parentNode) t.parentNode.removeChild(t); }, 260);
|
||||
}
|
||||
|
||||
let contexte = null;
|
||||
|
||||
// LA CONSOLE DIT CE QU'ELLE EST. Trois consoles identiques à trois URL différentes
|
||||
// sont un piège pour qui en ouvre deux : celle d'un site et celle d'un locataire ne
|
||||
// peuvent pas les mêmes gestes, et rien ne le disait.
|
||||
function appliquerContexte(ctx) {
|
||||
contexte = ctx || null;
|
||||
const badge = document.getElementById('portee');
|
||||
if (!badge || !ctx) return;
|
||||
const couleurs = {
|
||||
site: ['#7c4a02', 'color-mix(in srgb, #b9860b 18%, var(--surface))'],
|
||||
tenant: ['#0d3b34', 'color-mix(in srgb, var(--teal) 16%, var(--surface))'],
|
||||
poste: ['var(--muted)', 'var(--surface-3)'],
|
||||
orphelin: ['var(--rouge)', 'color-mix(in srgb, var(--rouge) 14%, var(--surface))'],
|
||||
};
|
||||
const [fg, bg] = couleurs[ctx.portee] || couleurs.poste;
|
||||
badge.textContent = ctx.titre;
|
||||
badge.style.color = fg;
|
||||
badge.style.background = bg;
|
||||
document.body.dataset.portee = ctx.portee;
|
||||
// Ce que la portée interdit ne se propose pas. Le serveur refuse de toute façon
|
||||
// (POUVOIR_REQUIS) ; ceci évite d'aller au refus pour l'apprendre.
|
||||
const verrou = (el, permis, pourquoi) => {
|
||||
if (!el) return;
|
||||
el.disabled = !permis;
|
||||
el.title = permis ? (el.title || '') : pourquoi;
|
||||
el.style.opacity = permis ? '' : '.45';
|
||||
};
|
||||
verrou(document.getElementById('btn-sauver'), ctx.peut.editer_le_plan,
|
||||
ctx.raison.editer_le_plan);
|
||||
document.querySelectorAll('button[onclick^="ouvrirIntrants"]').forEach(
|
||||
b => verrou(b, ctx.peut.editer_le_plan, ctx.raison.editer_le_plan));
|
||||
}
|
||||
|
||||
async function charger() {
|
||||
try {
|
||||
const ctxRep = await fetch('/api/contexte');
|
||||
if (ctxRep.ok) appliquerContexte(await ctxRep.json());
|
||||
} catch (e) { /* une console sans contexte reste utilisable, en aveugle */ }
|
||||
const reponse = await fetch('/api/inventaire');
|
||||
const data = await reponse.json();
|
||||
if (data.contexte) appliquerContexte(data.contexte);
|
||||
groupes = data.groupes;
|
||||
dependances = data.dependances || {};
|
||||
nomenclature = data.nomenclature || {};
|
||||
|
|
@ -3779,10 +3985,20 @@ class Gestionnaire(BaseHTTPRequestHandler):
|
|||
self.repondre(200, page.encode("utf-8"), "text/html; charset=utf-8")
|
||||
elif chemin == "/favicon.ico":
|
||||
self.repondre(204, b"", "image/x-icon")
|
||||
elif chemin == "/api/contexte":
|
||||
self.repondre_json(200, contexte())
|
||||
elif chemin == "/api/inventaire":
|
||||
self.repondre_json(200, inventaire_api(self.inventaire))
|
||||
ctx = contexte()
|
||||
if ctx["inventaire"] == "site":
|
||||
self.repondre_json(200, inventaire_api_du_site(ctx))
|
||||
else:
|
||||
self.repondre_json(200, {**inventaire_api(self.inventaire),
|
||||
"contexte": ctx})
|
||||
elif chemin == "/api/sondes":
|
||||
self.repondre_json(200, sonder_hotes(self.inventaire))
|
||||
if contexte()["inventaire"] == "site":
|
||||
self.repondre_json(200, sonder_donnees(inventaire_du_site()))
|
||||
else:
|
||||
self.repondre_json(200, sonder_hotes(self.inventaire))
|
||||
elif chemin == "/api/devis-reseau":
|
||||
tenants = devis_reseau.decouvrir()
|
||||
self.repondre_json(200, {
|
||||
|
|
@ -3827,6 +4043,16 @@ class Gestionnaire(BaseHTTPRequestHandler):
|
|||
except json.JSONDecodeError:
|
||||
self.repondre_json(400, {"erreur": "Corps JSON invalide."})
|
||||
return
|
||||
# LA GARDE EST ICI, PAS DANS LE NAVIGATEUR. Un bouton grise est une politesse ;
|
||||
# ce qui protege, c'est un serveur qui refuse. Et il DIT pourquoi : « interdit »
|
||||
# sans raison envoie chercher une panne la ou il n'y en a pas.
|
||||
ctx = contexte()
|
||||
besoin = POUVOIR_REQUIS.get(chemin)
|
||||
if besoin and not ctx["peut"].get(besoin):
|
||||
self.repondre_json(409, {"erreur": ctx["raison"].get(besoin, "Geste hors de "
|
||||
"la portee de cette console."),
|
||||
"portee": ctx["portee"], "pouvoir": besoin})
|
||||
return
|
||||
try:
|
||||
if chemin == "/api/inventaire":
|
||||
self.repondre_json(409, {"erreur": "hosts.yml est genere depuis le plan. "
|
||||
|
|
|
|||
|
|
@ -4310,6 +4310,81 @@ def preuve_remise_tenue() -> tuple[bool, str]:
|
|||
return True, f"{len(tenus)} remise(s) tenue(s) : {', '.join(tenus)}."
|
||||
|
||||
|
||||
def preuve_console_dit_sa_portee() -> tuple[bool, str]:
|
||||
"""Une console ne sert jamais un inventaire vide en silence, et rien n'echappe a sa garde.
|
||||
|
||||
POURQUOI (2026-09-16). Servie par le runner d'un SITE, la console d'exploitation
|
||||
affichait ZERO serveur, ZERO application, ZERO base — sans une erreur. `serveur_ops`
|
||||
retire le lien `instance` sur un runner d'hebergeur (a juste titre), `charger_yaml`
|
||||
rend un inventaire vide sur un fichier absent, et la page dessinait ce vide comme un
|
||||
plan vide. Le site a sept machines ; sa console n'en montrait aucune.
|
||||
|
||||
CE QU'ELLE VERIFIE, hors ligne :
|
||||
|
||||
1. La portee se DERIVE des deux symlinks — `instance/` (je configure), `underlay.yml`
|
||||
(je materialise) — et jamais d'un reglage declare. Un reglage serait une seconde
|
||||
liste, et une liste qui suit une autre prend du retard.
|
||||
2. Une console qui a quelque chose a piloter nomme sa SOURCE d'inventaire. Aucune
|
||||
portee pilotable ne rend `inventaire: null`.
|
||||
3. La source du SITE rend vraiment des machines quand le site en declare : c'est le
|
||||
defaut d'origine, et il ne doit pas pouvoir revenir.
|
||||
4. TOUTE route POST figure dans `POUVOIR_REQUIS`. C'est la garde anti-derive : une
|
||||
route d'ecriture ajoutee demain sans y penser s'executerait sur une console qui
|
||||
n'en a pas le pouvoir, et personne ne le verrait avant l'incident.
|
||||
|
||||
CE QU'ELLE NE PROUVE PAS : qu'une console REFUSE vraiment. Cela demande de la lancer et
|
||||
de lui parler ; ici on verifie qu'elle a de quoi refuser, et que rien n'y echappe.
|
||||
"""
|
||||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
import inventory_gui as gui
|
||||
|
||||
fautes: list[str] = []
|
||||
ctx = gui.contexte()
|
||||
|
||||
if ctx["portee"] not in ("poste", "tenant", "site", "orphelin"):
|
||||
fautes.append(f"portee inconnue : {ctx['portee']}")
|
||||
if ctx["portee"] != "orphelin" and not ctx.get("inventaire"):
|
||||
fautes.append(f"portee `{ctx['portee']}` sans source d'inventaire nommee — c'est "
|
||||
f"exactement le vide silencieux que cette preuve existe pour refuser")
|
||||
|
||||
# La source du site, verifiee CHEZ le site, quand un underlay est monte.
|
||||
if gui.FICHIER_UNDERLAY.exists():
|
||||
try:
|
||||
data = gui.inventaire_du_site()
|
||||
hotes = {h for g in (data.get("all") or {}).get("children", {}).values()
|
||||
for h in (g.get("hosts") or {})}
|
||||
except Exception as e: # noqa: BLE001
|
||||
hotes = set()
|
||||
fautes.append(f"l'inventaire du site ne s'execute pas ({type(e).__name__}) : "
|
||||
f"une console de site n'aurait rien a montrer")
|
||||
if not hotes:
|
||||
fautes.append("un underlay est monte et l'inventaire du site ne rend AUCUNE "
|
||||
"machine — la console de site afficherait un ecran vide")
|
||||
|
||||
# Toute route POST doit exiger un pouvoir. On lit la SOURCE, pas la table : une table
|
||||
# qui se decrirait elle-meme ne prouverait rien.
|
||||
source = (RACINE / "scripts" / "inventory_gui.py").read_text(encoding="utf-8")
|
||||
debut = source.find("def do_POST")
|
||||
routes = set(re.findall(r'chemin == "(/api/[a-z-]+)"', source[debut:])) if debut >= 0 else set()
|
||||
if not routes:
|
||||
fautes.append("aucune route POST trouvee dans le GUI : la lecture de la source a "
|
||||
"change de forme, et cette preuve ne prouve plus rien")
|
||||
manquantes = sorted(routes - set(gui.POUVOIR_REQUIS))
|
||||
if manquantes:
|
||||
fautes.append(f"route(s) POST sans pouvoir exige : {', '.join(manquantes)} — elles "
|
||||
f"s'executeraient sur une console qui n'en a pas le droit")
|
||||
inutiles = sorted(set(gui.POUVOIR_REQUIS) - routes)
|
||||
if inutiles:
|
||||
fautes.append(f"`POUVOIR_REQUIS` nomme des routes qui n'existent plus : "
|
||||
f"{', '.join(inutiles)}")
|
||||
|
||||
if fautes:
|
||||
return False, ("La console ne dit pas ce qu'elle peut :\n - " + "\n - ".join(fautes))
|
||||
return True, (f"Portee `{ctx['portee']}` derivee des symlinks, source d'inventaire "
|
||||
f"`{ctx['inventaire']}`, et {len(routes)} route(s) POST exigent toutes "
|
||||
f"un pouvoir.")
|
||||
|
||||
|
||||
PREUVES: list[dict] = [
|
||||
{"id": "P01", "titre": "Lint (ansible-lint)", "refs": ["AFF-006"],
|
||||
"cmds": [["ansible-lint", "-q"]]},
|
||||
|
|
@ -4490,6 +4565,8 @@ PREUVES: list[dict] = [
|
|||
"refs": [], "func": preuve_une_derivation_vide_se_dit},
|
||||
{"id": "P80", "titre": "Remise au client : inscrite, nommee, et son second temps a l'heure",
|
||||
"refs": [], "func": preuve_remise_tenue},
|
||||
{"id": "P81", "titre": "La console dit sa portee, et ne sert pas un inventaire vide en silence",
|
||||
"refs": [], "func": preuve_console_dit_sa_portee},
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue