[IMP] qemu deploy: custom resource profile, per-VM vCPU, safer confirmations

Resources per VM offered x1..x4 only — anything else meant deploying with the
wrong sizing and resizing afterwards. « [5] Custom » now asks vCPU, RAM and
disk once for the whole fleet, and vCPU joins disk and RAM in the per-VM
customisation. Presets 1, 2, 4, 6, 8, 16, 24, 32.

The two places ask the SAME three questions, so they are one definition each
(_qemu_ask_disk / _qemu_ask_ram / _qemu_ask_cpu) instead of two copies that
would drift. vCPU is no longer a single global value: it travels per VM
through the whole chain, down to --vcpus.

Overcommit is allowed on an explicit choice — KVM permits more vCPU than
cores — and only warned about. The x1..x4 path still caps at the host core
count: that one is an automatic computation, not a decision.

Three prompts default to yes (install ERPLibre, ~/.ssh/config, deploy): they
are what one wants nearly every time, and typing « o » on each was noise.
Flipping a destructive-by-omission default demanded the guards below.

Before deploying, a final review lists everything that will change — VMs to
create with their effective disk (ERPLibre's +5 G included), VMs left
untouched, install profile and branch, SSH key, ~/.ssh/config, parallelism.
Answering no asks for confirmation rather than dropping every answer given
over a dozen prompts.

Name collisions are now reported BEFORE the wait, with their two very
different consequences: an already-defined VM is skipped and nothing is
overwritten, while a qcow2 left behind by a deleted VM makes deploy_qemu fail,
since it refuses to overwrite without --force. Continuing requires an explicit
yes; the default is no.

Also translated two strings of this flow that had no entry and stayed in
English (« Resolving VM IPs… », « no IP »).

Verified by driving the prompts with scripted answers: custom profile applied
and left intact when blank, per-VM override, 32 vCPU on 28 cores warned but
accepted, review with and without ERPLibre install, no → no → deploy, no →
yes → cancel, and collisions defaulting to no. Rendering checked in fr and en.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathieu Benoit 2026-08-01 05:37:02 -04:00
parent c1afa2f4d2
commit b664128a05
2 changed files with 414 additions and 84 deletions

View file

@ -3579,6 +3579,10 @@ class TODO:
131072,
262144,
)
# KVM autorise plus de vCPU que de cœurs (surengagement) : on prévient
# plutôt que d'écrêter, contrairement au multiplicateur x1..x4 qui, lui,
# est un calcul automatique et se borne aux cœurs de l'hôte.
_QEMU_CPU_PRESETS = (1, 2, 4, 6, 8, 16, 24, 32)
@staticmethod
def _plural(word, count):
@ -3634,15 +3638,72 @@ class TODO:
return ""
return answer
# Les trois invites ci-dessous sont posées à DEUX endroits — le profil
# global « Personnalisé » et la personnalisation par VM. Elles vivent donc
# ici : une seule définition, mêmes suggestions, mêmes validations.
# Chacune renvoie None pour « garder la valeur actuelle ».
def _qemu_ask_disk(self, label, current):
raw = self._qemu_ask_value(label, current, self._QEMU_DISK_PRESETS)
if not raw:
return None
parsed = self._qemu_parse_disk(raw)
if not parsed:
print(f"{t('Invalid size.')}")
return parsed
def _qemu_ask_ram(self, label, current):
raw = self._qemu_ask_value(
label, current, self._QEMU_RAM_PRESETS, fmt=self._qemu_ram_label
)
if not raw:
return None
try:
mb = int(raw)
except ValueError:
mb = 0
if mb <= 0:
print(f"{t('Invalid size.')}")
return None
return mb
def _qemu_ask_cpu(self, label, current, host_cpu):
raw = self._qemu_ask_value(label, current, self._QEMU_CPU_PRESETS)
if not raw:
return None
try:
n = int(raw)
except ValueError:
n = 0
if n <= 0:
print(f"{t('Invalid vCPU count.')}")
return None
if n > host_cpu:
print(f"{t('More vCPU than host cores')} ({host_cpu}).")
return n
@staticmethod
def _qemu_shared_value(values, fmt=str):
"""Valeur commune à toutes les VM, ou « varié » si elles diffèrent.
Sert d'« actuel » aux invites globales, où une seule réponse couvre un
parc qui n'est pas forcément homogène."""
uniq = set(values)
return fmt(uniq.pop()) if len(uniq) == 1 else t("varies")
# vCPU de base (x1) par VM. Le multiplicateur monte de là.
_QEMU_BASE_VCPUS = 2
def _qemu_prompt_resource_mult(self, selected, host_cpu, free_ram):
"""Multiplicateur de ressources par VM : x1 (minimum catalogue) .. x4.
Multiplie la RAM (base = minimum de la version) et les vCPU (base
_QEMU_BASE_VCPUS), en bornant les vCPU au nombre de cœurs de l'hôte et
en signalant si la RAM totale dépasse la RAM libre. Renvoie (mult,
base_vcpus)."""
def _qemu_prompt_resources(self, selected, host_cpu, free_ram):
"""Ressources par VM : multiplicateur x1..x4, ou « Personnalisé ».
x1..x4 multiplie la RAM (base = minimum de la version) et les vCPU
(base _QEMU_BASE_VCPUS) en bornant ces derniers aux cœurs de l'hôte.
« Personnalisé » pose les mêmes trois questions que la personnalisation
par VM, mais une seule fois pour tout le parc.
`selected` = liste de (d, v, ram_min, disk, arch). Renvoie
(label, selected) selected porte désormais les valeurs FINALES et
un vCPU par VM : (d, v, ram, disk, arch, vcpus)."""
base_ram = sum(s[2] for s in selected) # RAM min totale (x1)
base_vcpus = self._QEMU_BASE_VCPUS
print(f"\n{t('Resources per VM (x1 = catalog minimum):')}")
@ -3664,31 +3725,63 @@ class TODO:
f" [{n}] x{n}{star} {vcpus} vCPU/VM, "
f"{t('total RAM')} ~{total} Mo{warn}"
)
sel = input(f"{t('Choice (1-4, default 1):')} ").strip()
print(f" [5] {t('Custom - set vCPU, RAM and disk')}")
sel = input(f"{t('Choice (1-5, default 1):')} ").strip()
try:
n = int(sel)
if 1 <= n <= 4:
return n, base_vcpus
mult = int(sel)
except ValueError:
pass
return 1, base_vcpus
mult = 1
if not 1 <= mult <= 5:
mult = 1
def _qemu_customize_vms(self, selected):
"""Personnalise chaque VM avant déploiement : NOM, taille de DISQUE et
RAM. `selected` = liste de (d, v, ram, disk, a) ram (Mo) et disk
(« 20G ») sont les valeurs FINALES (multiplicateur déjà appliqué).
if mult != 5:
vcpus = min(base_vcpus * mult, host_cpu)
return f"x{mult}", [
(d, v, ram * mult, disk, a, vcpus)
for (d, v, ram, disk, a) in selected
]
# Personnalisé : une réponse vide garde la valeur du catalogue, qui
# peut différer d'une VM à l'autre — d'où « varié » comme « actuel ».
cpu = self._qemu_ask_cpu(
t("vCPU per VM, blank = keep"), base_vcpus, host_cpu
)
ram = self._qemu_ask_ram(
t("New RAM in MB, blank = keep"),
self._qemu_shared_value([s[2] for s in selected]),
)
disk = self._qemu_ask_disk(
t("New disk size in G, blank = keep"),
self._qemu_shared_value([s[3] for s in selected]),
)
return t("custom"), [
(
d,
v,
ram or vram,
disk or vdisk,
a,
cpu or base_vcpus,
)
for (d, v, vram, vdisk, a) in selected
]
def _qemu_customize_vms(self, selected, host_cpu):
"""Personnalise chaque VM avant déploiement : NOM, DISQUE, RAM et vCPU.
`selected` = liste de (d, v, ram, disk, a, vcpus) les valeurs sont
déjà FINALES (profil de ressources appliqué).
Renvoie (names, selected_maj). Défaut : rien ne change."""
names = [
self._qemu_infra_name(d, v, a) for d, v, _r, _dk, a in selected
self._qemu_infra_name(d, v, a) for d, v, _r, _dk, a, _c in selected
]
sel = [list(s) for s in selected] # mutable (d, v, ram, disk, a)
sel = [list(s) for s in selected] # mutable
def show():
print(f"\n{t('VMs (default = no change):')}")
for i, (nm, s) in enumerate(zip(names, sel), 1):
d, v, ram, disk, a = s
d, v, ram, disk, a, vcpus = s
print(
f" [{i}] {nm} ({d} {v} [{a}]) "
f" [{i}] {nm} ({d} {v} [{a}]) {vcpus} vCPU "
f"RAM {ram}Mo {t('disk')} {disk}"
)
@ -3705,38 +3798,119 @@ class TODO:
continue
if not (0 <= i < len(sel)):
continue
# Pour la VM i : nom, disque, RAM (vide = garder la valeur).
# Pour la VM i : nom, disque, RAM, vCPU (vide = garder la valeur).
new = input(
f" {names[i]}{t('new name (blank = keep):')} "
).strip()
if new:
names[i] = new
dk = self._qemu_ask_value(
t("New disk size in G, blank = keep"),
sel[i][3],
self._QEMU_DISK_PRESETS,
dk = self._qemu_ask_disk(
t("New disk size in G, blank = keep"), sel[i][3]
)
if dk:
parsed = self._qemu_parse_disk(dk)
if parsed:
sel[i][3] = parsed
else:
print(f"{t('Invalid size.')}")
rm = self._qemu_ask_value(
t("New RAM in MB, blank = keep"),
sel[i][2],
self._QEMU_RAM_PRESETS,
fmt=self._qemu_ram_label,
sel[i][3] = dk
rm = self._qemu_ask_ram(
t("New RAM in MB, blank = keep"), sel[i][2]
)
if rm:
try:
sel[i][2] = int(rm)
except ValueError:
print(f"{t('Invalid size.')}")
sel[i][2] = rm
cpu = self._qemu_ask_cpu(
t("New vCPU count, blank = keep"), sel[i][5], host_cpu
)
if cpu:
sel[i][5] = cpu
if len(set(names)) != len(names):
print(f"{t('Duplicate names detected; keeping as entered.')}")
return names, [tuple(s) for s in sel]
def _confirm_or_discard(self, question):
"""Confirmation à défaut OUI, avec double validation sur le NON.
Un « non » distrait ferait perdre toutes les réponses déjà saisies. On
ne renonce donc que si l'abandon est confirmé ; sinon on repose la
question."""
while True:
if self._is_yes_default_yes(input(f"\n{question}")):
return True
if self._is_yes(
input(f" {t('Discard everything and start over? (y/N): ')}")
):
return False
def _qemu_confirm_collisions(self, deployed, pending):
"""Signale les noms qui heurtent l'existant, et demande confirmation.
Deux cas, de gravité différente : une VM déjà définie est simplement
ignorée rien n'est écrasé — tandis qu'un qcow2 resté seul (VM
supprimée sans son disque) fait échouer deploy_qemu, qui refuse
d'écraser sans --force. Défaut NON : on ne poursuit que sur un « oui »
explicite."""
orphans = []
for name, *_rest in pending:
path = f"/var/lib/libvirt/images/{name}.qcow2"
if os.path.exists(path):
orphans.append((name, path))
if not deployed and not orphans:
return True
print(f"\n{t('Name collisions detected')} :")
skipped = t("VM already defined - SKIPPED, nothing overwritten")
for name in deployed:
print(f" {name:<28.28} {skipped}")
for name, path in orphans:
print(
f" {name:<28.28} "
f"{t('disk present without VM - deployment will FAIL')}"
)
print(f" {'':<28} {path}")
print(f" {'':<28} {t('Remove it by hand, or rename the VM.')}")
return self._is_yes(
input(f"{t('Continue despite these collisions? (y/N): ')}")
)
def _qemu_print_recap(
self,
pending,
deployed,
branch,
install_label,
install_prod,
ssh_key,
add_ssh_config,
parallelism,
):
"""État final soumis à approbation : tout ce qui va changer sur
l'hôte, y compris ce qui ne changera PAS (VM existantes)."""
print(f"\n── {t('Final review before deployment')} ──")
print(f" {t('VMs to create:')} {len(pending)}")
for name, d, v, ram, disk, a, vcpus in pending:
# Le disque annoncé est celui qui sera réellement créé : ERPLibre
# ajoute ERPLIBRE_EXTRA_DISK_GB à la demande initiale.
gigs = self._parse_disk_gb(disk) + (
self.ERPLIBRE_EXTRA_DISK_GB if branch else 0
)
print(
f" {name:<30} {d} {v:<7} [{a:<5}] "
f"{vcpus} vCPU RAM {ram}Mo {t('disk')} {gigs}G"
)
if deployed:
print(f" {t('Existing, left untouched:')} {', '.join(deployed)}")
if branch:
env = (
t("production (/opt, confined)")
if install_prod
else t("development (~/git)")
)
print(
f" {t('ERPLibre install:')} {t('branch')} {branch}, "
f"{t('profile')} {install_label}, {env}"
)
else:
print(f" {t('ERPLibre install:')} {t('no')}")
print(f" {t('SSH key:')} {ssh_key or t('none')}")
cfg = t("one entry per VM") if add_ssh_config else t("untouched")
print(f" {t('~/.ssh/config:')} {cfg}")
print(f" {t('Parallelism:')} {parallelism} {t('at a time')}")
def _qemu_build_deploy_parts(
self, d, v, arch, name, eram, evcpus, disk, ssh_key, branch, dry_run
):
@ -3917,35 +4091,35 @@ class TODO:
print(t("Nothing selected."))
return
# 2b) Multiplicateur de ressources par VM (x1 = minimum .. x4) : monte
# la RAM et les vCPU selon ce que l'hôte peut fournir.
# 2b) Ressources par VM : multiplicateur x1..x4 ou « Personnalisé ».
# Le profil est CUIT dans `selected`, qui porte dès lors les valeurs
# finales — RAM, disque et vCPU — pour chaque VM.
host_cpu = os.cpu_count() or 2
free_ram = self._host_free_ram_mb()
res_mult, base_vcpus = self._qemu_prompt_resource_mult(
res_label, selected = self._qemu_prompt_resources(
selected, host_cpu, free_ram
)
# On CUIT le multiplicateur dans `selected` (RAM finale) et on fixe un
# nombre de vCPU uniforme -> la personnalisation par VM (RAM absolue)
# n'est plus ambiguë. vCPU n'est pas éditable par VM (choix global).
evcpus = min(base_vcpus * res_mult, host_cpu)
selected = [
(d, v, ram * res_mult, disk, a)
for (d, v, ram, disk, a) in selected
]
# 2c) Personnalisation par VM : nom, disque, RAM (à la demande).
names, selected = self._qemu_customize_vms(selected)
# 2c) Personnalisation par VM : nom, disque, RAM, vCPU (à la demande).
names, selected = self._qemu_customize_vms(selected, host_cpu)
# 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)
print(f"\n{t('Deployment plan')} ({len(selected)} VM, x{res_mult}) :")
for i, (d, v, ram, disk, a) in enumerate(selected):
total_cpu = sum(s[5] for s in selected)
print(f"\n{t('Deployment plan')} ({len(selected)} VM, {res_label}) :")
for i, (d, v, ram, disk, a, vcpus) in enumerate(selected):
print(
f" - {names[i]:<30} {d} {v:<7} [{a:<5}] "
f"{evcpus} vCPU RAM {ram}Mo {t('disk')} {disk}"
f"{vcpus} vCPU RAM {ram}Mo {t('disk')} {disk}"
)
print(f"\n {t('Total RAM (all running):')} {total_ram} Mo")
cpu_warn = (
f"{t('> host cores')} ({host_cpu})"
if (total_cpu > host_cpu)
else ""
)
print(f"\n {t('Total vCPU (all running):')} {total_cpu}{cpu_warn}")
print(f" {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")
@ -3961,14 +4135,14 @@ class TODO:
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):
for i, (d, v, ram, disk, a, vcpus) in enumerate(selected):
parts = self._qemu_build_deploy_parts(
d,
v,
a,
names[i],
ram,
evcpus,
vcpus,
disk,
default_key,
None,
@ -3993,35 +4167,45 @@ class TODO:
False # dev (~/git, SELinux dev) vs prod (/opt, confiné)
)
install_cmd = None # commande finale selon le profil choisi
install_label = None
ans = input(
t("Install ERPLibre into ~/git/erplibre on each VM? (y/N): ")
t("Install ERPLibre into ~/git/erplibre on each VM? (Y/n): ")
)
if self._is_yes(ans):
if self._is_yes_default_yes(ans):
install_branch = self._qemu_pick_branch()
install_prod = self._qemu_ask_prod()
_label, install_cmd = self._qemu_pick_install_profile()
install_label, install_cmd = self._qemu_pick_install_profile()
install_monitor = self._is_yes_default_yes(
input(t("Interactive monitoring dashboard? (y/N): "))
)
add_ssh_config = self._is_yes(
input(t("Add each VM to ~/.ssh/config? (y/N): "))
add_ssh_config = self._is_yes_default_yes(
input(t("Add each VM to ~/.ssh/config? (Y/n): "))
)
# 5) Sépare les VM à CRÉER des déjà existantes AVANT de proposer le
# parallélisme : on connaît alors le vrai nombre à déployer (affiché
# dans le prompt) et on peut numéroter chaque tâche.
pending = [] # (name, d, v, ram, disk, a)
pending = [] # (name, d, v, ram, disk, a, vcpus)
deployed = []
for i, (d, v, ram, disk, a) in enumerate(selected):
for i, (d, v, ram, disk, a, vcpus) in enumerate(selected):
name = names[i]
if self._qemu_domain_exists(name):
print(f"{name}: {t('already exists, skipped.')}")
deployed.append(name)
else:
pending.append((name, d, v, ram, disk, a))
pending.append((name, d, v, ram, disk, a, vcpus))
n_jobs = len(pending)
# Collisions de noms : une VM déjà définie est ignorée (rien n'est
# écrasé), mais un qcow2 orphelin fera ÉCHOUER deploy_qemu, qui refuse
# d'écraser sans --force. On le dit avant, pas après l'attente.
if not self._qemu_confirm_collisions(deployed, pending):
print(t("Cancelled."))
return
if not pending:
print(t("Nothing to create - every VM already exists."))
return
# Nombre de déploiements en parallèle. Défaut = nombre de CPU de l'hôte
# (borné par le nombre de VM). On affiche aussi le NB DE VM à déployer.
default_par = min(n_jobs, os.cpu_count() or 4) or 1
@ -4034,23 +4218,34 @@ class TODO:
except ValueError:
parallelism = default_par
# Confirmation puis déploiement (en parallèle).
ans = input(f"\n{t('Deploy these VMs now? (y/N): ')}")
if not self._is_yes(ans):
# 6) Récapitulatif final, puis confirmation. Toutes les réponses
# données jusqu'ici sont rassemblées ici : c'est le dernier point où
# une erreur de saisie se rattrape sans avoir rien créé.
self._qemu_print_recap(
pending,
deployed,
install_branch,
install_label,
install_prod,
ssh_key,
add_ssh_config,
parallelism,
)
if not self._confirm_or_discard(t("Deploy these VMs now? (Y/n): ")):
print(t("Cancelled."))
return
# Jobs numérotés (k/N) : l'ID suit l'ORDRE de préparation, stable même
# si les résultats reviennent dans le désordre (exécution parallèle).
jobs = [] # (id, name, parts)
for k, (name, d, v, ram, disk, a) in enumerate(pending, 1):
for k, (name, d, v, ram, disk, a, vcpus) in enumerate(pending, 1):
parts = self._qemu_build_deploy_parts(
d,
v,
a,
name,
ram,
evcpus,
vcpus,
disk,
ssh_key,
install_branch,

View file

@ -262,9 +262,9 @@ TRANSLATIONS = {
"fr": "Ajouter cette VM à ~/.ssh/config ? (o/N, défaut : non) : ",
"en": "Add this VM to ~/.ssh/config? (y/N, default: no): ",
},
"Add each VM to ~/.ssh/config? (y/N): ": {
"fr": "Ajouter chaque VM à ~/.ssh/config ? (o/N, défaut : non) : ",
"en": "Add each VM to ~/.ssh/config? (y/N, default: no): ",
"Add each VM to ~/.ssh/config? (Y/n): ": {
"fr": "Ajouter chaque VM à ~/.ssh/config ? (O/n, défaut : oui) : ",
"en": "Add each VM to ~/.ssh/config? (Y/n, default: yes): ",
},
"Waiting for the VM IP (DHCP lease)...": {
"fr": "Attente de l'IP de la VM (bail DHCP)...",
@ -1249,6 +1249,46 @@ TRANSLATIONS = {
"fr": "Choix (1-4, défaut 1) :",
"en": "Choice (1-4, default 1):",
},
"Choice (1-5, default 1):": {
"fr": "Choix (1-5, défaut 1) :",
"en": "Choice (1-5, default 1):",
},
"Custom - set vCPU, RAM and disk": {
"fr": "Personnalisé — choisir vCPU, RAM et disque",
"en": "Custom — set vCPU, RAM and disk",
},
"custom": {
"fr": "personnalisé",
"en": "custom",
},
"varies": {
"fr": "varié",
"en": "varies",
},
"vCPU per VM, blank = keep": {
"fr": "vCPU par VM, vide = garder",
"en": "vCPU per VM, blank = keep",
},
"New vCPU count, blank = keep": {
"fr": "Nouveau nombre de vCPU, vide = garder",
"en": "New vCPU count, blank = keep",
},
"Invalid vCPU count.": {
"fr": "Nombre de vCPU invalide.",
"en": "Invalid vCPU count.",
},
"More vCPU than host cores": {
"fr": "Plus de vCPU que de cœurs sur l'hôte",
"en": "More vCPU than host cores",
},
"> host cores": {
"fr": "> cœurs de l'hôte",
"en": "> host cores",
},
"Total vCPU (all running):": {
"fr": "vCPU total (toutes démarrées) :",
"en": "Total vCPU (all running):",
},
"Deploy VM(s) (one or many)": {
"fr": "🚀 Déployer une ou plusieurs VM",
"en": "🚀 Deploy VM(s) (one or many)",
@ -2205,21 +2245,116 @@ TRANSLATIONS = {
"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, défaut : non) : ",
"en": "Install ERPLibre into ~/git/erplibre on each VM? (y/N, default: no): ",
"Install ERPLibre into ~/git/erplibre on each VM? (Y/n): ": {
"fr": "Installer ERPLibre dans ~/git/erplibre sur chaque VM ? "
"(O/n, défaut : oui) : ",
"en": "Install ERPLibre into ~/git/erplibre on each VM? "
"(Y/n, default: yes): ",
},
"Deploy these VMs now? (y/N): ": {
"fr": "Déployer ces VM maintenant ? (o/N, défaut : non) : ",
"en": "Deploy these VMs now? (y/N, default: no): ",
"Deploy these VMs now? (Y/n): ": {
"fr": "Déployer ces VM maintenant ? (O/n, défaut : oui) : ",
"en": "Deploy these VMs now? (Y/n, default: yes): ",
},
"Discard everything and start over? (y/N): ": {
"fr": "Tout abandonner et recommencer ? (o/N, défaut : non) : ",
"en": "Discard everything and start over? (y/N, default: no): ",
},
"Final review before deployment": {
"fr": "Récapitulatif final avant déploiement",
"en": "Final review before deployment",
},
"VMs to create:": {
"fr": "VM à créer :",
"en": "VMs to create:",
},
"Existing, left untouched:": {
"fr": "Existantes, laissées intactes :",
"en": "Existing, left untouched:",
},
"ERPLibre install:": {
"fr": "Installation ERPLibre :",
"en": "ERPLibre install:",
},
"profile": {
"fr": "profil",
"en": "profile",
},
"branch": {
"fr": "branche",
"en": "branch",
},
"production (/opt, confined)": {
"fr": "production (/opt, confiné)",
"en": "production (/opt, confined)",
},
"development (~/git)": {
"fr": "développement (~/git)",
"en": "development (~/git)",
},
"no": {
"fr": "non",
"en": "no",
},
"SSH key:": {
"fr": "Clé SSH :",
"en": "SSH key:",
},
"~/.ssh/config:": {
"fr": "~/.ssh/config :",
"en": "~/.ssh/config:",
},
"one entry per VM": {
"fr": "une entrée par VM",
"en": "one entry per VM",
},
"untouched": {
"fr": "inchangé",
"en": "untouched",
},
"Parallelism:": {
"fr": "Parallélisme :",
"en": "Parallelism:",
},
"at a time": {
"fr": "à la fois",
"en": "at a time",
},
"Nothing to create - every VM already exists.": {
"fr": "Rien à créer — toutes les VM existent déjà.",
"en": "Nothing to create — every VM already exists.",
},
"Name collisions detected": {
"fr": "Collisions de noms détectées",
"en": "Name collisions detected",
},
"VM already defined - SKIPPED, nothing overwritten": {
"fr": "VM déjà définie — IGNORÉE, rien ne sera écrasé",
"en": "VM already defined — SKIPPED, nothing overwritten",
},
"disk present without VM - deployment will FAIL": {
"fr": "disque présent sans VM — le déploiement ÉCHOUERA",
"en": "disk present without VM - deployment will FAIL",
},
"Remove it by hand, or rename the VM.": {
"fr": "L'effacer à la main, ou renommer la VM.",
"en": "Remove it by hand, or rename the VM.",
},
"Continue despite these collisions? (y/N): ": {
"fr": "Continuer malgré ces collisions ? (o/N, défaut : non) : ",
"en": "Continue despite these collisions? (y/N, default: no): ",
},
"Cancelled.": {
"fr": "Annulé.",
"en": "Cancelled.",
},
"already exists, skipped.": {
"fr": "existe déjà, ignorée.",
"en": "already exists, skipped.",
"Resolving VM IPs (parallel, emulated boot is slow)...": {
"fr": "Résolution des IP des VM (en parallèle, le démarrage émulé "
"est lent)...",
"en": "Resolving VM IPs (parallel, emulated boot is slow)...",
},
"no IP": {
"fr": "pas d'IP",
"en": "no IP",
},
"Cloning ERPLibre on each VM": {
"fr": "Clonage d'ERPLibre sur chaque VM",