Set-OPS-Public/scripts/tests/test_portee_console.py
Daniel Allaire 1d582045f3 console : la face ne se juge que la ou la carte du site est montee
Sur le runner de Technolibre, la vue Lien avec le site disait la face a republier :
la cle d'amorcage d'ops-01 vient du plan du site, absent du runner, et la face
recalculee la perdait. Republier d'ici l'aurait retiree. La face se lit partout et
ne se juge plus que la ou le site est monte, comme la fiche. Un test le garde.

94 preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:22:31 -04:00

162 lines
7.9 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)
def test_le_runner_du_locataire_ne_juge_pas_sa_face(self):
"""LE DEFAUT, NOMME (2026-10-11, runner de Technolibre). La face porte la cle du
runner du site, lue dans le plan du SITE. Le runner du locataire ne l'a pas : il
recalculait une face sans elle, la disait « a republier », et republier d'ici
l'aurait retiree."""
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")
lien = gui.lien_du_locataire(loc)
self.assertTrue(lien["face"]["publiee"])
self.assertFalse(lien["face"]["jugeable"])
self.assertIsNone(lien["face"]["ecarts"], "sans la carte du site, aucun verdict")
if __name__ == "__main__":
unittest.main(verbosity=2)