Compare commits

..

7 commits

Author SHA1 Message Date
ca796a67a4 [FIX] script todo: accept o/oui at every yes/no prompt
Generalise the _is_yes() helper (y/yes/o/oui) across the whole file so
French answers work everywhere, and add _is_no() (n/no/non) for the
default-yes prompts. Converted: system-install, Pycharm, SSH-password
(default yes via _is_no), template overwrite, keep-temp-database (was
locale-gated, now accepts both), git-repo fetch and the mobile
personalize/debug/picture prompts. Drop the now-unused get_lang import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 07:10:55 +00:00
84f63c326a [FIX] script todo: accept "o"/"oui" at QEMU yes/no prompts
The confirmations only matched "y", but the French prompts show "(o/N)",
so answering "o" (oui) was treated as no — the infra deployment aborted
with "Annulé." right after the user confirmed. Add a _is_yes() helper
(y/yes/o/oui) and use it for the infra deploy confirmation, the ERPLibre
install prompt, the disk-overwrite prompt and the SHA256 verify prompt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 07:07:20 +00:00
3e2224b782 [IMP] script todo: add "Deploy ERPLibre infra" QEMU command
New QEMU/KVM entry that stands up a fleet of minimal VMs, one per
selected cloud image. It:

- lets you pick distros then versions (multi-select, "all" per level, or
  the whole catalogue), reading the specs straight from deploy_qemu.py so
  there is no duplication;
- prints a plan with each VM's minimum RAM/disk, the total concurrent RAM
  and virtual disk, and the host's available RAM, warning when the fleet
  cannot all run at once;
- deploys sequentially after confirmation (minimum sizing per version),
  skipping VMs that already exist;
- optionally clones ERPLibre into ~/git/erplibre on each VM, asking which
  branch (list fetched via git ls-remote) and pulling git in through
  cloud-init.

