[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] --> <!-- [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 `deploy_qemu.py` deploys a Linux VM (libvirt/KVM) from an official cloud
cloud image, using `qemu-img` + `cloud-init` + `virt-install`. It: 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. 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). 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. 4. Runs `virt-install` importing the disk + the seed as a CD-ROM.
5. Waits for the DHCP lease and prints the SSH command. 5. Waits for the DHCP lease and prints the SSH command.
<!-- [fr] --> <!-- [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 `deploy_qemu.py` déploie une VM Linux (libvirt/KVM) à partir d'une image
cloud Ubuntu officielle, via `qemu-img` + `cloud-init` + `virt-install`. Il : 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). téléchargement).
2. La convertit en un disque de travail qcow2 dédié et le redimensionne. 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). 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 ## 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`). - `--image-dir` — image cache directory (default `/var/lib/libvirt/images/iso`).
- `--download-only` — download the image then exit (no VM). - `--download-only` — download the image then exit (no VM).
- `--name` — VM name (required for deployment). - `--name` — VM name (required for deployment).
- `--memory`, `--vcpus`, `--disk-size` — VM sizing. When omitted, `--memory` - `--memory`, `--vcpus`, `--disk-size` — VM sizing. When omitted, `--memory`
and `--disk-size` default to the **minimum required by the chosen Ubuntu and `--disk-size` default to the **minimum required by the chosen version**
version** (libosinfo values: 20.04 → 2048 MB/5G, 22.04 → 2048 MB/10G, (libosinfo values, see `--list-images`: Ubuntu 24.04+ → 3072 MB/20G, Debian
24.04+ → 3072 MB/20G); `--vcpus` defaults to 2. → 1024 MB/10G, Fedora → 2048 MB/15G); `--vcpus` defaults to 2.
- `--ssh-key`, `--ask-password`, `--password-hash` — authentication. - `--ssh-key`, `--ask-password`, `--password-hash` — authentication.
- `-y` / `--assume-yes` — auto-accept dependency installation. - `-y` / `--assume-yes` — auto-accept dependency installation.
- `--no-install-deps` — never auto-install dependencies. - `--no-install-deps` — never auto-install dependencies.
@ -231,15 +239,18 @@ vous.
## Principales options ## 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 - `--image-dir` — répertoire de cache des images (défaut
`/var/lib/libvirt/images/iso`). `/var/lib/libvirt/images/iso`).
- `--download-only` — télécharge l'image puis quitte (sans VM). - `--download-only` — télécharge l'image puis quitte (sans VM).
- `--name` — nom de la VM (requis pour le déploiement). - `--name` — nom de la VM (requis pour le déploiement).
- `--memory`, `--vcpus`, `--disk-size` — dimensionnement de la VM. Omis, - `--memory`, `--vcpus`, `--disk-size` — dimensionnement de la VM. Omis,
`--memory` et `--disk-size` prennent le **minimum requis par la version `--memory` et `--disk-size` prennent le **minimum requis par la version**
Ubuntu** choisie (valeurs libosinfo : 20.04 → 2048 Mo/5G, 22.04 → choisie (valeurs libosinfo, voir `--list-images` : Ubuntu 24.04+ →
2048 Mo/10G, 24.04+ → 3072 Mo/20G) ; `--vcpus` vaut 2 par défaut. 3072 Mo/20G, Debian → 1024 Mo/10G, Fedora → 2048 Mo/15G) ; `--vcpus`
vaut 2 par défaut.
- `--ssh-key`, `--ask-password`, `--password-hash` — authentification. - `--ssh-key`, `--ask-password`, `--password-hash` — authentification.
- `-y` / `--assume-yes` — accepte automatiquement l'installation des - `-y` / `--assume-yes` — accepte automatiquement l'installation des
dépendances. 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 `deploy_qemu.py` déploie une VM Linux (libvirt/KVM) à partir d'une image
cloud Ubuntu officielle, via `qemu-img` + `cloud-init` + `virt-install`. Il : 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). téléchargement).
2. La convertit en un disque de travail qcow2 dédié et le redimensionne. 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). 3. Génère `user-data` / `meta-data` et construit le `seed.iso` (cloud-init).
@ -115,15 +118,18 @@ vous.
## Principales options ## 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 - `--image-dir` — répertoire de cache des images (défaut
`/var/lib/libvirt/images/iso`). `/var/lib/libvirt/images/iso`).
- `--download-only` — télécharge l'image puis quitte (sans VM). - `--download-only` — télécharge l'image puis quitte (sans VM).
- `--name` — nom de la VM (requis pour le déploiement). - `--name` — nom de la VM (requis pour le déploiement).
- `--memory`, `--vcpus`, `--disk-size` — dimensionnement de la VM. Omis, - `--memory`, `--vcpus`, `--disk-size` — dimensionnement de la VM. Omis,
`--memory` et `--disk-size` prennent le **minimum requis par la version `--memory` et `--disk-size` prennent le **minimum requis par la version**
Ubuntu** choisie (valeurs libosinfo : 20.04 → 2048 Mo/5G, 22.04 → choisie (valeurs libosinfo, voir `--list-images` : Ubuntu 24.04+ →
2048 Mo/10G, 24.04+ → 3072 Mo/20G) ; `--vcpus` vaut 2 par défaut. 3072 Mo/20G, Debian → 1024 Mo/10G, Fedora → 2048 Mo/15G) ; `--vcpus`
vaut 2 par défaut.
- `--ssh-key`, `--ask-password`, `--password-hash` — authentification. - `--ssh-key`, `--ask-password`, `--password-hash` — authentification.
- `-y` / `--assume-yes` — accepte automatiquement l'installation des - `-y` / `--assume-yes` — accepte automatiquement l'installation des
dépendances. 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 `deploy_qemu.py` deploys a Linux VM (libvirt/KVM) from an official cloud
cloud image, using `qemu-img` + `cloud-init` + `virt-install`. It: 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. 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). 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. 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 ## 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`). - `--image-dir` — image cache directory (default `/var/lib/libvirt/images/iso`).
- `--download-only` — download the image then exit (no VM). - `--download-only` — download the image then exit (no VM).
- `--name` — VM name (required for deployment). - `--name` — VM name (required for deployment).
- `--memory`, `--vcpus`, `--disk-size` — VM sizing. When omitted, `--memory` - `--memory`, `--vcpus`, `--disk-size` — VM sizing. When omitted, `--memory`
and `--disk-size` default to the **minimum required by the chosen Ubuntu and `--disk-size` default to the **minimum required by the chosen version**
version** (libosinfo values: 20.04 → 2048 MB/5G, 22.04 → 2048 MB/10G, (libosinfo values, see `--list-images`: Ubuntu 24.04+ → 3072 MB/20G, Debian
24.04+ → 3072 MB/20G); `--vcpus` defaults to 2. → 1024 MB/10G, Fedora → 2048 MB/15G); `--vcpus` defaults to 2.
- `--ssh-key`, `--ask-password`, `--password-hash` — authentication. - `--ssh-key`, `--ask-password`, `--password-hash` — authentication.
- `-y` / `--assume-yes` — auto-accept dependency installation. - `-y` / `--assume-yes` — auto-accept dependency installation.
- `--no-install-deps` — never auto-install dependencies. - `--no-install-deps` — never auto-install dependencies.

