Compare commits
No commits in common. "935919e07ba1ac92a83712a907e67fac06eb2d39" and "dc42bb715aefa311f1ef02d371f8fff0f12ad0af" have entirely different histories.
935919e07b
...
dc42bb715a
7 changed files with 24 additions and 1654 deletions
|
|
@ -1,459 +0,0 @@
|
||||||
<!---------------------------->
|
|
||||||
<!-- 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.
|
|
||||||
|
|
@ -1,261 +0,0 @@
|
||||||
|
|
||||||
# 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.
|
|
||||||
|
|
@ -1,251 +0,0 @@
|
||||||
|
|
||||||
# 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,16 +10,7 @@ Reprend le workflow des notes :
|
||||||
|
|
||||||
Exemples
|
Exemples
|
||||||
--------
|
--------
|
||||||
# Le plus simple : image téléchargée automatiquement (chemin déduit de
|
# Déploiement minimal (clé SSH, aucun mot de passe)
|
||||||
# --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 \\
|
sudo ./qemu-deploy.py /var/lib/libvirt/images/iso/noble.img \\
|
||||||
--name test-vm --ssh-key ~/.ssh/id_ed25519.pub
|
--name test-vm --ssh-key ~/.ssh/id_ed25519.pub
|
||||||
|
|
||||||
|
|
@ -65,22 +56,12 @@ UBUNTU_VERSIONS: dict[str, tuple[str, str]] = {
|
||||||
|
|
||||||
CLOUD_IMG_BASE = "https://cloud-images.ubuntu.com"
|
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:
|
def image_url(codename: str, arch: str) -> str:
|
||||||
"""URL de l'image cloud « current » pour un nom de code + architecture."""
|
"""URL de l'image cloud « current » pour un nom de code + architecture."""
|
||||||
return f"{CLOUD_IMG_BASE}/{codename}/current/{codename}-server-cloudimg-{arch}.img"
|
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
|
# Utilitaires d'exécution
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
@ -101,14 +82,7 @@ class Runner:
|
||||||
print(f" [dry-run] {printable}")
|
print(f" [dry-run] {printable}")
|
||||||
return
|
return
|
||||||
print(f" $ {printable}")
|
print(f" $ {printable}")
|
||||||
try:
|
subprocess.run(cmd, check=check)
|
||||||
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:
|
def need_tool(name: str) -> None:
|
||||||
|
|
@ -116,278 +90,6 @@ def need_tool(name: str) -> None:
|
||||||
sys.exit(f"Erreur : outil requis introuvable dans le PATH : {name!r}")
|
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
|
# Étapes
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
@ -403,14 +105,7 @@ def download_image(url: str, dest: Path, dry_run: bool) -> None:
|
||||||
print(f" [dry-run] téléchargement {url} -> {dest}")
|
print(f" [dry-run] téléchargement {url} -> {dest}")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
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}")
|
print(f" Téléchargement {url}")
|
||||||
tmp = dest.with_suffix(dest.suffix + ".part")
|
tmp = dest.with_suffix(dest.suffix + ".part")
|
||||||
|
|
||||||
|
|
@ -460,12 +155,8 @@ def verify_sha256(url: str, image: Path, dry_run: bool) -> None:
|
||||||
for chunk in iter(lambda: fh.read(1 << 20), b""):
|
for chunk in iter(lambda: fh.read(1 << 20), b""):
|
||||||
h.update(chunk)
|
h.update(chunk)
|
||||||
if h.hexdigest() != expected:
|
if h.hexdigest() != expected:
|
||||||
image.unlink(
|
|
||||||
missing_ok=True
|
|
||||||
) # évite la réutilisation du cache corrompu
|
|
||||||
sys.exit(
|
sys.exit(
|
||||||
f"SHA256 NON conforme ! Image supprimée : {image}\n"
|
f"SHA256 NON conforme !\n attendu : {expected}\n obtenu : {h.hexdigest()}"
|
||||||
f" attendu : {expected}\n obtenu : {h.hexdigest()}"
|
|
||||||
)
|
)
|
||||||
print(" SHA256 conforme.")
|
print(" SHA256 conforme.")
|
||||||
|
|
||||||
|
|
@ -694,10 +385,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
p.add_argument(
|
p.add_argument(
|
||||||
"image_path",
|
"image_path",
|
||||||
type=Path,
|
type=Path,
|
||||||
nargs="?",
|
help="Chemin de cache de l'image cloud (.img). "
|
||||||
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é.",
|
"Si le fichier existe, il n'est PAS re-téléchargé.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -716,13 +404,6 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
default="amd64",
|
default="amd64",
|
||||||
help="Architecture de l'image (défaut : 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(
|
g_img.add_argument(
|
||||||
"--verify",
|
"--verify",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|
@ -730,11 +411,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
)
|
)
|
||||||
|
|
||||||
g_vm = p.add_argument_group("VM")
|
g_vm = p.add_argument_group("VM")
|
||||||
g_vm.add_argument(
|
g_vm.add_argument("--name", required=True, help="Nom de la VM (virsh).")
|
||||||
"--name",
|
|
||||||
help="Nom de la VM (virsh). Requis pour déployer ; inutile avec "
|
|
||||||
"--download-only.",
|
|
||||||
)
|
|
||||||
g_vm.add_argument(
|
g_vm.add_argument(
|
||||||
"--hostname", help="Nom d'hôte interne (défaut : --name)."
|
"--hostname", help="Nom d'hôte interne (défaut : --name)."
|
||||||
)
|
)
|
||||||
|
|
@ -861,23 +538,6 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Affiche les commandes et le user-data sans rien exécuter.",
|
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
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -906,6 +566,7 @@ def load_ssh_keys(paths: list[str]) -> list[str]:
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
args = build_parser().parse_args()
|
args = build_parser().parse_args()
|
||||||
|
args.hostname = args.hostname or args.name
|
||||||
|
|
||||||
codename, default_osinfo = UBUNTU_VERSIONS[args.version]
|
codename, default_osinfo = UBUNTU_VERSIONS[args.version]
|
||||||
if args.codename:
|
if args.codename:
|
||||||
|
|
@ -913,36 +574,6 @@ def main() -> None:
|
||||||
osinfo = args.osinfo or default_osinfo
|
osinfo = args.osinfo or default_osinfo
|
||||||
url = image_url(codename, args.arch)
|
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)
|
pw_hash = resolve_password(args)
|
||||||
ssh_keys = load_ssh_keys(args.ssh_key)
|
ssh_keys = load_ssh_keys(args.ssh_key)
|
||||||
if not pw_hash and not ssh_keys:
|
if not pw_hash and not ssh_keys:
|
||||||
|
|
@ -952,11 +583,15 @@ def main() -> None:
|
||||||
file=sys.stderr,
|
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"
|
disk = args.disk_dir / f"{args.name}.qcow2"
|
||||||
seed = args.seed_dir / f"{args.name}-seed.iso"
|
seed = args.seed_dir / f"{args.name}-seed.iso"
|
||||||
|
|
||||||
if not args.dry_run:
|
if not args.dry_run:
|
||||||
ensure_tools(runner, args.assume_yes, args.no_install_deps)
|
for tool in ("qemu-img", "virt-install"):
|
||||||
|
need_tool(tool)
|
||||||
|
|
||||||
print(f"\n== 1/5 Image cloud ({args.version} / {codename}) ==")
|
print(f"\n== 1/5 Image cloud ({args.version} / {codename}) ==")
|
||||||
download_image(url, args.image_path, args.dry_run)
|
download_image(url, args.image_path, args.dry_run)
|
||||||
|
|
|
||||||
|
|
@ -61,11 +61,5 @@
|
||||||
"prompt_description_key": "Generate git patch to /tmp",
|
"prompt_description_key": "Generate git patch to /tmp",
|
||||||
"bash_command": "PATCH=\"/tmp/patch_$(date +%Y%m%d_%H%M%S).patch\"; git -C \"$(pwd)\" diff HEAD > \"$PATCH\" && printf \"\\n✓ Patch created: %s\\n\\n=== Guide to apply the patch ===\\n Check compatibility : git apply --check %s\\n Apply (git) : git apply %s\\n Apply (patch) : patch -p1 < %s\\n Revert (git) : git apply -R %s\\n\" \"$PATCH\" \"$PATCH\" \"$PATCH\" \"$PATCH\" \"$PATCH\""
|
"bash_command": "PATCH=\"/tmp/patch_$(date +%Y%m%d_%H%M%S).patch\"; git -C \"$(pwd)\" diff HEAD > \"$PATCH\" && printf \"\\n✓ Patch created: %s\\n\\n=== Guide to apply the patch ===\\n Check compatibility : git apply --check %s\\n Apply (git) : git apply %s\\n Apply (patch) : patch -p1 < %s\\n Revert (git) : git apply -R %s\\n\" \"$PATCH\" \"$PATCH\" \"$PATCH\" \"$PATCH\" \"$PATCH\""
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"qemu_from_makefile": [
|
|
||||||
{
|
|
||||||
"prompt_description_key": "QEMU - Sample dry-run (demo-vm, Ubuntu 24.04)",
|
|
||||||
"bash_command": "./script/qemu/deploy_qemu.py --name demo-vm --version 24.04 --dry-run"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import getpass
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shlex
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -668,11 +667,6 @@ class TODO:
|
||||||
"Deploy - Install NTFY notification server"
|
"Deploy - Install NTFY notification server"
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"prompt_description": t(
|
|
||||||
"QEMU/KVM - Deploy an Ubuntu VM (libvirt)"
|
|
||||||
)
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
help_info = self.fill_help_info(choices)
|
help_info = self.fill_help_info(choices)
|
||||||
|
|
||||||
|
|
@ -709,200 +703,9 @@ class TODO:
|
||||||
self._deploy_ssh_install_nginx()
|
self._deploy_ssh_install_nginx()
|
||||||
elif status == "14":
|
elif status == "14":
|
||||||
self._deploy_ntfy_server()
|
self._deploy_ntfy_server()
|
||||||
elif status == "15":
|
|
||||||
self.prompt_execute_qemu()
|
|
||||||
else:
|
else:
|
||||||
print(t("Command not found !"))
|
print(t("Command not found !"))
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# QEMU / KVM (libvirt) VM deployment
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
def _qemu_script_path(self):
|
|
||||||
"""Chemin absolu vers script/qemu/deploy_qemu.py."""
|
|
||||||
path = os.path.join(
|
|
||||||
os.path.dirname(os.path.abspath(__file__)),
|
|
||||||
"..",
|
|
||||||
"qemu",
|
|
||||||
"deploy_qemu.py",
|
|
||||||
)
|
|
||||||
return os.path.realpath(path)
|
|
||||||
|
|
||||||
def _qemu_default_ssh_key(self):
|
|
||||||
"""Première clé publique SSH trouvée dans ~/.ssh, sinon ''."""
|
|
||||||
for name in ("id_ed25519.pub", "id_rsa.pub"):
|
|
||||||
path = os.path.expanduser(f"~/.ssh/{name}")
|
|
||||||
if os.path.exists(path):
|
|
||||||
return path
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def _qemu_prompt_version(self):
|
|
||||||
"""Demande la version Ubuntu ; retourne une chaîne (défaut 24.04)."""
|
|
||||||
versions = ["20.04", "22.04", "24.04", "24.10", "25.04", "25.10"]
|
|
||||||
print(f"\n{t('Ubuntu version:')}")
|
|
||||||
for i, v in enumerate(versions, 1):
|
|
||||||
suffix = " (LTS)" if v in ("20.04", "22.04", "24.04") else ""
|
|
||||||
print(f" [{i}] {v}{suffix}")
|
|
||||||
sel = input(t("Choice (number or version, default: 24.04): ")).strip()
|
|
||||||
if not sel:
|
|
||||||
return "24.04"
|
|
||||||
try:
|
|
||||||
idx = int(sel) - 1
|
|
||||||
if 0 <= idx < len(versions):
|
|
||||||
return versions[idx]
|
|
||||||
except ValueError:
|
|
||||||
if sel in versions:
|
|
||||||
return sel
|
|
||||||
print(t("Invalid selection, using 24.04"))
|
|
||||||
return "24.04"
|
|
||||||
|
|
||||||
def prompt_execute_qemu(self):
|
|
||||||
print(f"🤖 {t('Deploy a QEMU/KVM virtual machine (libvirt)!')}")
|
|
||||||
script_path = self._qemu_script_path()
|
|
||||||
if not os.path.isfile(script_path):
|
|
||||||
print(f"{t('QEMU deploy script not found: ')}{script_path}")
|
|
||||||
return False
|
|
||||||
choices = [
|
|
||||||
{"prompt_description": t("Deploy a new Ubuntu VM")},
|
|
||||||
{
|
|
||||||
"prompt_description": t(
|
|
||||||
"Preview a deployment (dry-run, no sudo)"
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{"prompt_description": t("Download an Ubuntu cloud image only")},
|
|
||||||
{"prompt_description": t("List VMs (virsh list --all)")},
|
|
||||||
{"prompt_description": t("Show a VM IP address")},
|
|
||||||
]
|
|
||||||
config_entries = self.config_file.get_config("qemu_from_makefile")
|
|
||||||
if config_entries:
|
|
||||||
choices.extend(config_entries)
|
|
||||||
help_info = self.fill_help_info(choices)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
status = click.prompt(help_info)
|
|
||||||
print()
|
|
||||||
if status == "0":
|
|
||||||
return False
|
|
||||||
elif status == "1":
|
|
||||||
self._qemu_deploy_vm(dry_run=False)
|
|
||||||
elif status == "2":
|
|
||||||
self._qemu_deploy_vm(dry_run=True)
|
|
||||||
elif status == "3":
|
|
||||||
self._qemu_download_image()
|
|
||||||
elif status == "4":
|
|
||||||
self._qemu_list_vms()
|
|
||||||
elif status == "5":
|
|
||||||
self._qemu_show_ip()
|
|
||||||
else:
|
|
||||||
cmd_no_found = True
|
|
||||||
try:
|
|
||||||
int_cmd = int(status)
|
|
||||||
if 0 < int_cmd <= len(choices):
|
|
||||||
cmd_no_found = False
|
|
||||||
instance = choices[int_cmd - 1]
|
|
||||||
self.execute_from_configuration(instance)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if cmd_no_found:
|
|
||||||
print(t("Command not found !"))
|
|
||||||
|
|
||||||
def _qemu_deploy_vm(self, dry_run=False):
|
|
||||||
script_path = self._qemu_script_path()
|
|
||||||
name = input(t("VM name (required): ")).strip()
|
|
||||||
if not name:
|
|
||||||
print(t("VM name is required!"))
|
|
||||||
return
|
|
||||||
version = self._qemu_prompt_version()
|
|
||||||
memory = input(t("RAM in MB (default: 8192): ")).strip() or "8192"
|
|
||||||
vcpus = input(t("vCPUs (default: 4): ")).strip() or "4"
|
|
||||||
disk_size = input(t("Disk size (default: 20G): ")).strip() or "20G"
|
|
||||||
|
|
||||||
default_key = self._qemu_default_ssh_key()
|
|
||||||
key_hint = default_key or t("none")
|
|
||||||
ssh_key = input(f"{t('SSH public key path')} ({key_hint}): ").strip()
|
|
||||||
if not ssh_key:
|
|
||||||
ssh_key = default_key
|
|
||||||
if ssh_key:
|
|
||||||
# Résout ~ avec le HOME de l'utilisateur courant : le script tourne
|
|
||||||
# sous sudo (HOME=/root) et ne pourrait pas déduire ~ correctement.
|
|
||||||
ssh_key = os.path.expanduser(ssh_key)
|
|
||||||
|
|
||||||
use_password = False
|
|
||||||
if not ssh_key:
|
|
||||||
ans = (
|
|
||||||
input(t("No SSH key found. Set a password instead? (Y/n): "))
|
|
||||||
.strip()
|
|
||||||
.lower()
|
|
||||||
)
|
|
||||||
use_password = ans != "n"
|
|
||||||
|
|
||||||
force = False
|
|
||||||
if not dry_run:
|
|
||||||
ans = (
|
|
||||||
input(t("Overwrite existing VM disk if present? (y/N): "))
|
|
||||||
.strip()
|
|
||||||
.lower()
|
|
||||||
)
|
|
||||||
force = ans == "y"
|
|
||||||
|
|
||||||
parts = []
|
|
||||||
if not dry_run:
|
|
||||||
parts.append("sudo")
|
|
||||||
parts.append(script_path)
|
|
||||||
parts += ["--name", name, "--version", version]
|
|
||||||
parts += [
|
|
||||||
"--memory",
|
|
||||||
memory,
|
|
||||||
"--vcpus",
|
|
||||||
vcpus,
|
|
||||||
"--disk-size",
|
|
||||||
disk_size,
|
|
||||||
]
|
|
||||||
if ssh_key:
|
|
||||||
parts += ["--ssh-key", ssh_key]
|
|
||||||
if use_password:
|
|
||||||
parts.append("--ask-password")
|
|
||||||
if force:
|
|
||||||
parts.append("--force")
|
|
||||||
if dry_run:
|
|
||||||
parts.append("--dry-run")
|
|
||||||
else:
|
|
||||||
parts.append("-y") # accepte l'installation des dépendances
|
|
||||||
|
|
||||||
cmd = " ".join(shlex.quote(p) for p in parts)
|
|
||||||
print(f"{t('Will execute:')} {cmd}")
|
|
||||||
self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
||||||
|
|
||||||
def _qemu_download_image(self):
|
|
||||||
script_path = self._qemu_script_path()
|
|
||||||
version = self._qemu_prompt_version()
|
|
||||||
ans = input(t("Verify SHA256 after download? (y/N): ")).strip().lower()
|
|
||||||
parts = [
|
|
||||||
"sudo",
|
|
||||||
script_path,
|
|
||||||
"--download-only",
|
|
||||||
"--version",
|
|
||||||
version,
|
|
||||||
]
|
|
||||||
if ans == "y":
|
|
||||||
parts.append("--verify")
|
|
||||||
cmd = " ".join(shlex.quote(p) for p in parts)
|
|
||||||
print(f"{t('Will execute:')} {cmd}")
|
|
||||||
self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
||||||
|
|
||||||
def _qemu_list_vms(self):
|
|
||||||
cmd = "sudo virsh list --all"
|
|
||||||
print(f"{t('Will execute:')} {cmd}")
|
|
||||||
self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
||||||
|
|
||||||
def _qemu_show_ip(self):
|
|
||||||
name = input(t("VM name: ")).strip()
|
|
||||||
if not name:
|
|
||||||
print(t("VM name is required!"))
|
|
||||||
return
|
|
||||||
cmd = f"sudo virsh domifaddr {shlex.quote(name)} --source lease"
|
|
||||||
print(f"{t('Will execute:')} {cmd}")
|
|
||||||
self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
||||||
|
|
||||||
def _deploy_clone_erplibre(self):
|
def _deploy_clone_erplibre(self):
|
||||||
default_path = os.path.expanduser("~/erplibre")
|
default_path = os.path.expanduser("~/erplibre")
|
||||||
target_path = (
|
target_path = (
|
||||||
|
|
@ -930,7 +733,9 @@ class TODO:
|
||||||
print(
|
print(
|
||||||
f"\n{t('Deploy a local NTFY push notification server (Ubuntu/Arch)')}"
|
f"\n{t('Deploy a local NTFY push notification server (Ubuntu/Arch)')}"
|
||||||
)
|
)
|
||||||
port = input(t("NTFY server port (default: 8080): ")).strip() or "8080"
|
port = (
|
||||||
|
input(t("NTFY server port (default: 8080): ")).strip() or "8080"
|
||||||
|
)
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
hostname = socket.gethostname()
|
hostname = socket.gethostname()
|
||||||
|
|
@ -941,7 +746,9 @@ class TODO:
|
||||||
|
|
||||||
default_url = f"http://{local_ip}:{port}"
|
default_url = f"http://{local_ip}:{port}"
|
||||||
base_url = (
|
base_url = (
|
||||||
input(f"{t('NTFY base URL')} (default: {default_url}): ").strip()
|
input(
|
||||||
|
f"{t('NTFY base URL')} (default: {default_url}): "
|
||||||
|
).strip()
|
||||||
or default_url
|
or default_url
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1076,9 +883,7 @@ class TODO:
|
||||||
|
|
||||||
def _get_ssh_params(self):
|
def _get_ssh_params(self):
|
||||||
"""Prompt for SSH connection parameters. Returns dict or None on cancel."""
|
"""Prompt for SSH connection parameters. Returns dict or None on cancel."""
|
||||||
host = click.prompt(
|
host = click.prompt(t("Remote host (user@hostname or hostname): ")).strip()
|
||||||
t("Remote host (user@hostname or hostname): ")
|
|
||||||
).strip()
|
|
||||||
if not host:
|
if not host:
|
||||||
print(t("SSH host is required!"))
|
print(t("SSH host is required!"))
|
||||||
return None
|
return None
|
||||||
|
|
@ -1087,13 +892,13 @@ class TODO:
|
||||||
or "erplibre"
|
or "erplibre"
|
||||||
)
|
)
|
||||||
port = click.prompt(t("SSH port (default: 22): ")).strip() or "22"
|
port = click.prompt(t("SSH port (default: 22): ")).strip() or "22"
|
||||||
key = click.prompt(
|
key = (
|
||||||
t("SSH key path (default: ~/.ssh/id_rsa, empty for none): ")
|
|
||||||
).strip()
|
|
||||||
path = (
|
|
||||||
click.prompt(
|
click.prompt(
|
||||||
t("Remote path (default: ~/erplibre_deploy_2): ")
|
t("SSH key path (default: ~/.ssh/id_rsa, empty for none): ")
|
||||||
).strip()
|
).strip()
|
||||||
|
)
|
||||||
|
path = (
|
||||||
|
click.prompt(t("Remote path (default: ~/erplibre_deploy_2): ")).strip()
|
||||||
or "~/erplibre_deploy_2"
|
or "~/erplibre_deploy_2"
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -984,99 +984,6 @@ TRANSLATIONS = {
|
||||||
"fr": "Script d'installation NTFY introuvable : ",
|
"fr": "Script d'installation NTFY introuvable : ",
|
||||||
"en": "NTFY install script not found: ",
|
"en": "NTFY install script not found: ",
|
||||||
},
|
},
|
||||||
# QEMU / KVM (libvirt) VM deployment
|
|
||||||
"QEMU/KVM - Deploy an Ubuntu VM (libvirt)": {
|
|
||||||
"fr": "QEMU/KVM - Déployer une VM Ubuntu (libvirt)",
|
|
||||||
"en": "QEMU/KVM - Deploy an Ubuntu VM (libvirt)",
|
|
||||||
},
|
|
||||||
"Deploy a QEMU/KVM virtual machine (libvirt)!": {
|
|
||||||
"fr": "Déployer une machine virtuelle QEMU/KVM (libvirt) !",
|
|
||||||
"en": "Deploy a QEMU/KVM virtual machine (libvirt)!",
|
|
||||||
},
|
|
||||||
"QEMU deploy script not found: ": {
|
|
||||||
"fr": "Script de déploiement QEMU introuvable : ",
|
|
||||||
"en": "QEMU deploy script not found: ",
|
|
||||||
},
|
|
||||||
"Deploy a new Ubuntu VM": {
|
|
||||||
"fr": "Déployer une nouvelle VM Ubuntu",
|
|
||||||
"en": "Deploy a new Ubuntu VM",
|
|
||||||
},
|
|
||||||
"Preview a deployment (dry-run, no sudo)": {
|
|
||||||
"fr": "Prévisualiser un déploiement (dry-run, sans sudo)",
|
|
||||||
"en": "Preview a deployment (dry-run, no sudo)",
|
|
||||||
},
|
|
||||||
"Download an Ubuntu cloud image only": {
|
|
||||||
"fr": "Télécharger seulement une image cloud Ubuntu",
|
|
||||||
"en": "Download an Ubuntu cloud image only",
|
|
||||||
},
|
|
||||||
"List VMs (virsh list --all)": {
|
|
||||||
"fr": "Lister les VM (virsh list --all)",
|
|
||||||
"en": "List VMs (virsh list --all)",
|
|
||||||
},
|
|
||||||
"Show a VM IP address": {
|
|
||||||
"fr": "Afficher l'adresse IP d'une VM",
|
|
||||||
"en": "Show a VM IP address",
|
|
||||||
},
|
|
||||||
"Ubuntu version:": {
|
|
||||||
"fr": "Version Ubuntu :",
|
|
||||||
"en": "Ubuntu version:",
|
|
||||||
},
|
|
||||||
"Choice (number or version, default: 24.04): ": {
|
|
||||||
"fr": "Choix (numéro ou version, défaut : 24.04) : ",
|
|
||||||
"en": "Choice (number or version, default: 24.04): ",
|
|
||||||
},
|
|
||||||
"Invalid selection, using 24.04": {
|
|
||||||
"fr": "Sélection invalide, utilisation de 24.04",
|
|
||||||
"en": "Invalid selection, using 24.04",
|
|
||||||
},
|
|
||||||
"VM name (required): ": {
|
|
||||||
"fr": "Nom de la VM (requis) : ",
|
|
||||||
"en": "VM name (required): ",
|
|
||||||
},
|
|
||||||
"VM name is required!": {
|
|
||||||
"fr": "Le nom de la VM est requis !",
|
|
||||||
"en": "VM name is required!",
|
|
||||||
},
|
|
||||||
"VM name: ": {
|
|
||||||
"fr": "Nom de la VM : ",
|
|
||||||
"en": "VM name: ",
|
|
||||||
},
|
|
||||||
"RAM in MB (default: 8192): ": {
|
|
||||||
"fr": "RAM en Mo (défaut : 8192) : ",
|
|
||||||
"en": "RAM in MB (default: 8192): ",
|
|
||||||
},
|
|
||||||
"vCPUs (default: 4): ": {
|
|
||||||
"fr": "vCPU (défaut : 4) : ",
|
|
||||||
"en": "vCPUs (default: 4): ",
|
|
||||||
},
|
|
||||||
"Disk size (default: 20G): ": {
|
|
||||||
"fr": "Taille du disque (défaut : 20G) : ",
|
|
||||||
"en": "Disk size (default: 20G): ",
|
|
||||||
},
|
|
||||||
"SSH public key path": {
|
|
||||||
"fr": "Chemin de la clé publique SSH",
|
|
||||||
"en": "SSH public key path",
|
|
||||||
},
|
|
||||||
"none": {
|
|
||||||
"fr": "aucune",
|
|
||||||
"en": "none",
|
|
||||||
},
|
|
||||||
"No SSH key found. Set a password instead? (Y/n): ": {
|
|
||||||
"fr": "Aucune clé SSH trouvée. Définir un mot de passe ? (O/n) : ",
|
|
||||||
"en": "No SSH key found. Set a password instead? (Y/n): ",
|
|
||||||
},
|
|
||||||
"Overwrite existing VM disk if present? (y/N): ": {
|
|
||||||
"fr": "Écraser le disque de la VM s'il existe ? (o/N) : ",
|
|
||||||
"en": "Overwrite existing VM disk if present? (y/N): ",
|
|
||||||
},
|
|
||||||
"Verify SHA256 after download? (y/N): ": {
|
|
||||||
"fr": "Vérifier le SHA256 après téléchargement ? (o/N) : ",
|
|
||||||
"en": "Verify SHA256 after download? (y/N): ",
|
|
||||||
},
|
|
||||||
"QEMU - Sample dry-run (demo-vm, Ubuntu 24.04)": {
|
|
||||||
"fr": "QEMU - Exemple dry-run (demo-vm, Ubuntu 24.04)",
|
|
||||||
"en": "QEMU - Sample dry-run (demo-vm, Ubuntu 24.04)",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue