[FIX] proxmox : donner l'URL au remède qui retire l'entrée du cache

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
This commit is contained in:
Mathieu Benoit 2026-09-17 03:48:09 -04:00
parent f34c0d2ded
commit 34ac3e775d
2 changed files with 43 additions and 1 deletions

View file

@ -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)} || {{ "

View file

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