View file

@ -1,8 +1,10 @@
#!/usr/bin/env python3 #!/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 : Choisissez la distribution avec --distro (ubuntu par défaut, debian, fedora)
1. Télécharge l'image cloud Ubuntu (si absente du cache -> pas de double téléchargement). 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. 2. Convertit/copie l'image en un qcow2 de travail dédié à la VM.
3. Redimensionne le disque virtuel. 3. Redimensionne le disque virtuel.
4. Génère user-data / meta-data et construit le seed.iso (cidata). 4. Génère user-data / meta-data et construit le seed.iso (cidata).
@ -11,11 +13,20 @@ Reprend le workflow des notes :
Exemples Exemples
-------- --------
# Le plus simple : image téléchargée automatiquement (chemin déduit de # 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 # --distro/--version, mis en cache dans /var/lib/libvirt/images/iso) et
# manquants installés après confirmation. # outils manquants installés après confirmation.
sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \\ sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \\
--ssh-key ~/.ssh/id_ed25519.pub --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 # 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 sudo ./script/qemu/deploy_qemu.py --download-only --version 24.04 --verify
@ -49,13 +60,12 @@ import urllib.request
from pathlib import Path from pathlib import Path
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Table des versions Ubuntu -> (nom de code, --osinfo, RAM min. Mo, disque min.) # Registre des distributions : version -> (code, --osinfo, RAM min Mo, disque).
# Le nom de code sert à construire l'URL de l'image cloud. La RAM et le disque # Le « code » est le nom de code (Ubuntu/Debian) ou le numéro de release
# minimaux proviennent de libosinfo (osinfo-db) : ce sont les seuils sous # (Fedora). RAM/disque minimaux proviennent de libosinfo (osinfo-db) : ce sont
# lesquels virt-install émet un avertissement. Ils servent de valeurs PAR # les seuils sous lesquels virt-install avertit. Ils servent de valeurs PAR
# DÉFAUT — une VM Ubuntu démarre avec ça sans gaspiller la RAM de l'hôte. Les # DÉFAUT — la VM démarre au plus juste sans gaspiller la RAM de l'hôte.
# valeurs LTS sont stables ; --codename et --osinfo surchargent si la table # --codename / --osinfo / --memory / --disk-size surchargent au besoin.
# vieillit ; --memory / --disk-size surchargent le dimensionnement.
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
UBUNTU_VERSIONS: dict[str, tuple[str, str, int, str]] = { UBUNTU_VERSIONS: dict[str, tuple[str, str, int, str]] = {
"20.04": ("focal", "ubuntu20.04", 2048, "5G"), "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"), "24.10": ("oracular", "ubuntu24.10", 3072, "20G"),
"25.04": ("plucky", "ubuntu25.04", 3072, "20G"), "25.04": ("plucky", "ubuntu25.04", 3072, "20G"),
"25.10": ("questing", "ubuntu25.10", 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" 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 / # 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 # --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. # façon sous sudo (virt-install). Surchargez avec --image-dir au besoin.
DEFAULT_IMAGE_DIR = Path("/var/lib/libvirt/images/iso") DEFAULT_IMAGE_DIR = Path("/var/lib/libvirt/images/iso")
# Emplacements de la base osinfo-db (détection d'un --osinfo connu).
def image_url(codename: str, arch: str) -> str: OSINFO_DB_DIRS: tuple[str, ...] = (
"""URL de l'image cloud « current » pour un nom de code + architecture.""" "/usr/share/osinfo/os",
return f"{CLOUD_IMG_BASE}/{codename}/current/{codename}-server-cloudimg-{arch}.img" "/usr/local/share/osinfo/os",
os.path.expanduser("~/.local/share/osinfo/os"),
)
def default_image_name(codename: str, arch: str) -> str: def distro_arch(distro: str, arch: str) -> str:
"""Nom de fichier local dérivé du nom de code + architecture.""" """Nom d'architecture attendu par la distro (Fedora utilise x86_64)."""
return f"{codename}-server-cloudimg-{arch}.img" 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}") print(f" Téléchargement {url}")
tmp = dest.with_suffix(dest.suffix + ".part") 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: def _progress(block_num: int, block_size: int, total: int) -> None:
if total > 0: # N'émet qu'au changement de pourcentage entier : sur un TTY, réécrit
pct = min(100, block_num * block_size * 100 // total) # 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) print(f"\r {pct:3d}%", end="", flush=True)
else:
print(f" {pct:3d}%", flush=True)
try: try:
urllib.request.urlretrieve( 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 except Exception as exc: # pragma: no cover - dépend du réseau
tmp.unlink(missing_ok=True) tmp.unlink(missing_ok=True)
sys.exit(f"\nÉchec du téléchargement : {exc}") sys.exit(f"\nÉchec du téléchargement : {exc}")
print() if is_tty:
print()
tmp.replace(dest) tmp.replace(dest)
@ -608,13 +778,42 @@ def network_name(network_arg: str) -> str | None:
return 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: 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: if not name:
return return
print(f" Activation du réseau libvirt '{name}' (si nécessaire)") if runner.dry_run:
runner.run(["virsh", "net-start", name], privileged=True, check=False) print(f" [dry-run] réseau libvirt '{name}' activé si nécessaire")
runner.run(["virsh", "net-autostart", name], privileged=True, check=False) 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( def virt_install(
@ -686,35 +885,42 @@ def ssh_command(user: str, ip: str, has_key: bool) -> str:
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
def build_parser() -> argparse.ArgumentParser: def build_parser() -> argparse.ArgumentParser:
versions_help = "\n".join( versions_help = "\n".join(
f" {v:<7} {code:<10} RAM≥{ram:<5}Mo disque≥{disk:<4} " f" {distro:<7} {default:<7} (défaut) versions : "
f"{image_url(code, 'amd64')}" + ", ".join(versions)
for v, (code, _osinfo, ram, disk) in UBUNTU_VERSIONS.items() for distro, (versions, default) in DISTROS.items()
) )
p = argparse.ArgumentParser( p = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
description=__doc__, description=__doc__,
epilog="Versions Ubuntu disponibles (--version) et URL de l'image cloud :\n" epilog="Distros et versions (--distro / --version), specs via "
+ versions_help, "--list-images :\n" + versions_help,
) )
p.add_argument( p.add_argument(
"image_path", "image_path",
type=Path, type=Path,
nargs="?", nargs="?",
default=None, default=None,
help="Chemin de cache de l'image cloud (.img). Optionnel : si absent, " help="Chemin de cache de l'image cloud. Optionnel : si absent, il est "
"il est déduit de --version/--codename + --arch dans --image-dir. " "déduit de --distro/--version/--codename + --arch dans --image-dir. "
"Si le fichier existe, il n'est PAS re-téléchargé.", "Si le fichier existe, il n'est PAS re-téléchargé.",
) )
g_img = p.add_argument_group("Image") g_img = p.add_argument_group("Image")
g_img.add_argument( g_img.add_argument(
"--version", "--distro",
default="24.04", default="ubuntu",
choices=UBUNTU_VERSIONS, choices=DISTROS,
help="Version Ubuntu (défaut : 24.04). Voir la liste ci-dessous.", help="Distribution : ubuntu, debian ou fedora (défaut : ubuntu).",
) )
g_img.add_argument( 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( g_img.add_argument(
"--arch", "--arch",
@ -747,8 +953,8 @@ def build_parser() -> argparse.ArgumentParser:
"--memory", "--memory",
type=int, type=int,
default=None, default=None,
help="RAM en Mo (défaut : minimum requis par la version Ubuntu, " help="RAM en Mo (défaut : minimum requis par la version choisie, "
"voir la liste ci-dessous).", "voir --list-images).",
) )
g_vm.add_argument( g_vm.add_argument(
"--vcpus", type=int, default=2, help="Nombre de vCPU (défaut : 2)." "--vcpus", type=int, default=2, help="Nombre de vCPU (défaut : 2)."
@ -757,7 +963,7 @@ def build_parser() -> argparse.ArgumentParser:
"--disk-size", "--disk-size",
default=None, default=None,
help="Taille du disque virtuel, ex. 120G (défaut : minimum requis " 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( g_vm.add_argument(
"--disk-dir", "--disk-dir",
@ -888,6 +1094,12 @@ def build_parser() -> argparse.ArgumentParser:
action="store_true", action="store_true",
help="N'installe jamais les dépendances manquantes (échoue si absentes).", 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 return p
@ -917,21 +1129,43 @@ def load_ssh_keys(paths: list[str]) -> list[str]:
def main() -> None: def main() -> None:
args = build_parser().parse_args() 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: if args.codename:
codename = args.codename code = args.codename
osinfo = args.osinfo or default_osinfo osinfo = args.osinfo or default_osinfo
# Dimensionnement par défaut = minimum requis par la version (libosinfo). # Dimensionnement par défaut = minimum requis par la version (libosinfo).
if args.memory is None: if args.memory is None:
args.memory = min_ram args.memory = min_ram
if args.disk_size is None: if args.disk_size is None:
args.disk_size = min_disk 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. # Chemin de l'image : déduit automatiquement si non fourni.
if args.image_path is None: if args.image_path is None:
args.image_path = args.image_dir / default_image_name( args.image_path = args.image_dir / default_image_name(
codename, args.arch args.distro, code, args.arch, args.version
) )
runner = Runner( runner = Runner(
@ -941,11 +1175,12 @@ def main() -> None:
# -- Mode téléchargement seul : aucun outil ni VM requis. -------------- # -- Mode téléchargement seul : aucun outil ni VM requis. --------------
if args.download_only: if args.download_only:
print( 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}") print(f" Destination : {args.image_path}")
download_image(url, args.image_path, args.dry_run) download_image(url, args.image_path, args.dry_run)
if args.verify: if do_verify:
verify_sha256(url, args.image_path, args.dry_run) verify_sha256(url, args.image_path, args.dry_run)
print("\nTerminé (téléchargement seul).") print("\nTerminé (téléchargement seul).")
return return
@ -973,9 +1208,9 @@ def main() -> None:
if not args.dry_run: if not args.dry_run:
ensure_tools(runner, args.assume_yes, args.no_install_deps) 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) download_image(url, args.image_path, args.dry_run)
if args.verify: if do_verify:
verify_sha256(url, args.image_path, args.dry_run) verify_sha256(url, args.image_path, args.dry_run)
print(f"\n== 2-3/5 Disque de travail {disk} ({args.disk_size}) ==") 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) cloud_cfg = build_cloud_config(args, pw_hash, ssh_keys)
build_seed(cloud_cfg, args.hostname, seed, runner) 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) 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) has_key = bool(ssh_keys)
print("\nTerminé. Suivi :") print("\nTerminé. Suivi :")

View file

@ -735,16 +735,48 @@ class TODO:
return path return path
return "" return ""
def _qemu_prompt_version(self): # distro -> (versions affichées, version par défaut). Source de vérité =
"""Demande la version Ubuntu ; retourne une chaîne (défaut 24.04).""" # deploy_qemu.py ; ceci ne sert qu'au sélecteur interactif.
versions = ["20.04", "22.04", "24.04", "24.10", "25.04", "25.10"] _QEMU_DISTROS = {
print(f"\n{t('Ubuntu version:')}") "ubuntu": (
for i, v in enumerate(versions, 1): ["20.04", "22.04", "24.04", "24.10", "25.04", "25.10", "26.04"],
suffix = " (LTS)" if v in ("20.04", "22.04", "24.04") else "" "24.04",
print(f" [{i}] {v}{suffix}") ),
sel = input(t("Choice (number or version, default: 24.04): ")).strip() "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: 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: try:
idx = int(sel) - 1 idx = int(sel) - 1
if 0 <= idx < len(versions): if 0 <= idx < len(versions):
@ -752,8 +784,14 @@ class TODO:
except ValueError: except ValueError:
if sel in versions: if sel in versions:
return sel return sel
print(t("Invalid selection, using 24.04")) print(f"{t('Invalid selection, using')} {default}")
return "24.04" 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): def prompt_execute_qemu(self):
print(f"🤖 {t('Deploy a QEMU/KVM virtual machine (libvirt)!')}") 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}") print(f"{t('QEMU deploy script not found: ')}{script_path}")
return False return False
choices = [ choices = [
{"prompt_description": t("Deploy a new Ubuntu VM")}, {"prompt_description": t("Deploy a new VM")},
{ {
"prompt_description": t( "prompt_description": t(
"Preview a deployment (dry-run, no sudo)" "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("List VMs (virsh list --all)")},
{"prompt_description": t("Show a VM IP address")}, {"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") config_entries = self.config_file.get_config("qemu_from_makefile")
if config_entries: if config_entries:
@ -792,6 +831,8 @@ class TODO:
self._qemu_list_vms() self._qemu_list_vms()
elif status == "5": elif status == "5":
self._qemu_show_ip() self._qemu_show_ip()
elif status == "6":
self._qemu_list_images()
else: else:
cmd_no_found = True cmd_no_found = True
try: try:
@ -811,9 +852,10 @@ class TODO:
if not name: if not name:
print(t("VM name is required!")) print(t("VM name is required!"))
return 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 # 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() memory = input(t("RAM in MB (blank = version minimum): ")).strip()
vcpus = input(t("vCPUs (default: 2): ")).strip() vcpus = input(t("vCPUs (default: 2): ")).strip()
disk_size = input(t("Disk size (blank = version minimum): ")).strip() disk_size = input(t("Disk size (blank = version minimum): ")).strip()
@ -850,7 +892,7 @@ class TODO:
if not dry_run: if not dry_run:
parts.append("sudo") parts.append("sudo")
parts.append(script_path) parts.append(script_path)
parts += ["--name", name, "--version", version] parts += ["--name", name, "--distro", distro, "--version", version]
if memory: if memory:
parts += ["--memory", memory] parts += ["--memory", memory]
if vcpus: if vcpus:
@ -874,12 +916,15 @@ class TODO:
def _qemu_download_image(self): def _qemu_download_image(self):
script_path = self._qemu_script_path() 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() ans = input(t("Verify SHA256 after download? (y/N): ")).strip().lower()
parts = [ parts = [
"sudo", "sudo",
script_path, script_path,
"--download-only", "--download-only",
"--distro",
distro,
"--version", "--version",
version, version,
] ]

View file

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