[IMP] script qemu: multi-distro (Debian/Fedora), resilient osinfo, 26.04

Generalise the deployer beyond Ubuntu with a distro registry and a new
--distro flag (ubuntu default, debian, fedora). Each distro keeps its
own codenames, osinfo ids and per-version minimum RAM/disk. Image URLs
are built per distro (Ubuntu current/, Debian latest/, Fedora resolved
from the release index since it has no "latest" link).

Add --list-images to print the whole catalogue with specs, exposed in
the todo menu ("List available images and specs"); the deploy/download
flows now prompt for the distro first.

Resilient osinfo: when the local osinfo-db does not know an id (e.g.
ubuntu26.04, fedora43+), fall back to virt-install detect=on,require=off
instead of failing. Ubuntu 26.04 (resolute) is now enabled.

Also: throttle the download progress to integer-percent steps (single
updating line on a TTY, at most 101 lines when captured by the menu),
and skip the spurious "network is already active" error by checking the
libvirt network state before net-start.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathieu Benoit 2026-07-19 06:38:04 +00:00
parent 0d985ab449
commit 9c8e7454f3
6 changed files with 435 additions and 116 deletions

View file

@ -4,24 +4,30 @@
<!---------------------------->
<!-- [en] -->
# QEMU/KVM — Ubuntu VM deployment
# QEMU/KVM — Linux VM deployment (Ubuntu / Debian / Fedora)
`deploy_qemu.py` deploys an Ubuntu VM (libvirt/KVM) from an official Ubuntu
cloud image, using `qemu-img` + `cloud-init` + `virt-install`. It:
`deploy_qemu.py` deploys a Linux VM (libvirt/KVM) from an official cloud
image, using `qemu-img` + `cloud-init` + `virt-install`. Pick the
distribution with `--distro` (`ubuntu` default, `debian`, `fedora`) and the
release with `--version`; run `--list-images` to see the full catalogue with
minimum specs. It:
1. **Downloads the Ubuntu cloud image by itself** (cached, no double download).
1. **Downloads the cloud image by itself** (cached, no double download).
2. Converts it to a dedicated qcow2 working disk and resizes it.
3. Generates `user-data` / `meta-data` and builds the `seed.iso` (cloud-init).
4. Runs `virt-install` importing the disk + the seed as a CD-ROM.
5. Waits for the DHCP lease and prints the SSH command.
<!-- [fr] -->
# QEMU/KVM — Déploiement de VM Ubuntu
# QEMU/KVM — Déploiement de VM Linux (Ubuntu / Debian / Fedora)
`deploy_qemu.py` déploie une VM Ubuntu (libvirt/KVM) à partir d'une image
cloud Ubuntu officielle, via `qemu-img` + `cloud-init` + `virt-install`. Il :
`deploy_qemu.py` déploie une VM Linux (libvirt/KVM) à partir d'une image
cloud officielle, via `qemu-img` + `cloud-init` + `virt-install`. Choisissez
la distribution avec `--distro` (`ubuntu` par défaut, `debian`, `fedora`) et
la version avec `--version` ; `--list-images` affiche tout le catalogue avec
les specs minimales. Il :
1. **Télécharge lui-même l'image cloud Ubuntu** (mise en cache, sans double
1. **Télécharge lui-même l'image cloud** (mise en cache, sans double
téléchargement).
2. La convertit en un disque de travail qcow2 dédié et le redimensionne.
3. Génère `user-data` / `meta-data` et construit le `seed.iso` (cloud-init).
@ -201,14 +207,16 @@ parameters and builds the command for you.
## Main options
- `--version` — Ubuntu version (default `24.04`).
- `--distro``ubuntu` (default), `debian` or `fedora`.
- `--version` — release for the distro (default: the distro's default).
- `--list-images` — print all distros/versions and their specs, then exit.
- `--image-dir` — image cache directory (default `/var/lib/libvirt/images/iso`).
- `--download-only` — download the image then exit (no VM).
- `--name` — VM name (required for deployment).
- `--memory`, `--vcpus`, `--disk-size` — VM sizing. When omitted, `--memory`
and `--disk-size` default to the **minimum required by the chosen Ubuntu
version** (libosinfo values: 20.04 → 2048 MB/5G, 22.04 → 2048 MB/10G,
24.04+ → 3072 MB/20G); `--vcpus` defaults to 2.
and `--disk-size` default to the **minimum required by the chosen version**
(libosinfo values, see `--list-images`: Ubuntu 24.04+ → 3072 MB/20G, Debian
→ 1024 MB/10G, Fedora → 2048 MB/15G); `--vcpus` defaults to 2.
- `--ssh-key`, `--ask-password`, `--password-hash` — authentication.
- `-y` / `--assume-yes` — auto-accept dependency installation.
- `--no-install-deps` — never auto-install dependencies.
@ -231,15 +239,18 @@ vous.
## Principales options
- `--version` — version Ubuntu (défaut `24.04`).
- `--distro``ubuntu` (défaut), `debian` ou `fedora`.
- `--version` — version de la distro (défaut : celle par défaut de la distro).
- `--list-images` — affiche toutes les distros/versions et leurs specs.
- `--image-dir` — répertoire de cache des images (défaut
`/var/lib/libvirt/images/iso`).
- `--download-only` — télécharge l'image puis quitte (sans VM).
- `--name` — nom de la VM (requis pour le déploiement).
- `--memory`, `--vcpus`, `--disk-size` — dimensionnement de la VM. Omis,
`--memory` et `--disk-size` prennent le **minimum requis par la version
Ubuntu** choisie (valeurs libosinfo : 20.04 → 2048 Mo/5G, 22.04 →
2048 Mo/10G, 24.04+ → 3072 Mo/20G) ; `--vcpus` vaut 2 par défaut.
`--memory` et `--disk-size` prennent le **minimum requis par la version**
choisie (valeurs libosinfo, voir `--list-images` : Ubuntu 24.04+ →
3072 Mo/20G, Debian → 1024 Mo/10G, Fedora → 2048 Mo/15G) ; `--vcpus`
vaut 2 par défaut.
- `--ssh-key`, `--ask-password`, `--password-hash` — authentification.
- `-y` / `--assume-yes` — accepte automatiquement l'installation des
dépendances.

View file

@ -1,10 +1,13 @@
# QEMU/KVM — Déploiement de VM Ubuntu
# QEMU/KVM — Déploiement de VM Linux (Ubuntu / Debian / Fedora)
`deploy_qemu.py` déploie une VM Ubuntu (libvirt/KVM) à partir d'une image
cloud Ubuntu officielle, via `qemu-img` + `cloud-init` + `virt-install`. Il :
`deploy_qemu.py` déploie une VM Linux (libvirt/KVM) à partir d'une image
cloud officielle, via `qemu-img` + `cloud-init` + `virt-install`. Choisissez
la distribution avec `--distro` (`ubuntu` par défaut, `debian`, `fedora`) et
la version avec `--version` ; `--list-images` affiche tout le catalogue avec
les specs minimales. Il :
1. **Télécharge lui-même l'image cloud Ubuntu** (mise en cache, sans double
1. **Télécharge lui-même l'image cloud** (mise en cache, sans double
téléchargement).
2. La convertit en un disque de travail qcow2 dédié et le redimensionne.
3. Génère `user-data` / `meta-data` et construit le `seed.iso` (cloud-init).
@ -115,15 +118,18 @@ vous.
## Principales options
- `--version` — version Ubuntu (défaut `24.04`).
- `--distro``ubuntu` (défaut), `debian` ou `fedora`.
- `--version` — version de la distro (défaut : celle par défaut de la distro).
- `--list-images` — affiche toutes les distros/versions et leurs specs.
- `--image-dir` — répertoire de cache des images (défaut
`/var/lib/libvirt/images/iso`).
- `--download-only` — télécharge l'image puis quitte (sans VM).
- `--name` — nom de la VM (requis pour le déploiement).
- `--memory`, `--vcpus`, `--disk-size` — dimensionnement de la VM. Omis,
`--memory` et `--disk-size` prennent le **minimum requis par la version
Ubuntu** choisie (valeurs libosinfo : 20.04 → 2048 Mo/5G, 22.04 →
2048 Mo/10G, 24.04+ → 3072 Mo/20G) ; `--vcpus` vaut 2 par défaut.
`--memory` et `--disk-size` prennent le **minimum requis par la version**
choisie (valeurs libosinfo, voir `--list-images` : Ubuntu 24.04+ →
3072 Mo/20G, Debian → 1024 Mo/10G, Fedora → 2048 Mo/15G) ; `--vcpus`
vaut 2 par défaut.
- `--ssh-key`, `--ask-password`, `--password-hash` — authentification.
- `-y` / `--assume-yes` — accepte automatiquement l'installation des
dépendances.

