[FIX] qemu: really prepare the host in the Deployment profile
The « ERPLibre Deployment (+ QEMU + dev) » profile installed packages with a one-liner ending in « || true » and hiding stderr, so a broken host looked installed. Reproduced on erplibre-arch-latest: every binary was present, yet virt-install failed and « virsh list --all » could not reach any hypervisor. Three distinct causes, all unhandled: - The user was never added to the libvirt group. Without it a non-root libvirt client falls back to qemu:///session, where the « default » network does not exist, so « --network network=default » fails while everything looks installed. Being in the group grants the RIGHT to reach qemu:///system but does NOT change the default URI, so virt-install and virsh now pass « --connect qemu:///system » explicitly (new LIBVIRT_URI constant). - dnsmasq was missing: ensure_tools only installed DAEMON_PACKAGES when the daemon was absent, and libvirt was already there. setup_host now forces them. - On a rolling distro, « make install_os » upgrades the kernel and the package manager removes /lib/modules/<running kernel>. modprobe bridge then fails and libvirt cannot create virbr0 (« Unable to create bridge virbr0: Package not installed »). No package fixes that, only a reboot: it is now diagnosed and reported instead of surfacing as an unreadable virsh error. The profile now calls « deploy_qemu.py --setup-host », which reuses the existing ensure_* chain, so package names stay defined in one place (TOOL_PACKAGES / DAEMON_PACKAGES) and keep working for apt, dnf, pacman, zypper and brew. It fails loudly instead of « || true ». Verified on the Arch VM that failed: setup-host reports the stale kernel and exits 1; after a reboot it installs dnsmasq, joins libvirt/kvm, starts the default network, and reports « Hôte prêt » (exit 0, idempotent on rerun). The generated command now reads « virt-install --connect qemu:///system ». Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a9ef5a0e8e
commit
fba3b8aed1
2 changed files with 205 additions and 22 deletions
|
|
@ -47,6 +47,7 @@ from __future__ import annotations
|
|||
|
||||
import argparse
|
||||
import getpass
|
||||
import grp
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
|
|
@ -148,6 +149,7 @@ def host_arch() -> str:
|
|||
"s390x": "s390x",
|
||||
}.get(machine, "amd64")
|
||||
|
||||
|
||||
ARCH_CLOUD_BASE = "https://geo.mirror.pkgbuild.com/images/latest"
|
||||
|
||||
CLOUD_IMG_BASE = "https://cloud-images.ubuntu.com"
|
||||
|
|
@ -237,7 +239,9 @@ def resolve_fedora_url(version: str, arch: str, dry_run: bool) -> str:
|
|||
for base in bases:
|
||||
index = f"{base}/{version}/Cloud/{a}/images/"
|
||||
try:
|
||||
with urllib.request.urlopen(index, timeout=30) as resp: # noqa: S310
|
||||
with urllib.request.urlopen(
|
||||
index, timeout=30
|
||||
) as resp: # noqa: S310
|
||||
html = resp.read().decode(errors="replace")
|
||||
except Exception as exc: # pragma: no cover - dépend du réseau
|
||||
last_err = str(exc)
|
||||
|
|
@ -377,6 +381,14 @@ def need_tool(name: str) -> None:
|
|||
# --------------------------------------------------------------------------- #
|
||||
# Outils indispensables au déploiement complet (le mode --download-only n'en
|
||||
# requiert aucun).
|
||||
# URI libvirt visée par TOUS les clients (virsh, virt-install). À ne jamais
|
||||
# laisser implicite : pour un utilisateur non root, libvirt choisit
|
||||
# « qemu:///session », où le réseau « default » N'EXISTE PAS — virt-install
|
||||
# échoue alors sur « --network network=default » de façon incompréhensible.
|
||||
# Appartenir au groupe libvirt donne le DROIT d'accéder à qemu:///system mais
|
||||
# ne change PAS l'URI par défaut : il faut l'imposer.
|
||||
LIBVIRT_URI = "qemu:///system"
|
||||
|
||||
REQUIRED_TOOLS: tuple[str, ...] = ("qemu-img", "virt-install", "virsh")
|
||||
# Pour construire le seed cloud-init il faut AU MOINS un de ces outils.
|
||||
SEED_TOOLS: tuple[str, ...] = ("cloud-localds", "genisoimage")
|
||||
|
|
@ -613,11 +625,142 @@ def ensure_libvirt_service(runner: Runner) -> None:
|
|||
)
|
||||
|
||||
|
||||
def ensure_tools(runner: Runner, assume_yes: bool, no_install: bool) -> None:
|
||||
def invoking_user() -> str:
|
||||
"""Utilisateur réel derrière l'appel, même sous sudo."""
|
||||
return os.environ.get("SUDO_USER") or getpass.getuser()
|
||||
|
||||
|
||||
def ensure_libvirt_group(runner: Runner) -> bool:
|
||||
"""Ajoute l'utilisateur au groupe libvirt. Renvoie True s'il l'était déjà.
|
||||
|
||||
SANS ce groupe, les clients libvirt d'un utilisateur non root retombent sur
|
||||
« qemu:///session », où le réseau « default » N'EXISTE PAS : virt-install
|
||||
échoue alors sur « --network network=default » alors que tous les paquets
|
||||
sont pourtant installés. C'est le trou qui manquait au profil
|
||||
« ERPLibre Déploiement » : il installait QEMU sans jamais rendre
|
||||
qemu:///system accessible.
|
||||
"""
|
||||
user = invoking_user()
|
||||
if user == "root":
|
||||
return True
|
||||
lst_group = []
|
||||
for group in ("libvirt", "kvm"):
|
||||
try:
|
||||
grp.getgrnam(group)
|
||||
except KeyError:
|
||||
continue # groupe absent sur cette distro
|
||||
try:
|
||||
if user in grp.getgrnam(group).gr_mem:
|
||||
continue
|
||||
except KeyError:
|
||||
pass
|
||||
lst_group.append(group)
|
||||
if not lst_group:
|
||||
print(f" Utilisateur « {user} » déjà dans les groupes libvirt/kvm.")
|
||||
return True
|
||||
for group in lst_group:
|
||||
print(f" Ajout de « {user} » au groupe « {group} »…")
|
||||
runner.run(
|
||||
["usermod", "-aG", group, user], privileged=True, check=False
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def kernel_modules_stale() -> str:
|
||||
"""Renvoie un message si les modules du noyau EN COURS ont disparu.
|
||||
|
||||
Sur une distro roulante, « make install_os » met le noyau à jour et le
|
||||
gestionnaire de paquets SUPPRIME /lib/modules/<noyau en cours>. Tant qu'on
|
||||
n'a pas redémarré, « modprobe bridge » échoue et libvirt ne peut pas créer
|
||||
virbr0 : « Unable to create bridge virbr0: Package not installed ». Aucun
|
||||
paquet ne corrige ça, seul un redémarrage le fait — d'où ce diagnostic.
|
||||
"""
|
||||
if not shutil.which("uname"):
|
||||
return ""
|
||||
running = os.uname().release
|
||||
path_module = f"/lib/modules/{running}"
|
||||
if os.path.isdir(path_module):
|
||||
return ""
|
||||
return (
|
||||
f"Le noyau en cours ({running}) n'a plus ses modules "
|
||||
f"({path_module} absent) : il a été mis à jour depuis le démarrage.\n"
|
||||
" libvirt ne pourra pas créer le pont virbr0 tant que la machine\n"
|
||||
" n'aura pas REDÉMARRÉ. Redémarrez, puis relancez --setup-host."
|
||||
)
|
||||
|
||||
|
||||
def setup_host(runner: Runner, assume_yes: bool, no_install: bool) -> None:
|
||||
"""Prépare l'hôte à faire tourner des VM : paquets, démon, groupe, réseau.
|
||||
|
||||
Point d'entrée unique du profil d'installation « ERPLibre Déploiement ».
|
||||
Il réutilise les mêmes fonctions que le déploiement, donc les noms de
|
||||
paquets restent définis à UN SEUL endroit (TOOL_PACKAGES /
|
||||
DAEMON_PACKAGES) et restent valides pour apt, dnf, pacman, zypper et brew.
|
||||
"""
|
||||
print("\n== Préparation de l'hôte pour QEMU/libvirt ==")
|
||||
# ensure_tools installe les clients ET, si le démon manque, les paquets de
|
||||
# DAEMON_PACKAGES (qemu système + libvirt + firmware UEFI), puis démarre le
|
||||
# service. ensure_emulator ne sert QU'À l'émulation croisée (arm64 sur x86)
|
||||
# et n'a pas de clé pour l'architecture hôte.
|
||||
ensure_tools(runner, assume_yes, no_install, force_daemon=True)
|
||||
ensure_libvirt_service(runner)
|
||||
already_in_group = ensure_libvirt_group(runner)
|
||||
# Diagnostiqué AVANT le réseau : sans les modules du noyau en cours, le
|
||||
# pont virbr0 est impossible et l'erreur de virsh est indéchiffrable.
|
||||
stale = kernel_modules_stale()
|
||||
if stale:
|
||||
print(f"\n⚠ {stale}")
|
||||
ensure_network("default", runner)
|
||||
|
||||
if runner.dry_run:
|
||||
print("\n[dry-run] Rien n'a été modifié, vérification ignorée.")
|
||||
return
|
||||
|
||||
print("\n== Vérification ==")
|
||||
ok = libvirt_ready(runner.use_sudo)
|
||||
print(f" hyperviseur qemu:///system : {'OK' if ok else 'INJOIGNABLE'}")
|
||||
active, autostart = network_state("default", runner.use_sudo)
|
||||
print(
|
||||
f" réseau libvirt « default » : "
|
||||
f"{'actif' if active else 'INACTIF'}"
|
||||
f" / {'autostart' if autostart else 'PAS autostart'}"
|
||||
)
|
||||
if not (ok and active):
|
||||
sys.exit(
|
||||
"Erreur : l'hôte n'est pas prêt.\n"
|
||||
+ (f" {stale}\n" if stale else "")
|
||||
+ " Sinon vérifiez :\n"
|
||||
" sudo systemctl enable --now libvirtd\n"
|
||||
" sudo virsh -c qemu:///system net-start default"
|
||||
)
|
||||
if not already_in_group:
|
||||
print(
|
||||
f"\n⚠ « {invoking_user()} » vient d'être ajouté au groupe libvirt."
|
||||
" Les groupes ne s'appliquent qu'aux NOUVELLES sessions :"
|
||||
" reconnectez-vous (ou « newgrp libvirt »), sinon virt-install"
|
||||
" continuera d'utiliser qemu:///session et le réseau « default »"
|
||||
" restera introuvable."
|
||||
)
|
||||
print("\nHôte prêt.")
|
||||
|
||||
|
||||
def ensure_tools(
|
||||
runner: Runner,
|
||||
assume_yes: bool,
|
||||
no_install: bool,
|
||||
force_daemon: bool = False,
|
||||
) -> None:
|
||||
"""Vérifie outils, démon libvirt et émulateur ; installe/démarre ce qui
|
||||
manque, puis vérifie la connexion à l'hyperviseur."""
|
||||
manque, puis vérifie la connexion à l'hyperviseur.
|
||||
|
||||
`force_daemon` réinstalle DAEMON_PACKAGES même quand le démon répond déjà.
|
||||
Vécu sur Arch : libvirt était présent (posé par un ancien one-liner qui ne
|
||||
listait pas dnsmasq), donc daemon_missing() renvoyait False et dnsmasq
|
||||
n'était jamais installé -> « Failed to start network default ». Les
|
||||
gestionnaires de paquets ignorent ce qui est déjà là : c'est bon marché.
|
||||
"""
|
||||
missing = missing_tools()
|
||||
need_daemon = daemon_missing()
|
||||
need_daemon = daemon_missing() or force_daemon
|
||||
|
||||
# Tout est là et l'hyperviseur répond : rien à faire.
|
||||
if not missing and not need_daemon and libvirt_ready(runner.use_sudo):
|
||||
|
|
@ -1138,7 +1281,13 @@ def network_name(network_arg: str) -> str | None:
|
|||
|
||||
def network_state(name: str, use_sudo: bool) -> tuple[bool, bool]:
|
||||
"""(actif, autostart) d'un réseau libvirt, via « virsh net-info »."""
|
||||
cmd = (["sudo"] if use_sudo else []) + ["virsh", "net-info", name]
|
||||
cmd = (["sudo"] if use_sudo else []) + [
|
||||
"virsh",
|
||||
"-c",
|
||||
LIBVIRT_URI,
|
||||
"net-info",
|
||||
name,
|
||||
]
|
||||
try:
|
||||
res = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
|
|
@ -1156,9 +1305,15 @@ def ensure_network(name: str | None, runner: Runner) -> None:
|
|||
return
|
||||
if runner.dry_run:
|
||||
print(f" [dry-run] réseau libvirt '{name}' activé si nécessaire")
|
||||
runner.run(["virsh", "net-start", name], privileged=True, check=False)
|
||||
runner.run(
|
||||
["virsh", "net-autostart", name], privileged=True, check=False
|
||||
["virsh", "-c", LIBVIRT_URI, "net-start", name],
|
||||
privileged=True,
|
||||
check=False,
|
||||
)
|
||||
runner.run(
|
||||
["virsh", "-c", LIBVIRT_URI, "net-autostart", name],
|
||||
privileged=True,
|
||||
check=False,
|
||||
)
|
||||
return
|
||||
active, autostart = network_state(name, runner.use_sudo)
|
||||
|
|
@ -1167,10 +1322,16 @@ def ensure_network(name: str | None, runner: Runner) -> None:
|
|||
return
|
||||
print(f" Configuration du réseau libvirt '{name}'…")
|
||||
if not active:
|
||||
runner.run(["virsh", "net-start", name], privileged=True, check=False)
|
||||
runner.run(
|
||||
["virsh", "-c", LIBVIRT_URI, "net-start", name],
|
||||
privileged=True,
|
||||
check=False,
|
||||
)
|
||||
if not autostart:
|
||||
runner.run(
|
||||
["virsh", "net-autostart", name], privileged=True, check=False
|
||||
["virsh", "-c", LIBVIRT_URI, "net-autostart", name],
|
||||
privileged=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1188,6 +1349,11 @@ def virt_install(
|
|||
console_target = "sclp" if args.arch == "s390x" else "serial"
|
||||
cmd = [
|
||||
"virt-install",
|
||||
# Sans --connect, un utilisateur non root vise qemu:///session : le
|
||||
# réseau « default » n'y existe pas et la création échoue même quand
|
||||
# tous les paquets sont installés et le réseau actif côté système.
|
||||
"--connect",
|
||||
LIBVIRT_URI,
|
||||
"--name",
|
||||
args.name,
|
||||
"--memory",
|
||||
|
|
@ -1284,7 +1450,8 @@ def wait_for_ip(name: str, use_sudo: bool, timeout: int) -> str | None:
|
|||
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 »)."""
|
||||
aveuglément la 1re (souvent le bail précoce périmé, « No route to host »).
|
||||
"""
|
||||
base = (["sudo"] if use_sudo else []) + [
|
||||
"virsh",
|
||||
"domifaddr",
|
||||
|
|
@ -1544,6 +1711,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
action="store_true",
|
||||
help="N'installe jamais les dépendances manquantes (échoue si absentes).",
|
||||
)
|
||||
g_run.add_argument(
|
||||
"--setup-host",
|
||||
action="store_true",
|
||||
help="Prépare l'hôte (paquets QEMU/libvirt, démon, groupe libvirt, "
|
||||
"réseau default) puis quitte. Ne déploie aucune VM.",
|
||||
)
|
||||
g_run.add_argument(
|
||||
"--list-images",
|
||||
action="store_true",
|
||||
|
|
@ -1590,6 +1763,19 @@ def main() -> None:
|
|||
list_images()
|
||||
return
|
||||
|
||||
# Préparation de l'hôte : aucune image, aucune distro cible, aucun --name.
|
||||
# Traité AVANT la résolution de version/image pour rester utilisable seul.
|
||||
if args.setup_host:
|
||||
setup_host(
|
||||
Runner(
|
||||
use_sudo=not args.no_sudo and os.geteuid() != 0,
|
||||
dry_run=args.dry_run,
|
||||
),
|
||||
args.assume_yes,
|
||||
args.no_install_deps,
|
||||
)
|
||||
return
|
||||
|
||||
versions, default_version = DISTROS[args.distro]
|
||||
if args.version is None:
|
||||
args.version = default_version
|
||||
|
|
|
|||
|
|
@ -3070,18 +3070,15 @@ class TODO:
|
|||
# sshd répondait au moins (mieux qu'un abandon silencieux).
|
||||
return ssh_up
|
||||
|
||||
# Paquets QEMU/libvirt pour le profil « Déploiement » (nos 3 gestionnaires).
|
||||
# Paquets SYSTÈME du profil « ERPLibre Déploiement » : QEMU/libvirt/
|
||||
# virtinst + libguestfs (virt-resize/virt-filesystems, requis pour la
|
||||
# RÉDUCTION SÛRE de disque). Ce sont des binaires système (OCaml) : ils ne
|
||||
# peuvent PAS vivre dans .venv.erplibre (venv Python) — d'où leur place ici.
|
||||
_QEMU_QEMU_PKGS = (
|
||||
"(sudo apt-get install -y qemu-kvm libvirt-daemon-system virtinst "
|
||||
"cloud-image-utils libguestfs-tools 2>/dev/null || sudo dnf install "
|
||||
"-y qemu-kvm libvirt virt-install cloud-utils guestfs-tools "
|
||||
"2>/dev/null || sudo pacman -S --needed --noconfirm qemu-desktop "
|
||||
"libvirt virt-install libguestfs 2>/dev/null || true)"
|
||||
)
|
||||
# Préparation hôte QEMU/libvirt du profil « ERPLibre Déploiement ».
|
||||
# Délègue à deploy_qemu.py --setup-host : les noms de paquets y sont déjà
|
||||
# définis pour apt/dnf/pacman/zypper/brew (TOOL_PACKAGES, DAEMON_PACKAGES),
|
||||
# et il fait ce que l'ancien one-liner ne faisait PAS — démarrer le démon,
|
||||
# ajouter l'utilisateur au groupe libvirt et activer le réseau « default ».
|
||||
# Sans le groupe, virt-install retombe sur qemu:///session où « default »
|
||||
# n'existe pas : la VM échoue alors que tous les paquets sont installés.
|
||||
# L'ancien one-liner finissait par « || true » et masquait ses erreurs.
|
||||
_QEMU_QEMU_PKGS = "./script/qemu/deploy_qemu.py --setup-host --assume-yes"
|
||||
|
||||
def _qemu_ask_prod(self):
|
||||
"""Environnement cible : dev (défaut) ou prod. En PROD : ERPLibre est
|
||||
|
|
|
|||
Loading…
Reference in a new issue