[FIX] qemu monitor: follow the DHCP lease, and keep the log talking

Monitoring stayed on the same address for 1170 s, never catching up with
the VM. Two holes, in the very re-resolution meant to prevent that.

The lease fallback required an answer on port 22. But dnsmasq keeps one
lease per MAC: when cloud-init sets the real hostname and the DHCP client
asks again, the lease MOVES the address. The old one no longer belongs to
the VM and sshd will never answer there. The lease therefore wins as soon
as it stops listing the current address.

The other hole explains the silence: virsh was muted on both branches, so
an unreachable libvirt kept the initial IP without a single line saying
so. The log went quiet for a quarter of an hour for the same reason --
cloud-init holds the package lock while writing nothing.

--- FR ---

Le suivi restait sur la même adresse pendant 1170 s, sans jamais rattraper
la VM. Deux trous, dans la re-résolution censée l'éviter.

Le repli par bail exigeait une réponse sur le port 22. Or dnsmasq garde un
bail par MAC : quand cloud-init pose le vrai nom d'hôte et que le client
DHCP redemande, le bail DÉPLACE l'adresse. L'ancienne n'appartient plus à
la VM et sshd n'y répondra jamais. Le bail l'emporte donc dès qu'il cesse
de lister l'adresse courante.

L'autre trou explique le silence : virsh était muet sur les deux branches,
si bien qu'un libvirt injoignable conservait l'IP initiale sans une ligne
pour le dire. Le log se taisait un quart d'heure pour la même raison —
cloud-init tient le verrou des paquets sans rien écrire.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-08-11 08:36:13 -04:00
parent 42391575d6
commit 571ccf3c90
3 changed files with 80 additions and 5 deletions

View file

