From a42633c8ce7a065e889229120303ea195830a1f3 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 15 Jul 2026 11:48:00 +0000 Subject: [PATCH] [IMP] script qemu: auto image + libvirt install The script previously required a manually supplied image path and only installed the client tools, so a bare host aborted at virt-install with a missing libvirt-sock. It now derives and downloads the Ubuntu cloud image from --version, detects and installs the full libvirt+QEMU stack (daemon and system emulator included) through the host package manager, then enables libvirtd. Adds a --download-only mode, a hypervisor preflight check, clean error messages instead of Python tracebacks, and a bilingual usage guide (README). Generated by Claude Code 2.1.210 claude-opus-4-8 Co-Authored-By: Mathieu Benoit --- script/qemu/README.base.md | 459 +++++++++++++++++++++++++++++++++++++ script/qemu/README.fr.md | 261 +++++++++++++++++++++ script/qemu/README.md | 251 ++++++++++++++++++++ script/qemu/deploy_qemu.py | 389 ++++++++++++++++++++++++++++++- 4 files changed, 1348 insertions(+), 12 deletions(-) create mode 100644 script/qemu/README.base.md create mode 100644 script/qemu/README.fr.md create mode 100644 script/qemu/README.md diff --git a/script/qemu/README.base.md b/script/qemu/README.base.md new file mode 100644 index 0000000..362c346 --- /dev/null +++ b/script/qemu/README.base.md @@ -0,0 +1,459 @@ + + + + + + +# QEMU/KVM — Ubuntu VM deployment + +`deploy_qemu.py` deploys an Ubuntu VM (libvirt/KVM) from an official Ubuntu +cloud image, using `qemu-img` + `cloud-init` + `virt-install`. It: + +1. **Downloads the Ubuntu 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. + + +# QEMU/KVM — Déploiement de VM Ubuntu + +`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 : + +1. **Télécharge lui-même l'image cloud Ubuntu** (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). +4. Lance `virt-install` en important le disque + le seed en CD-ROM. +5. Attend le bail DHCP et affiche la commande SSH. + + +## Prerequisites + +- A host with KVM available (bare-metal or nested virtualization enabled). +- `sudo` rights (the deployment writes to `/var/lib/libvirt/images` and drives + libvirt). + +## Installation + +The script **auto-installs the missing pieces it needs**: on first run it +detects your package manager (apt / dnf / pacman / zypper / brew), lists the +missing components (the client tools, **plus the libvirt daemon and the QEMU +system emulator**), asks for confirmation, installs them with `sudo`, then +enables and starts `libvirtd`. Use `-y` to accept automatically or +`--no-install-deps` to disable this behaviour. + +To install everything manually on Ubuntu/Debian (recommended full KVM stack): + + +## Prérequis + +- Un hôte disposant de KVM (bare-metal ou virtualisation imbriquée activée). +- Les droits `sudo` (le déploiement écrit dans `/var/lib/libvirt/images` et + pilote libvirt). + +## Installation + +Le script **installe automatiquement les composants manquants** : au premier +lancement, il détecte votre gestionnaire de paquets (apt / dnf / pacman / +zypper / brew), liste les composants absents (les outils clients, **ainsi que +le démon libvirt et l'émulateur QEMU système**), demande confirmation, les +installe avec `sudo`, puis active et démarre `libvirtd`. Utilisez `-y` pour +accepter automatiquement ou `--no-install-deps` pour désactiver ce +comportement. + +Pour tout installer manuellement sur Ubuntu/Debian (pile KVM complète +recommandée) : + + +```bash +sudo apt install qemu-utils virtinst libvirt-clients cloud-image-utils \ + libvirt-daemon-system qemu-system-x86 +sudo systemctl enable --now libvirtd +sudo usermod -aG libvirt,kvm "$USER" # re-login / reconnectez-vous +``` + + +`libvirt-daemon-system` provides the `libvirtd` daemon (and the +`/var/run/libvirt/libvirt-sock` socket) and `qemu-system-x86` the emulator — +without them `virt-install` fails with *"Failed to connect socket to +'/var/run/libvirt/libvirt-sock'"*. The script installs and starts them for +you; this manual command is only needed if you prefer to prepare the host +yourself or run with `--no-install-deps`. + +## Usage + +Simplest form — the image is downloaded automatically (path derived from +`--version`, cached in `/var/lib/libvirt/images/iso`): + + +`libvirt-daemon-system` fournit le démon `libvirtd` (et le socket +`/var/run/libvirt/libvirt-sock`) et `qemu-system-x86` l'émulateur — sans eux +`virt-install` échoue avec *« Failed to connect socket to +'/var/run/libvirt/libvirt-sock' »*. Le script les installe et les démarre pour +vous ; cette commande manuelle n'est utile que si vous préférez préparer +l'hôte vous-même ou utiliser `--no-install-deps`. + +## Utilisation + +Forme la plus simple — l'image est téléchargée automatiquement (chemin déduit +de `--version`, mis en cache dans `/var/lib/libvirt/images/iso`) : + + +```bash +sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \ + --ssh-key ~/.ssh/id_ed25519.pub +``` + + +Download (and verify) an image without creating a VM: + + +Télécharger (et vérifier) une image sans créer de VM : + + +```bash +sudo ./script/qemu/deploy_qemu.py --download-only --version 24.04 --verify +``` + + +Deploy with an interactive password instead of an SSH key: + + +Déployer avec un mot de passe interactif au lieu d'une clé SSH : + + +```bash +sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 --ask-password +``` + + +Larger VM (8 GB RAM, 8 vCPU, 120 GB disk), overwriting an existing disk: + + +VM plus grande (8 Go RAM, 8 vCPU, disque 120 Go), en écrasant un disque +existant : + + +```bash +sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \ + --memory 8192 --vcpus 8 --disk-size 120G --ask-password --force +``` + + +Preview what would happen, without doing anything (no sudo, no download): + + +Prévisualiser ce qui serait fait, sans rien exécuter (sans sudo, sans +téléchargement) : + + +```bash +./script/qemu/deploy_qemu.py --name test-vm --version 24.04 --dry-run +``` + + +Non-interactive deployment (accept dependency install automatically): + + +Déploiement non interactif (accepte automatiquement l'installation des +dépendances) : + + +```bash +sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \ + --ssh-key ~/.ssh/id_ed25519.pub -y +``` + + +Supported Ubuntu versions: `20.04`, `22.04`, `24.04` (default), `24.10`, +`25.04`, `25.10`. Provide an explicit image path as a positional argument to +override the automatic download location. + +## After deployment + + +Versions Ubuntu supportées : `20.04`, `22.04`, `24.04` (défaut), `24.10`, +`25.04`, `25.10`. Fournissez un chemin d'image en argument positionnel pour +surcharger l'emplacement de téléchargement automatique. + +## Après le déploiement + + +```bash +virsh list --all +virsh console test-vm # Ctrl+] to quit / pour quitter +virsh domifaddr test-vm --source lease # find the IP / trouver l'IP +ssh erplibre@ +``` + + +The default user is `erplibre` (change it with `--user`). + +## Via the TODO menu + +The script is integrated into the interactive assistant. Run `make todo` (or +`./script/todo/todo.py`), then go to **Execute → Deploy → QEMU/KVM - Deploy an +Ubuntu VM (libvirt)**. From there you can deploy a VM, preview a dry-run, +download an image, list VMs and show a VM IP address — the menu asks for the +parameters and builds the command for you. + +## Main options + +- `--version` — Ubuntu version (default `24.04`). +- `--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. +- `--ssh-key`, `--ask-password`, `--password-hash` — authentication. +- `-y` / `--assume-yes` — auto-accept dependency installation. +- `--no-install-deps` — never auto-install dependencies. +- `--dry-run` — show the commands without executing anything. +- `--force` — overwrite the existing working qcow2 disk. + +Run `./script/qemu/deploy_qemu.py --help` for the full list. + + +L'utilisateur par défaut est `erplibre` (modifiable avec `--user`). + +## Via le menu TODO + +Le script est intégré à l'assistant interactif. Lancez `make todo` (ou +`./script/todo/todo.py`), puis allez dans **Execute → Deploy → QEMU/KVM - +Deploy an Ubuntu VM (libvirt)**. De là, vous pouvez déployer une VM, +prévisualiser un dry-run, télécharger une image, lister les VM et afficher +l'IP d'une VM — le menu demande les paramètres et construit la commande pour +vous. + +## Principales options + +- `--version` — version Ubuntu (défaut `24.04`). +- `--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. +- `--ssh-key`, `--ask-password`, `--password-hash` — authentification. +- `-y` / `--assume-yes` — accepte automatiquement l'installation des + dépendances. +- `--no-install-deps` — n'installe jamais les dépendances automatiquement. +- `--dry-run` — affiche les commandes sans rien exécuter. +- `--force` — écrase le disque de travail qcow2 existant. + +Lancez `./script/qemu/deploy_qemu.py --help` pour la liste complète. + + +## Managing VMs + +List, stop and remove VMs (the qcow2 disk under `/var/lib/libvirt/images` +is kept unless you delete it): + + +## Gestion des VM + +Lister, arrêter et supprimer les VM (le disque qcow2 sous +`/var/lib/libvirt/images` est conservé tant que vous ne le supprimez pas) : + + +```bash +sudo virsh list --all # toutes les VM et leur état / all VMs and state +sudo virsh shutdown # arrêt propre ACPI / graceful shutdown +sudo virsh destroy # arrêt forcé / force off (pull the plug) +sudo virsh undefine # supprime la définition / remove definition +sudo virsh domifaddr # adresse IP de la VM / VM IP address +``` + + +`destroy` only powers the VM off (disk kept); `undefine` removes its +definition. To fully recreate a VM with the same name, `destroy` + `undefine` +it first, or redeploy with `--force`. + +## SSH access from another machine (ProxyJump) + +With the default NAT network the VM is reachable **only from the KVM host**. +To reach it from another machine **without changing the network**, use the +host as a jump host (it already reaches the VM). Get the VM IP with +`sudo virsh domifaddr `, then from the other machine: + + +`destroy` ne fait qu'éteindre la VM (disque conservé) ; `undefine` supprime sa +définition. Pour recréer proprement une VM du même nom, faites `destroy` + +`undefine` d'abord, ou redéployez avec `--force`. + +## Accès SSH depuis une autre machine (ProxyJump) + +Avec le réseau NAT par défaut, la VM n'est joignable que **depuis l'hôte +KVM**. Pour l'atteindre depuis une autre machine **sans toucher au réseau**, +utilisez l'hôte comme rebond (il joint déjà la VM). Récupérez l'IP de la VM +avec `sudo virsh domifaddr `, puis depuis l'autre machine : + + +```bash +# Rebond SSH vers la VM / jump through the KVM host +ssh -J user@ erplibre@ + +# Tunnel d'un service, ex. Odoo 8069 / tunnel a service, then http://localhost:8069 +ssh -L 8069::8069 user@ +``` + + +To make it permanent, add this to `~/.ssh/config` on the other machine (then +just `ssh myvm`): + + +Pour le rendre permanent, ajoutez ceci à `~/.ssh/config` sur l'autre machine +(ensuite `ssh myvm` suffit) : + + +```text +Host myvm + HostName # ex. 192.168.122.50 (reseau NAT) + User erplibre + ProxyJump user@ # IP LAN de l'hote KVM +``` + + +This works over Wi-Fi and needs no VM shutdown — the simplest option for +personal access. Prefer a bridge (below) if the VM must be a full server +exposed on the LAN. + +## QEMU inside QEMU (nested) & exposing the VM via a bridge + +If the KVM host is **itself a VM** (QEMU-in-QEMU), the deployment works only +when **nested virtualization** is enabled on the outer/physical host and the +middle VM uses CPU mode `host-passthrough`. Check from inside the KVM host +(the first command must be non-empty): + + +Ça marche en Wi-Fi et sans arrêter la VM — l'option la plus simple pour un +accès personnel. Préférez un pont (ci-dessous) si la VM doit être un serveur +à part entière exposé sur le LAN. + +## QEMU dans QEMU (imbriqué) & exposer la VM via un pont + +Si l'hôte KVM est **lui-même une VM** (QEMU dans QEMU), le déploiement ne +fonctionne que si la **virtualisation imbriquée** est activée sur l'hôte +physique et que la VM intermédiaire utilise le mode CPU `host-passthrough`. +Vérifiez depuis l'hôte KVM (la première commande doit être non vide) : + + +```bash +grep -E -o '(vmx|svm)' /proc/cpuinfo | sort -u # extensions visibles / visible +# Sur l'hote PHYSIQUE / on the PHYSICAL host: +cat /sys/module/kvm_intel/parameters/nested # Intel -> Y/1 +cat /sys/module/kvm_amd/parameters/nested # AMD -> Y/1 +``` + + +To enable nesting on the physical host (Intel shown; use `kvm_amd` on AMD), +then recreate the middle VM with `host-passthrough`: + + +Pour activer l'imbrication sur l'hôte physique (Intel montré ; `kvm_amd` sur +AMD), puis recréer la VM intermédiaire en `host-passthrough` : + + +```bash +echo "options kvm_intel nested=1" | sudo tee /etc/modprobe.d/kvm-nested.conf +sudo modprobe -r kvm_intel && sudo modprobe kvm_intel # ou / or reboot +``` + + +If nesting is unavailable, QEMU still runs via software emulation (TCG) — it +works but is slow. + +### Bridge for external access + +A NAT VM is isolated; a **bridged** VM gets an IP directly on the LAN, +reachable by any machine. On the KVM host, create a bridge `br0` over the +physical NIC (**wired only** — Wi-Fi cannot be bridged). netplan (Ubuntu +server) — replace `enp3s0` with your interface: + + +Si l'imbrication est indisponible, QEMU tourne quand même en émulation +logicielle (TCG) — ça marche mais c'est lent. + +### Pont pour l'accès externe + +Une VM en NAT est isolée ; une VM **pontée** obtient une IP directement sur le +LAN, joignable par n'importe quelle machine. Sur l'hôte KVM, créez un pont +`br0` sur la carte physique (**filaire uniquement** — le Wi-Fi ne se ponte +pas). netplan (Ubuntu serveur) — remplacez `enp3s0` par votre interface : + + +```yaml +# /etc/netplan/01-br0.yaml +network: + version: 2 + renderer: networkd + ethernets: + enp3s0: {dhcp4: no, dhcp6: no} + bridges: + br0: + interfaces: [enp3s0] + dhcp4: yes + parameters: {stp: false, forward-delay: 0} +``` + + +Apply safely (auto-reverts if you lose the connection) and verify — or use +NetworkManager (Ubuntu desktop): + + +Appliquez avec filet de sécurité (annulation auto en cas de coupure) et +vérifiez — ou via NetworkManager (Ubuntu bureau) : + + +```bash +# netplan +sudo netplan try && sudo netplan apply +ip addr show br0 # br0 porte l'IP du LAN / br0 holds the LAN IP + +# NetworkManager (alternative) +nmcli con add type bridge ifname br0 con-name br0 +nmcli con add type ethernet ifname enp3s0 master br0 con-name br0-port +nmcli con modify br0 ipv4.method auto +nmcli con down "Wired connection 1" ; nmcli con up br0 +``` + + +Then attach the VM to the bridge — **either at creation**: + + +Rattachez ensuite la VM au pont — **soit à la création** : + + +```bash +sudo ./script/qemu/deploy_qemu.py --name --version 24.04 \ + --ssh-key ~/.ssh/id_ed25519.pub --network bridge=br0,model=virtio -y --force +``` + + +**or by editing a VM already created**: stop it, replace its `` +block (`type='network'` / `` → `type='bridge'` / +``), then start it again: + + +**soit par édition d'une VM déjà créée** : arrêtez-la, remplacez son bloc +`` (`type='network'` / `` → +`type='bridge'` / ``), puis redémarrez-la : + + +```bash +sudo virsh shutdown +sudo virsh edit # mettre l'interface en bridge=br0 +sudo virsh start +sudo virsh domifaddr # nouvelle IP LAN / new LAN IP +``` + + +The VM now gets a LAN IP from your router, reachable by other machines. From +the Internet you additionally need a port-forward on your router (or a VPN); +in a nested setup the outer host must also forward/expose the middle VM. + + +La VM obtient maintenant une IP LAN de votre routeur, joignable par les autres +machines. Depuis Internet, il faut en plus une redirection de port sur votre +routeur (ou un VPN) ; en configuration imbriquée, l'hôte externe doit aussi +rediriger/exposer la VM intermédiaire. diff --git a/script/qemu/README.fr.md b/script/qemu/README.fr.md new file mode 100644 index 0000000..c3788e9 --- /dev/null +++ b/script/qemu/README.fr.md @@ -0,0 +1,261 @@ + +# QEMU/KVM — Déploiement de VM Ubuntu + +`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 : + +1. **Télécharge lui-même l'image cloud Ubuntu** (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). +4. Lance `virt-install` en important le disque + le seed en CD-ROM. +5. Attend le bail DHCP et affiche la commande SSH. + +## Prérequis + +- Un hôte disposant de KVM (bare-metal ou virtualisation imbriquée activée). +- Les droits `sudo` (le déploiement écrit dans `/var/lib/libvirt/images` et + pilote libvirt). + +## Installation + +Le script **installe automatiquement les composants manquants** : au premier +lancement, il détecte votre gestionnaire de paquets (apt / dnf / pacman / +zypper / brew), liste les composants absents (les outils clients, **ainsi que +le démon libvirt et l'émulateur QEMU système**), demande confirmation, les +installe avec `sudo`, puis active et démarre `libvirtd`. Utilisez `-y` pour +accepter automatiquement ou `--no-install-deps` pour désactiver ce +comportement. + +Pour tout installer manuellement sur Ubuntu/Debian (pile KVM complète +recommandée) : + +```bash +sudo apt install qemu-utils virtinst libvirt-clients cloud-image-utils \ + libvirt-daemon-system qemu-system-x86 +sudo systemctl enable --now libvirtd +sudo usermod -aG libvirt,kvm "$USER" # re-login / reconnectez-vous +``` + +`libvirt-daemon-system` fournit le démon `libvirtd` (et le socket +`/var/run/libvirt/libvirt-sock`) et `qemu-system-x86` l'émulateur — sans eux +`virt-install` échoue avec *« Failed to connect socket to +'/var/run/libvirt/libvirt-sock' »*. Le script les installe et les démarre pour +vous ; cette commande manuelle n'est utile que si vous préférez préparer +l'hôte vous-même ou utiliser `--no-install-deps`. + +## Utilisation + +Forme la plus simple — l'image est téléchargée automatiquement (chemin déduit +de `--version`, mis en cache dans `/var/lib/libvirt/images/iso`) : + +```bash +sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \ + --ssh-key ~/.ssh/id_ed25519.pub +``` + +Télécharger (et vérifier) une image sans créer de VM : + +```bash +sudo ./script/qemu/deploy_qemu.py --download-only --version 24.04 --verify +``` + +Déployer avec un mot de passe interactif au lieu d'une clé SSH : + +```bash +sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 --ask-password +``` + +VM plus grande (8 Go RAM, 8 vCPU, disque 120 Go), en écrasant un disque +existant : + +```bash +sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \ + --memory 8192 --vcpus 8 --disk-size 120G --ask-password --force +``` + +Prévisualiser ce qui serait fait, sans rien exécuter (sans sudo, sans +téléchargement) : + +```bash +./script/qemu/deploy_qemu.py --name test-vm --version 24.04 --dry-run +``` + +Déploiement non interactif (accepte automatiquement l'installation des +dépendances) : + +```bash +sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \ + --ssh-key ~/.ssh/id_ed25519.pub -y +``` + +Versions Ubuntu supportées : `20.04`, `22.04`, `24.04` (défaut), `24.10`, +`25.04`, `25.10`. Fournissez un chemin d'image en argument positionnel pour +surcharger l'emplacement de téléchargement automatique. + +## Après le déploiement + +```bash +virsh list --all +virsh console test-vm # Ctrl+] to quit / pour quitter +virsh domifaddr test-vm --source lease # find the IP / trouver l'IP +ssh erplibre@ +``` + +L'utilisateur par défaut est `erplibre` (modifiable avec `--user`). + +## Via le menu TODO + +Le script est intégré à l'assistant interactif. Lancez `make todo` (ou +`./script/todo/todo.py`), puis allez dans **Execute → Deploy → QEMU/KVM - +Deploy an Ubuntu VM (libvirt)**. De là, vous pouvez déployer une VM, +prévisualiser un dry-run, télécharger une image, lister les VM et afficher +l'IP d'une VM — le menu demande les paramètres et construit la commande pour +vous. + +## Principales options + +- `--version` — version Ubuntu (défaut `24.04`). +- `--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. +- `--ssh-key`, `--ask-password`, `--password-hash` — authentification. +- `-y` / `--assume-yes` — accepte automatiquement l'installation des + dépendances. +- `--no-install-deps` — n'installe jamais les dépendances automatiquement. +- `--dry-run` — affiche les commandes sans rien exécuter. +- `--force` — écrase le disque de travail qcow2 existant. + +Lancez `./script/qemu/deploy_qemu.py --help` pour la liste complète. + +## Gestion des VM + +Lister, arrêter et supprimer les VM (le disque qcow2 sous +`/var/lib/libvirt/images` est conservé tant que vous ne le supprimez pas) : + +```bash +sudo virsh list --all # toutes les VM et leur état / all VMs and state +sudo virsh shutdown # arrêt propre ACPI / graceful shutdown +sudo virsh destroy # arrêt forcé / force off (pull the plug) +sudo virsh undefine # supprime la définition / remove definition +sudo virsh domifaddr # adresse IP de la VM / VM IP address +``` + +`destroy` ne fait qu'éteindre la VM (disque conservé) ; `undefine` supprime sa +définition. Pour recréer proprement une VM du même nom, faites `destroy` + +`undefine` d'abord, ou redéployez avec `--force`. + +## Accès SSH depuis une autre machine (ProxyJump) + +Avec le réseau NAT par défaut, la VM n'est joignable que **depuis l'hôte +KVM**. Pour l'atteindre depuis une autre machine **sans toucher au réseau**, +utilisez l'hôte comme rebond (il joint déjà la VM). Récupérez l'IP de la VM +avec `sudo virsh domifaddr `, puis depuis l'autre machine : + +```bash +# Rebond SSH vers la VM / jump through the KVM host +ssh -J user@ erplibre@ + +# Tunnel d'un service, ex. Odoo 8069 / tunnel a service, then http://localhost:8069 +ssh -L 8069::8069 user@ +``` + +Pour le rendre permanent, ajoutez ceci à `~/.ssh/config` sur l'autre machine +(ensuite `ssh myvm` suffit) : + +```text +Host myvm + HostName # ex. 192.168.122.50 (reseau NAT) + User erplibre + ProxyJump user@ # IP LAN de l'hote KVM +``` + +Ça marche en Wi-Fi et sans arrêter la VM — l'option la plus simple pour un +accès personnel. Préférez un pont (ci-dessous) si la VM doit être un serveur +à part entière exposé sur le LAN. + +## QEMU dans QEMU (imbriqué) & exposer la VM via un pont + +Si l'hôte KVM est **lui-même une VM** (QEMU dans QEMU), le déploiement ne +fonctionne que si la **virtualisation imbriquée** est activée sur l'hôte +physique et que la VM intermédiaire utilise le mode CPU `host-passthrough`. +Vérifiez depuis l'hôte KVM (la première commande doit être non vide) : + +```bash +grep -E -o '(vmx|svm)' /proc/cpuinfo | sort -u # extensions visibles / visible +# Sur l'hote PHYSIQUE / on the PHYSICAL host: +cat /sys/module/kvm_intel/parameters/nested # Intel -> Y/1 +cat /sys/module/kvm_amd/parameters/nested # AMD -> Y/1 +``` + +Pour activer l'imbrication sur l'hôte physique (Intel montré ; `kvm_amd` sur +AMD), puis recréer la VM intermédiaire en `host-passthrough` : + +```bash +echo "options kvm_intel nested=1" | sudo tee /etc/modprobe.d/kvm-nested.conf +sudo modprobe -r kvm_intel && sudo modprobe kvm_intel # ou / or reboot +``` + +Si l'imbrication est indisponible, QEMU tourne quand même en émulation +logicielle (TCG) — ça marche mais c'est lent. + +### Pont pour l'accès externe + +Une VM en NAT est isolée ; une VM **pontée** obtient une IP directement sur le +LAN, joignable par n'importe quelle machine. Sur l'hôte KVM, créez un pont +`br0` sur la carte physique (**filaire uniquement** — le Wi-Fi ne se ponte +pas). netplan (Ubuntu serveur) — remplacez `enp3s0` par votre interface : + +```yaml +# /etc/netplan/01-br0.yaml +network: + version: 2 + renderer: networkd + ethernets: + enp3s0: {dhcp4: no, dhcp6: no} + bridges: + br0: + interfaces: [enp3s0] + dhcp4: yes + parameters: {stp: false, forward-delay: 0} +``` + +Appliquez avec filet de sécurité (annulation auto en cas de coupure) et +vérifiez — ou via NetworkManager (Ubuntu bureau) : + +```bash +# netplan +sudo netplan try && sudo netplan apply +ip addr show br0 # br0 porte l'IP du LAN / br0 holds the LAN IP + +# NetworkManager (alternative) +nmcli con add type bridge ifname br0 con-name br0 +nmcli con add type ethernet ifname enp3s0 master br0 con-name br0-port +nmcli con modify br0 ipv4.method auto +nmcli con down "Wired connection 1" ; nmcli con up br0 +``` + +Rattachez ensuite la VM au pont — **soit à la création** : + +```bash +sudo ./script/qemu/deploy_qemu.py --name --version 24.04 \ + --ssh-key ~/.ssh/id_ed25519.pub --network bridge=br0,model=virtio -y --force +``` + +**soit par édition d'une VM déjà créée** : arrêtez-la, remplacez son bloc +`` (`type='network'` / `` → +`type='bridge'` / ``), puis redémarrez-la : + +```bash +sudo virsh shutdown +sudo virsh edit # mettre l'interface en bridge=br0 +sudo virsh start +sudo virsh domifaddr # nouvelle IP LAN / new LAN IP +``` + +La VM obtient maintenant une IP LAN de votre routeur, joignable par les autres +machines. Depuis Internet, il faut en plus une redirection de port sur votre +routeur (ou un VPN) ; en configuration imbriquée, l'hôte externe doit aussi +rediriger/exposer la VM intermédiaire. \ No newline at end of file diff --git a/script/qemu/README.md b/script/qemu/README.md new file mode 100644 index 0000000..206ad44 --- /dev/null +++ b/script/qemu/README.md @@ -0,0 +1,251 @@ + +# QEMU/KVM — Ubuntu VM deployment + +`deploy_qemu.py` deploys an Ubuntu VM (libvirt/KVM) from an official Ubuntu +cloud image, using `qemu-img` + `cloud-init` + `virt-install`. It: + +1. **Downloads the Ubuntu 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. + +## Prerequisites + +- A host with KVM available (bare-metal or nested virtualization enabled). +- `sudo` rights (the deployment writes to `/var/lib/libvirt/images` and drives + libvirt). + +## Installation + +The script **auto-installs the missing pieces it needs**: on first run it +detects your package manager (apt / dnf / pacman / zypper / brew), lists the +missing components (the client tools, **plus the libvirt daemon and the QEMU +system emulator**), asks for confirmation, installs them with `sudo`, then +enables and starts `libvirtd`. Use `-y` to accept automatically or +`--no-install-deps` to disable this behaviour. + +To install everything manually on Ubuntu/Debian (recommended full KVM stack): + +```bash +sudo apt install qemu-utils virtinst libvirt-clients cloud-image-utils \ + libvirt-daemon-system qemu-system-x86 +sudo systemctl enable --now libvirtd +sudo usermod -aG libvirt,kvm "$USER" # re-login / reconnectez-vous +``` + +`libvirt-daemon-system` provides the `libvirtd` daemon (and the +`/var/run/libvirt/libvirt-sock` socket) and `qemu-system-x86` the emulator — +without them `virt-install` fails with *"Failed to connect socket to +'/var/run/libvirt/libvirt-sock'"*. The script installs and starts them for +you; this manual command is only needed if you prefer to prepare the host +yourself or run with `--no-install-deps`. + +## Usage + +Simplest form — the image is downloaded automatically (path derived from +`--version`, cached in `/var/lib/libvirt/images/iso`): + +```bash +sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \ + --ssh-key ~/.ssh/id_ed25519.pub +``` + +Download (and verify) an image without creating a VM: + +```bash +sudo ./script/qemu/deploy_qemu.py --download-only --version 24.04 --verify +``` + +Deploy with an interactive password instead of an SSH key: + +```bash +sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 --ask-password +``` + +Larger VM (8 GB RAM, 8 vCPU, 120 GB disk), overwriting an existing disk: + +```bash +sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \ + --memory 8192 --vcpus 8 --disk-size 120G --ask-password --force +``` + +Preview what would happen, without doing anything (no sudo, no download): + +```bash +./script/qemu/deploy_qemu.py --name test-vm --version 24.04 --dry-run +``` + +Non-interactive deployment (accept dependency install automatically): + +```bash +sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \ + --ssh-key ~/.ssh/id_ed25519.pub -y +``` + +Supported Ubuntu versions: `20.04`, `22.04`, `24.04` (default), `24.10`, +`25.04`, `25.10`. Provide an explicit image path as a positional argument to +override the automatic download location. + +## After deployment + +```bash +virsh list --all +virsh console test-vm # Ctrl+] to quit / pour quitter +virsh domifaddr test-vm --source lease # find the IP / trouver l'IP +ssh erplibre@ +``` + +The default user is `erplibre` (change it with `--user`). + +## Via the TODO menu + +The script is integrated into the interactive assistant. Run `make todo` (or +`./script/todo/todo.py`), then go to **Execute → Deploy → QEMU/KVM - Deploy an +Ubuntu VM (libvirt)**. From there you can deploy a VM, preview a dry-run, +download an image, list VMs and show a VM IP address — the menu asks for the +parameters and builds the command for you. + +## Main options + +- `--version` — Ubuntu version (default `24.04`). +- `--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. +- `--ssh-key`, `--ask-password`, `--password-hash` — authentication. +- `-y` / `--assume-yes` — auto-accept dependency installation. +- `--no-install-deps` — never auto-install dependencies. +- `--dry-run` — show the commands without executing anything. +- `--force` — overwrite the existing working qcow2 disk. + +Run `./script/qemu/deploy_qemu.py --help` for the full list. + +## Managing VMs + +List, stop and remove VMs (the qcow2 disk under `/var/lib/libvirt/images` +is kept unless you delete it): + +```bash +sudo virsh list --all # toutes les VM et leur état / all VMs and state +sudo virsh shutdown # arrêt propre ACPI / graceful shutdown +sudo virsh destroy # arrêt forcé / force off (pull the plug) +sudo virsh undefine # supprime la définition / remove definition +sudo virsh domifaddr # adresse IP de la VM / VM IP address +``` + +`destroy` only powers the VM off (disk kept); `undefine` removes its +definition. To fully recreate a VM with the same name, `destroy` + `undefine` +it first, or redeploy with `--force`. + +## SSH access from another machine (ProxyJump) + +With the default NAT network the VM is reachable **only from the KVM host**. +To reach it from another machine **without changing the network**, use the +host as a jump host (it already reaches the VM). Get the VM IP with +`sudo virsh domifaddr `, then from the other machine: + +```bash +# Rebond SSH vers la VM / jump through the KVM host +ssh -J user@ erplibre@ + +# Tunnel d'un service, ex. Odoo 8069 / tunnel a service, then http://localhost:8069 +ssh -L 8069::8069 user@ +``` + +To make it permanent, add this to `~/.ssh/config` on the other machine (then +just `ssh myvm`): + +```text +Host myvm + HostName # ex. 192.168.122.50 (reseau NAT) + User erplibre + ProxyJump user@ # IP LAN de l'hote KVM +``` + +This works over Wi-Fi and needs no VM shutdown — the simplest option for +personal access. Prefer a bridge (below) if the VM must be a full server +exposed on the LAN. + +## QEMU inside QEMU (nested) & exposing the VM via a bridge + +If the KVM host is **itself a VM** (QEMU-in-QEMU), the deployment works only +when **nested virtualization** is enabled on the outer/physical host and the +middle VM uses CPU mode `host-passthrough`. Check from inside the KVM host +(the first command must be non-empty): + +```bash +grep -E -o '(vmx|svm)' /proc/cpuinfo | sort -u # extensions visibles / visible +# Sur l'hote PHYSIQUE / on the PHYSICAL host: +cat /sys/module/kvm_intel/parameters/nested # Intel -> Y/1 +cat /sys/module/kvm_amd/parameters/nested # AMD -> Y/1 +``` + +To enable nesting on the physical host (Intel shown; use `kvm_amd` on AMD), +then recreate the middle VM with `host-passthrough`: + +```bash +echo "options kvm_intel nested=1" | sudo tee /etc/modprobe.d/kvm-nested.conf +sudo modprobe -r kvm_intel && sudo modprobe kvm_intel # ou / or reboot +``` + +If nesting is unavailable, QEMU still runs via software emulation (TCG) — it +works but is slow. + +### Bridge for external access + +A NAT VM is isolated; a **bridged** VM gets an IP directly on the LAN, +reachable by any machine. On the KVM host, create a bridge `br0` over the +physical NIC (**wired only** — Wi-Fi cannot be bridged). netplan (Ubuntu +server) — replace `enp3s0` with your interface: + +```yaml +# /etc/netplan/01-br0.yaml +network: + version: 2 + renderer: networkd + ethernets: + enp3s0: {dhcp4: no, dhcp6: no} + bridges: + br0: + interfaces: [enp3s0] + dhcp4: yes + parameters: {stp: false, forward-delay: 0} +``` + +Apply safely (auto-reverts if you lose the connection) and verify — or use +NetworkManager (Ubuntu desktop): + +```bash +# netplan +sudo netplan try && sudo netplan apply +ip addr show br0 # br0 porte l'IP du LAN / br0 holds the LAN IP + +# NetworkManager (alternative) +nmcli con add type bridge ifname br0 con-name br0 +nmcli con add type ethernet ifname enp3s0 master br0 con-name br0-port +nmcli con modify br0 ipv4.method auto +nmcli con down "Wired connection 1" ; nmcli con up br0 +``` + +Then attach the VM to the bridge — **either at creation**: + +```bash +sudo ./script/qemu/deploy_qemu.py --name --version 24.04 \ + --ssh-key ~/.ssh/id_ed25519.pub --network bridge=br0,model=virtio -y --force +``` + +**or by editing a VM already created**: stop it, replace its `` +block (`type='network'` / `` → `type='bridge'` / +``), then start it again: + +```bash +sudo virsh shutdown +sudo virsh edit # mettre l'interface en bridge=br0 +sudo virsh start +sudo virsh domifaddr # nouvelle IP LAN / new LAN IP +``` + +The VM now gets a LAN IP from your router, reachable by other machines. From +the Internet you additionally need a port-forward on your router (or a VPN); +in a nested setup the outer host must also forward/expose the middle VM. diff --git a/script/qemu/deploy_qemu.py b/script/qemu/deploy_qemu.py index cd5a920..a41f5eb 100755 --- a/script/qemu/deploy_qemu.py +++ b/script/qemu/deploy_qemu.py @@ -10,7 +10,16 @@ Reprend le workflow des notes : Exemples -------- - # Déploiement minimal (clé SSH, aucun mot de passe) + # 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. + sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \\ + --ssh-key ~/.ssh/id_ed25519.pub + + # 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 + + # Déploiement minimal en fournissant explicitement le chemin d'image sudo ./qemu-deploy.py /var/lib/libvirt/images/iso/noble.img \\ --name test-vm --ssh-key ~/.ssh/id_ed25519.pub @@ -56,12 +65,22 @@ UBUNTU_VERSIONS: dict[str, tuple[str, str]] = { CLOUD_IMG_BASE = "https://cloud-images.ubuntu.com" +# 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" +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" + + # --------------------------------------------------------------------------- # # Utilitaires d'exécution # --------------------------------------------------------------------------- # @@ -82,7 +101,14 @@ class Runner: print(f" [dry-run] {printable}") return print(f" $ {printable}") - subprocess.run(cmd, check=check) + try: + subprocess.run(cmd, check=check) + except subprocess.CalledProcessError as exc: + # Sortie propre plutôt qu'une trace Python illisible. + sys.exit( + f"\nÉchec de la commande (code {exc.returncode}) :\n" + f" {printable}" + ) def need_tool(name: str) -> None: @@ -90,6 +116,278 @@ def need_tool(name: str) -> None: sys.exit(f"Erreur : outil requis introuvable dans le PATH : {name!r}") +# --------------------------------------------------------------------------- # +# Détection et installation des dépendances système +# --------------------------------------------------------------------------- # +# Outils indispensables au déploiement complet (le mode --download-only n'en +# requiert aucun). +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") + +# outil -> nom de paquet, selon le gestionnaire de paquets détecté. +TOOL_PACKAGES: dict[str, dict[str, str]] = { + "apt": { + "qemu-img": "qemu-utils", + "virt-install": "virtinst", + "virsh": "libvirt-clients", + "cloud-localds": "cloud-image-utils", + "genisoimage": "genisoimage", + "openssl": "openssl", + }, + "dnf": { + "qemu-img": "qemu-img", + "virt-install": "virt-install", + "virsh": "libvirt-client", + "cloud-localds": "cloud-utils", + "genisoimage": "genisoimage", + "openssl": "openssl", + }, + "pacman": { + "qemu-img": "qemu-img", + "virt-install": "virt-install", + "virsh": "libvirt", + "cloud-localds": "cloud-image-utils", + "genisoimage": "cdrtools", + "openssl": "openssl", + }, + "zypper": { + "qemu-img": "qemu-tools", + "virt-install": "virt-install", + "virsh": "libvirt-client", + "cloud-localds": "cloud-utils-cloud-localds", + "genisoimage": "genisoimage", + "openssl": "openssl", + }, + "brew": { + "qemu-img": "qemu", + "virt-install": "virt-manager", + "virsh": "libvirt", + "cloud-localds": "cdrtools", + "genisoimage": "cdrtools", + "openssl": "openssl", + }, +} + +# Paquets fournissant le démon libvirt + l'émulateur QEMU système. Ils sont +# INDISPENSABLES pour exécuter la VM : virsh/virt-install (clients) seuls ne +# créent pas le socket /var/run/libvirt/libvirt-sock. +DAEMON_PACKAGES: dict[str, list[str]] = { + "apt": ["libvirt-daemon-system", "qemu-system-x86"], + "dnf": ["libvirt-daemon-kvm", "qemu-kvm"], + "pacman": ["libvirt", "qemu-desktop", "dnsmasq"], + "zypper": ["libvirt-daemon", "libvirt-daemon-qemu", "qemu-kvm"], + "brew": [], +} + +# Gestionnaires de paquets, dans l'ordre de préférence : +# (clé TOOL_PACKAGES, binaire à détecter, commande d'installation, sudo, refresh) +PKG_MANAGERS: tuple[ + tuple[str, str, list[str], bool, list[str] | None], ... +] = ( + ( + "apt", + "apt-get", + ["apt-get", "install", "-y"], + True, + ["apt-get", "update"], + ), + ("dnf", "dnf", ["dnf", "install", "-y"], True, None), + ( + "pacman", + "pacman", + ["pacman", "-S", "--needed", "--noconfirm"], + True, + None, + ), + ( + "zypper", + "zypper", + ["zypper", "--non-interactive", "install"], + True, + None, + ), + ("brew", "brew", ["brew", "install"], False, None), +) + + +def detect_pkg_manager() -> ( + tuple[str, list[str], bool, list[str] | None] | None +): + """Retourne (clé, cmd d'install, use_sudo, cmd de refresh) ou None.""" + for key, binary, install_cmd, use_sudo, refresh in PKG_MANAGERS: + if shutil.which(binary): + return key, install_cmd, use_sudo, refresh + return None + + +def missing_tools() -> list[str]: + """Binaires requis absents du PATH (dont un outil de seed si aucun présent).""" + missing = [t for t in REQUIRED_TOOLS if shutil.which(t) is None] + if not any(shutil.which(t) for t in SEED_TOOLS): + missing.append(SEED_TOOLS[0]) # on installera cloud-localds + return missing + + +def prompt_yes_no(question: str, default: bool = True) -> bool: + """Question oui/non. Lit /dev/tty pour rester visible même si stdout est + redirigé (cas d'un lancement depuis le menu todo).""" + suffix = " [O/n] " if default else " [o/N] " + prompt = question + suffix + try: + with open("/dev/tty", "r+") as tty: + tty.write(prompt) + tty.flush() + ans = tty.readline().strip().lower() + except OSError: + try: + ans = input(prompt).strip().lower() + except EOFError: + return default + if not ans: + return default + return ans in ("o", "oui", "y", "yes") + + +def daemon_missing() -> bool: + """Vrai si le démon libvirt OU l'émulateur QEMU système est absent.""" + libvirtd = shutil.which("libvirtd") or any( + os.path.exists(p) for p in ("/usr/sbin/libvirtd", "/usr/bin/libvirtd") + ) + emulator = ( + shutil.which("qemu-system-x86_64") + or shutil.which("qemu-system-x86") + or shutil.which("kvm") + ) + return not (libvirtd and emulator) + + +def libvirt_ready(use_sudo: bool) -> bool: + """Vrai si l'hyperviseur qemu:///system répond (démon libvirt démarré).""" + if shutil.which("virsh") is None: + return False + cmd = (["sudo"] if use_sudo else []) + [ + "virsh", + "-c", + "qemu:///system", + "version", + ] + try: + res = subprocess.run(cmd, capture_output=True, text=True, timeout=15) + return res.returncode == 0 + except (OSError, subprocess.SubprocessError): + return False + + +def ensure_libvirt_service(runner: Runner) -> None: + """S'assure que le démon libvirt tourne ; sinon le démarre puis vérifie.""" + if libvirt_ready(runner.use_sudo): + return + if shutil.which("systemctl"): + print( + " Démarrage du démon libvirt" + " (systemctl enable --now libvirtd)…" + ) + runner.run( + ["systemctl", "enable", "--now", "libvirtd"], + privileged=True, + check=False, + ) + runner.run( + ["systemctl", "start", "libvirtd.socket"], + privileged=True, + check=False, + ) + # Le socket peut mettre un instant à apparaître. + for _ in range(10): + if libvirt_ready(runner.use_sudo): + return + time.sleep(1) + sys.exit( + "Erreur : impossible de se connecter à l'hyperviseur libvirt" + " (/var/run/libvirt/libvirt-sock).\n" + " Le démon libvirtd n'est pas démarré. Essayez :\n" + " sudo systemctl enable --now libvirtd\n" + " puis vérifiez : sudo virsh -c qemu:///system version" + ) + + +def ensure_tools(runner: Runner, assume_yes: bool, no_install: bool) -> None: + """Vérifie outils, démon libvirt et émulateur ; installe/démarre ce qui + manque, puis vérifie la connexion à l'hyperviseur.""" + missing = missing_tools() + need_daemon = daemon_missing() + + # Tout est là et l'hyperviseur répond : rien à faire. + if not missing and not need_daemon and libvirt_ready(runner.use_sudo): + return + + pm = detect_pkg_manager() + if pm is None: + sys.exit( + " Gestionnaire de paquets non reconnu " + "(apt/dnf/pacman/zypper/brew).\n" + " Installez manuellement : " + ", ".join(missing) + ) + pm_key, install_cmd, use_sudo, refresh = pm + + pkg_map = TOOL_PACKAGES[pm_key] + packages = [pkg_map.get(t, t) for t in missing] + if need_daemon: + packages += DAEMON_PACKAGES.get(pm_key, []) + packages = list(dict.fromkeys(packages)) # dédoublonne, ordre gardé + + if packages: + label = list(missing) + if need_daemon: + label.append("démon libvirt / émulateur QEMU") + print(" Composants manquants : " + ", ".join(label)) + + if no_install: + sys.exit( + " Installation automatique désactivée (--no-install-deps).\n" + " Installez manuellement : " + " ".join(packages) + ) + + full_cmd = install_cmd + packages + printable = " ".join(full_cmd) + if use_sudo and runner.use_sudo: + printable = "sudo " + printable + + print(f" Gestionnaire détecté : {pm_key}") + print(f" Commande d'installation : {printable}") + + if not assume_yes and not prompt_yes_no( + " Installer ces dépendances maintenant ?" + ): + sys.exit( + " Installation refusée.\n Commande manuelle : " + printable + ) + + if refresh: + runner.run(refresh, privileged=use_sudo, check=False) + runner.run(full_cmd, privileged=use_sudo) + + still = missing_tools() + # Repli : si seul l'outil de seed manque encore (le nom du paquet + # cloud-localds varie selon la distro), tente genisoimage. + if still == [SEED_TOOLS[0]]: + alt = pkg_map.get("genisoimage", "genisoimage") + print(f" Repli sur {alt} (autre outil de seed cloud-init)…") + runner.run(install_cmd + [alt], privileged=use_sudo, check=False) + still = missing_tools() + if still: + sys.exit( + " Outils toujours absents après installation : " + + ", ".join(still) + + f"\n Essayez manuellement : {printable}" + ) + print(" Dépendances installées avec succès.") + + # Démarre le démon (si nécessaire) et vérifie l'accès à l'hyperviseur. + ensure_libvirt_service(runner) + + # --------------------------------------------------------------------------- # # Étapes # --------------------------------------------------------------------------- # @@ -105,7 +403,14 @@ def download_image(url: str, dest: Path, dry_run: bool) -> None: print(f" [dry-run] téléchargement {url} -> {dest}") return - dest.parent.mkdir(parents=True, exist_ok=True) + try: + dest.parent.mkdir(parents=True, exist_ok=True) + except PermissionError: + sys.exit( + f"\nPermission refusée pour écrire dans {dest.parent}.\n" + " Relancez avec sudo, ou choisissez --image-dir vers un dossier" + " accessible en écriture." + ) print(f" Téléchargement {url}") tmp = dest.with_suffix(dest.suffix + ".part") @@ -155,8 +460,12 @@ def verify_sha256(url: str, image: Path, dry_run: bool) -> None: for chunk in iter(lambda: fh.read(1 << 20), b""): h.update(chunk) if h.hexdigest() != expected: + image.unlink( + missing_ok=True + ) # évite la réutilisation du cache corrompu sys.exit( - f"SHA256 NON conforme !\n attendu : {expected}\n obtenu : {h.hexdigest()}" + f"SHA256 NON conforme ! Image supprimée : {image}\n" + f" attendu : {expected}\n obtenu : {h.hexdigest()}" ) print(" SHA256 conforme.") @@ -385,7 +694,10 @@ def build_parser() -> argparse.ArgumentParser: p.add_argument( "image_path", type=Path, - help="Chemin de cache de l'image cloud (.img). " + 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. " "Si le fichier existe, il n'est PAS re-téléchargé.", ) @@ -404,6 +716,13 @@ def build_parser() -> argparse.ArgumentParser: default="amd64", help="Architecture de l'image (défaut : amd64).", ) + g_img.add_argument( + "--image-dir", + type=Path, + default=DEFAULT_IMAGE_DIR, + help="Répertoire de cache des images cloud quand image_path est " + f"omis (défaut : {DEFAULT_IMAGE_DIR}).", + ) g_img.add_argument( "--verify", action="store_true", @@ -411,7 +730,11 @@ def build_parser() -> argparse.ArgumentParser: ) g_vm = p.add_argument_group("VM") - g_vm.add_argument("--name", required=True, help="Nom de la VM (virsh).") + g_vm.add_argument( + "--name", + help="Nom de la VM (virsh). Requis pour déployer ; inutile avec " + "--download-only.", + ) g_vm.add_argument( "--hostname", help="Nom d'hôte interne (défaut : --name)." ) @@ -538,6 +861,23 @@ def build_parser() -> argparse.ArgumentParser: action="store_true", help="Affiche les commandes et le user-data sans rien exécuter.", ) + g_run.add_argument( + "--download-only", + action="store_true", + help="Télécharge (et vérifie si --verify) l'image cloud puis quitte, " + "sans créer de VM. --name n'est pas requis.", + ) + g_run.add_argument( + "-y", + "--assume-yes", + action="store_true", + help="Accepte automatiquement l'installation des dépendances manquantes.", + ) + g_run.add_argument( + "--no-install-deps", + action="store_true", + help="N'installe jamais les dépendances manquantes (échoue si absentes).", + ) return p @@ -566,7 +906,6 @@ def load_ssh_keys(paths: list[str]) -> list[str]: def main() -> None: args = build_parser().parse_args() - args.hostname = args.hostname or args.name codename, default_osinfo = UBUNTU_VERSIONS[args.version] if args.codename: @@ -574,6 +913,36 @@ def main() -> None: osinfo = args.osinfo or default_osinfo url = image_url(codename, args.arch) + # 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 + ) + + runner = Runner( + use_sudo=not args.no_sudo and os.geteuid() != 0, dry_run=args.dry_run + ) + + # -- 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}) ==" + ) + print(f" Destination : {args.image_path}") + download_image(url, args.image_path, args.dry_run) + if args.verify: + verify_sha256(url, args.image_path, args.dry_run) + print("\nTerminé (téléchargement seul).") + return + + # -- Déploiement complet ----------------------------------------------- + if not args.name: + sys.exit( + "Erreur : --name est requis pour déployer une VM " + "(ou utilisez --download-only)." + ) + args.hostname = args.hostname or args.name + pw_hash = resolve_password(args) ssh_keys = load_ssh_keys(args.ssh_key) if not pw_hash and not ssh_keys: @@ -583,15 +952,11 @@ def main() -> None: file=sys.stderr, ) - runner = Runner( - use_sudo=not args.no_sudo and os.geteuid() != 0, dry_run=args.dry_run - ) disk = args.disk_dir / f"{args.name}.qcow2" seed = args.seed_dir / f"{args.name}-seed.iso" if not args.dry_run: - for tool in ("qemu-img", "virt-install"): - need_tool(tool) + ensure_tools(runner, args.assume_yes, args.no_install_deps) print(f"\n== 1/5 Image cloud ({args.version} / {codename}) ==") download_image(url, args.image_path, args.dry_run)