Compare commits
7 commits
935919e07b
...
ca796a67a4
| Author | SHA1 | Date | |
|---|---|---|---|
| ca796a67a4 | |||
| 84f63c326a | |||
| 3e2224b782 | |||
| 3b3efe5e43 | |||
| 9c8e7454f3 | |||
| 0d985ab449 | |||
| a202143c30 |
6 changed files with 967 additions and 172 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
# 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)
|
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,6 +599,7 @@ 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}")
|
||||||
|
if is_tty:
|
||||||
print()
|
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:
|
||||||
|
print(f" [dry-run] réseau libvirt '{name}' activé si nécessaire")
|
||||||
runner.run(["virsh", "net-start", 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)
|
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 :")
|
||||||
|
|
|
||||||
|
|
@ -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é où 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 » où 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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue