[FIX] qemu install : attendre le verrou apt d'apt-daily et de l'agent
A desktop install repeated « Could not get lock » for minutes, then could run on an index never refreshed. Disabling the apt-daily timer does not interrupt the apt-get it already started, so its service is stopped too. The guest agent is laid by a detached unit that runs its own apt-get update: « cloud-init status --wait » says done while that lock is still held, so the wait covers that unit. Retries come back every two seconds, DPkg::Lock::Timeout not covering the list lock, and an update that never succeeds says so at once. --- FR --- Une installation de bureau répétait « Impossible d'obtenir le verrou » pendant des minutes, puis pouvait tourner sur un index jamais rafraîchi. Désactiver le minuteur apt-daily n'interrompt pas l'apt-get qu'il a lancé : son service est donc arrêté aussi. L'agent invité est posé par une unité détachée qui fait son propre apt-get update : « cloud-init status --wait » dit « done » pendant que ce verrou est tenu, et l'attente couvre donc cette unité. La reprise repasse toutes les deux secondes, DPkg::Lock::Timeout ne couvrant pas le verrou des listes, et un update qui n'aboutit pas le dit sur-le-champ. Assisted-by: Claude Opus 5
This commit is contained in:
parent
41bb10ab65
commit
f6cc0bfc53
3 changed files with 157 additions and 2 deletions
|
|
@ -415,7 +415,16 @@ class QemuInstallMixin:
|
|||
L'attente dure jusqu'à 15 min et n'écrivait RIEN : sur une architecture
|
||||
émulée, le log restait muet un quart d'heure juste après avoir annoncé
|
||||
le début de l'installation, ce qui se lit comme un blocage. Deux lignes
|
||||
l'encadrent, et le « status » final dit si elle a abouti ou expiré."""
|
||||
l'encadrent, et le « status » final dit si elle a abouti ou expiré.
|
||||
|
||||
« cloud-init status --wait » NE SUFFIT PAS. La pose de l'agent invité
|
||||
est lancée en service DÉTACHÉ — « systemd-run --no-block » — pour que
|
||||
cloud-init rende la main en quelques secondes ; ce service, lui, fait
|
||||
un « apt-get update » puis une installation, et garde donc le verrou
|
||||
des paquets bien après que cloud-init s'est dit terminé. L'étape
|
||||
suivante trouvait le verrou pris, échouait jusqu'à sa borne, puis
|
||||
installait sur un index jamais rafraîchi : « Impossible de trouver le
|
||||
paquet », un message qui n'accuse personne."""
|
||||
return (
|
||||
"if command -v cloud-init >/dev/null 2>&1; then "
|
||||
'echo "== '
|
||||
|
|
@ -426,6 +435,14 @@ class QemuInstallMixin:
|
|||
+ f'echo " {t("cloud-init:")} $(cloud-init status 2>/dev/null '
|
||||
'| head -1)"; '
|
||||
"fi; "
|
||||
# Le service détaché de l'agent invité, s'il court encore. Le nom
|
||||
# est celui que le déploiement lui donne ; « --collect » l'efface
|
||||
# une fois fini, donc « is-active » redevient faux tout seul.
|
||||
"if systemctl is-active --quiet erplibre-qga 2>/dev/null; then "
|
||||
f'echo " {t("waiting for the guest agent install (apt lock)")}"; '
|
||||
"n=0; while systemctl is-active --quiet erplibre-qga 2>/dev/null; "
|
||||
"do n=$((n+1)); [ $n -ge 150 ] && break; sleep 2; done; "
|
||||
"fi; "
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -468,6 +485,13 @@ class QemuInstallMixin:
|
|||
return ""
|
||||
return (
|
||||
"if command -v apt-get >/dev/null 2>&1; then "
|
||||
# Les SERVICES autant que les minuteurs. « disable --now » sur un
|
||||
# minuteur l'empêche de repartir mais n'interrompt pas l'apt-get
|
||||
# qu'il a DÉJÀ lancé : celui-ci garde /var/lib/apt/lists/lock
|
||||
# jusqu'au bout de sa mise à jour, et l'installation qui suit
|
||||
# répète « Impossible d'obtenir le verrou » pendant des minutes.
|
||||
"sudo systemctl stop apt-daily.service apt-daily-upgrade.service "
|
||||
">/dev/null 2>&1 || true; "
|
||||
"sudo systemctl disable --now unattended-upgrades.service "
|
||||
"apt-daily.timer apt-daily-upgrade.timer "
|
||||
">/dev/null 2>&1 || true; "
|
||||
|
|
@ -669,8 +693,19 @@ class QemuInstallMixin:
|
|||
return (
|
||||
f'echo "== {t("Installing the desktop (long):")} {label} =="; '
|
||||
"if command -v apt-get >/dev/null 2>&1; then "
|
||||
# « DPkg::Lock::Timeout » ne couvre PAS le verrou des listes :
|
||||
# il ne vaut que pour celui de dpkg. « apt-get update » échoue
|
||||
# donc en moins d'une seconde quand une tâche quotidienne le
|
||||
# tient, et dormir dix secondes entre deux essais coûte des
|
||||
# minutes à ne rien faire. On repasse plus souvent, et on rend la
|
||||
# main dès que le verrou se libère.
|
||||
"n=0; until sudo apt-get -o DPkg::Lock::Timeout=120 update -qq; do "
|
||||
"n=$((n+1)); [ $n -ge 30 ] && break; sleep 10; done; "
|
||||
"n=$((n+1)); [ $n -ge 60 ] && "
|
||||
# Le dire ICI. Sans cette ligne, l'installation continue sur un
|
||||
# index jamais rafraîchi et échoue plus bas sur « Impossible de
|
||||
# trouver le paquet », qui accuse le dépôt et non le verrou.
|
||||
f'{{ echo " ⚠ {t("apt-get update never succeeded: the lock stayed held")}"; '
|
||||
"break; }; sleep 2; done; "
|
||||
"sudo DEBIAN_FRONTEND=noninteractive "
|
||||
"apt-get -o DPkg::Lock::Timeout=600 install -y "
|
||||
f"{de['apt']} {rem['apt']['packages']} "
|
||||
|
|
|
|||
|
|
@ -5678,6 +5678,14 @@ TRANSLATIONS = {
|
|||
"fr": "fuseau non posé",
|
||||
"en": "timezone not set",
|
||||
},
|
||||
"waiting for the guest agent install (apt lock)": {
|
||||
"fr": "attente de la pose de l'agent invité (verrou apt)",
|
||||
"en": "waiting for the guest agent install (apt lock)",
|
||||
},
|
||||
"apt-get update never succeeded: the lock stayed held": {
|
||||
"fr": "apt-get update n'a jamais abouti : le verrou est resté pris",
|
||||
"en": "apt-get update never succeeded: the lock stayed held",
|
||||
},
|
||||
"Timezone": {
|
||||
"fr": "Fuseau horaire",
|
||||
"en": "Timezone",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import pathlib
|
|||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
sys.argv = ["todo.py"]
|
||||
from script.todo.todo import TODO # noqa: E402
|
||||
|
|
@ -1329,5 +1330,116 @@ class TestGnomeSiteExtensions(unittest.TestCase):
|
|||
self.assertIn("dbus-run-session", self.block)
|
||||
|
||||
|
||||
class TestLeVerrouAptNeCoutePasDesMinutes(unittest.TestCase):
|
||||
"""« Impossible d'obtenir le verrou /var/lib/apt/lists/lock. Il est
|
||||
occupé par le processus N (apt-get) », répété pendant des minutes au
|
||||
début de chaque installation de bureau.
|
||||
|
||||
Deux causes, et la première est la vraie : « disable --now » arrête un
|
||||
MINUTEUR mais n'interrompt pas l'apt-get qu'il a déjà lancé, lequel garde
|
||||
le verrou jusqu'au bout de sa mise à jour.
|
||||
|
||||
La seconde est le rythme : « DPkg::Lock::Timeout » ne couvre pas ce
|
||||
verrou-là — il ne vaut que pour celui de dpkg — si bien qu'apt rend la
|
||||
main en moins d'une seconde, et qu'un sommeil de dix secondes entre deux
|
||||
essais est du temps payé pour rien.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.todo = TODO.__new__(TODO)
|
||||
self.desarme = self.todo._qemu_no_auto_upgrade(prod=False)
|
||||
self.cmd = self.todo._qemu_desktop_remote_cmd("gnome", "deb")
|
||||
|
||||
def test_le_service_deja_lance_est_arrete_lui_aussi(self):
|
||||
"""C'est lui qui tient le verrou, pas le minuteur."""
|
||||
self.assertIn("stop apt-daily.service", self.desarme)
|
||||
self.assertIn("apt-daily-upgrade.service", self.desarme)
|
||||
|
||||
def test_les_minuteurs_restent_desarmes(self):
|
||||
"""Les arrêter sans les désactiver les laisserait repartir en
|
||||
pleine installation."""
|
||||
self.assertIn("apt-daily.timer", self.desarme)
|
||||
self.assertIn("apt-daily-upgrade.timer", self.desarme)
|
||||
|
||||
def test_en_production_on_ne_touche_a_rien(self):
|
||||
"""Les correctifs de sécurité automatiques doivent rester actifs."""
|
||||
self.assertEqual(self.todo._qemu_no_auto_upgrade(prod=True), "")
|
||||
|
||||
def test_la_boucle_repasse_souvent(self):
|
||||
"""Un essai coûte moins d'une seconde : dormir dix secondes entre
|
||||
deux multiplie par cinq l'attente d'un verrou qui se libère."""
|
||||
i = self.cmd.index("until sudo apt-get")
|
||||
boucle = self.cmd[i : self.cmd.index("done;", i)]
|
||||
self.assertIn("sleep 2", boucle)
|
||||
self.assertNotIn("sleep 10", boucle)
|
||||
|
||||
def test_lattente_couvre_notre_propre_service_detache(self):
|
||||
"""Le vrai teneur du verrou, et c'est nous.
|
||||
|
||||
La pose de l'agent invité part en service DÉTACHÉ pour que
|
||||
cloud-init rende la main en quelques secondes. Ce service fait un
|
||||
« apt-get update » puis une installation : « cloud-init status
|
||||
--wait » dit « done » pendant qu'il tient encore le verrou, et
|
||||
l'étape suivante épuise ses reprises pour rien.
|
||||
"""
|
||||
attente = self.todo._qemu_cloud_init_wait()
|
||||
self.assertIn("erplibre-qga", attente)
|
||||
self.assertIn("is-active", attente)
|
||||
|
||||
def test_le_nom_du_service_est_celui_que_le_deploiement_donne(self):
|
||||
"""Deux noms qui divergent et l'attente ne trouve jamais rien."""
|
||||
from script.qemu import deploy_qemu
|
||||
|
||||
src = pathlib.Path(deploy_qemu.__file__).read_text(encoding="utf-8")
|
||||
self.assertIn("--unit=erplibre-qga", src)
|
||||
|
||||
def test_un_update_qui_nabouti_pas_le_dit(self):
|
||||
"""Sans cette ligne, l'installation continue sur un index jamais
|
||||
rafraîchi et échoue plus bas sur « Impossible de trouver le
|
||||
paquet » — qui accuse le dépôt et non le verrou."""
|
||||
i = self.cmd.index("until sudo apt-get")
|
||||
boucle = self.cmd[i : self.cmd.index("done;", i)]
|
||||
self.assertIn("echo", boucle)
|
||||
self.assertIn("⚠", boucle)
|
||||
|
||||
def test_une_apostrophe_traduite_ne_casse_pas_la_commande(self):
|
||||
"""Les messages sont traduits, et le français est plein
|
||||
d'apostrophes. Une seule mal placée casse la commande distante
|
||||
ENTIÈRE : la VM ne dit alors pas pourquoi elle n'a rien fait.
|
||||
|
||||
La langue n'est pas changée pour l'éprouver — « set_lang » la
|
||||
PERSISTE dans env_var.sh, et un test qui la déplace fait échouer
|
||||
tout ce qui suit. On remplace la traduction elle-même, le temps du
|
||||
contrôle, par une chaîne qui porte le caractère dangereux.
|
||||
"""
|
||||
import tempfile
|
||||
|
||||
piege = "l'agent n'a pas fini « attendre »"
|
||||
with mock.patch("script.todo.qemu_install.t", lambda k: piege):
|
||||
todo = TODO.__new__(TODO)
|
||||
cmd = (
|
||||
todo._qemu_cloud_init_wait()
|
||||
+ todo._qemu_no_auto_upgrade(prod=False)
|
||||
+ todo._qemu_desktop_remote_cmd("gnome", "deb")
|
||||
)
|
||||
with tempfile.NamedTemporaryFile(
|
||||
"w", suffix=".sh", delete=False, encoding="utf-8"
|
||||
) as fh:
|
||||
fh.write(cmd)
|
||||
chemin = fh.name
|
||||
res = subprocess.run(
|
||||
["bash", "-n", chemin], capture_output=True, text=True
|
||||
)
|
||||
self.assertEqual(res.returncode, 0, res.stderr[:400])
|
||||
|
||||
def test_la_boucle_reste_bornee(self):
|
||||
"""Sans borne, un verrou jamais rendu tiendrait l'installation pour
|
||||
toujours."""
|
||||
i = self.cmd.index("until sudo apt-get")
|
||||
boucle = self.cmd[i : self.cmd.index("done;", i)]
|
||||
self.assertRegex(boucle, r"-ge \d+ \]")
|
||||
self.assertIn("break", boucle)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Reference in a new issue