[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 <mathben@technolibre.ca>
This commit is contained in:
parent
dc42bb715a
commit
a42633c8ce
4 changed files with 1348 additions and 12 deletions
459
script/qemu/README.base.md
Normal file
459
script/qemu/README.base.md
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
<!---------------------------->
|
||||
<!-- multilingual suffix: en, fr -->
|
||||
<!-- no suffix: en -->
|
||||
<!---------------------------->
|
||||
|
||||
<!-- [en] -->
|
||||
# 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.
|
||||
|
||||
<!-- [fr] -->
|
||||
# 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.
|
||||
|
||||
<!-- [en] -->
|
||||
## 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):
|
||||
|
||||
<!-- [fr] -->
|
||||
## 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) :
|
||||
|
||||
<!-- [common] -->
|
||||
```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
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
`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`):
|
||||
|
||||
<!-- [fr] -->
|
||||
`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`) :
|
||||
|
||||
<!-- [common] -->
|
||||
```bash
|
||||
sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \
|
||||
--ssh-key ~/.ssh/id_ed25519.pub
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
Download (and verify) an image without creating a VM:
|
||||
|
||||
<!-- [fr] -->
|
||||
Télécharger (et vérifier) une image sans créer de VM :
|
||||
|
||||
<!-- [common] -->
|
||||
```bash
|
||||
sudo ./script/qemu/deploy_qemu.py --download-only --version 24.04 --verify
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
Deploy with an interactive password instead of an SSH key:
|
||||
|
||||
<!-- [fr] -->
|
||||
Déployer avec un mot de passe interactif au lieu d'une clé SSH :
|
||||
|
||||
<!-- [common] -->
|
||||
```bash
|
||||
sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 --ask-password
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
Larger VM (8 GB RAM, 8 vCPU, 120 GB disk), overwriting an existing disk:
|
||||
|
||||
<!-- [fr] -->
|
||||
VM plus grande (8 Go RAM, 8 vCPU, disque 120 Go), en écrasant un disque
|
||||
existant :
|
||||
|
||||
<!-- [common] -->
|
||||
```bash
|
||||
sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \
|
||||
--memory 8192 --vcpus 8 --disk-size 120G --ask-password --force
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
Preview what would happen, without doing anything (no sudo, no download):
|
||||
|
||||
<!-- [fr] -->
|
||||
Prévisualiser ce qui serait fait, sans rien exécuter (sans sudo, sans
|
||||
téléchargement) :
|
||||
|
||||
<!-- [common] -->
|
||||
```bash
|
||||
./script/qemu/deploy_qemu.py --name test-vm --version 24.04 --dry-run
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
Non-interactive deployment (accept dependency install automatically):
|
||||
|
||||
<!-- [fr] -->
|
||||
Déploiement non interactif (accepte automatiquement l'installation des
|
||||
dépendances) :
|
||||
|
||||
<!-- [common] -->
|
||||
```bash
|
||||
sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \
|
||||
--ssh-key ~/.ssh/id_ed25519.pub -y
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
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
|
||||
|
||||
<!-- [fr] -->
|
||||
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
|
||||
|
||||
<!-- [common] -->
|
||||
```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@<IP>
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
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.
|
||||
|
||||
<!-- [fr] -->
|
||||
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.
|
||||
|
||||
<!-- [en] -->
|
||||
## Managing VMs
|
||||
|
||||
List, stop and remove VMs (the qcow2 disk under `/var/lib/libvirt/images`
|
||||
is kept unless you delete it):
|
||||
|
||||
<!-- [fr] -->
|
||||
## 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) :
|
||||
|
||||
<!-- [common] -->
|
||||
```bash
|
||||
sudo virsh list --all # toutes les VM et leur état / all VMs and state
|
||||
sudo virsh shutdown <nom-vm> # arrêt propre ACPI / graceful shutdown
|
||||
sudo virsh destroy <nom-vm> # arrêt forcé / force off (pull the plug)
|
||||
sudo virsh undefine <nom-vm> # supprime la définition / remove definition
|
||||
sudo virsh domifaddr <nom-vm> # adresse IP de la VM / VM IP address
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
`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 <nom-vm>`, then from the other machine:
|
||||
|
||||
<!-- [fr] -->
|
||||
`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 <nom-vm>`, puis depuis l'autre machine :
|
||||
|
||||
<!-- [common] -->
|
||||
```bash
|
||||
# Rebond SSH vers la VM / jump through the KVM host
|
||||
ssh -J user@<ip-hote> erplibre@<ip-vm>
|
||||
|
||||
# Tunnel d'un service, ex. Odoo 8069 / tunnel a service, then http://localhost:8069
|
||||
ssh -L 8069:<ip-vm>:8069 user@<ip-hote>
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
To make it permanent, add this to `~/.ssh/config` on the other machine (then
|
||||
just `ssh myvm`):
|
||||
|
||||
<!-- [fr] -->
|
||||
Pour le rendre permanent, ajoutez ceci à `~/.ssh/config` sur l'autre machine
|
||||
(ensuite `ssh myvm` suffit) :
|
||||
|
||||
<!-- [common] -->
|
||||
```text
|
||||
Host myvm
|
||||
HostName <ip-vm> # ex. 192.168.122.50 (reseau NAT)
|
||||
User erplibre
|
||||
ProxyJump user@<ip-hote> # IP LAN de l'hote KVM
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
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):
|
||||
|
||||
<!-- [fr] -->
|
||||
Ç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) :
|
||||
|
||||
<!-- [common] -->
|
||||
```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
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
To enable nesting on the physical host (Intel shown; use `kvm_amd` on AMD),
|
||||
then recreate the middle VM with `host-passthrough`:
|
||||
|
||||
<!-- [fr] -->
|
||||
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` :
|
||||
|
||||
<!-- [common] -->
|
||||
```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
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
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:
|
||||
|
||||
<!-- [fr] -->
|
||||
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 :
|
||||
|
||||
<!-- [common] -->
|
||||
```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}
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
Apply safely (auto-reverts if you lose the connection) and verify — or use
|
||||
NetworkManager (Ubuntu desktop):
|
||||
|
||||
<!-- [fr] -->
|
||||
Appliquez avec filet de sécurité (annulation auto en cas de coupure) et
|
||||
vérifiez — ou via NetworkManager (Ubuntu bureau) :
|
||||
|
||||
<!-- [common] -->
|
||||
```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
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
Then attach the VM to the bridge — **either at creation**:
|
||||
|
||||
<!-- [fr] -->
|
||||
Rattachez ensuite la VM au pont — **soit à la création** :
|
||||
|
||||
<!-- [common] -->
|
||||
```bash
|
||||
sudo ./script/qemu/deploy_qemu.py --name <nom-vm> --version 24.04 \
|
||||
--ssh-key ~/.ssh/id_ed25519.pub --network bridge=br0,model=virtio -y --force
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
**or by editing a VM already created**: stop it, replace its `<interface>`
|
||||
block (`type='network'` / `<source network='default'/>` → `type='bridge'` /
|
||||
`<source bridge='br0'/>`), then start it again:
|
||||
|
||||
<!-- [fr] -->
|
||||
**soit par édition d'une VM déjà créée** : arrêtez-la, remplacez son bloc
|
||||
`<interface>` (`type='network'` / `<source network='default'/>` →
|
||||
`type='bridge'` / `<source bridge='br0'/>`), puis redémarrez-la :
|
||||
|
||||
<!-- [common] -->
|
||||
```bash
|
||||
sudo virsh shutdown <nom-vm>
|
||||
sudo virsh edit <nom-vm> # mettre l'interface en bridge=br0
|
||||
sudo virsh start <nom-vm>
|
||||
sudo virsh domifaddr <nom-vm> # nouvelle IP LAN / new LAN IP
|
||||
```
|
||||
|
||||
<!-- [en] -->
|
||||
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.
|
||||
|
||||
<!-- [fr] -->
|
||||
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.
|
||||
261
script/qemu/README.fr.md
Normal file
261
script/qemu/README.fr.md
Normal file
|
|
@ -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@<IP>
|
||||
```
|
||||
|
||||
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 <nom-vm> # arrêt propre ACPI / graceful shutdown
|
||||
sudo virsh destroy <nom-vm> # arrêt forcé / force off (pull the plug)
|
||||
sudo virsh undefine <nom-vm> # supprime la définition / remove definition
|
||||
sudo virsh domifaddr <nom-vm> # 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 <nom-vm>`, puis depuis l'autre machine :
|
||||
|
||||
```bash
|
||||
# Rebond SSH vers la VM / jump through the KVM host
|
||||
ssh -J user@<ip-hote> erplibre@<ip-vm>
|
||||
|
||||
# Tunnel d'un service, ex. Odoo 8069 / tunnel a service, then http://localhost:8069
|
||||
ssh -L 8069:<ip-vm>:8069 user@<ip-hote>
|
||||
```
|
||||
|
||||
Pour le rendre permanent, ajoutez ceci à `~/.ssh/config` sur l'autre machine
|
||||
(ensuite `ssh myvm` suffit) :
|
||||
|
||||
```text
|
||||
Host myvm
|
||||
HostName <ip-vm> # ex. 192.168.122.50 (reseau NAT)
|
||||
User erplibre
|
||||
ProxyJump user@<ip-hote> # 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 <nom-vm> --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
|
||||
`<interface>` (`type='network'` / `<source network='default'/>` →
|
||||
`type='bridge'` / `<source bridge='br0'/>`), puis redémarrez-la :
|
||||
|
||||
```bash
|
||||
sudo virsh shutdown <nom-vm>
|
||||
sudo virsh edit <nom-vm> # mettre l'interface en bridge=br0
|
||||
sudo virsh start <nom-vm>
|
||||
sudo virsh domifaddr <nom-vm> # 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.
|
||||
251
script/qemu/README.md
Normal file
251
script/qemu/README.md
Normal file
|
|
@ -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@<IP>
|
||||
```
|
||||
|
||||
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 <nom-vm> # arrêt propre ACPI / graceful shutdown
|
||||
sudo virsh destroy <nom-vm> # arrêt forcé / force off (pull the plug)
|
||||
sudo virsh undefine <nom-vm> # supprime la définition / remove definition
|
||||
sudo virsh domifaddr <nom-vm> # 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 <nom-vm>`, then from the other machine:
|
||||
|
||||
```bash
|
||||
# Rebond SSH vers la VM / jump through the KVM host
|
||||
ssh -J user@<ip-hote> erplibre@<ip-vm>
|
||||
|
||||
# Tunnel d'un service, ex. Odoo 8069 / tunnel a service, then http://localhost:8069
|
||||
ssh -L 8069:<ip-vm>:8069 user@<ip-hote>
|
||||
```
|
||||
|
||||
To make it permanent, add this to `~/.ssh/config` on the other machine (then
|
||||
just `ssh myvm`):
|
||||
|
||||
```text
|
||||
Host myvm
|
||||
HostName <ip-vm> # ex. 192.168.122.50 (reseau NAT)
|
||||
User erplibre
|
||||
ProxyJump user@<ip-hote> # 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 <nom-vm> --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 `<interface>`
|
||||
block (`type='network'` / `<source network='default'/>` → `type='bridge'` /
|
||||
`<source bridge='br0'/>`), then start it again:
|
||||
|
||||
```bash
|
||||
sudo virsh shutdown <nom-vm>
|
||||
sudo virsh edit <nom-vm> # mettre l'interface en bridge=br0
|
||||
sudo virsh start <nom-vm>
|
||||
sudo virsh domifaddr <nom-vm> # 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.
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue