71 lines
2.8 KiB
Python
71 lines
2.8 KiB
Python
|
|
"""`raser` : une destruction qui ECHOUE ne doit pas etre annoncee comme reussie.
|
||
|
|
|
||
|
|
Le 2026-08-10, `raser` a rapporte « 6/6 VM detruites » alors que les six etaient toujours
|
||
|
|
la. L'API Proxmox accepte le `DELETE`, rend un UPID et la main IMMEDIATEMENT ; la
|
||
|
|
destruction se fait en tache de fond, et elle avait echoue APRES sur
|
||
|
|
« VM is locked (clone) » — un verrou laisse par des clonages interrompus.
|
||
|
|
|
||
|
|
`raser` ne lisait que l'accuse de reception. Confondre « demande acceptee » et « travail
|
||
|
|
fait » est le pire mensonge possible pour la SEULE commande destructive du moteur : on
|
||
|
|
croit la place libre, on relance la construction, et rien ne se cree sans qu'on comprenne
|
||
|
|
pourquoi.
|
||
|
|
|
||
|
|
Ce test fabrique exactement cette situation : le faux cluster accepte tout, puis rend une
|
||
|
|
tache terminee en erreur. `raser` doit sortir en 1 et n'annoncer aucune destruction.
|
||
|
|
"""
|
||
|
|
import io
|
||
|
|
import pathlib
|
||
|
|
import sys
|
||
|
|
from contextlib import redirect_stdout
|
||
|
|
|
||
|
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||
|
|
import raser # noqa: E402
|
||
|
|
|
||
|
|
BASE, NOM = raser.instance_active()
|
||
|
|
COUPLES = raser.plan_derive(BASE)
|
||
|
|
if not COUPLES:
|
||
|
|
raise SystemExit("Plan sans serveur : rien a eprouver.")
|
||
|
|
NOM_1, VMID_1 = COUPLES[0]
|
||
|
|
|
||
|
|
MOTIF = "VM is locked (clone)"
|
||
|
|
|
||
|
|
|
||
|
|
class ClusterQuiEchoueApresCoup:
|
||
|
|
"""Accepte tout, puis rend une tache TERMINEE EN ERREUR — le cas reel du 2026-08-10."""
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def depuis_hebergeur(cls):
|
||
|
|
return cls(), None
|
||
|
|
|
||
|
|
def __call__(self, chemin, methode="GET", corps=None):
|
||
|
|
if chemin.startswith("/cluster/resources"):
|
||
|
|
# Le plan et le cluster s'accordent : le verrou de nom ne doit pas se
|
||
|
|
# declencher, sinon on n'atteindrait jamais la destruction.
|
||
|
|
return [{"type": "qemu", "vmid": VMID_1, "name": NOM_1, "node": "asgard"}]
|
||
|
|
if chemin.endswith("/status/current"):
|
||
|
|
return {"status": "stopped"}
|
||
|
|
if chemin.endswith("/status/stop"):
|
||
|
|
return "UPID:asgard:0:0:0:qmstop:0:test:"
|
||
|
|
if "/tasks/" in chemin and chemin.endswith("/status"):
|
||
|
|
return {"status": "stopped", "exitstatus": MOTIF}
|
||
|
|
if methode == "DELETE":
|
||
|
|
return "UPID:asgard:0:0:0:qmdestroy:0:test:"
|
||
|
|
return {}
|
||
|
|
|
||
|
|
def rate(self, rep):
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
raser.Cluster = ClusterQuiEchoueApresCoup
|
||
|
|
sortie = io.StringIO()
|
||
|
|
with redirect_stdout(sortie):
|
||
|
|
code = raser.main(["--instance", NOM, "--confirmer"])
|
||
|
|
texte = sortie.getvalue()
|
||
|
|
|
||
|
|
print(f"\n>>> code de sortie : {code} (1 = echec attendu)")
|
||
|
|
assert code == 1, "raser a rendu 0 alors que la destruction a ECHOUE"
|
||
|
|
assert "ECHEC" in texte, "raser n'a pas signale l'echec"
|
||
|
|
assert MOTIF in texte, "raser n'a pas rapporte la cause rendue par le cluster"
|
||
|
|
assert "1/1 VM detruites" not in texte, "raser annonce une destruction qui n'a pas eu lieu"
|
||
|
|
print(">>> l'echec est vu, nomme, et compte : aucune destruction fictive annoncee")
|