Naming is erplibre-<distro>-<version> (e.g. erplibre-ubuntu-2404).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 06:52:39 +00:00
3b3efe5e43 [IMP] script todo: breadcrumb above the "Command:" prompt
Every menu now prints a breadcrumb line (e.g. "📍 TODO › Execute ›
Deploy › QEMU/KVM") right above "Command:", so it is always clear where
you are and the path can be copied to describe a menu unambiguously.

The trail is derived from the call stack via a method-name -> label map,
so no menu method had to change: fill_help_info and the three inline
menus just render self._menu_header() instead of t("Command:").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 06:41:39 +00:00
9c8e7454f3 [IMP] script qemu: multi-distro (Debian/Fedora), resilient osinfo, 26.04
Generalise the deployer beyond Ubuntu with a distro registry and a new
--distro flag (ubuntu default, debian, fedora). Each distro keeps its
own codenames, osinfo ids and per-version minimum RAM/disk. Image URLs
are built per distro (Ubuntu current/, Debian latest/, Fedora resolved
from the release index since it has no "latest" link).

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 06:38:04 +00:00
0d985ab449 [FIX] script todo: list VMs before asking which IP to show
The "Show a VM IP address" entry prompted for a VM name with no visible
list, so the user had to guess the name/ID. It now runs virsh list --all
first and asks for a "VM name or ID", making the expected input obvious.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 06:27:00 +00:00
a202143c30 [IMP] script qemu: default VM sizing to per-version minimums
Replace the fixed 8192 MB / 4 vCPU / 20G defaults with the minimum
resources required by the chosen Ubuntu version (libosinfo/osinfo-db
values): 20.04 -> 2048 MB/5G, 22.04 -> 2048 MB/10G, 24.04+ -> 3072
MB/20G. --vcpus now defaults to 2. This stops a small host from being
starved by an oversized default (an 8 GB VM failed to allocate on a
6.7 GB host) and silences the libvirt "less than recommended" warning.

In the todo menu, leaving the RAM/vCPU/disk fields blank now means
"version minimum" (the flag is simply not passed) instead of forcing
8192/4/20G. Any explicit value still overrides. README regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 06:26:43 +00:00
6 changed files with 967 additions and 172 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -5,9 +5,11 @@
import configparser import configparser
import datetime import datetime
import getpass import getpass
import inspect
import json import json
import logging import logging
import os import os
import re
import shlex import shlex
import shutil import shutil
import subprocess import subprocess
@ -25,7 +27,7 @@ from script.config import config_file
from script.execute import execute from script.execute import execute
from script.todo.database_manager import DatabaseManager from script.todo.database_manager import DatabaseManager
from script.todo.kdbx_manager import KdbxManager from script.todo.kdbx_manager import KdbxManager
from script.todo.todo_i18n import get_lang, lang_is_configured, set_lang, t from script.todo.todo_i18n import lang_is_configured, set_lang, t
from script.todo.version_manager import get_odoo_version from script.todo.version_manager import get_odoo_version
ERROR_LOG_PATH = ".erplibre.error.txt" ERROR_LOG_PATH = ".erplibre.error.txt"
@ -122,7 +124,7 @@ class TODO:
self._ask_language() self._ask_language()
print(t("Opening TODO ...")) print(t("Opening TODO ..."))
print(f"🤖 {t('=> Enter your choice by number and press Enter!')}") print(f"🤖 {t('=> Enter your choice by number and press Enter!')}")
help_info = f"""{t("Command:")} help_info = f"""{self._menu_header()}
[1] {t("Execute")} [1] {t("Execute")}
[2] {t("Install")} [2] {t("Install")}
[3] {t("Question")} [3] {t("Question")}
@ -168,7 +170,7 @@ class TODO:
def execute_prompt_ia(self): def execute_prompt_ia(self):
while True: while True:
help_info = f"""{t("Command:")} help_info = f"""{self._menu_header()}
[0] {t("Back")} [0] {t("Back")}
{t("Write your question ")}""" {t("Write your question ")}"""
status = click.prompt(help_info) status = click.prompt(help_info)
@ -194,7 +196,7 @@ class TODO:
print() print()
def prompt_execute(self): def prompt_execute(self):
help_info = f"""{t("Command:")} help_info = f"""{self._menu_header()}
[1] {t("Automation - Demonstration of developed features")} [1] {t("Automation - Demonstration of developed features")}
[2] {t("Code - Developer tools")} [2] {t("Code - Developer tools")}
[3] {t("Config - Configuration file management")} [3] {t("Config - Configuration file management")}
@ -291,7 +293,7 @@ class TODO:
.strip() .strip()
.lower() .lower()
) )
if first_installation_input == "y": if self._is_yes(first_installation_input):
cmd = "./script/version/update_env_version.py --install" cmd = "./script/version/update_env_version.py --install"
self.execute.exec_command_live(cmd, source_erplibre=True) self.execute.exec_command_live(cmd, source_erplibre=True)
print("Wait after OS installation before continue.") print("Wait after OS installation before continue.")
@ -321,7 +323,7 @@ class TODO:
pycharm_configuration_input = ( pycharm_configuration_input = (
input("💬 Open Pycharm? (Y/N): ").strip().lower() input("💬 Open Pycharm? (Y/N): ").strip().lower()
) )
if pycharm_configuration_input == "y": if self._is_yes(pycharm_configuration_input):
pycharm_bin = "pycharm" if has_pycharm else "pycharm-community" pycharm_bin = "pycharm" if has_pycharm else "pycharm-community"
cmd = f"cd {os.getcwd()} && {pycharm_bin} ./" cmd = f"cd {os.getcwd()} && {pycharm_bin} ./"
@ -512,8 +514,48 @@ class TODO:
if callback: if callback:
callback(instance) callback(instance)
# Étiquettes du fil d'Ariane par méthode de menu. Le fil est dérivé de la
# pile d'appels (aucune méthode de menu à modifier). Labels courts et
# stables, pensés pour être copiés afin de situer précisément un menu.
_MENU_LABELS = {
"run": "TODO",
"prompt_execute": "Execute",
"execute_prompt_ia": "Question",
"prompt_install": "Install",
"prompt_execute_function": "Automation",
"prompt_execute_code": "Code",
"prompt_execute_config": "Config",
"prompt_execute_database": "Database",
"prompt_execute_doc": "Doc",
"prompt_execute_git": "Git",
"prompt_execute_git_local_server": "Git local server",
"prompt_execute_gpt_code": "GPT code",
"prompt_execute_process": "Process",
"prompt_execute_instance": "Run",
"prompt_execute_rtk": "RTK",
"prompt_execute_update": "Update",
"prompt_execute_deploy": "Deploy",
"prompt_execute_qemu": "QEMU/KVM",
}
def _menu_header(self):
"""En-tête de menu : fil d'Ariane (dérivé de la pile d'appels) suivi de
la ligne « Commande : ». Le fil situe le menu courant et se copie pour
décrire sans ambiguïté l'on se trouve."""
crumbs = []
for frame_info in reversed(inspect.stack()):
if frame_info.frame.f_locals.get("self") is not self:
continue
label = self._MENU_LABELS.get(frame_info.function)
if label and (not crumbs or crumbs[-1] != label):
crumbs.append(label)
header = ""
if crumbs:
header = "📍 " + " ".join(crumbs) + "\n"
return header + t("Command:")
def fill_help_info(self, choices): def fill_help_info(self, choices):
help_info = t("Command:") + "\n" help_info = self._menu_header() + "\n"
help_end = f"[0] {t('Back')}\n" help_end = f"[0] {t('Back')}\n"
for i, instance in enumerate(choices): for i, instance in enumerate(choices):
desc_key = instance.get("prompt_description_key") desc_key = instance.get("prompt_description_key")
@ -735,16 +777,48 @@ class TODO:
return path return path
return "" return ""
def _qemu_prompt_version(self): # distro -> (versions affichées, version par défaut). Source de vérité =
"""Demande la version Ubuntu ; retourne une chaîne (défaut 24.04).""" # deploy_qemu.py ; ceci ne sert qu'au sélecteur interactif.
versions = ["20.04", "22.04", "24.04", "24.10", "25.04", "25.10"] _QEMU_DISTROS = {
print(f"\n{t('Ubuntu version:')}") "ubuntu": (
for i, v in enumerate(versions, 1): ["20.04", "22.04", "24.04", "24.10", "25.04", "25.10", "26.04"],
suffix = " (LTS)" if v in ("20.04", "22.04", "24.04") else "" "24.04",
print(f" [{i}] {v}{suffix}") ),
sel = input(t("Choice (number or version, default: 24.04): ")).strip() "debian": (["11", "12", "13"], "12"),
"fedora": (["41", "42", "43", "44"], "42"),
}
def _qemu_prompt_distro(self):
"""Demande la distribution (défaut : ubuntu)."""
distros = list(self._QEMU_DISTROS)
print(f"\n{t('Distribution:')}")
for i, d in enumerate(distros, 1):
print(f" [{i}] {d}")
sel = input(t("Choice (number or name, default: ubuntu): ")).strip()
if not sel: if not sel:
return "24.04" return "ubuntu"
try:
idx = int(sel) - 1
if 0 <= idx < len(distros):
return distros[idx]
except ValueError:
if sel in distros:
return sel
print(t("Invalid selection, using ubuntu"))
return "ubuntu"
def _qemu_prompt_version(self, distro):
"""Demande la version pour la distro (défaut = version par défaut)."""
versions, default = self._QEMU_DISTROS.get(distro, ([], ""))
print(f"\n{t('Version for')} {distro} :")
for i, v in enumerate(versions, 1):
suffix = " *" if v == default else ""
print(f" [{i}] {v}{suffix}")
sel = input(
f"{t('Choice (number or version, blank = default):')} "
).strip()
if not sel:
return default
try: try:
idx = int(sel) - 1 idx = int(sel) - 1
if 0 <= idx < len(versions): if 0 <= idx < len(versions):
@ -752,8 +826,14 @@ class TODO:
except ValueError: except ValueError:
if sel in versions: if sel in versions:
return sel return sel
print(t("Invalid selection, using 24.04")) print(f"{t('Invalid selection, using')} {default}")
return "24.04" return default
def _qemu_list_images(self):
"""Affiche la liste des distros/versions et leurs specs."""
cmd = f"{self._qemu_script_path()} --list-images"
print(f"{t('Will execute:')} {cmd}")
self.execute.exec_command_live(cmd, source_erplibre=False)
def prompt_execute_qemu(self): def prompt_execute_qemu(self):
print(f"🤖 {t('Deploy a QEMU/KVM virtual machine (libvirt)!')}") print(f"🤖 {t('Deploy a QEMU/KVM virtual machine (libvirt)!')}")
@ -762,15 +842,21 @@ class TODO:
print(f"{t('QEMU deploy script not found: ')}{script_path}") print(f"{t('QEMU deploy script not found: ')}{script_path}")
return False return False
choices = [ choices = [
{"prompt_description": t("Deploy a new Ubuntu VM")}, {"prompt_description": t("Deploy a new VM")},
{ {
"prompt_description": t( "prompt_description": t(
"Preview a deployment (dry-run, no sudo)" "Preview a deployment (dry-run, no sudo)"
) )
}, },
{"prompt_description": t("Download an Ubuntu cloud image only")}, {"prompt_description": t("Download a cloud image only")},
{"prompt_description": t("List VMs (virsh list --all)")}, {"prompt_description": t("List VMs (virsh list --all)")},
{"prompt_description": t("Show a VM IP address")}, {"prompt_description": t("Show a VM IP address")},
{"prompt_description": t("List available images and specs")},
{
"prompt_description": t(
"Deploy ERPLibre infra (one minimal VM per image)"
)
},
] ]
config_entries = self.config_file.get_config("qemu_from_makefile") config_entries = self.config_file.get_config("qemu_from_makefile")
if config_entries: if config_entries:
@ -792,6 +878,10 @@ class TODO:
self._qemu_list_vms() self._qemu_list_vms()
elif status == "5": elif status == "5":
self._qemu_show_ip() self._qemu_show_ip()
elif status == "6":
self._qemu_list_images()
elif status == "7":
self._qemu_deploy_infra()
else: else:
cmd_no_found = True cmd_no_found = True
try: try:
@ -811,10 +901,13 @@ class TODO:
if not name: if not name:
print(t("VM name is required!")) print(t("VM name is required!"))
return return
version = self._qemu_prompt_version() distro = self._qemu_prompt_distro()
memory = input(t("RAM in MB (default: 8192): ")).strip() or "8192" version = self._qemu_prompt_version(distro)
vcpus = input(t("vCPUs (default: 4): ")).strip() or "4" # Laisser vide => le script applique le minimum requis par la version
disk_size = input(t("Disk size (default: 20G): ")).strip() or "20G" # choisie (libosinfo). On n'envoie alors pas le flag.
memory = input(t("RAM in MB (blank = version minimum): ")).strip()
vcpus = input(t("vCPUs (default: 2): ")).strip()
disk_size = input(t("Disk size (blank = version minimum): ")).strip()
default_key = self._qemu_default_ssh_key() default_key = self._qemu_default_ssh_key()
key_hint = default_key or t("none") key_hint = default_key or t("none")
@ -828,35 +921,26 @@ class TODO:
use_password = False use_password = False
if not ssh_key: if not ssh_key:
ans = ( ans = input(t("No SSH key found. Set a password instead? (Y/n): "))
input(t("No SSH key found. Set a password instead? (Y/n): ")) # Défaut oui : tout sauf une réponse négative explicite vaut oui.
.strip() use_password = not self._is_no(ans)
.lower()
)
use_password = ans != "n"
force = False force = False
if not dry_run: if not dry_run:
ans = ( ans = input(t("Overwrite existing VM disk if present? (y/N): "))
input(t("Overwrite existing VM disk if present? (y/N): ")) force = self._is_yes(ans)
.strip()
.lower()
)
force = ans == "y"
parts = [] parts = []
if not dry_run: if not dry_run:
parts.append("sudo") parts.append("sudo")
parts.append(script_path) parts.append(script_path)
parts += ["--name", name, "--version", version] parts += ["--name", name, "--distro", distro, "--version", version]
parts += [ if memory:
"--memory", parts += ["--memory", memory]
memory, if vcpus:
"--vcpus", parts += ["--vcpus", vcpus]
vcpus, if disk_size:
"--disk-size", parts += ["--disk-size", disk_size]
disk_size,
]
if ssh_key: if ssh_key:
parts += ["--ssh-key", ssh_key] parts += ["--ssh-key", ssh_key]
if use_password: if use_password:
@ -874,16 +958,19 @@ class TODO:
def _qemu_download_image(self): def _qemu_download_image(self):
script_path = self._qemu_script_path() script_path = self._qemu_script_path()
version = self._qemu_prompt_version() distro = self._qemu_prompt_distro()
ans = input(t("Verify SHA256 after download? (y/N): ")).strip().lower() version = self._qemu_prompt_version(distro)
ans = input(t("Verify SHA256 after download? (y/N): "))
parts = [ parts = [
"sudo", "sudo",
script_path, script_path,
"--download-only", "--download-only",
"--distro",
distro,
"--version", "--version",
version, version,
] ]
if ans == "y": if self._is_yes(ans):
parts.append("--verify") parts.append("--verify")
cmd = " ".join(shlex.quote(p) for p in parts) cmd = " ".join(shlex.quote(p) for p in parts)
print(f"{t('Will execute:')} {cmd}") print(f"{t('Will execute:')} {cmd}")
@ -895,7 +982,11 @@ class TODO:
self.execute.exec_command_live(cmd, source_erplibre=False) self.execute.exec_command_live(cmd, source_erplibre=False)
def _qemu_show_ip(self): def _qemu_show_ip(self):
name = input(t("VM name: ")).strip() # Affiche d'abord les VM (avec leur ID) pour que l'utilisateur sache
# quel nom/ID saisir, puis demande lequel.
self._qemu_list_vms()
print()
name = input(t("VM name or ID: ")).strip()
if not name: if not name:
print(t("VM name is required!")) print(t("VM name is required!"))
return return
@ -903,6 +994,306 @@ class TODO:
print(f"{t('Will execute:')} {cmd}") print(f"{t('Will execute:')} {cmd}")
self.execute.exec_command_live(cmd, source_erplibre=False) self.execute.exec_command_live(cmd, source_erplibre=False)
# ------------------------------------------------------------------ #
# QEMU : déploiement d'un parc « infra ERPLibre »
# ------------------------------------------------------------------ #
ERPLIBRE_GIT_URL = "https://github.com/erplibre/erplibre"
def _qemu_import_module(self):
"""Importe deploy_qemu.py comme module (source de vérité des specs)."""
import importlib.util
path = self._qemu_script_path()
spec = importlib.util.spec_from_file_location("deploy_qemu", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
@staticmethod
def _qemu_infra_name(distro, version):
"""Nom de VM stable pour le parc, ex. erplibre-ubuntu-2404."""
return f"erplibre-{distro}-{version.replace('.', '')}"
@staticmethod
def _parse_disk_gb(size):
"""« 20G » -> 20 (Go, best effort)."""
m = re.match(r"\s*(\d+)", str(size))
return int(m.group(1)) if m else 0
@staticmethod
def _is_yes(ans):
"""Réponse affirmative, FR et EN (o/oui/y/yes)."""
return ans.strip().lower() in ("y", "yes", "o", "oui")
@staticmethod
def _is_no(ans):
"""Réponse négative explicite, FR et EN (n/no/non). Utile pour les
invites « défaut oui » tout sauf « non » vaut oui."""
return ans.strip().lower() in ("n", "no", "non")
@staticmethod
def _host_free_ram_mb():
"""RAM disponible de l'hôte en Mo (MemAvailable), 0 si inconnu."""
try:
with open("/proc/meminfo") as fh:
for line in fh:
if line.startswith("MemAvailable:"):
return int(line.split()[1]) // 1024
except OSError:
pass
return 0
@staticmethod
def _parse_index_selection(raw, options):
"""« 1 3 » ou « 1,3 » -> sous-liste d'options (indices 1-based)."""
chosen = []
for tok in re.split(r"[\s,]+", raw.strip()):
if not tok:
continue
try:
idx = int(tok) - 1
except ValueError:
if tok in options and tok not in chosen:
chosen.append(tok)
continue
if 0 <= idx < len(options) and options[idx] not in chosen:
chosen.append(options[idx])
return chosen
def _qemu_domain_exists(self, name):
"""Vrai si une VM libvirt de ce nom est déjà définie."""
try:
res = subprocess.run(
["sudo", "virsh", "dominfo", name],
capture_output=True,
text=True,
timeout=15,
)
return res.returncode == 0
except (OSError, subprocess.SubprocessError):
return False
def _qemu_vm_ip(self, name, timeout=90):
"""Attend puis renvoie l'IPv4 d'une VM (bail DHCP), sinon None."""
deadline = time.time() + timeout
while time.time() < deadline:
try:
res = subprocess.run(
["sudo", "virsh", "domifaddr", name, "--source", "lease"],
capture_output=True,
text=True,
timeout=15,
)
except (OSError, subprocess.SubprocessError):
return None
m = re.search(r"(\d+\.\d+\.\d+\.\d+)", res.stdout)
if m:
return m.group(1)
time.sleep(3)
return None
def _qemu_pick_branch(self):
"""Liste les branches distantes d'ERPLibre et en fait choisir une."""
print(f"\n{t('Fetching ERPLibre branch list...')}")
branches = []
try:
res = subprocess.run(
["git", "ls-remote", "--heads", self.ERPLIBRE_GIT_URL],
capture_output=True,
text=True,
timeout=30,
)
for line in res.stdout.splitlines():
ref = line.split("\t")[-1]
if ref.startswith("refs/heads/"):
branches.append(ref[len("refs/heads/") :])
except (OSError, subprocess.SubprocessError):
pass
default = (
"master"
if "master" in branches
else (branches[0] if branches else "master")
)
if not branches:
return (
input(f"{t('Branch (default:')} {default}): ").strip()
or default
)
branches.sort()
print(f"{t('Branches:')}")
for i, b in enumerate(branches, 1):
star = " *" if b == default else ""
print(f" [{i}] {b}{star}")
sel = input(f"{t('Choice (number or name, default:')} {default}): ")
sel = sel.strip()
if not sel:
return default
try:
idx = int(sel) - 1
if 0 <= idx < len(branches):
return branches[idx]
except ValueError:
if sel in branches:
return sel
return default
def _qemu_install_erplibre_vm(self, name, ssh_key, branch):
"""Clone ERPLibre (branche donnée) dans ~/git/erplibre de la VM."""
ip = self._qemu_vm_ip(name)
if not ip:
print(f" {name}: {t('no IP obtained, ERPLibre clone skipped.')}")
return
remote = (
"set -e; mkdir -p ~/git; "
"command -v git >/dev/null 2>&1 || "
"{ sudo apt-get install -y git || sudo dnf install -y git; }; "
"if [ -d ~/git/erplibre/.git ]; then "
"echo 'ERPLibre already present'; else "
f"git clone --branch {shlex.quote(branch)} "
f"{self.ERPLIBRE_GIT_URL} ~/git/erplibre; fi"
)
ssh_opts = (
"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
"-o ConnectTimeout=15"
)
cmd = f"ssh {ssh_opts} erplibre@{ip} {shlex.quote(remote)}"
print(f"\n 📦 {name} ({ip}): {t('cloning ERPLibre')} ({branch})")
print(f" {t('Will execute:')} {cmd}")
self.execute.exec_command_live(cmd, source_erplibre=False)
def _qemu_deploy_infra(self):
print(f"🏗 {t('Deploy ERPLibre infra: one minimal VM per image')}")
try:
mod = self._qemu_import_module()
except Exception as exc:
print(f"{t('Cannot load QEMU catalog: ')}{exc}")
return
distros = list(mod.DISTROS)
# 1) Distributions (multi-sélection) ou catalogue complet.
print(f"\n{t('Distributions:')}")
for i, d in enumerate(distros, 1):
vers = ", ".join(mod.DISTROS[d][0])
print(f" [{i}] {d} ({vers})")
print(f" [all] {t('Whole catalog (every version)')}")
raw = input(t("Selection (numbers, or 'all', default: all): ")).strip()
catalog_all = raw.lower() in ("", "all", "*")
sel_distros = (
distros
if catalog_all
else self._parse_index_selection(raw.lower(), distros)
)
if not sel_distros:
print(t("Nothing selected."))
return
# 2) Versions par distro (multi-sélection) ; « all » si catalogue.
selected = [] # (distro, version, ram_mb, disk_str)
for d in sel_distros:
versions_map = mod.DISTROS[d][0]
vlist = list(versions_map)
if catalog_all:
chosen = vlist
else:
print(f"\n{t('Versions for')} {d} :")
for i, v in enumerate(vlist, 1):
_c, _o, ram, disk = versions_map[v]
print(f" [{i}] {v} (RAM≥{ram}Mo, {disk})")
print(f" [all] {t('select all')}")
r = input(
t("Selection (numbers, or 'all', default: all): ")
).strip()
chosen = (
vlist
if r.lower() in ("", "all", "*")
else self._parse_index_selection(r.lower(), vlist)
)
for v in chosen:
_c, _o, ram, disk = versions_map[v]
selected.append((d, v, ram, disk))
if not selected:
print(t("Nothing selected."))
return
# 3) Plan + estimation des ressources.
total_ram = sum(s[2] for s in selected)
total_disk = sum(self._parse_disk_gb(s[3]) for s in selected)
free_ram = self._host_free_ram_mb()
print(f"\n{t('Deployment plan')} ({len(selected)} VM) :")
for d, v, ram, disk in selected:
name = self._qemu_infra_name(d, v)
print(f" - {name:<26} {d} {v:<7} RAM {ram}Mo {t('disk')} {disk}")
print(f"\n {t('Total RAM (all running):')} {total_ram} Mo")
print(f" {t('Total virtual disk (thin qcow2):')} ~{total_disk} G")
if free_ram:
print(f" {t('Host RAM available:')} {free_ram} Mo")
if total_ram > free_ram:
warn = t(
"Total RAM exceeds host free RAM: not all VMs will run"
" at once."
)
print(f"{warn}")
# Clé SSH (partagée par tout le parc).
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:
ssh_key = os.path.expanduser(ssh_key)
# 4) Option : installer ERPLibre dans ~/git/erplibre de chaque VM.
install_branch = None
ans = input(
t("Install ERPLibre into ~/git/erplibre on each VM? (y/N): ")
)
if self._is_yes(ans):
install_branch = self._qemu_pick_branch()
# 5) Confirmation puis déploiement séquentiel.
ans = input(f"\n{t('Deploy these VMs now? (y/N): ')}")
if not self._is_yes(ans):
print(t("Cancelled."))
return
script_path = self._qemu_script_path()
deployed = []
for d, v, _ram, _disk in selected:
name = self._qemu_infra_name(d, v)
if self._qemu_domain_exists(name):
print(f"\n{name}: {t('already exists, skipped.')}")
deployed.append(name)
continue
parts = [
"sudo",
script_path,
"--distro",
d,
"--version",
v,
"--name",
name,
]
if ssh_key:
parts += ["--ssh-key", ssh_key]
if install_branch:
parts += ["--package", "git"]
parts.append("-y")
cmd = " ".join(shlex.quote(p) for p in parts)
print(f"\n{name}: {cmd}")
self.execute.exec_command_live(cmd, source_erplibre=False)
deployed.append(name)
# 6) Installation ERPLibre (clone) si demandée.
if install_branch:
print(f"\n{t('Cloning ERPLibre on each VM')} ({install_branch})…")
for name in deployed:
self._qemu_install_erplibre_vm(name, ssh_key, install_branch)
print(f"\n{t('ERPLibre infra deployment done.')}")
print(f" {t('Manage with:')} sudo virsh list --all")
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 = (
@ -1544,10 +1935,8 @@ class TODO:
if os.path.exists(dest_file): if os.path.exists(dest_file):
print(f"{t('File already exists: ')}{dest_file}") print(f"{t('File already exists: ')}{dest_file}")
overwrite = input( overwrite = input(t("Do you want to overwrite the file? (y/Y): "))
t("Do you want to overwrite the file? (y/Y): ") if not self._is_yes(overwrite):
).strip()
if overwrite not in ("y", "Y"):
print(t("Nothing to do.")) print(t("Nothing to do."))
return return
@ -2032,11 +2421,8 @@ class TODO:
print(f"\n{t('Tests failed with return code')} {status_code}") print(f"\n{t('Tests failed with return code')} {status_code}")
# Step 4: Cleanup # Step 4: Cleanup
lang = get_lang() keep_input = input(t("Keep the temporary database? (y/N): "))
keep_input = ( keep = self._is_yes(keep_input)
input(t("Keep the temporary database? (y/N): ")).strip().lower()
)
keep = keep_input in (("o", "oui") if lang == "fr" else ("y", "yes"))
if keep: if keep:
print(f"{t('Database kept')}: {db_name}") print(f"{t('Database kept')}: {db_name}")
else: else:
@ -2361,7 +2747,7 @@ class TODO:
git_repo_update_input = input( git_repo_update_input = input(
"💬 Would you like to fetch all your git repositories, you need it (y/Y) : " "💬 Would you like to fetch all your git repositories, you need it (y/Y) : "
) )
if git_repo_update_input.strip().lower() == "y": if self._is_yes(git_repo_update_input):
status = self.execute.exec_command_live( status = self.execute.exec_command_live(
f"./script/manifest/update_manifest_local_dev.sh", f"./script/manifest/update_manifest_local_dev.sh",
source_erplibre=False, source_erplibre=False,
@ -2479,7 +2865,7 @@ class TODO:
do_personalize = input( do_personalize = input(
"Do you want to personalize the mobile application (Y) : " "Do you want to personalize the mobile application (Y) : "
) )
if do_personalize.strip().lower() == "y": if self._is_yes(do_personalize):
project_name = ( project_name = (
input( input(
f'Your project name (Separate by space in title), default "{default_project_name}" : ' f'Your project name (Separate by space in title), default "{default_project_name}" : '
@ -2504,19 +2890,14 @@ class TODO:
).strip() ).strip()
or default_project_note_subject or default_project_note_subject
) )
do_debug = ( do_debug = self._is_yes(
input("Compilation with debug information, default No (Y) : ") input("Compilation with debug information, default No (Y) : ")
.strip()
.lower()
== "y"
) )
do_change_picture_menu = ( do_change_picture_menu = self._is_yes(
input( input(
"Want to change picture from menu, you need android-studio (Y) : " "Want to change picture from menu, you need"
" android-studio (Y) : "
) )
.strip()
.lower()
== "y"
) )
# Rename with script bash # Rename with script bash

View file

@ -997,17 +997,17 @@ TRANSLATIONS = {
"fr": "Script de déploiement QEMU introuvable : ", "fr": "Script de déploiement QEMU introuvable : ",
"en": "QEMU deploy script not found: ", "en": "QEMU deploy script not found: ",
}, },
"Deploy a new Ubuntu VM": { "Deploy a new VM": {
"fr": "Déployer une nouvelle VM Ubuntu", "fr": "Déployer une nouvelle VM",
"en": "Deploy a new Ubuntu VM", "en": "Deploy a new VM",
}, },
"Preview a deployment (dry-run, no sudo)": { "Preview a deployment (dry-run, no sudo)": {
"fr": "Prévisualiser un déploiement (dry-run, sans sudo)", "fr": "Prévisualiser un déploiement (dry-run, sans sudo)",
"en": "Preview a deployment (dry-run, no sudo)", "en": "Preview a deployment (dry-run, no sudo)",
}, },
"Download an Ubuntu cloud image only": { "Download a cloud image only": {
"fr": "Télécharger seulement une image cloud Ubuntu", "fr": "Télécharger seulement une image cloud",
"en": "Download an Ubuntu cloud image only", "en": "Download a cloud image only",
}, },
"List VMs (virsh list --all)": { "List VMs (virsh list --all)": {
"fr": "Lister les VM (virsh list --all)", "fr": "Lister les VM (virsh list --all)",
@ -1017,17 +1017,142 @@ TRANSLATIONS = {
"fr": "Afficher l'adresse IP d'une VM", "fr": "Afficher l'adresse IP d'une VM",
"en": "Show a VM IP address", "en": "Show a VM IP address",
}, },
"Ubuntu version:": { "List available images and specs": {
"fr": "Version Ubuntu :", "fr": "Lister les images disponibles et leurs specs",
"en": "Ubuntu version:", "en": "List available images and specs",
}, },
"Choice (number or version, default: 24.04): ": { "Distribution:": {
"fr": "Choix (numéro ou version, défaut : 24.04) : ", "fr": "Distribution :",
"en": "Choice (number or version, default: 24.04): ", "en": "Distribution:",
}, },
"Invalid selection, using 24.04": { "Choice (number or name, default: ubuntu): ": {
"fr": "Sélection invalide, utilisation de 24.04", "fr": "Choix (numéro ou nom, défaut : ubuntu) : ",
"en": "Invalid selection, using 24.04", "en": "Choice (number or name, default: ubuntu): ",
},
"Invalid selection, using ubuntu": {
"fr": "Sélection invalide, utilisation d'ubuntu",
"en": "Invalid selection, using ubuntu",
},
"Version for": {
"fr": "Version pour",
"en": "Version for",
},
"Choice (number or version, blank = default):": {
"fr": "Choix (numéro ou version, vide = défaut) :",
"en": "Choice (number or version, blank = default):",
},
"Invalid selection, using": {
"fr": "Sélection invalide, utilisation de",
"en": "Invalid selection, using",
},
"select all": {
"fr": "tout sélectionner",
"en": "select all",
},
"Deploy ERPLibre infra (one minimal VM per image)": {
"fr": "Déployer l'infra ERPLibre (une VM minimale par image)",
"en": "Deploy ERPLibre infra (one minimal VM per image)",
},
"Deploy ERPLibre infra: one minimal VM per image": {
"fr": "Déployer l'infra ERPLibre : une VM minimale par image",
"en": "Deploy ERPLibre infra: one minimal VM per image",
},
"Cannot load QEMU catalog: ": {
"fr": "Impossible de charger le catalogue QEMU : ",
"en": "Cannot load QEMU catalog: ",
},
"Distributions:": {
"fr": "Distributions :",
"en": "Distributions:",
},
"Whole catalog (every version)": {
"fr": "Tout le catalogue (chaque version)",
"en": "Whole catalog (every version)",
},
"Selection (numbers, or 'all', default: all): ": {
"fr": "Sélection (numéros, ou « all », défaut : all) : ",
"en": "Selection (numbers, or 'all', default: all): ",
},
"Nothing selected.": {
"fr": "Rien de sélectionné.",
"en": "Nothing selected.",
},
"Deployment plan": {
"fr": "Plan de déploiement",
"en": "Deployment plan",
},
"disk": {
"fr": "disque",
"en": "disk",
},
"Total RAM (all running):": {
"fr": "RAM totale (toutes actives) :",
"en": "Total RAM (all running):",
},
"Total virtual disk (thin qcow2):": {
"fr": "Disque virtuel total (qcow2 thin) :",
"en": "Total virtual disk (thin qcow2):",
},
"Host RAM available:": {
"fr": "RAM disponible de l'hôte :",
"en": "Host RAM available:",
},
"Total RAM exceeds host free RAM: not all VMs will run at once.": {
"fr": "La RAM totale dépasse la RAM libre de l'hôte : les VM ne "
"tourneront pas toutes en même temps.",
"en": "Total RAM exceeds host free RAM: not all VMs will run at once.",
},
"Install ERPLibre into ~/git/erplibre on each VM? (y/N): ": {
"fr": "Installer ERPLibre dans ~/git/erplibre sur chaque VM ? (o/N) : ",
"en": "Install ERPLibre into ~/git/erplibre on each VM? (y/N): ",
},
"Deploy these VMs now? (y/N): ": {
"fr": "Déployer ces VM maintenant ? (o/N) : ",
"en": "Deploy these VMs now? (y/N): ",
},
"Cancelled.": {
"fr": "Annulé.",
"en": "Cancelled.",
},
"already exists, skipped.": {
"fr": "existe déjà, ignorée.",
"en": "already exists, skipped.",
},
"Cloning ERPLibre on each VM": {
"fr": "Clonage d'ERPLibre sur chaque VM",
"en": "Cloning ERPLibre on each VM",
},
"ERPLibre infra deployment done.": {
"fr": "Déploiement de l'infra ERPLibre terminé.",
"en": "ERPLibre infra deployment done.",
},
"Manage with:": {
"fr": "Gérer avec :",
"en": "Manage with:",
},
"Fetching ERPLibre branch list...": {
"fr": "Récupération de la liste des branches ERPLibre...",
"en": "Fetching ERPLibre branch list...",
},
"Branch (default:": {
"fr": "Branche (défaut :",
"en": "Branch (default:",
},
"Branches:": {
"fr": "Branches :",
"en": "Branches:",
},
"Choice (number or name, default:": {
"fr": "Choix (numéro ou nom, défaut :",
"en": "Choice (number or name, default:",
},
"no IP obtained, ERPLibre clone skipped.": {
"fr": "aucune IP obtenue, clonage ERPLibre ignoré.",
"en": "no IP obtained, ERPLibre clone skipped.",
},
"cloning ERPLibre": {
"fr": "clonage d'ERPLibre",
"en": "cloning ERPLibre",
}, },
"VM name (required): ": { "VM name (required): ": {
"fr": "Nom de la VM (requis) : ", "fr": "Nom de la VM (requis) : ",
@ -1041,17 +1166,21 @@ TRANSLATIONS = {
"fr": "Nom de la VM : ", "fr": "Nom de la VM : ",
"en": "VM name: ", "en": "VM name: ",
}, },
"RAM in MB (default: 8192): ": { "VM name or ID: ": {
"fr": "RAM en Mo (défaut : 8192) : ", "fr": "Nom ou ID de la VM : ",
"en": "RAM in MB (default: 8192): ", "en": "VM name or ID: ",
}, },
"vCPUs (default: 4): ": { "RAM in MB (blank = version minimum): ": {
"fr": "vCPU (défaut : 4) : ", "fr": "RAM en Mo (vide = minimum de la version) : ",
"en": "vCPUs (default: 4): ", "en": "RAM in MB (blank = version minimum): ",
}, },
"Disk size (default: 20G): ": { "vCPUs (default: 2): ": {
"fr": "Taille du disque (défaut : 20G) : ", "fr": "vCPU (défaut : 2) : ",
"en": "Disk size (default: 20G): ", "en": "vCPUs (default: 2): ",
},
"Disk size (blank = version minimum): ": {
"fr": "Taille du disque (vide = minimum de la version) : ",
"en": "Disk size (blank = version minimum): ",
}, },
"SSH public key path": { "SSH public key path": {
"fr": "Chemin de la clé publique SSH", "fr": "Chemin de la clé publique SSH",