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>
188 lines
9.7 KiB
Python
188 lines
9.7 KiB
Python
#!/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()))
|
|
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 = {
|
|
(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},
|
|
}
|
|
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)
|
|
|
|
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))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|