@ -98,6 +98,15 @@ def _launch_one(
msg_wait = t("Waiting for the VM to start (boot + cloud-init)")
msg_slow = t("(an emulated architecture can be slow; this is normal)")
msg_ready = t("VM ready - starting the ERPLibre install")
msg_giveup = t(
"cloud-init still running after 20 min - install starts anyway"
" (it waits for cloud-init first)"
)
msg_novirsh = t(
"WARNING libvirt unreachable: the IP will not be refreshed"
" (libvirt group? re-login required)"
)
msg_moved = t("DHCP lease moved:")
# L'IP est RÉSOLUE À CHAQUE TOUR, jamais figée. Au 1er boot la VM prend un
# bail sous le nom par défaut de l'image, puis cloud-init pose le vrai nom
# d'hôte et le client DHCP en redemande un AUTRE. L'adresse connue au
@ -130,17 +139,38 @@ def _launch_one(
'vsh() { virsh --connect qemu:///system "$@" 2>/dev/null '
'|| sudo -n virsh --connect qemu:///system "$@" 2>/dev/null; }; '
)
# Deux trous rendaient cette ré-résolution incapable de rattraper une IP
# qui bouge — le cas exact où elle sert :
#
# - « vsh » est muet des deux côtés. Quand libvirt est injoignable (hors du
# groupe libvirt, et « sudo -n » refusé faute de tty dans ce processus
# détaché), la ré-résolution ne renvoie JAMAIS rien : l'IP de départ est
# gardée jusqu'au bout sans qu'une seule ligne du log ne le dise.
# - le repli sur les baux exigeait une réponse sur le port 22. Or dnsmasq
# ne garde qu'un bail par MAC : quand cloud-init pose le vrai nom d'hôte
# et que le client DHCP redemande, le bail DÉPLACE l'adresse. L'ancienne
# n'appartient plus à cette VM, mais on l'attendait quand même — et sshd
# n'y répondra jamais.
refresh = (
(
f"raw=$(vsh domifaddr {name_q} --source lease); vrc=$?; "
'if [ $vrc -ne 0 ] && [ -z "$vwarn" ]; then vwarn=1; '
f"echo {shlex.quote(' ' + msg_novirsh)} >> {log_q}; fi; "
"cands=$(echo \"$raw\" | grep -oE '([0-9]{1,3}\\.){3}[0-9]{1,3}' "
"| grep -v '^127\\.'); "
f"n=$(vsh domifaddr {name_q} --source agent "
"| grep -oE '([0-9]{1,3}\\.){3}[0-9]{1,3}' "
"| grep -v '^127\\.' | head -1); "
'if [ -z "$n" ]; then '
f"for c in $(vsh domifaddr {name_q} --source lease "
"| grep -oE '([0-9]{1,3}\\.){3}[0-9]{1,3}' "
"| grep -v '^127\\.'); do "
"for c in $cands; do "
'timeout 2 bash -c "echo > /dev/tcp/$c/22" 2>/dev/null '
'&& n="$c"; done; fi; '
# Le bail ne mentionne plus l'adresse courante : elle est périmée,
# on suit le bail sans attendre que sshd réponde.
'if [ -z "$n" ] && [ -n "$cands" ] && '
'! echo "$cands" | grep -Fqx "$ip"; then '
'n=$(echo "$cands" | tail -1); '
f'echo " {msg_moved} $ip -> $n" >> {log_q}; fi; '
'[ -n "$n" ] && ip="$n"; '
)
if name
@ -151,17 +181,24 @@ def _launch_one(
f"{vsh if name else ''}"
f"echo {shlex.quote('== ' + msg_wait + ' ==')} >> {log_q}; "
f"echo {shlex.quote(' ' + msg_slow)} >> {log_q}; "
f"seen=0; "
f"for i in $(seq 1 240); do "
f"{refresh}"
f'st=$(ssh {SSH_OPTS} -o BatchMode=yes "erplibre@$ip" '
f"{shlex.quote(ci_probe)} 2>/dev/null); "
f'case "$st" in '
f"*done*|*disabled*|*error*|*degraded*|*nocloudinit*) break;; "
f"*done*|*disabled*|*error*|*degraded*|*nocloudinit*) seen=1; break;; "
f"esac; "
f"if [ $((i % 6)) -eq 0 ]; then "
f'echo " ... $((i*5))s ($ip)" >> {log_q}; fi; '
f"sleep 5; done; "
# La boucle peut s'ÉPUISER au lieu de rompre : sous émulation,
# cloud-init dépasse volontiers 20 min. Annoncer « VM prête » dans les
# deux cas donnait un message faux juste avant le plus long silence du
# log — c'est l'installation qui attend alors la fin de cloud-init.
f'if [ "$seen" = 1 ]; then '
f"echo {shlex.quote('== ' + msg_ready + ' ==')} >> {log_q}; "
f"else echo {shlex.quote('== ' + msg_giveup + ' ==')} >> {log_q}; fi; "
f'echo " → $ip" >> {log_q}; '
f'ssh {SSH_OPTS} "erplibre@$ip" {shlex.quote(remote_cmd)} '
f">> {log_q} 2>&1; "

View file

@ -4341,9 +4341,21 @@ class TODO:
# tient le verrou apt/dnf/pacman -> sinon « unable to lock
# database » (Arch) / « Could not get lock » (apt). timeout pour ne
# pas bloquer indéfiniment si cloud-init traîne.
"command -v cloud-init >/dev/null 2>&1 && "
#
# Cette attente dure jusqu'à 15 min et n'écrivait RIEN : sur une
# architecture émulée, le log restait muet d'un quart d'heure juste
# après avoir annoncé le début de l'installation, ce qui se lit
# comme un blocage. On encadre donc l'attente de deux lignes, et le
# « status » final dit si elle a abouti ou expiré.
"if command -v cloud-init >/dev/null 2>&1; then "
+ 'echo "== '
+ t("Waiting for cloud-init to finish (up to 15 min)")
+ ' =="; '
"sudo timeout 900 cloud-init status --wait >/dev/null 2>&1 "
"|| true; "
+ f'echo " {t("cloud-init:")} $(cloud-init status 2>/dev/null '
'| head -1)"; '
"fi; "
# Coupé AVANT les apt-get ci-dessous : sinon apt-daily peut reprendre
# le verrou entre l'attente cloud-init et l'installation.
+ no_auto_upgrade +

View file

@ -1973,6 +1973,32 @@ TRANSLATIONS = {
"fr": "RAM (Mo)",
"en": "RAM (MB)",
},
"cloud-init still running after 20 min - install starts anyway"
" (it waits for cloud-init first)": {
"fr": "cloud-init tourne encore apres 20 min - l'installation demarre"
" quand meme (elle attend d'abord cloud-init)",
"en": "cloud-init still running after 20 min - install starts anyway"
" (it waits for cloud-init first)",
},
"Waiting for cloud-init to finish (up to 15 min)": {
"fr": "Attente de la fin de cloud-init (jusqu'a 15 min)",
"en": "Waiting for cloud-init to finish (up to 15 min)",
},
"cloud-init:": {
"fr": "cloud-init :",
"en": "cloud-init:",
},
"WARNING libvirt unreachable: the IP will not be refreshed"
" (libvirt group? re-login required)": {
"fr": "ATTENTION libvirt injoignable : l'IP ne sera pas rafraichie"
" (groupe libvirt ? reconnexion necessaire)",
"en": "WARNING libvirt unreachable: the IP will not be refreshed"
" (libvirt group? re-login required)",
},
"DHCP lease moved:": {
"fr": "bail DHCP deplace :",
"en": "DHCP lease moved:",
},
"An install is still running:": {
"fr": "Une installation est encore en cours :",
"en": "An install is still running:",