Set-OPS-Public/scripts/tests/test_devis_placement.py

142 lines
6.7 KiB
Python
Raw Permalink Normal View History

devis de placement : mourir n'est pas un diagnostic Soir de reconstruction, VPN pas encore monte. `make placement-plan` — le devis qu'on lance AVANT quarante minutes de deploiement — rendait `AttributeError: 'str' object has no attribute 'get'` : dix lignes de trace Python pour dire « le nom `asgard` ne se resout pas d'ici ». En panne, `Cluster.__call__` rend {"_erreur": "..."} — un DICT. Le devis l'iterait comme une liste, et un dict itere rend ses CLEFS. `Cluster.rate()` existait pour ca et n'etait appele nulle part ici. Les quatre appels passent desormais par une garde qui nomme la cause, l'hote interroge et le geste a tenter (resolution, VPN, jeton). ET LE DEVIS MESURAIT LE MAUVAIS TENANT. `placement_du_tenant()` lisait `instance/` en dur : viser patient 0 avec SETOPS_INSTANCE mesurait en silence le placement de l'instance montee. Le verdict etait juste — pour l'autre tenant. Les deux portaient les memes quatre valeurs, ce qui est exactement la circonstance ou l'erreur ne se voit pas. C'est la HUITIEME resolution d'instance ou d'inventaire codee en dur trouvee en trois jours ; a ce compte ce n'est plus une serie de bogues, c'est une piece manquante. L'en-tete annoncait aussi « tenant instance » — le nom du lien, pas celui du tenant. TROIS TESTS, aucun reseau touche (Cluster simule) : une panne devient un refus lisible ; une reponse qui n'est pas une liste est refusee — c'est le cas silencieux, celui qui franchirait la premiere garde ; le cas nominal traverse sans gene. Branches sur `make test`. Verifie ensuite contre le cluster reel : le devis nomme « OPS-Patient0 » et confirme ses quatre objets (asgard, TrueNAS, vmbr1, gabarit 99998). make test OK ; make verifier 38/38 ; make ci 38/38. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:26:32 -04:00
#!/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)
placement : le devis validait un pont que la flotte n'utilise pas Remarque de l'exploitant en preparant patient 0 : « le pont ne me semble pas approprie du tout, depuis qu'on cree des VNets pour des tenants ». Juste, et plus grave que cosmetique. `make placement-plan` confrontait `proxmox_clone_pont` (vmbr1) au cluster. Ce n'est PAS la que les VM de la flotte atterrissent : `instancier` pose dans chaque hote le pont DERIVE de sa zone (le VNet du tenant), et `make creer-vm` le passe au clone en ecrasant ce defaut. vmbr1 n'est que le repli des clones MANUELS, hors plan. Le devis mesurait donc un objet qui ne sert pas, et ignorait celui qui sert. SUR PATIENT 0 : avant, « pont vmbr1 existe -> CONFORME ». Apres, « reseaux VM : t29appl, t29donn, t29fron, t29serv INTROUVABLE — passer `make sdn-appliquer` AVANT de creer les VM ». Aucun de ses quatre VNets n'existe sur le cluster : le devis d'avant-vol declarait conforme un tenant dont les VM n'auraient eu nulle part ou naitre. D-80 avait pourtant ete corrigee le 2026-08-13 — la liaison de placement est noeud, stockage et gabarit, le pont se derive. Le devis continuait de compter quatre objets et de nommer le mauvais : une doctrine corrigee dans un document ne se propage pas toute seule dans le code qui l'applique. MESURE MAINTENANT : en `sdn`, les VNets derives confrontes a /cluster/sdn/vnets ; en `switch`, les ponts du noeud retenu. Avec le geste correctif quand il en manque. NON-REGRESSION sur l'ecosysteme de reference : ses six VNets existent, conforme, code 0. Quatre tests (Cluster simule, aucun reseau touche), harnais 38/38. Au passage, dans patient 0 : le commentaire annoncait « les QUATRE valeurs qui rattachent un tenant a une fabric » — trois, et le pont n'en est pas. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:39:26 -04:00
assert [l["objet"] for l in lignes] == ["noeud", "stockage", "reseaux VM", "gabarit"]
devis de placement : mourir n'est pas un diagnostic Soir de reconstruction, VPN pas encore monte. `make placement-plan` — le devis qu'on lance AVANT quarante minutes de deploiement — rendait `AttributeError: 'str' object has no attribute 'get'` : dix lignes de trace Python pour dire « le nom `asgard` ne se resout pas d'ici ». En panne, `Cluster.__call__` rend {"_erreur": "..."} — un DICT. Le devis l'iterait comme une liste, et un dict itere rend ses CLEFS. `Cluster.rate()` existait pour ca et n'etait appele nulle part ici. Les quatre appels passent desormais par une garde qui nomme la cause, l'hote interroge et le geste a tenter (resolution, VPN, jeton). ET LE DEVIS MESURAIT LE MAUVAIS TENANT. `placement_du_tenant()` lisait `instance/` en dur : viser patient 0 avec SETOPS_INSTANCE mesurait en silence le placement de l'instance montee. Le verdict etait juste — pour l'autre tenant. Les deux portaient les memes quatre valeurs, ce qui est exactement la circonstance ou l'erreur ne se voit pas. C'est la HUITIEME resolution d'instance ou d'inventaire codee en dur trouvee en trois jours ; a ce compte ce n'est plus une serie de bogues, c'est une piece manquante. L'en-tete annoncait aussi « tenant instance » — le nom du lien, pas celui du tenant. TROIS TESTS, aucun reseau touche (Cluster simule) : une panne devient un refus lisible ; une reponse qui n'est pas une liste est refusee — c'est le cas silencieux, celui qui franchirait la premiere garde ; le cas nominal traverse sans gene. Branches sur `make test`. Verifie ensuite contre le cluster reel : le devis nomme « OPS-Patient0 » et confirme ses quatre objets (asgard, TrueNAS, vmbr1, gabarit 99998). make test OK ; make verifier 38/38 ; make ci 38/38. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:26:32 -04:00
assert all(l["existe"] is False for l in lignes), \
"un cluster vide n'offre aucun des quatre objets"
placement : le devis validait un pont que la flotte n'utilise pas Remarque de l'exploitant en preparant patient 0 : « le pont ne me semble pas approprie du tout, depuis qu'on cree des VNets pour des tenants ». Juste, et plus grave que cosmetique. `make placement-plan` confrontait `proxmox_clone_pont` (vmbr1) au cluster. Ce n'est PAS la que les VM de la flotte atterrissent : `instancier` pose dans chaque hote le pont DERIVE de sa zone (le VNet du tenant), et `make creer-vm` le passe au clone en ecrasant ce defaut. vmbr1 n'est que le repli des clones MANUELS, hors plan. Le devis mesurait donc un objet qui ne sert pas, et ignorait celui qui sert. SUR PATIENT 0 : avant, « pont vmbr1 existe -> CONFORME ». Apres, « reseaux VM : t29appl, t29donn, t29fron, t29serv INTROUVABLE — passer `make sdn-appliquer` AVANT de creer les VM ». Aucun de ses quatre VNets n'existe sur le cluster : le devis d'avant-vol declarait conforme un tenant dont les VM n'auraient eu nulle part ou naitre. D-80 avait pourtant ete corrigee le 2026-08-13 — la liaison de placement est noeud, stockage et gabarit, le pont se derive. Le devis continuait de compter quatre objets et de nommer le mauvais : une doctrine corrigee dans un document ne se propage pas toute seule dans le code qui l'applique. MESURE MAINTENANT : en `sdn`, les VNets derives confrontes a /cluster/sdn/vnets ; en `switch`, les ponts du noeud retenu. Avec le geste correctif quand il en manque. NON-REGRESSION sur l'ecosysteme de reference : ses six VNets existent, conforme, code 0. Quatre tests (Cluster simule, aucun reseau touche), harnais 38/38. Au passage, dans patient 0 : le commentaire annoncait « les QUATRE valeurs qui rattachent un tenant a une fabric » — trois, et le pont n'en est pas. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:39:26 -04:00
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"
def test_le_devis_nomme_le_locataire_designe():
"""Par `--locataire`, aucun fichier source d'ou tirer le nom : le devis annoncait
« tenant « ? » » au pre-vol de M4 (2026-10-07). Il nomme celui qu'on lui a designe."""
import contextlib
import io
assert dp.nom_du_tenant(None, "OPS-Exemple") == "OPS-Exemple"
source = Path("/d/OPS-Monte/inventories/principal/group_vars/proxmox.yml")
assert dp.nom_du_tenant(source) == "OPS-Monte", "le chemin monte nomme toujours son tenant"
sortie = io.StringIO()
with contextlib.redirect_stdout(sortie):
dp.afficher([], dp.nom_du_tenant(None, "OPS-Exemple"), Path("/d/SITE-Exemple"))
entete = sortie.getvalue().splitlines()[0]
assert "« OPS-Exemple »" in entete and "?" not in entete, f"en-tete : {entete!r}"
def test_le_devis_verifie_le_gabarit_et_le_stockage_que_le_clonage_utilise():
"""Le site decide du gabarit et de son stockage ; `cloner-vm` les passe en `-e`. Le devis
lisait le tenant : il validait chez Technolibre un 99998 que plus rien ne clone, et
refusait chez Chezlepro un gabarit « None » (pre-vol de sa reconstruction, 2026-10-07)."""
site = {"vmid": 9006, "nom": "modeleSetOPS-minimal", "noeud": "vishnu", "stockage": "CephNVMe"}
perime = {**PLACEMENT, "proxmox_clone_vmid_modele": 99998, "proxmox_clone_stockage": "TrueNAS"}
vu = dp.ce_que_le_clonage_utilise(perime, site)
assert vu["proxmox_clone_vmid_modele"] == 9006 and vu["proxmox_clone_stockage"] == "CephNVMe", vu
sans = {k: v for k, v in perime.items() if k != "proxmox_clone_vmid_modele"}
assert dp.ce_que_le_clonage_utilise(sans, site)["proxmox_clone_vmid_modele"] == 9006, \
"un tenant qui suit la regle (aucun gabarit declare) recoit celui du site"
assert dp.ce_que_le_clonage_utilise(perime, {}) == perime, \
"sans gabarit declare par le site, le tenant reprend la main, tel quel"
assert perime["proxmox_clone_vmid_modele"] == 99998, "le placement du tenant n'est pas modifie en place"
devis de placement : mourir n'est pas un diagnostic Soir de reconstruction, VPN pas encore monte. `make placement-plan` — le devis qu'on lance AVANT quarante minutes de deploiement — rendait `AttributeError: 'str' object has no attribute 'get'` : dix lignes de trace Python pour dire « le nom `asgard` ne se resout pas d'ici ». En panne, `Cluster.__call__` rend {"_erreur": "..."} — un DICT. Le devis l'iterait comme une liste, et un dict itere rend ses CLEFS. `Cluster.rate()` existait pour ca et n'etait appele nulle part ici. Les quatre appels passent desormais par une garde qui nomme la cause, l'hote interroge et le geste a tenter (resolution, VPN, jeton). ET LE DEVIS MESURAIT LE MAUVAIS TENANT. `placement_du_tenant()` lisait `instance/` en dur : viser patient 0 avec SETOPS_INSTANCE mesurait en silence le placement de l'instance montee. Le verdict etait juste — pour l'autre tenant. Les deux portaient les memes quatre valeurs, ce qui est exactement la circonstance ou l'erreur ne se voit pas. C'est la HUITIEME resolution d'instance ou d'inventaire codee en dur trouvee en trois jours ; a ce compte ce n'est plus une serie de bogues, c'est une piece manquante. L'en-tete annoncait aussi « tenant instance » — le nom du lien, pas celui du tenant. TROIS TESTS, aucun reseau touche (Cluster simule) : une panne devient un refus lisible ; une reponse qui n'est pas une liste est refusee — c'est le cas silencieux, celui qui franchirait la premiere garde ; le cas nominal traverse sans gene. Branches sur `make test`. Verifie ensuite contre le cluster reel : le devis nomme « OPS-Patient0 » et confirme ses quatre objets (asgard, TrueNAS, vmbr1, gabarit 99998). make test OK ; make verifier 38/38 ; make ci 38/38. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:26:32 -04:00
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.")