[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 os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
@ -1170,8 +1171,21 @@ def virt_install(
|
||||||
runner.run(cmd, privileged=True)
|
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:
|
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 []) + [
|
base = (["sudo"] if use_sudo else []) + [
|
||||||
"virsh",
|
"virsh",
|
||||||
"domifaddr",
|
"domifaddr",
|
||||||
|
|
@ -1180,13 +1194,15 @@ def wait_for_ip(name: str, use_sudo: bool, timeout: int) -> str | None:
|
||||||
"lease",
|
"lease",
|
||||||
]
|
]
|
||||||
deadline = time.time() + timeout
|
deadline = time.time() + timeout
|
||||||
|
ips: list[str] = []
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
res = subprocess.run(base, capture_output=True, text=True)
|
res = subprocess.run(base, capture_output=True, text=True)
|
||||||
m = re.search(r"(\d+\.\d+\.\d+\.\d+)", res.stdout)
|
ips = re.findall(r"(\d+\.\d+\.\d+\.\d+)", res.stdout)
|
||||||
if m:
|
for ip in ips:
|
||||||
return m.group(1)
|
if _ip_reachable(ip):
|
||||||
|
return ip
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
return None
|
return ips[-1] if ips else None
|
||||||
|
|
||||||
|
|
||||||
def ssh_command(user: str, ip: str, has_key: bool) -> str:
|
def ssh_command(user: str, ip: str, has_key: bool) -> str:
|
||||||
|
|
|
||||||
|
|
@ -1678,25 +1678,84 @@ class TODO:
|
||||||
except (OSError, subprocess.SubprocessError):
|
except (OSError, subprocess.SubprocessError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _qemu_vm_ip(self, name, timeout=90):
|
@staticmethod
|
||||||
"""Attend puis renvoie l'IPv4 d'une VM (bail DHCP), sinon None."""
|
def _qemu_lease_candidates(name):
|
||||||
deadline = time.time() + timeout
|
"""Toutes les IPv4 du bail DHCP de la VM. Il peut y en avoir PLUSIEURS :
|
||||||
while time.time() < deadline:
|
au boot, l'image cloud demande d'abord une IP avec son hostname par
|
||||||
try:
|
défaut (« ubuntu ») -> 1er bail ; puis cloud-init fixe le vrai hostname
|
||||||
res = subprocess.run(
|
et le client redemande -> 2e bail (IP différente). Le 1er devient
|
||||||
["sudo", "virsh", "domifaddr", name, "--source", "lease"],
|
périmé (« No route to host »)."""
|
||||||
capture_output=True,
|
try:
|
||||||
text=True,
|
res = subprocess.run(
|
||||||
timeout=15,
|
["sudo", "virsh", "domifaddr", name, "--source", "lease"],
|
||||||
)
|
capture_output=True,
|
||||||
except (OSError, subprocess.SubprocessError):
|
text=True,
|
||||||
return None
|
timeout=15,
|
||||||
m = re.search(r"(\d+\.\d+\.\d+\.\d+)", res.stdout)
|
)
|
||||||
if m:
|
except (OSError, subprocess.SubprocessError):
|
||||||
return m.group(1)
|
return []
|
||||||
time.sleep(3)
|
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
|
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):
|
def _qemu_pick_branch(self):
|
||||||
"""Liste les branches distantes d'ERPLibre et en fait choisir une."""
|
"""Liste les branches distantes d'ERPLibre et en fait choisir une."""
|
||||||
print(f"\n{t('Fetching ERPLibre branch list...')}")
|
print(f"\n{t('Fetching ERPLibre branch list...')}")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue