[IMP] script todo: fusionner « Déployer VM » et « infra » + renommage granulaire

Le menu QEMU avait deux commandes de déploiement redondantes : [1] « Déployer
une VM » (mono, noms/ressources personnalisés) et [4] « Déployer l'infra »
(multi, matrice distro×version×archi). Elles sont FUSIONNÉES en une seule,
[1] « Déployer une ou plusieurs VM » (_qemu_deploy), qui gère aussi bien 1 VM
que N, avec :

- renommage GRANULAIRE des VM à la demande (_qemu_customize_names) : noms
  auto par défaut, on en renomme certains par numéros séparés de virgules ;
- multiplicateur de ressources (x1..x4) déjà présent ;
- aperçu dry-run ([2]) qui passe par le même flux et imprime les commandes
  deploy_qemu (--dry-run) via le helper partagé _qemu_build_deploy_parts.

Menu simplifié : [1] Déployer une/plusieurs VM, [2] Aperçu (dry-run),
[3] Télécharger une image, puis Gérer/Catalogue renumérotés. Code mort retiré
(_qemu_deploy_vm, _qemu_prompt_arch, _normalize_disk_size,
_qemu_offer_ssh_config).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathieu Benoit 2026-07-29 08:25:38 +00:00
parent 52b79551dd
commit 372aedb0eb
2 changed files with 119 additions and 188 deletions

View file

@ -949,18 +949,6 @@ class TODO:
print(f"{warn}")
return chosen
def _qemu_prompt_arch(self, distro):
"""Architecture pour un déploiement de VM unique. Propose amd64, plus
arm64/s390x si la distro choisie publie ces images."""
opts = ["amd64"]
if distro in self._QEMU_ARM64_DISTROS:
opts.append("arm64")
if distro in self._QEMU_S390X_DISTROS:
opts.append("s390x")
if len(opts) == 1:
return "amd64" # rien à demander
return self._qemu_ask_arch(opts, self._native_arch())
def _qemu_prompt_infra_arch(self):
"""Architecture du parc (défaut : native de l'hôte, marquée d'un *).
Toute arch non native est émulée ; le catalogue est ensuite restreint
@ -985,18 +973,13 @@ class TODO:
return False
choices = [
{"section": t("Deployment")},
{"prompt_description": t("Deploy a new VM")},
{"prompt_description": t("Deploy VM(s) (one or many)")},
{
"prompt_description": t(
"Preview a deployment (dry-run, no sudo)"
)
},
{"prompt_description": t("Download a cloud image only")},
{
"prompt_description": t(
"Deploy ERPLibre infra (one minimal VM per image)"
)
},
{"section": t("Manage")},
{"prompt_description": t("List VMs (virsh list --all)")},
{"prompt_description": t("Show a VM IP address")},
@ -1017,24 +1000,22 @@ class TODO:
if status == "0":
return False
elif status == "1":
self._qemu_deploy_vm(dry_run=False)
self._qemu_deploy(dry_run=False)
elif status == "2":
self._qemu_deploy_vm(dry_run=True)
self._qemu_deploy(dry_run=True)
elif status == "3":
self._qemu_download_image()
elif status == "4":
self._qemu_deploy_infra()
elif status == "5":
self._qemu_list_vms()
elif status == "6":
elif status == "5":
self._qemu_show_ip()
elif status == "7":
elif status == "6":
self._qemu_console()
elif status == "8":
elif status == "7":
self._qemu_delete_vm()
elif status == "9":
elif status == "8":
self._qemu_cleanup()
elif status == "10":
elif status == "9":
self._qemu_list_images()
else:
cmd_no_found = True
@ -1051,115 +1032,6 @@ class TODO:
if cmd_no_found:
print(t("Command not found !"))
@staticmethod
def _normalize_disk_size(size):
"""« 30 » -> « 30G » (unité par défaut Go). Laisse « 30G », « 1T »… ou
vide tels quels. Sans unité, qemu-img interpréterait des OCTETS."""
size = size.strip()
if re.fullmatch(r"\d+", size):
return size + "G"
return size
def _qemu_deploy_vm(self, dry_run=False):
script_path = self._qemu_script_path()
# Distro + version d'abord : permet de proposer un nom par défaut.
distro = self._qemu_prompt_distro()
version = self._qemu_prompt_version(distro)
arch = self._qemu_prompt_arch(distro)
default_name = self._qemu_infra_name(distro, version, arch)
name = (
input(f"{t('VM name (default: ')}{default_name}): ").strip()
or default_name
)
# Laisser vide => le script applique le minimum requis par la version
# 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 = self._normalize_disk_size(
input(t("Disk size (blank = version min; e.g. 30G, 1T): ")).strip()
)
default_key = self._qemu_default_ssh_key()
key_hint = default_key or t("none")
ssh_key = input(f"{t('SSH public key path')} ({key_hint}): ").strip()
if not ssh_key:
ssh_key = default_key
if ssh_key:
# Résout ~ avec le HOME de l'utilisateur courant : le script tourne
# sous sudo (HOME=/root) et ne pourrait pas déduire ~ correctement.
ssh_key = os.path.expanduser(ssh_key)
# Mot de passe console (défaut « erplibre ») : permet la connexion par
# la console série (virsh console) EN PLUS de la clé SSH. Sans lui, le
# compte est verrouillé et la console refuse tout login.
password = (
input(
t("Console password for 'erplibre' (default: erplibre): ")
).strip()
or "erplibre"
)
force = False
if not dry_run:
ans = input(t("Overwrite existing VM disk if present? (y/N): "))
force = self._is_yes(ans)
parts = []
if not dry_run:
parts.append("sudo")
parts.append(script_path)
parts += ["--name", name, "--distro", distro, "--version", version]
if arch and arch != "amd64":
parts += ["--arch", arch]
if memory:
parts += ["--memory", memory]
if vcpus:
parts += ["--vcpus", vcpus]
if disk_size:
parts += ["--disk-size", disk_size]
if ssh_key:
parts += ["--ssh-key", ssh_key]
if password:
parts += ["--password", password]
if force:
parts.append("--force")
if dry_run:
parts.append("--dry-run")
else:
parts.append("-y") # accepte l'installation des dépendances
cmd = " ".join(shlex.quote(p) for p in parts)
print(f"{t('Will execute:')} {cmd}")
if password:
print(f"🔑 {t('Console/SSH login:')} erplibre / {password}")
status = self.execute.exec_command_live(cmd, source_erplibre=False)
# Arrêt net sur erreur : sinon on enchaînait sur l'attente d'IP d'une
# VM jamais créée (blocage infini), l'utilisateur ne voyant rien.
if status != 0:
print(f"\n{t('Deployment failed (see the error above).')}")
return
if dry_run:
return
# Option : installer ERPLibre (clone + make install_os + install_odoo).
if self._is_yes(
input(
t("Install ERPLibre into ~/git/erplibre on this VM? (y/N): ")
)
):
branch = self._qemu_pick_branch()
if self._is_yes(
input(t("Interactive monitoring dashboard? (y/N): "))
):
self._qemu_install_erplibre_monitored([name], branch)
else:
self._qemu_install_erplibre_vm(name, ssh_key, branch)
# Proposer d'enregistrer la VM dans ~/.ssh/config (après le 1er boot).
self._qemu_offer_ssh_config(name, "erplibre")
def _qemu_download_image(self):
script_path = self._qemu_script_path()
distro = self._qemu_prompt_distro()
@ -1223,17 +1095,6 @@ class TODO:
print(f"{t('Will execute:')} {cmd}")
self.execute.exec_command_live(cmd, source_erplibre=False)
def _qemu_offer_ssh_config(self, name, user):
"""Propose d'enregistrer la VM dans ~/.ssh/config (bloc Host name)."""
if not self._is_yes(input(t("Add this VM to ~/.ssh/config? (y/N): "))):
return
print(t("Waiting for the VM IP (DHCP lease)..."))
ip = self._qemu_vm_ip(name)
if not ip:
print(t("No IP yet; add it later once the VM has booted."))
return
self._write_ssh_config_entry(name, user, ip)
def _write_ssh_config_entry(self, host, user, ip):
"""Écrit/remplace un bloc « Host <host> » dans ~/.ssh/config."""
cfg = os.path.expanduser("~/.ssh/config")
@ -2125,8 +1986,63 @@ class TODO:
pass
return 1, base_vcpus
def _qemu_deploy_infra(self):
print(f"🏗 {t('Deploy ERPLibre infra: one minimal VM per image')}")
def _qemu_customize_names(self, selected):
"""Noms des VM (liste parallèle à `selected`). Défaut = nom auto
(_qemu_infra_name) ; on peut en renommer certains À LA DEMANDE, par
leurs numéros séparés de virgules (vide = garder tous les noms auto)."""
names = [
self._qemu_infra_name(d, v, a) for d, v, _r, _dk, a in selected
]
print(f"\n{t('VM names (default = auto):')}")
for i, (nm, s) in enumerate(zip(names, selected), 1):
d, v, _r, _dk, a = s
print(f" [{i}] {nm} ({d} {v} [{a}])")
raw = input(
t("Rename which VMs? (numbers, comma-separated; blank = none): ")
).strip()
for tok in re.split(r"[\s,]+", raw):
if not tok:
continue
try:
i = int(tok) - 1
except ValueError:
continue
if 0 <= i < len(names):
new = input(f" {names[i]} {t('->')} ").strip()
if new:
names[i] = new
if len(set(names)) != len(names):
print(f"{t('Duplicate names detected; keeping as entered.')}")
return names
def _qemu_build_deploy_parts(
self, d, v, arch, name, eram, evcpus, disk, ssh_key, branch, dry_run
):
"""Construit la commande deploy_qemu.py d'UNE VM (utilisée pour l'aperçu
dry-run ET le déploiement réel)."""
parts = [] if dry_run else ["sudo"]
parts += [
self._qemu_script_path(),
"--distro", d, "--version", v, "--name", name,
"--memory", str(eram), "--vcpus", str(evcpus),
"--password", "erplibre",
]
if not dry_run:
# --no-wait-ip : ne bloque pas 90s/VM, l'IP est collectée après.
parts.append("--no-wait-ip")
if arch and arch != "amd64":
parts += ["--arch", arch]
if ssh_key:
parts += ["--ssh-key", ssh_key]
if branch:
# ERPLibre dépasse le minimum : +5 Go de disque.
bigger = self._parse_disk_gb(disk) + self.ERPLIBRE_EXTRA_DISK_GB
parts += ["--disk-size", f"{bigger}G"]
parts.append("--dry-run" if dry_run else "-y")
return parts
def _qemu_deploy(self, dry_run=False):
print(f"🚀 {t('Deploy ERPLibre VM(s)!')}")
try:
mod = self._qemu_import_module()
except Exception as exc:
@ -2277,15 +2193,17 @@ class TODO:
"""(RAM effective, vCPU) pour une VM selon le multiplicateur."""
return ram * res_mult, min(base_vcpus * res_mult, host_cpu)
# 2c) Noms des VM (auto par défaut, renommage granulaire à la demande).
names = self._qemu_customize_names(selected)
# 3) Plan + estimation des ressources (avec le multiplicateur appliqué).
total_ram = sum(s[2] * res_mult for s in selected)
total_disk = sum(self._parse_disk_gb(s[3]) for s in selected)
print(f"\n{t('Deployment plan')} ({len(selected)} VM, x{res_mult}) :")
for d, v, ram, disk, a in selected:
name = self._qemu_infra_name(d, v, a)
for i, (d, v, ram, disk, a) in enumerate(selected):
eram, evcpus = eff_res(ram)
print(
f" - {name:<30} {d} {v:<7} [{a:<5}] "
f" - {names[i]:<30} {d} {v:<7} [{a:<5}] "
f"{evcpus} vCPU RAM {eram}Mo {t('disk')} {disk}"
)
print(f"\n {t('Total RAM (all running):')} {total_ram} Mo")
@ -2299,6 +2217,20 @@ class TODO:
)
print(f"{warn}")
# Aperçu (dry-run) : montre les commandes deploy_qemu sans rien créer
# (ni sudo, ni installation), puis on s'arrête.
if dry_run:
default_key = self._qemu_default_ssh_key()
print(f"\n{t('Preview (dry-run):')}")
for i, (d, v, ram, disk, a) in enumerate(selected):
eram, evcpus = eff_res(ram)
parts = self._qemu_build_deploy_parts(
d, v, a, names[i], eram, evcpus, disk,
default_key, None, dry_run=True,
)
print(" " + " ".join(shlex.quote(p) for p in parts))
return
# Clé SSH (partagée par tout le parc).
default_key = self._qemu_default_ssh_key()
key_hint = default_key or t("none")
@ -2341,49 +2273,19 @@ class TODO:
print(t("Cancelled."))
return
script_path = self._qemu_script_path()
deployed = []
jobs = [] # (name, parts) des VM à créer
for d, v, ram, disk, a in selected:
name = self._qemu_infra_name(d, v, a)
for i, (d, v, ram, disk, a) in enumerate(selected):
name = names[i]
if self._qemu_domain_exists(name):
print(f"{name}: {t('already exists, skipped.')}")
deployed.append(name)
continue
eram, evcpus = eff_res(ram)
parts = [
"sudo",
script_path,
"--distro",
d,
"--version",
v,
"--name",
name,
# Ressources selon le multiplicateur choisi (x1 = minimum).
"--memory",
str(eram),
"--vcpus",
str(evcpus),
# Mot de passe console « erplibre » (+ clé SSH). --no-wait-ip :
# ne bloque pas 90s par VM, l'IP est collectée après coup.
"--password",
"erplibre",
"--no-wait-ip",
]
if a and a != "amd64":
parts += ["--arch", a]
if ssh_key:
parts += ["--ssh-key", ssh_key]
if install_branch:
# ERPLibre dépasse le minimum : +5 Go de disque (sinon il ne
# reste que ~97 Mo après l'installation). git/make sont posés
# par le bootstrap (pas via cloud-init, trop lent au boot).
bigger = (
self._parse_disk_gb(disk) + self.ERPLIBRE_EXTRA_DISK_GB
)
parts += ["--disk-size", f"{bigger}G"]
parts.append("-y")
parts = self._qemu_build_deploy_parts(
d, v, a, name, eram, evcpus, disk,
ssh_key, install_branch, dry_run=False,
)
jobs.append((name, parts))
if jobs:

View file

@ -1245,6 +1245,35 @@ TRANSLATIONS = {
"fr": "Choix (1-4, défaut 1) :",
"en": "Choice (1-4, default 1):",
},
"Deploy VM(s) (one or many)": {
"fr": "Déployer une ou plusieurs VM",
"en": "Deploy VM(s) (one or many)",
},
"Deploy ERPLibre VM(s)!": {
"fr": "Déployer une ou plusieurs VM ERPLibre !",
"en": "Deploy ERPLibre VM(s)!",
},
"VM names (default = auto):": {
"fr": "Noms des VM (défaut = auto) :",
"en": "VM names (default = auto):",
},
"Rename which VMs? (numbers, comma-separated; blank = none): ": {
"fr": "Renommer quelles VM ? (numéros séparés par des virgules ; "
"vide = aucune) : ",
"en": "Rename which VMs? (numbers, comma-separated; blank = none): ",
},
"->": {
"fr": "->",
"en": "->",
},
"Duplicate names detected; keeping as entered.": {
"fr": "Noms en double détectés ; conservés tels quels.",
"en": "Duplicate names detected; keeping as entered.",
},
"Preview (dry-run):": {
"fr": "Aperçu (dry-run) :",
"en": "Preview (dry-run):",
},
"(includes emulated architectures — some VMs are slow)": {
"fr": "(inclut des architectures émulées — certaines VM sont lentes)",
"en": "(includes emulated architectures — some VMs are slow)",