la console mesure le pouvoir sur la voute, pas sur la carte
Le document des responsabilites attache chaque pouvoir a une voute : calculer n'en demande
aucune, configurer demande celle du tenant, materialiser celle du site. Le code lisait les
symlinks, c'est-a-dire les cartes. Le runner de Chezlepro-locataire montait la carte du
site sans en avoir jamais eu la voute : sa console se declarait poste et offrait 126 etapes
sur 126. Ces gestes seraient partis puis tombes sur un secret vide — un echec au milieu du
chemin, la ou un refus net aurait dit la verite avant de commencer.
contexte() derive desormais les pouvoirs des voutes presentes, et la portee decoule des
pouvoirs au lieu de les preceder. On ne prouve pas qu'une voute s'ouvre, le mot de passe se
tape a l'execution ; mais son absence est decisive et se mesure sans rien ouvrir. Lire une
carte reste permis : le pouvoir fabric suit toujours le symlink, consulter un miroir n'est
pas engendrer.
serveur_ops retire aussi le lien quand la fabric n'est plus declaree — il ne retirait rien,
et un runner gardait le pouvoir que son plan ne lui donnait plus. Il ne retire qu'un lien,
jamais un fichier : une vraie carte a cette place n'a pas ete ecrite par ce role, et il le
dit plutot que de detruire ce qui n'est pas le sien.
Valide : syntax-check et ansible-lint sur le role (profil production, 0/0), make test a 0
echec, P81 et P83 vertes. Six tests montent quatre faux disques et exigent la portee qui
leur revient, dont le defaut lui-meme : carte presente, voute absente, portee tenant.
Limite : P02 reste en echec pour la raison anterieure deja consignee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:12:18 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""La portee de la console : un pouvoir se mesure sur ce qu'on peut OUVRIR.
|
|
|
|
|
|
|
|
|
|
`docs/responsabilites-locataire-hebergeur.md` §2 attache chaque pouvoir a une voute :
|
|
|
|
|
|
|
|
|
|
calculer plan -> inventaire aucune voute
|
|
|
|
|
configurer des roles sur ses machines voute du TENANT
|
|
|
|
|
materialiser creer / detruire des VM voute du SITE
|
|
|
|
|
|
|
|
|
|
Le code lisait les SYMLINKS — les cartes. Mesure du 2026-09-20 : le runner de
|
|
|
|
|
Chezlepro-locataire montait la carte du site sans en avoir jamais eu la voute ; sa
|
|
|
|
|
console se declarait « poste » et offrait 126 etapes sur 126, creation et destruction de
|
|
|
|
|
machines comprises. Ces gestes seraient partis, puis tombes sur un secret vide.
|
|
|
|
|
|
|
|
|
|
Ces cas fabriquent les quatre situations sur disque et exigent la portee qui leur revient.
|
|
|
|
|
Le quatrieme est le defaut lui-meme, nomme pour qu'il ne revienne pas.
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
import tempfile
|
|
|
|
|
import unittest
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
RACINE = Path(__file__).resolve().parents[2]
|
|
|
|
|
sys.path.insert(0, str(RACINE / "scripts"))
|
|
|
|
|
|
|
|
|
|
import inventory_gui as gui # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestPorteeDeLaConsole(unittest.TestCase):
|
|
|
|
|
"""Chaque cas monte un faux disque, puis lit ce que la console en conclut."""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.dossier = Path(tempfile.mkdtemp(prefix="portee-"))
|
|
|
|
|
self._instance = gui.INSTANCE
|
|
|
|
|
self._underlay = gui.FICHIER_UNDERLAY
|
|
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
|
gui.INSTANCE = self._instance
|
|
|
|
|
gui.FICHIER_UNDERLAY = self._underlay
|
|
|
|
|
import shutil
|
|
|
|
|
shutil.rmtree(self.dossier, ignore_errors=True)
|
|
|
|
|
|
|
|
|
|
def _monter(self, instance: bool, voute_tenant: bool,
|
|
|
|
|
fabric: bool, voute_site: bool) -> dict:
|
|
|
|
|
eco = self.dossier / "OPS-Essai"
|
|
|
|
|
site = self.dossier / "SITE-Essai"
|
|
|
|
|
if instance:
|
|
|
|
|
(eco / "inventories" / "principal" / "group_vars" / "all").mkdir(parents=True)
|
|
|
|
|
if voute_tenant:
|
|
|
|
|
(eco / "inventories/principal/group_vars/all/vault.yml").write_text(
|
|
|
|
|
"$ANSIBLE_VAULT;1.1;AES256\n", encoding="utf-8")
|
|
|
|
|
if fabric:
|
|
|
|
|
site.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
(site / "underlay.yml").write_text("underlay: {}\n", encoding="utf-8")
|
|
|
|
|
if voute_site:
|
|
|
|
|
(site / "underlay.vault.yml").write_text(
|
|
|
|
|
"$ANSIBLE_VAULT;1.1;AES256\n", encoding="utf-8")
|
|
|
|
|
gui.INSTANCE = eco
|
|
|
|
|
gui.FICHIER_UNDERLAY = site / "underlay.yml"
|
|
|
|
|
return gui.contexte()
|
|
|
|
|
|
|
|
|
|
def test_le_runner_du_site_materialise_et_ne_configure_pas(self):
|
|
|
|
|
ctx = self._monter(instance=False, voute_tenant=False, fabric=True, voute_site=True)
|
|
|
|
|
self.assertEqual(ctx["portee"], "site")
|
|
|
|
|
self.assertTrue(ctx["peut"]["materialiser"])
|
|
|
|
|
self.assertFalse(ctx["peut"]["configurer"])
|
|
|
|
|
|
|
|
|
|
def test_le_runner_du_locataire_configure_et_n_engendre_pas(self):
|
|
|
|
|
ctx = self._monter(instance=True, voute_tenant=True, fabric=False, voute_site=False)
|
|
|
|
|
self.assertEqual(ctx["portee"], "tenant")
|
|
|
|
|
self.assertTrue(ctx["peut"]["configurer"])
|
|
|
|
|
self.assertFalse(ctx["peut"]["materialiser"])
|
|
|
|
|
|
|
|
|
|
def test_le_poste_du_mainteneur_porte_les_deux_voutes(self):
|
|
|
|
|
ctx = self._monter(instance=True, voute_tenant=True, fabric=True, voute_site=True)
|
|
|
|
|
self.assertEqual(ctx["portee"], "poste")
|
|
|
|
|
self.assertTrue(all(ctx["peut"][p] for p in
|
|
|
|
|
("configurer", "materialiser", "editer_le_plan", "fabric")))
|
|
|
|
|
|
|
|
|
|
def test_la_carte_sans_la_voute_n_engendre_rien(self):
|
|
|
|
|
"""LE DEFAUT, NOMME. Chezlepro-locataire, le 2026-09-20 : la carte, pas les cles."""
|
|
|
|
|
ctx = self._monter(instance=True, voute_tenant=True, fabric=True, voute_site=False)
|
|
|
|
|
self.assertEqual(ctx["portee"], "tenant",
|
|
|
|
|
"une carte lue ne vaut pas une voute ouverte")
|
|
|
|
|
self.assertFalse(ctx["peut"]["materialiser"])
|
|
|
|
|
# Il LIT toujours la fabric — c'est un miroir, et le dire vaut mieux que le taire.
|
|
|
|
|
self.assertTrue(ctx["peut"]["fabric"])
|
|
|
|
|
self.assertIn("voute", ctx["raison"]["materialiser"].lower())
|
|
|
|
|
|
|
|
|
|
def test_calculer_ne_demande_aucune_voute(self):
|
|
|
|
|
"""La seule des trois portees qui se passe de secret : le plan suffit."""
|
|
|
|
|
ctx = self._monter(instance=True, voute_tenant=False, fabric=False, voute_site=False)
|
|
|
|
|
self.assertTrue(ctx["peut"]["editer_le_plan"])
|
|
|
|
|
self.assertFalse(ctx["peut"]["configurer"])
|
|
|
|
|
self.assertEqual(ctx["portee"], "orphelin")
|
|
|
|
|
|
|
|
|
|
def test_une_console_sans_rien_le_dit(self):
|
|
|
|
|
ctx = self._monter(instance=False, voute_tenant=False, fabric=False, voute_site=False)
|
|
|
|
|
self.assertEqual(ctx["portee"], "orphelin")
|
|
|
|
|
self.assertFalse(any(ctx["peut"].values()))
|
2026-10-11 17:31:13 -04:00
|
|
|
self.assertFalse(any(ctx["contrat"].values()))
|
|
|
|
|
|
|
|
|
|
def test_chaque_console_lit_son_bout_du_contrat(self):
|
|
|
|
|
"""Lot 2 : le locataire lit son lien avec le site, le site ses locataires ; le
|
|
|
|
|
poste monte les deux depots, il lit les deux. Le cote suit la BRANCHE, pas la
|
|
|
|
|
voute : lire ces deux fichiers ne demande aucun secret."""
|
|
|
|
|
attendus = {
|
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
|
|
|
(True, False): {"lien_avec_le_site": True, "locataires_heberges": False, "a_traiter": True},
|
|
|
|
|
(False, True): {"lien_avec_le_site": False, "locataires_heberges": True, "a_traiter": True},
|
|
|
|
|
(True, True): {"lien_avec_le_site": True, "locataires_heberges": True, "a_traiter": True},
|
2026-10-11 17:31:13 -04:00
|
|
|
}
|
|
|
|
|
for (instance, fabric), contrat in attendus.items():
|
|
|
|
|
with self.subTest(instance=instance, fabric=fabric):
|
|
|
|
|
ctx = self._monter(instance=instance, voute_tenant=False,
|
|
|
|
|
fabric=fabric, voute_site=False)
|
|
|
|
|
self.assertEqual(ctx["contrat"], contrat)
|
|
|
|
|
import shutil
|
|
|
|
|
shutil.rmtree(self.dossier, ignore_errors=True)
|
|
|
|
|
self.dossier.mkdir()
|
|
|
|
|
|
|
|
|
|
def test_un_site_de_reprise_ne_juge_pas_la_fiche_d_un_autre(self):
|
|
|
|
|
"""LE DEFAUT, NOMME (2026-10-11). Le locataire porte la fiche de son hebergeur
|
|
|
|
|
ACTIF ; SITE-Technolibre, qui le declare en reprise, la jugeait contre la sienne
|
|
|
|
|
et la disait perimee a chaque lecture."""
|
|
|
|
|
site = self.dossier / "SITE-Reprise"
|
|
|
|
|
site.mkdir()
|
|
|
|
|
(site / "underlay.yml").write_text(
|
|
|
|
|
"underlay:\n index: 39\n tenants:\n OPS-Essai: 18\n", encoding="utf-8")
|
|
|
|
|
loc = self.dossier / "OPS-Essai"
|
|
|
|
|
(loc / "plan").mkdir(parents=True)
|
|
|
|
|
(loc / "plan" / "nomenclature.yml").write_text("index: 18\n", encoding="utf-8")
|
|
|
|
|
(loc / "face-reseau.yml").write_text(
|
|
|
|
|
"locataire: OPS-Essai\nhebergeur: SITE-Actif\nindex: 18\nmachines: {}\n",
|
|
|
|
|
encoding="utf-8")
|
|
|
|
|
(loc / "fiche-site.yml").write_text("site: SITE-Actif\n", encoding="utf-8")
|
|
|
|
|
charge = gui.locataires_du_site(site)
|
|
|
|
|
(l,) = charge["locataires"]
|
|
|
|
|
self.assertEqual(l["relation"], "reprise")
|
|
|
|
|
self.assertIsNone(l["fiche"]["ecarts"], "la fiche d'un autre site ne se juge pas ici")
|
|
|
|
|
self.assertEqual(l["index_attribue"], 18)
|
la console mesure le pouvoir sur la voute, pas sur la carte
Le document des responsabilites attache chaque pouvoir a une voute : calculer n'en demande
aucune, configurer demande celle du tenant, materialiser celle du site. Le code lisait les
symlinks, c'est-a-dire les cartes. Le runner de Chezlepro-locataire montait la carte du
site sans en avoir jamais eu la voute : sa console se declarait poste et offrait 126 etapes
sur 126. Ces gestes seraient partis puis tombes sur un secret vide — un echec au milieu du
chemin, la ou un refus net aurait dit la verite avant de commencer.
contexte() derive desormais les pouvoirs des voutes presentes, et la portee decoule des
pouvoirs au lieu de les preceder. On ne prouve pas qu'une voute s'ouvre, le mot de passe se
tape a l'execution ; mais son absence est decisive et se mesure sans rien ouvrir. Lire une
carte reste permis : le pouvoir fabric suit toujours le symlink, consulter un miroir n'est
pas engendrer.
serveur_ops retire aussi le lien quand la fabric n'est plus declaree — il ne retirait rien,
et un runner gardait le pouvoir que son plan ne lui donnait plus. Il ne retire qu'un lien,
jamais un fichier : une vraie carte a cette place n'a pas ete ecrite par ce role, et il le
dit plutot que de detruire ce qui n'est pas le sien.
Valide : syntax-check et ansible-lint sur le role (profil production, 0/0), make test a 0
echec, P81 et P83 vertes. Six tests montent quatre faux disques et exigent la portee qui
leur revient, dont le defaut lui-meme : carte presente, voute absente, portee tenant.
Limite : P02 reste en echec pour la raison anterieure deja consignee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:12:18 -04:00
|
|
|
|
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
|
|
|
def _git(self, depot, *args):
|
|
|
|
|
import subprocess
|
|
|
|
|
subprocess.run(["git", "-C", str(depot), *args], check=True, capture_output=True,
|
|
|
|
|
env={"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t", "GIT_COMMITTER_NAME": "t",
|
|
|
|
|
"GIT_COMMITTER_EMAIL": "t@t", "HOME": str(self.dossier), "PATH": "/usr/bin:/bin"})
|
|
|
|
|
|
|
|
|
|
def test_le_niveau_de_la_forge_se_lit_sans_fetch(self):
|
|
|
|
|
"""Lot 3a : « a jour », « en retard », « en avance », « inconnu », mesures sur de
|
|
|
|
|
vrais depots. En retard sans fetch : la forge porte un commit que le depot n'a pas
|
|
|
|
|
tire — et la lecture ne doit pas l'avoir tire pour autant."""
|
|
|
|
|
forge, ici, autre = self.dossier / "forge.git", self.dossier / "ici", self.dossier / "autre"
|
|
|
|
|
self._git(self.dossier, "init", "-q", "--bare", "-b", "main", str(forge))
|
|
|
|
|
self._git(self.dossier, "clone", "-q", str(forge), str(ici))
|
|
|
|
|
self._git(ici, "commit", "-q", "--allow-empty", "-m", "un")
|
|
|
|
|
self._git(ici, "push", "-q", "origin", "HEAD:main")
|
|
|
|
|
self._git(ici, "branch", "-q", "--set-upstream-to", "origin/main")
|
|
|
|
|
self.assertEqual(gui.etat_forge(ici)["verdict"], "a-jour")
|
|
|
|
|
self._git(self.dossier, "clone", "-q", str(forge), str(autre))
|
|
|
|
|
self._git(autre, "commit", "-q", "--allow-empty", "-m", "deux")
|
|
|
|
|
self._git(autre, "push", "-q", "origin", "HEAD:main")
|
|
|
|
|
e = gui.etat_forge(ici)
|
|
|
|
|
self.assertEqual(e["verdict"], "retard", e)
|
|
|
|
|
self.assertEqual(gui.etat_forge(ici)["verdict"], "retard", "la lecture n'a rien tire")
|
|
|
|
|
self._git(ici, "pull", "-q", "--ff-only")
|
|
|
|
|
self._git(ici, "commit", "-q", "--allow-empty", "-m", "trois")
|
|
|
|
|
self.assertEqual(gui.etat_forge(ici)["verdict"], "avance")
|
|
|
|
|
self._git(ici, "branch", "-q", "--unset-upstream")
|
|
|
|
|
self.assertEqual(gui.etat_forge(ici)["verdict"], "inconnu")
|
|
|
|
|
|
|
|
|
|
def test_la_veille_d_un_locataire_sans_fiche_le_dit(self):
|
|
|
|
|
"""Une veille qui ne voit rien doit le dire en « ok » ; une fiche absente, en « agir »."""
|
|
|
|
|
loc = self.dossier / "OPS-Essai"
|
|
|
|
|
(loc / "plan").mkdir(parents=True)
|
|
|
|
|
(loc / "plan" / "nomenclature.yml").write_text("index: 18\n", encoding="utf-8")
|
|
|
|
|
(loc / "face-reseau.yml").write_text(
|
|
|
|
|
"locataire: OPS-Essai\nhebergeur: SITE-Absent\nindex: 18\nmachines: {}\n", encoding="utf-8")
|
|
|
|
|
elements = gui.veille_locataire(loc)
|
|
|
|
|
fiche = [e for e in elements if e["source"] == "FICHE"]
|
|
|
|
|
self.assertEqual([e["gravite"] for e in fiche], ["agir"])
|
|
|
|
|
self.assertTrue(all(e["gravite"] in ("agir", "savoir", "ok") for e in elements))
|
|
|
|
|
self.assertTrue(any(e["source"] == "GÉNOME" for e in elements))
|
|
|
|
|
|
la console mesure le pouvoir sur la voute, pas sur la carte
Le document des responsabilites attache chaque pouvoir a une voute : calculer n'en demande
aucune, configurer demande celle du tenant, materialiser celle du site. Le code lisait les
symlinks, c'est-a-dire les cartes. Le runner de Chezlepro-locataire montait la carte du
site sans en avoir jamais eu la voute : sa console se declarait poste et offrait 126 etapes
sur 126. Ces gestes seraient partis puis tombes sur un secret vide — un echec au milieu du
chemin, la ou un refus net aurait dit la verite avant de commencer.
contexte() derive desormais les pouvoirs des voutes presentes, et la portee decoule des
pouvoirs au lieu de les preceder. On ne prouve pas qu'une voute s'ouvre, le mot de passe se
tape a l'execution ; mais son absence est decisive et se mesure sans rien ouvrir. Lire une
carte reste permis : le pouvoir fabric suit toujours le symlink, consulter un miroir n'est
pas engendrer.
serveur_ops retire aussi le lien quand la fabric n'est plus declaree — il ne retirait rien,
et un runner gardait le pouvoir que son plan ne lui donnait plus. Il ne retire qu'un lien,
jamais un fichier : une vraie carte a cette place n'a pas ete ecrite par ce role, et il le
dit plutot que de detruire ce qui n'est pas le sien.
Valide : syntax-check et ansible-lint sur le role (profil production, 0/0), make test a 0
echec, P81 et P83 vertes. Six tests montent quatre faux disques et exigent la portee qui
leur revient, dont le defaut lui-meme : carte presente, voute absente, portee tenant.
Limite : P02 reste en echec pour la raison anterieure deja consignee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:12:18 -04:00
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main(verbosity=2)
|