83 lines
3.3 KiB
Python
83 lines
3.3 KiB
Python
|
|
#!/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": "<urlopen error [Errno -3] Temporary failure "
|
||
|
|
"in name resolution>"})
|
||
|
|
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", "pont", "gabarit"]
|
||
|
|
assert all(l["existe"] is False for l in lignes), \
|
||
|
|
"un cluster vide n'offre aucun des quatre objets"
|
||
|
|
|
||
|
|
|
||
|
|
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.")
|