Deux routes en lecture, /api/lien-site et /api/locataires-heberges, et deux vues sous Veille : le locataire voit sa face reseau publiee et la fiche que son site lui a deposee, le site voit ce que chaque locataire lui a publie. Chaque fichier est montre avec son commit, que l'autre bout lit. Un site de reprise ne juge plus la fiche deposee par l'hebergeur actif : il la declarait perimee a chaque lecture (SITE-Technolibre). Un test le garde. 94 preuves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
146 lines
7 KiB
Python
146 lines
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},
|
|
(False, True): {"lien_avec_le_site": False, "locataires_heberges": True},
|
|
(True, True): {"lien_avec_le_site": True, "locataires_heberges": 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)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|