#!/usr/bin/env python3 """Le devis de placement dit ce qui ne va pas, au lieu de mourir. Ce que ce test garde, et pourquoi il existe. Ce devis est ce qu'on lance AVANT quarante minutes de deploiement, pour savoir si le terrain est bon — sur un site inconnu, un soir d'intervention. C'est donc le pire endroit du depot pour un message illisible. Le 2026-08-20, il mourait sur `AttributeError: 'str' object has no attribute 'get'` : `Cluster.__call__` rend `{"_erreur": "..."}` en cas de panne, un DICT, et le devis l'iterait comme une liste — un dict itere rend ses CLEFS. Dix lignes de trace Python pour dire « le nom `asgard` ne se resout pas ». `Cluster.rate()` existait pour ca et n'etait pas appele. Le test ne verifie pas que le cluster repond — il verifie que le devis REFUSE PROPREMENT quand il ne repond pas. Aucun reseau n'est touche : le Cluster est simule. """ import sys from pathlib import Path RACINE = Path(__file__).resolve().parent.parent.parent sys.path.insert(0, str(RACINE / "scripts")) import devis_placement as dp # noqa: E402 from proxmox_api import Cluster # noqa: E402 class _ClusterSimule(Cluster): """Un Cluster qui ne parle a personne : il rend ce qu'on lui dit de rendre.""" def __init__(self, reponse): self.hote = "cluster-de-test" self._reponse = reponse def __call__(self, chemin, methode="GET", corps=None): return self._reponse PLACEMENT = {"proxmox_clone_noeud": "n1", "proxmox_clone_stockage": "s1", "proxmox_clone_pont": "vmbr0", "proxmox_clone_vmid_modele": 99998} def test_une_panne_reseau_devient_un_refus_lisible(): api = _ClusterSimule({"_erreur": ""}) try: dp.constater(api, PLACEMENT) except SystemExit as e: message = str(e) assert "ne repond pas" in message, message assert "name resolution" in message, "la cause reelle doit etre citee" assert "cluster-de-test" in message, "l'hote interroge doit etre nomme" assert "VPN" in message, "le geste a tenter doit etre suggere" else: raise AssertionError("une reponse d'erreur aurait du produire un refus") def test_une_reponse_qui_n_est_pas_une_liste_est_refusee(): """Le cas silencieux : un dict SANS `_erreur` passerait la premiere garde.""" api = _ClusterSimule({"data": "quelque chose d'inattendu"}) try: dp.constater(api, PLACEMENT) except SystemExit as e: assert "Reponse inattendue" in str(e), str(e) else: raise AssertionError("une reponse non iterable aurait du produire un refus") def test_une_reponse_normale_traverse_la_garde(): """La garde ne doit pas gener le cas nominal : quatre lignes de constat.""" api = _ClusterSimule([]) # cluster joignable, mais qui n'offre rien lignes = dp.constater(api, PLACEMENT) assert [l["objet"] for l in lignes] == ["noeud", "stockage", "reseaux VM", "gabarit"] assert all(l["existe"] is False for l in lignes), \ "un cluster vide n'offre aucun des quatre objets" def test_le_reseau_mesure_est_celui_ou_les_VM_atterrissent(tmp=None): """Le pont du plan n'est PAS ce qu'on verifie — c'est le VNet derive de chaque hote. Le defaut du 2026-08-20 : le devis validait `proxmox_clone_pont` (vmbr1), qui n'est que le repli des clones manuels, et rendait CONFORME sur un tenant dont AUCUN VNet n'existait. `make creer-vm` passe le pont derive par hote et ecrase ce defaut. """ import tempfile import yaml as _yaml with tempfile.TemporaryDirectory() as d: base = Path(d) inv = base / "inventories" / "production" inv.mkdir(parents=True) (inv / "hosts.yml").write_text(_yaml.safe_dump({ "all": {"children": {"hotes_actifs": {"hosts": { "a-01": {"proxmox_pont": "t29fron"}, "b-01": {"proxmox_pont": "t29serv"}}}}}}), encoding="utf-8") api = _ClusterSimule([{"vnet": "t29fron"}]) # un seul des deux existe ligne = [l for l in dp.constater(api, PLACEMENT, base, "sdn") if l["objet"] == "reseaux VM"][0] assert ligne["existe"] is False, "un VNet manquant doit faire echouer le devis" assert "t29serv" in ligne["note"], ligne["note"] assert "sdn-appliquer" in ligne["note"], "le geste correctif doit etre nomme" assert "vmbr" not in str(ligne["voulu"]), \ "le devis ne doit plus presenter le pont des clones manuels" if __name__ == "__main__": tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")] for t in tests: t() print(f"OK — {len(tests)} test(s) devis de placement : " f"une panne de cluster se lit, elle ne se decode pas.")