View file

@ -1,10 +1,13 @@
# QEMU/KVM — Ubuntu VM deployment
# QEMU/KVM — Linux VM deployment (Ubuntu / Debian / Fedora)
`deploy_qemu.py` deploys an Ubuntu VM (libvirt/KVM) from an official Ubuntu
cloud image, using `qemu-img` + `cloud-init` + `virt-install`. It:
`deploy_qemu.py` deploys a Linux VM (libvirt/KVM) from an official cloud
image, using `qemu-img` + `cloud-init` + `virt-install`. Pick the
distribution with `--distro` (`ubuntu` default, `debian`, `fedora`) and the
release with `--version`; run `--list-images` to see the full catalogue with
minimum specs. It:
1. **Downloads the Ubuntu cloud image by itself** (cached, no double download).
1. **Downloads the cloud image by itself** (cached, no double download).
2. Converts it to a dedicated qcow2 working disk and resizes it.
3. Generates `user-data` / `meta-data` and builds the `seed.iso` (cloud-init).
4. Runs `virt-install` importing the disk + the seed as a CD-ROM.
@ -108,14 +111,16 @@ parameters and builds the command for you.
## Main options
- `--version` — Ubuntu version (default `24.04`).
- `--distro``ubuntu` (default), `debian` or `fedora`.
- `--version` — release for the distro (default: the distro's default).
- `--list-images` — print all distros/versions and their specs, then exit.
- `--image-dir` — image cache directory (default `/var/lib/libvirt/images/iso`).
- `--download-only` — download the image then exit (no VM).
- `--name` — VM name (required for deployment).
- `--memory`, `--vcpus`, `--disk-size` — VM sizing. When omitted, `--memory`
and `--disk-size` default to the **minimum required by the chosen Ubuntu
version** (libosinfo values: 20.04 → 2048 MB/5G, 22.04 → 2048 MB/10G,
24.04+ → 3072 MB/20G); `--vcpus` defaults to 2.
and `--disk-size` default to the **minimum required by the chosen version**
(libosinfo values, see `--list-images`: Ubuntu 24.04+ → 3072 MB/20G, Debian
→ 1024 MB/10G, Fedora → 2048 MB/15G); `--vcpus` defaults to 2.
- `--ssh-key`, `--ask-password`, `--password-hash` — authentication.
- `-y` / `--assume-yes` — auto-accept dependency installation.
- `--no-install-deps` — never auto-install dependencies.

View file

@ -1,8 +1,10 @@
#!/usr/bin/env python3
"""Déploiement rapide de VM Ubuntu (cloud-image) avec qemu-img + cloud-init + virt-install.
"""Déploiement rapide de VM Linux (Ubuntu/Debian/Fedora) via cloud-image + qemu-img + cloud-init + virt-install.
Reprend le workflow des notes :
1. Télécharge l'image cloud Ubuntu (si absente du cache -> pas de double téléchargement).
Choisissez la distribution avec --distro (ubuntu par défaut, debian, fedora)
et la version avec --version. « --list-images » affiche tout le catalogue et
les specs minimales. Reprend le workflow des notes :
1. Télécharge l'image cloud (si absente du cache -> pas de double téléchargement).
2. Convertit/copie l'image en un qcow2 de travail dédié à la VM.
3. Redimensionne le disque virtuel.
4. Génère user-data / meta-data et construit le seed.iso (cidata).
@ -11,11 +13,20 @@ Reprend le workflow des notes :
Exemples
--------
# Le plus simple : image téléchargée automatiquement (chemin déduit de
# --version, mis en cache dans /var/lib/libvirt/images/iso) et outils
# manquants installés après confirmation.
# --distro/--version, mis en cache dans /var/lib/libvirt/images/iso) et
# outils manquants installés après confirmation.
sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \\
--ssh-key ~/.ssh/id_ed25519.pub
# Debian 12 / Fedora 42 (mêmes options, --distro change la source d'image)
sudo ./script/qemu/deploy_qemu.py --distro debian --version 12 \\
--name deb12 --ssh-key ~/.ssh/id_ed25519.pub
sudo ./script/qemu/deploy_qemu.py --distro fedora --version 42 \\
--name fed42 --ssh-key ~/.ssh/id_ed25519.pub
# Voir tout le catalogue (distros, versions, specs minimales)
./script/qemu/deploy_qemu.py --list-images
# Télécharger (et vérifier) une image, sans créer de VM
sudo ./script/qemu/deploy_qemu.py --download-only --version 24.04 --verify
@ -49,13 +60,12 @@ import urllib.request
from pathlib import Path
# --------------------------------------------------------------------------- #
# Table des versions Ubuntu -> (nom de code, --osinfo, RAM min. Mo, disque min.)
# Le nom de code sert à construire l'URL de l'image cloud. La RAM et le disque
# minimaux proviennent de libosinfo (osinfo-db) : ce sont les seuils sous
# lesquels virt-install émet un avertissement. Ils servent de valeurs PAR
# DÉFAUT — une VM Ubuntu démarre avec ça sans gaspiller la RAM de l'hôte. Les
# valeurs LTS sont stables ; --codename et --osinfo surchargent si la table
# vieillit ; --memory / --disk-size surchargent le dimensionnement.
# Registre des distributions : version -> (code, --osinfo, RAM min Mo, disque).
# Le « code » est le nom de code (Ubuntu/Debian) ou le numéro de release
# (Fedora). RAM/disque minimaux proviennent de libosinfo (osinfo-db) : ce sont
# les seuils sous lesquels virt-install avertit. Ils servent de valeurs PAR
# DÉFAUT — la VM démarre au plus juste sans gaspiller la RAM de l'hôte.
# --codename / --osinfo / --memory / --disk-size surchargent au besoin.
# --------------------------------------------------------------------------- #
UBUNTU_VERSIONS: dict[str, tuple[str, str, int, str]] = {
"20.04": ("focal", "ubuntu20.04", 2048, "5G"),
@ -64,25 +74,172 @@ UBUNTU_VERSIONS: dict[str, tuple[str, str, int, str]] = {
"24.10": ("oracular", "ubuntu24.10", 3072, "20G"),
"25.04": ("plucky", "ubuntu25.04", 3072, "20G"),
"25.10": ("questing", "ubuntu25.10", 3072, "20G"),
# "26.04": ("resolute", "ubuntu26.04", 3072, "20G"),
"26.04": ("resolute", "ubuntu26.04", 3072, "20G"),
}
DEBIAN_VERSIONS: dict[str, tuple[str, str, int, str]] = {
"11": ("bullseye", "debian11", 1024, "10G"),
"12": ("bookworm", "debian12", 1024, "10G"),
"13": ("trixie", "debian13", 1024, "10G"),
}
FEDORA_VERSIONS: dict[str, tuple[str, str, int, str]] = {
"41": ("41", "fedora41", 2048, "15G"),
"42": ("42", "fedora42", 2048, "15G"),
"43": ("43", "fedora43", 2048, "15G"),
"44": ("44", "fedora44", 2048, "15G"),
}
# distro -> (table des versions, version par défaut).
DISTROS: dict[str, tuple[dict[str, tuple[str, str, int, str]], str]] = {
"ubuntu": (UBUNTU_VERSIONS, "24.04"),
"debian": (DEBIAN_VERSIONS, "12"),
"fedora": (FEDORA_VERSIONS, "42"),
}
# Traduction de l'arch générique (amd64/arm64) vers le nom propre à la distro.
ARCH_ALIASES: dict[str, dict[str, str]] = {
"fedora": {"amd64": "x86_64", "arm64": "aarch64"},
}
CLOUD_IMG_BASE = "https://cloud-images.ubuntu.com"
DEBIAN_CLOUD_BASE = "https://cloud.debian.org/images/cloud"
FEDORA_BASE = "https://download.fedoraproject.org/pub/fedora/linux/releases"
# Répertoire de cache par défaut des images cloud (cohérent avec --disk-dir /
# --seed-dir). L'écriture y nécessite root : le déploiement tourne de toute
# façon sous sudo (virt-install). Surchargez avec --image-dir au besoin.
DEFAULT_IMAGE_DIR = Path("/var/lib/libvirt/images/iso")
def image_url(codename: str, arch: str) -> str:
"""URL de l'image cloud « current » pour un nom de code + architecture."""
return f"{CLOUD_IMG_BASE}/{codename}/current/{codename}-server-cloudimg-{arch}.img"
# Emplacements de la base osinfo-db (détection d'un --osinfo connu).
OSINFO_DB_DIRS: tuple[str, ...] = (
"/usr/share/osinfo/os",
"/usr/local/share/osinfo/os",
os.path.expanduser("~/.local/share/osinfo/os"),
)
def default_image_name(codename: str, arch: str) -> str:
"""Nom de fichier local dérivé du nom de code + architecture."""
return f"{codename}-server-cloudimg-{arch}.img"
def distro_arch(distro: str, arch: str) -> str:
"""Nom d'architecture attendu par la distro (Fedora utilise x86_64)."""
return ARCH_ALIASES.get(distro, {}).get(arch, arch)
def image_url(distro: str, code: str, arch: str, version: str) -> str:
"""URL directe de l'image cloud (Ubuntu/Debian). Fedora est résolu à
part via resolve_fedora_url (pas de lien « latest » stable)."""
a = distro_arch(distro, arch)
if distro == "ubuntu":
return (
f"{CLOUD_IMG_BASE}/{code}/current/"
f"{code}-server-cloudimg-{a}.img"
)
if distro == "debian":
return (
f"{DEBIAN_CLOUD_BASE}/{code}/latest/"
f"debian-{version}-genericcloud-{a}.qcow2"
)
raise ValueError(f"URL directe indisponible pour la distro {distro!r}")
def resolve_fedora_url(version: str, arch: str, dry_run: bool) -> str:
"""Résout l'URL du qcow2 « Fedora Cloud Base Generic » depuis l'index
HTML des releases (Fedora ne publie pas de lien « latest »)."""
a = distro_arch("fedora", arch)
index = f"{FEDORA_BASE}/{version}/Cloud/{a}/images/"
pattern = re.compile(
rf"Fedora-Cloud-Base-Generic-{version}-[0-9.]+\.{a}\.qcow2"
)
if dry_run:
return index + f"Fedora-Cloud-Base-Generic-{version}-<build>.{a}.qcow2"
try:
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
sys.exit(f"Impossible de lister les images Fedora {version} : {exc}")
names = sorted(set(pattern.findall(html)))
if not names:
sys.exit(
"Aucune image « Fedora-Cloud-Base-Generic » trouvée pour "
f"Fedora {version} ({a}) dans {index}"
)
return index + names[-1]
def resolve_image_url(
distro: str, code: str, arch: str, version: str, dry_run: bool
) -> str:
"""URL de l'image cloud, tous distros confondus."""
if distro == "fedora":
return resolve_fedora_url(version, arch, dry_run)
return image_url(distro, code, arch, version)
def default_image_name(distro: str, code: str, arch: str, version: str) -> str:
"""Nom de fichier local pour le cache d'image."""
a = distro_arch(distro, arch)
if distro == "ubuntu":
return f"{code}-server-cloudimg-{a}.img"
if distro == "debian":
return f"debian-{version}-genericcloud-{a}.qcow2"
return f"fedora-cloud-{version}-{a}.qcow2"
def osinfo_known(short_id: str) -> bool:
"""Vrai si l'id osinfo (ex. « ubuntu26.04 ») figure dans osinfo-db."""
needle = f"<short-id>{short_id}</short-id>"
for base in OSINFO_DB_DIRS:
if not os.path.isdir(base):
continue
for root, _dirs, files in os.walk(base):
for fn in files:
if not fn.endswith(".xml"):
continue
try:
with open(
os.path.join(root, fn),
encoding="utf-8",
errors="replace",
) as fh:
if needle in fh.read():
return True
except OSError:
continue
return False
def osinfo_arg(osinfo: str) -> str:
"""Valeur --osinfo résiliente à un osinfo-db périmé : si l'id est connu on
l'utilise ; sinon on bascule en détection best-effort au lieu d'échouer
(utile pour une distro plus récente que la base locale, ex. ubuntu26.04,
fedora43+)."""
if "=" in osinfo or "," in osinfo:
return osinfo # forme avancée déjà fournie par l'utilisateur
if osinfo_known(osinfo):
return osinfo
print(
f" osinfo « {osinfo} » inconnu de la base locale (osinfo-db) — repli "
"sur la détection auto (detect=on,require=off).\n"
" Astuce : « sudo apt upgrade osinfo-db » pour des métadonnées à jour."
)
return "detect=on,require=off"
def list_images() -> None:
"""Affiche toutes les distros/versions et leurs specs (--list-images)."""
print("Images cloud disponibles (distro / version / specs) :\n")
for distro, (versions, default) in DISTROS.items():
print(f" {distro} (défaut : {default})")
for v, (code, osinfo, ram, disk) in versions.items():
star = "*" if v == default else " "
note = (
""
if osinfo_known(osinfo)
else " [osinfo local absent → auto]"
)
print(
f" {star} {v:<7} {code:<10} osinfo={osinfo:<12} "
f"RAM≥{ram}Mo disque≥{disk}{note}"
)
print()
print("Exemple : deploy_qemu.py --distro debian --version 12 --name vm1")
# --------------------------------------------------------------------------- #
@ -417,11 +574,23 @@ def download_image(url: str, dest: Path, dry_run: bool) -> None:
)
print(f" Téléchargement {url}")
tmp = dest.with_suffix(dest.suffix + ".part")
is_tty = sys.stdout.isatty()
last_pct = [-1]
def _progress(block_num: int, block_size: int, total: int) -> None:
if total > 0:
pct = min(100, block_num * block_size * 100 // total)
# N'émet qu'au changement de pourcentage entier : sur un TTY, réécrit
# la même ligne (\r) ; sinon (sortie capturée par le menu todo) au plus
# 101 lignes au lieu de centaines de répétitions du même pourcentage.
if total <= 0:
return
pct = min(100, block_num * block_size * 100 // total)
if pct == last_pct[0]:
return
last_pct[0] = pct
if is_tty:
print(f"\r {pct:3d}%", end="", flush=True)
else:
print(f" {pct:3d}%", flush=True)
try:
urllib.request.urlretrieve(
@ -430,7 +599,8 @@ def download_image(url: str, dest: Path, dry_run: bool) -> None:
except Exception as exc: # pragma: no cover - dépend du réseau
tmp.unlink(missing_ok=True)
sys.exit(f"\nÉchec du téléchargement : {exc}")
print()
if is_tty:
print()
tmp.replace(dest)
@ -608,13 +778,42 @@ def network_name(network_arg: str) -> str | None:
return 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]
try:
res = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
except (OSError, subprocess.SubprocessError):
return (False, False)
active = bool(re.search(r"Active:\s*yes", res.stdout, re.IGNORECASE))
autostart = bool(re.search(r"Autostart:\s*yes", res.stdout, re.IGNORECASE))
return (active, autostart)
def ensure_network(name: str | None, runner: Runner) -> None:
"""Active le réseau libvirt (idempotent : tolère « déjà actif »)."""
"""Active le réseau libvirt si besoin. On vérifie d'abord son état pour
éviter le faux « error: network is already active » de virsh quand il
tourne déjà (message purement bruyant, sans conséquence)."""
if not name:
return
print(f" Activation du réseau libvirt '{name}' (si nécessaire)")
runner.run(["virsh", "net-start", name], privileged=True, check=False)
runner.run(["virsh", "net-autostart", name], privileged=True, check=False)
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
)
return
active, autostart = network_state(name, runner.use_sudo)
if active and autostart:
print(f" Réseau libvirt '{name}' déjà actif.")
return
print(f" Configuration du réseau libvirt '{name}'")
if not active:
runner.run(["virsh", "net-start", name], privileged=True, check=False)
if not autostart:
runner.run(
["virsh", "net-autostart", name], privileged=True, check=False
)
def virt_install(
@ -686,35 +885,42 @@ def ssh_command(user: str, ip: str, has_key: bool) -> str:
# --------------------------------------------------------------------------- #
def build_parser() -> argparse.ArgumentParser:
versions_help = "\n".join(
f" {v:<7} {code:<10} RAM≥{ram:<5}Mo disque≥{disk:<4} "
f"{image_url(code, 'amd64')}"
for v, (code, _osinfo, ram, disk) in UBUNTU_VERSIONS.items()
f" {distro:<7} {default:<7} (défaut) versions : "
+ ", ".join(versions)
for distro, (versions, default) in DISTROS.items()
)
p = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description=__doc__,
epilog="Versions Ubuntu disponibles (--version) et URL de l'image cloud :\n"
+ versions_help,
epilog="Distros et versions (--distro / --version), specs via "
"--list-images :\n" + versions_help,
)
p.add_argument(
"image_path",
type=Path,
nargs="?",
default=None,
help="Chemin de cache de l'image cloud (.img). Optionnel : si absent, "
"il est déduit de --version/--codename + --arch dans --image-dir. "
help="Chemin de cache de l'image cloud. Optionnel : si absent, il est "
"déduit de --distro/--version/--codename + --arch dans --image-dir. "
"Si le fichier existe, il n'est PAS re-téléchargé.",
)
g_img = p.add_argument_group("Image")
g_img.add_argument(
"--version",
default="24.04",
choices=UBUNTU_VERSIONS,
help="Version Ubuntu (défaut : 24.04). Voir la liste ci-dessous.",
"--distro",
default="ubuntu",
choices=DISTROS,
help="Distribution : ubuntu, debian ou fedora (défaut : ubuntu).",
)
g_img.add_argument(
"--codename", help="Force le nom de code (surcharge --version)."
"--version",
default=None,
help="Version de la distro (défaut : la version par défaut de la "
"distro). Voir --list-images pour la liste complète.",
)
g_img.add_argument(
"--codename",
help="Force le nom de code / la release (surcharge --version).",
)
g_img.add_argument(
"--arch",
@ -747,8 +953,8 @@ def build_parser() -> argparse.ArgumentParser:
"--memory",
type=int,
default=None,
help="RAM en Mo (défaut : minimum requis par la version Ubuntu, "
"voir la liste ci-dessous).",
help="RAM en Mo (défaut : minimum requis par la version choisie, "
"voir --list-images).",
)
g_vm.add_argument(
"--vcpus", type=int, default=2, help="Nombre de vCPU (défaut : 2)."
@ -757,7 +963,7 @@ def build_parser() -> argparse.ArgumentParser:
"--disk-size",
default=None,
help="Taille du disque virtuel, ex. 120G (défaut : minimum requis "
"par la version Ubuntu, voir la liste ci-dessous).",
"par la version choisie, voir --list-images).",
)
g_vm.add_argument(
"--disk-dir",
@ -888,6 +1094,12 @@ def build_parser() -> argparse.ArgumentParser:
action="store_true",
help="N'installe jamais les dépendances manquantes (échoue si absentes).",
)
g_run.add_argument(
"--list-images",
action="store_true",
help="Liste les distros/versions disponibles et leurs specs, "
"puis quitte.",
)
return p
@ -917,21 +1129,43 @@ def load_ssh_keys(paths: list[str]) -> list[str]:
def main() -> None:
args = build_parser().parse_args()
codename, default_osinfo, min_ram, min_disk = UBUNTU_VERSIONS[args.version]
if args.list_images:
list_images()
return
versions, default_version = DISTROS[args.distro]
if args.version is None:
args.version = default_version
if args.version not in versions:
sys.exit(
f"Version {args.version!r} inconnue pour {args.distro}. "
f"Choix : {', '.join(versions)} (voir --list-images)."
)
code, default_osinfo, min_ram, min_disk = versions[args.version]
if args.codename:
codename = args.codename
code = args.codename
osinfo = args.osinfo or default_osinfo
# Dimensionnement par défaut = minimum requis par la version (libosinfo).
if args.memory is None:
args.memory = min_ram
if args.disk_size is None:
args.disk_size = min_disk
url = image_url(codename, args.arch)
url = resolve_image_url(
args.distro, code, args.arch, args.version, args.dry_run
)
# --verify s'appuie sur un SHA256SUMS style Ubuntu ; Debian/Fedora
# publient des sommes dans un autre format -> on saute proprement.
do_verify = args.verify and args.distro == "ubuntu"
if args.verify and not do_verify:
print(
f" Note : --verify n'est pris en charge que pour ubuntu "
f"(ignoré pour {args.distro})."
)
# Chemin de l'image : déduit automatiquement si non fourni.
if args.image_path is None:
args.image_path = args.image_dir / default_image_name(
codename, args.arch
args.distro, code, args.arch, args.version
)
runner = Runner(
@ -941,11 +1175,12 @@ def main() -> None:
# -- Mode téléchargement seul : aucun outil ni VM requis. --------------
if args.download_only:
print(
f"\n== Téléchargement image cloud ({args.version} / {codename}) =="
f"\n== Téléchargement image cloud "
f"({args.distro} {args.version} / {code}) =="
)
print(f" Destination : {args.image_path}")
download_image(url, args.image_path, args.dry_run)
if args.verify:
if do_verify:
verify_sha256(url, args.image_path, args.dry_run)
print("\nTerminé (téléchargement seul).")
return
@ -973,9 +1208,9 @@ def main() -> None:
if not args.dry_run:
ensure_tools(runner, args.assume_yes, args.no_install_deps)
print(f"\n== 1/5 Image cloud ({args.version} / {codename}) ==")
print(f"\n== 1/5 Image cloud ({args.distro} {args.version} / {code}) ==")
download_image(url, args.image_path, args.dry_run)
if args.verify:
if do_verify:
verify_sha256(url, args.image_path, args.dry_run)
print(f"\n== 2-3/5 Disque de travail {disk} ({args.disk_size}) ==")
@ -985,9 +1220,10 @@ def main() -> None:
cloud_cfg = build_cloud_config(args, pw_hash, ssh_keys)
build_seed(cloud_cfg, args.hostname, seed, runner)
print(f"\n== 5/5 virt-install (--osinfo {osinfo}) ==")
resolved_osinfo = osinfo_arg(osinfo)
print(f"\n== 5/5 virt-install (--osinfo {resolved_osinfo}) ==")
ensure_network(network_name(args.network), runner)
virt_install(args, disk, seed, osinfo, runner)
virt_install(args, disk, seed, resolved_osinfo, runner)
has_key = bool(ssh_keys)
print("\nTerminé. Suivi :")

View file

@ -735,16 +735,48 @@ class TODO:
return path
return ""
def _qemu_prompt_version(self):
"""Demande la version Ubuntu ; retourne une chaîne (défaut 24.04)."""
versions = ["20.04", "22.04", "24.04", "24.10", "25.04", "25.10"]
print(f"\n{t('Ubuntu version:')}")
for i, v in enumerate(versions, 1):
suffix = " (LTS)" if v in ("20.04", "22.04", "24.04") else ""
print(f" [{i}] {v}{suffix}")
sel = input(t("Choice (number or version, default: 24.04): ")).strip()
# distro -> (versions affichées, version par défaut). Source de vérité =
# deploy_qemu.py ; ceci ne sert qu'au sélecteur interactif.
_QEMU_DISTROS = {
"ubuntu": (
["20.04", "22.04", "24.04", "24.10", "25.04", "25.10", "26.04"],
"24.04",
),
"debian": (["11", "12", "13"], "12"),
"fedora": (["41", "42", "43", "44"], "42"),
}
def _qemu_prompt_distro(self):
"""Demande la distribution (défaut : ubuntu)."""
distros = list(self._QEMU_DISTROS)
print(f"\n{t('Distribution:')}")
for i, d in enumerate(distros, 1):
print(f" [{i}] {d}")
sel = input(t("Choice (number or name, default: ubuntu): ")).strip()
if not sel:
return "24.04"
return "ubuntu"
try:
idx = int(sel) - 1
if 0 <= idx < len(distros):
return distros[idx]
except ValueError:
if sel in distros:
return sel
print(t("Invalid selection, using ubuntu"))
return "ubuntu"
def _qemu_prompt_version(self, distro):
"""Demande la version pour la distro (défaut = version par défaut)."""
versions, default = self._QEMU_DISTROS.get(distro, ([], ""))
print(f"\n{t('Version for')} {distro} :")
for i, v in enumerate(versions, 1):
suffix = " *" if v == default else ""
print(f" [{i}] {v}{suffix}")
sel = input(
f"{t('Choice (number or version, blank = default):')} "
).strip()
if not sel:
return default
try:
idx = int(sel) - 1
if 0 <= idx < len(versions):
@ -752,8 +784,14 @@ class TODO:
except ValueError:
if sel in versions:
return sel
print(t("Invalid selection, using 24.04"))
return "24.04"
print(f"{t('Invalid selection, using')} {default}")
return default
def _qemu_list_images(self):
"""Affiche la liste des distros/versions et leurs specs."""
cmd = f"{self._qemu_script_path()} --list-images"
print(f"{t('Will execute:')} {cmd}")
self.execute.exec_command_live(cmd, source_erplibre=False)
def prompt_execute_qemu(self):
print(f"🤖 {t('Deploy a QEMU/KVM virtual machine (libvirt)!')}")
@ -762,15 +800,16 @@ class TODO:
print(f"{t('QEMU deploy script not found: ')}{script_path}")
return False
choices = [
{"prompt_description": t("Deploy a new Ubuntu VM")},
{"prompt_description": t("Deploy a new VM")},
{
"prompt_description": t(
"Preview a deployment (dry-run, no sudo)"
)
},
{"prompt_description": t("Download an Ubuntu cloud image only")},
{"prompt_description": t("Download a cloud image only")},
{"prompt_description": t("List VMs (virsh list --all)")},
{"prompt_description": t("Show a VM IP address")},
{"prompt_description": t("List available images and specs")},
]
config_entries = self.config_file.get_config("qemu_from_makefile")
if config_entries:
@ -792,6 +831,8 @@ class TODO:
self._qemu_list_vms()
elif status == "5":
self._qemu_show_ip()
elif status == "6":
self._qemu_list_images()
else:
cmd_no_found = True
try:
@ -811,9 +852,10 @@ class TODO:
if not name:
print(t("VM name is required!"))
return
version = self._qemu_prompt_version()
distro = self._qemu_prompt_distro()
version = self._qemu_prompt_version(distro)
# Laisser vide => le script applique le minimum requis par la version
# Ubuntu choisie (libosinfo). On n'envoie alors pas le flag.
# choisie (libosinfo). On n'envoie alors pas le flag.
memory = input(t("RAM in MB (blank = version minimum): ")).strip()
vcpus = input(t("vCPUs (default: 2): ")).strip()
disk_size = input(t("Disk size (blank = version minimum): ")).strip()
@ -850,7 +892,7 @@ class TODO:
if not dry_run:
parts.append("sudo")
parts.append(script_path)
parts += ["--name", name, "--version", version]
parts += ["--name", name, "--distro", distro, "--version", version]
if memory:
parts += ["--memory", memory]
if vcpus:
@ -874,12 +916,15 @@ class TODO:
def _qemu_download_image(self):
script_path = self._qemu_script_path()
version = self._qemu_prompt_version()
distro = self._qemu_prompt_distro()
version = self._qemu_prompt_version(distro)
ans = input(t("Verify SHA256 after download? (y/N): ")).strip().lower()
parts = [
"sudo",
script_path,
"--download-only",
"--distro",
distro,
"--version",
version,
]

View file

@ -997,17 +997,17 @@ TRANSLATIONS = {
"fr": "Script de déploiement QEMU introuvable : ",
"en": "QEMU deploy script not found: ",
},
"Deploy a new Ubuntu VM": {
"fr": "Déployer une nouvelle VM Ubuntu",
"en": "Deploy a new Ubuntu VM",
"Deploy a new VM": {
"fr": "Déployer une nouvelle VM",
"en": "Deploy a new VM",
},
"Preview a deployment (dry-run, no sudo)": {
"fr": "Prévisualiser un déploiement (dry-run, sans sudo)",
"en": "Preview a deployment (dry-run, no sudo)",
},
"Download an Ubuntu cloud image only": {
"fr": "Télécharger seulement une image cloud Ubuntu",
"en": "Download an Ubuntu cloud image only",
"Download a cloud image only": {
"fr": "Télécharger seulement une image cloud",
"en": "Download a cloud image only",
},
"List VMs (virsh list --all)": {
"fr": "Lister les VM (virsh list --all)",
@ -1017,17 +1017,33 @@ TRANSLATIONS = {
"fr": "Afficher l'adresse IP d'une VM",
"en": "Show a VM IP address",
},
"Ubuntu version:": {
"fr": "Version Ubuntu :",
"en": "Ubuntu version:",
"List available images and specs": {
"fr": "Lister les images disponibles et leurs specs",
"en": "List available images and specs",
},
"Choice (number or version, default: 24.04): ": {
"fr": "Choix (numéro ou version, défaut : 24.04) : ",
"en": "Choice (number or version, default: 24.04): ",
"Distribution:": {
"fr": "Distribution :",
"en": "Distribution:",
},
"Invalid selection, using 24.04": {
"fr": "Sélection invalide, utilisation de 24.04",
"en": "Invalid selection, using 24.04",
"Choice (number or name, default: ubuntu): ": {
"fr": "Choix (numéro ou nom, défaut : ubuntu) : ",
"en": "Choice (number or name, default: ubuntu): ",
},
"Invalid selection, using ubuntu": {
"fr": "Sélection invalide, utilisation d'ubuntu",
"en": "Invalid selection, using ubuntu",
},
"Version for": {
"fr": "Version pour",
"en": "Version for",
},
"Choice (number or version, blank = default):": {
"fr": "Choix (numéro ou version, vide = défaut) :",
"en": "Choice (number or version, blank = default):",
},
"Invalid selection, using": {
"fr": "Sélection invalide, utilisation de",
"en": "Invalid selection, using",
},
"VM name (required): ": {
"fr": "Nom de la VM (requis) : ",