"""`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")