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."""