From 34ac3e775d7c7779fac48f0a2fbe1045321c82dd Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 17 Sep 2026 03:48:09 -0400 Subject: [PATCH] =?UTF-8?q?[FIX]=20proxmox=20:=20donner=20l'URL=20au=20rem?= =?UTF-8?q?=C3=A8de=20qui=20retire=20l'entr=C3=A9e=20du=20cache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quand la somme d'une image ne correspond pas, le message nomme le geste à faire EN PREMIER, cet hôte pouvant être lui-même derrière le cache. Il rendait « printf %s | … --oublie » : une f-string dont le marqueur était tombé, et un printf sans opérande. Recopiée, la commande lit un flux vide, n'efface rien et sort à 0 — on croit avoir purgé, on efface l'image, on relance, et le magasin ressert les mêmes octets indéfiniment. Un remède qui réussit sans rien faire est pire que pas de remède. La voie libvirt donnait déjà l'URL ; celle-ci la donne aussi. Le test la joue dans un vrai shell, la ligne traversant deux quotages. --- EN --- When an image's checksum does not match, the message names the step to take FIRST, this host possibly being behind the cache itself. It printed « printf %s | … --oublie »: an f-string whose placeholder had been lost, and a printf with no operand. Copied, the command reads an empty stream, erases nothing and exits 0 — you believe you purged, you erase the image, you retry, and the store serves the same bytes for ever. A remedy that succeeds without doing anything is worse than none. The libvirt path already gave the URL; this one now does too. The test plays it in a real shell, the line crossing two levels of quoting. Assisted-by: Claude Opus 5 --- script/proxmox/proxmox_deploy.py | 9 +++++++- test/test_proxmox_deploy.py | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/script/proxmox/proxmox_deploy.py b/script/proxmox/proxmox_deploy.py index 9784c77..f52a609 100644 --- a/script/proxmox/proxmox_deploy.py +++ b/script/proxmox/proxmox_deploy.py @@ -993,10 +993,17 @@ def image_fetch_cmd( # depuis le magasin. L'entrée s'en retire d'abord — « --purge » # efface tout, et « --purge-older-than » n'atteint jamais un objet # que chaque service rajeunit. + # L'URL est DANS la commande proposée. Sans elle, « printf %s » n'a + # pas d'opérande, n'écrit rien, et « --oublie » lit un flux vide puis + # sort à 0 : l'opérateur croit avoir purgé, efface l'image, relance, + # et le magasin ressert les mêmes octets. Un remède qui réussit sans + # rien faire est pire que pas de remède. aide = ( f"rm -f {shlex.quote(cible)} et relancer ;" " derrière un cache de téléchargement, en retirer l'entrée" - f" d'abord : printf %s | sudo erplibre_go_qemu_cache --oublie" + " d'abord : printf '%s\\n' " + + shlex.quote(f"GET {url}") + + " | sudo erplibre_go_qemu_cache --oublie" ) cmd += ( f" && {{ {somme(cible)} || {{ " diff --git a/test/test_proxmox_deploy.py b/test/test_proxmox_deploy.py index cf45906..e159706 100644 --- a/test/test_proxmox_deploy.py +++ b/test/test_proxmox_deploy.py @@ -416,6 +416,41 @@ class TestLesCommandes(unittest.TestCase): # Vers l'erreur standard : la sortie normale est lue par la machine. self.assertIn(">&2", cmd) + def test_the_remedy_names_the_url_it_forgets(self): + """Le message dit de lancer cette commande EN PREMIER, et une + commande qu'on ne peut pas recopier telle quelle vaut moins que pas + de commande. + + « printf %s » sans opérande n'écrit rien ; « --oublie » lit alors un + flux vide, n'efface rien et sort à 0. L'opérateur croit avoir purgé, + efface l'image, relance — et le magasin ressert les mêmes octets, + indéfiniment. Le succès silencieux est le pire des deux.""" + url = "https://exemple.invalide/img/deb.qcow2" + cmd = pve.image_fetch_cmd(url, "deb.qcow2", sha256="ab" * 32) + self.assertIn(url, cmd) + oubli = cmd[cmd.index("printf") : cmd.index("--oublie")] + self.assertIn(url, oubli) + self.assertIn("GET ", oubli) + + def test_the_remedy_survives_a_real_shell(self): + """Joué pour de vrai : c'est la LIGNE RENDUE qui compte, pas la + chaîne Python. Elle traverse deux quotages — celui de l'aide, puis + celui du « echo » qui la porte — et une interpolation tombée ne se + voit qu'ici.""" + import subprocess + + url = "https://exemple.invalide/img/deb.qcow2" + cmd = pve.image_fetch_cmd(url, "deb.qcow2", sha256="ab" * 32) + aide = cmd[cmd.index("echo 'rm -f") + len("echo ") :] + aide = aide[: aide.index(" >&2")] + rendu = subprocess.run( + ["sh", "-c", f"echo {aide}"], capture_output=True, text=True + ) + self.assertEqual(rendu.returncode, 0, rendu.stderr) + self.assertIn(f"'GET {url}'", rendu.stdout) + # Le tube est complet : un « printf » nu ne serait pas recopiable. + self.assertIn("| sudo erplibre_go_qemu_cache --oublie", rendu.stdout) + def test_the_failure_is_still_a_failure(self): """Un message n'absout pas : « false » garde le code de retour, sans quoi le déploiement continuerait sur une image substituée."""