[FIX] script qemu/todo: bonne IP quand la VM a plusieurs baux DHCP
Symptôme : ~/.ssh/config (et l'install) pointaient sur .31 alors que la VM était joignable sur .32 -> SSH KO (« No route to host »), console OK. Cause : au boot, une image cloud demande d'abord une IP DHCP avec son hostname par défaut (« ubuntu ») -> 1er bail ; puis cloud-init fixe le vrai hostname et le client redemande -> 2e bail (IP différente). La MÊME MAC a donc DEUX baux ; le 1er (« ubuntu ») devient périmé. Le code retenait aveuglément le PREMIER IPv4 de « virsh domifaddr » = le bail périmé. Plus visible sur s390x/arm64 émulé (boot lent -> les deux baux coexistent). Fix : - todo._qemu_vm_ip : renvoie en priorité l'IP dont le bail dnsmasq porte le hostname == nom de la VM (bail définitif), sinon une IP JOIGNABLE (sshd up, test TCP:22), sinon le dernier bail — jamais le 1er au hasard. Attente portée à 10 min (boot émulé lent). Helpers _qemu_lease_candidates / _qemu_ip_reachable / _qemu_lease_ip_for_host. - deploy_qemu.wait_for_ip : même logique (IP joignable, sinon la plus récente) pour l'IP affichée en fin de déploiement standalone. Validé sur la VM s390x réelle : candidats [.31, .32] -> choisit .32 (hostname-match + seule joignable) ; ssh via alias OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0930f30cd3
commit
bd94c277d3
2 changed files with 97 additions and 22 deletions
|
|
@ -51,6 +51,7 @@ import hashlib
|
|||
import os
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
|
@ -1170,8 +1171,21 @@ def virt_install(
|
|||
runner.run(cmd, privileged=True)
|
||||
|
||||
|
||||
def _ip_reachable(ip: str, port: int = 22, timeout: float = 3) -> bool:
|
||||
"""Vrai si le port SSH répond (distingue le bail actif du bail périmé)."""
|
||||
try:
|
||||
with socket.create_connection((ip, port), timeout=timeout):
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def wait_for_ip(name: str, use_sudo: bool, timeout: int) -> str | None:
|
||||
"""Interroge les baux DHCP libvirt jusqu'à obtenir l'IPv4 de la VM."""
|
||||
"""Interroge les baux DHCP libvirt jusqu'à obtenir l'IPv4 de la VM. Une VM
|
||||
peut avoir PLUSIEURS baux (l'image demande d'abord une IP avec le hostname
|
||||
par défaut « ubuntu », puis cloud-init fixe le vrai hostname -> 2e bail) :
|
||||
on renvoie une IP JOIGNABLE (sshd up), sinon la plus récente, jamais
|
||||
aveuglément la 1re (souvent le bail précoce périmé, « No route to host »)."""
|
||||
base = (["sudo"] if use_sudo else []) + [
|
||||
"virsh",
|
||||
"domifaddr",
|
||||
|
|
@ -1180,13 +1194,15 @@ def wait_for_ip(name: str, use_sudo: bool, timeout: int) -> str | None:
|
|||
"lease",
|
||||
]
|
||||
deadline = time.time() + timeout
|
||||
ips: list[str] = []
|
||||
while time.time() < deadline:
|
||||
res = subprocess.run(base, capture_output=True, text=True)
|
||||
m = re.search(r"(\d+\.\d+\.\d+\.\d+)", res.stdout)
|
||||
if m:
|
||||
return m.group(1)
|
||||
ips = re.findall(r"(\d+\.\d+\.\d+\.\d+)", res.stdout)
|
||||
for ip in ips:
|
||||
if _ip_reachable(ip):
|
||||
return ip
|
||||
time.sleep(3)
|
||||
return None
|
||||
return ips[-1] if ips else None
|
||||
|
||||
|
||||
def ssh_command(user: str, ip: str, has_key: bool) -> str:
|
||||
|
|
|
|||
|
|
@ -1678,25 +1678,84 @@ class TODO:
|
|||
except (OSError, subprocess.SubprocessError):
|
||||
return False
|
||||
|
||||
def _qemu_vm_ip(self, name, timeout=90):
|
||||
"""Attend puis renvoie l'IPv4 d'une VM (bail DHCP), sinon None."""
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["sudo", "virsh", "domifaddr", name, "--source", "lease"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return None
|
||||
m = re.search(r"(\d+\.\d+\.\d+\.\d+)", res.stdout)
|
||||
if m:
|
||||
return m.group(1)
|
||||
time.sleep(3)
|
||||
@staticmethod
|
||||
def _qemu_lease_candidates(name):
|
||||
"""Toutes les IPv4 du bail DHCP de la VM. Il peut y en avoir PLUSIEURS :
|
||||
au boot, l'image cloud demande d'abord une IP avec son hostname par
|
||||
défaut (« ubuntu ») -> 1er bail ; puis cloud-init fixe le vrai hostname
|
||||
et le client redemande -> 2e bail (IP différente). Le 1er devient
|
||||
périmé (« No route to host »)."""
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["sudo", "virsh", "domifaddr", name, "--source", "lease"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return []
|
||||
return re.findall(r"(\d+\.\d+\.\d+\.\d+)", res.stdout)
|
||||
|
||||
@staticmethod
|
||||
def _qemu_ip_reachable(ip, port=22, timeout=3):
|
||||
"""Vrai si le port SSH répond (sshd up) : distingue le bail actif du
|
||||
bail périmé sans dépendre du ping (souvent filtré)."""
|
||||
import socket
|
||||
|
||||
try:
|
||||
with socket.create_connection((ip, port), timeout=timeout):
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _qemu_lease_ip_for_host(name, candidates):
|
||||
"""Parmi `candidates`, l'IP dont le bail dnsmasq porte le hostname de la
|
||||
VM (le bail DÉFINITIF, pas le bail précoce « ubuntu »). None sinon."""
|
||||
try:
|
||||
res = subprocess.run(
|
||||
[
|
||||
"sudo",
|
||||
"sh",
|
||||
"-c",
|
||||
"cat /var/lib/libvirt/dnsmasq/*.status 2>/dev/null",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return None
|
||||
# Plusieurs tableaux JSON concaténés : on parse chaque objet {...}.
|
||||
for obj in re.findall(r"\{[^{}]*\}", res.stdout or ""):
|
||||
if re.search(rf'"hostname":\s*"{re.escape(name)}"', obj):
|
||||
m = re.search(r'"ip-address":\s*"([\d.]+)"', obj)
|
||||
if m and m.group(1) in candidates:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
def _qemu_vm_ip(self, name, timeout=600):
|
||||
"""IPv4 utilisable d'une VM. Gère le cas des baux multiples (hostname
|
||||
changé au boot) : renvoie en priorité le bail dont le hostname == nom
|
||||
de la VM, sinon une IP JOIGNABLE (sshd up), pour ne jamais retenir le
|
||||
bail précoce périmé. Attend jusqu'à `timeout` (boot émulé lent)."""
|
||||
deadline = time.time() + timeout
|
||||
cands = []
|
||||
while time.time() < deadline:
|
||||
cands = self._qemu_lease_candidates(name)
|
||||
if cands:
|
||||
# 1) bail définitif (hostname == nom de la VM)
|
||||
host_ip = self._qemu_lease_ip_for_host(name, cands)
|
||||
if host_ip:
|
||||
return host_ip
|
||||
# 2) sinon, une IP déjà joignable (sshd up)
|
||||
for ip in cands:
|
||||
if self._qemu_ip_reachable(ip):
|
||||
return ip
|
||||
time.sleep(3)
|
||||
# Meilleur effort : le dernier bail (le plus récent) plutôt que le 1er.
|
||||
return cands[-1] if cands else None
|
||||
|
||||
def _qemu_pick_branch(self):
|
||||
"""Liste les branches distantes d'ERPLibre et en fait choisir une."""
|
||||
print(f"\n{t('Fetching ERPLibre branch list...')}")
|
||||
|
|
|
|||
Loading…
Reference in a new issue