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)
|
## Écrire, puis relire (D-68)
|
||||||
|
|
||||||
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
|
`--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
|
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
|
||||||
dépôt annonce.**
|
dépôt annonce.**
|
||||||
|
|
||||||
|
|
|
||||||
55
CHANGELOG.md
55
CHANGELOG.md
|
|
@ -1,5 +1,60 @@
|
||||||
# CHANGELOG — Set-OPS
|
# 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
|
## 2026-09-16 (2) — Les responsabilites, deduites des pouvoirs
|
||||||
|
|
||||||
Un partage de responsabilites se redige d'habitude en distribuant des devoirs. Celui-ci
|
Un partage de responsabilites se redige d'habitude en distribuant des devoirs. Celui-ci
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
> [`docs/audit/affirmations.md`](affirmations.md).
|
> [`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`
|
- **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
|
## 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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). |
|
| 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). |
|
| 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). |
|
| 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 |
|
| 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. |
|
| 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
|
## 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
|
## 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
|
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
|
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
|
||||||
propriétaire, que rien n'est codé en dur.
|
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 :
|
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 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 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ù
|
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
|
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:
|
def _fichier_frontiere() -> Path:
|
||||||
"""Les intrants de la frontiere — a la RACINE du depot de site depuis le 2026-08-22.
|
"""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>
|
<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>
|
<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 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="grandit"></div>
|
||||||
<div class="resume" id="resume"></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">
|
<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);
|
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() {
|
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 reponse = await fetch('/api/inventaire');
|
||||||
const data = await reponse.json();
|
const data = await reponse.json();
|
||||||
|
if (data.contexte) appliquerContexte(data.contexte);
|
||||||
groupes = data.groupes;
|
groupes = data.groupes;
|
||||||
dependances = data.dependances || {};
|
dependances = data.dependances || {};
|
||||||
nomenclature = data.nomenclature || {};
|
nomenclature = data.nomenclature || {};
|
||||||
|
|
@ -3779,10 +3985,20 @@ class Gestionnaire(BaseHTTPRequestHandler):
|
||||||
self.repondre(200, page.encode("utf-8"), "text/html; charset=utf-8")
|
self.repondre(200, page.encode("utf-8"), "text/html; charset=utf-8")
|
||||||
elif chemin == "/favicon.ico":
|
elif chemin == "/favicon.ico":
|
||||||
self.repondre(204, b"", "image/x-icon")
|
self.repondre(204, b"", "image/x-icon")
|
||||||
|
elif chemin == "/api/contexte":
|
||||||
|
self.repondre_json(200, contexte())
|
||||||
elif chemin == "/api/inventaire":
|
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":
|
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":
|
elif chemin == "/api/devis-reseau":
|
||||||
tenants = devis_reseau.decouvrir()
|
tenants = devis_reseau.decouvrir()
|
||||||
self.repondre_json(200, {
|
self.repondre_json(200, {
|
||||||
|
|
@ -3827,6 +4043,16 @@ class Gestionnaire(BaseHTTPRequestHandler):
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
self.repondre_json(400, {"erreur": "Corps JSON invalide."})
|
self.repondre_json(400, {"erreur": "Corps JSON invalide."})
|
||||||
return
|
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:
|
try:
|
||||||
if chemin == "/api/inventaire":
|
if chemin == "/api/inventaire":
|
||||||
self.repondre_json(409, {"erreur": "hosts.yml est genere depuis le plan. "
|
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)}."
|
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] = [
|
PREUVES: list[dict] = [
|
||||||
{"id": "P01", "titre": "Lint (ansible-lint)", "refs": ["AFF-006"],
|
{"id": "P01", "titre": "Lint (ansible-lint)", "refs": ["AFF-006"],
|
||||||
"cmds": [["ansible-lint", "-q"]]},
|
"cmds": [["ansible-lint", "-q"]]},
|
||||||
|
|
@ -4490,6 +4565,8 @@ PREUVES: list[dict] = [
|
||||||
"refs": [], "func": preuve_une_derivation_vide_se_dit},
|
"refs": [], "func": preuve_une_derivation_vide_se_dit},
|
||||||
{"id": "P80", "titre": "Remise au client : inscrite, nommee, et son second temps a l'heure",
|
{"id": "P80", "titre": "Remise au client : inscrite, nommee, et son second temps a l'heure",
|
||||||
"refs": [], "func": preuve_remise_tenue},
|
"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