Compare commits

...

4 commits

Author SHA1 Message Date
19e3801d2c [IMP] script todo: deploy the ERPLibre infra in parallel
The infra fleet was deployed one VM at a time, each blocking up to 90s
waiting for its DHCP lease. Deploys now run concurrently through a
ThreadPoolExecutor (default min(count, 4), promptable), with each job's
output captured and printed per VM as it finishes (no interleaving). VMs
are created with --no-wait-ip so workers return quickly; IPs are then
collected once for the ~/.ssh/config step. Preferred over shelling out
to GNU parallel: no external dependency, grouped output, and it stays
integrated with the ssh-config and ERPLibre-clone steps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 08:15:41 +00:00
4b988bbe46 [FIX] script qemu: don't run apt at first boot by default
cloud-init's package_update/package_upgrade forced apt to hit the distro
mirrors on first boot; on a slow or unreachable network that stalls
cloud-init and delays SSH availability. They are now off by default
(SSH is already present in the cloud images and enabled via runcmd);
--apt-update opts back in (and runs upgrade too unless --no-upgrade).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 08:02:11 +00:00
78e7cf55c6 [IMP] script todo: default erplibre/erplibre login + ~/.ssh/config entry
VMs were created with user "erplibre" but NO password (lock_passwd:
true), so the serial console refused every login and only SSH-by-key
worked — hence "can't connect".

- The todo deploy flow (single VM and infra) now sets a default console
  password "erplibre" for the "erplibre" user, in addition to the SSH
  key, so virsh console and password SSH both work. It is shown as
  "Console/SSH login: erplibre / erplibre" and can be changed at the
  single-VM prompt.
- The console entry now prints the default login and the Ctrl+] hint.
- After creating a VM, offer to add it to ~/.ssh/config (Host <name>,
  HostName <ip>, User erplibre) so "ssh <name>" just works; the infra
  flow asks once and adds every VM. Existing blocks for the same Host
  are replaced, the rest of the file is preserved, mode kept 600.

Note: VMs created before this change have no console password — delete
and redeploy them to get the erplibre/erplibre login.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 07:47:33 +00:00
9876a03ac7 [IMP] script todo: section the big menus + QEMU console & all-IPs
Group the longer menus with section headers (reordering items so the
numbering stays sequential within each section):

- Execute: Development / Data / Sources & documentation / AI &
  automation / Deployment, network & security / Preferences.
- QEMU/KVM: Deployment / Manage / Catalog.
- Database: Backup / Restore / Danger zone.
- RTK: Setup / Status / Optimize.
- Config: Generate / Advanced.

The QEMU config-entry lookup now skips section rows so appended
makefile entries still map to the right number.

Also in the QEMU menu:
- new "Open the console on a VM" entry (lists VMs, asks which, reminds
  Ctrl+] to quit, runs virsh console);
- "Show a VM IP address" accepts "all" to print every VM's IP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 07:41:00 +00:00
3 changed files with 400 additions and 101 deletions

View file

@ -735,8 +735,14 @@ def build_cloud_config(
f" layout: {args.keyboard_layout}",
f" variant: {args.keyboard_variant}",
]
lines.append("package_update: true")
lines.append(f"package_upgrade: {'false' if args.no_upgrade else 'true'}")
# apt update/upgrade désactivés par défaut : sur un réseau lent/instable
# ils font pendre cloud-init au 1er boot (et retardent la dispo SSH). SSH
# est déjà présent dans les images cloud ; on l'active via runcmd sans apt.
# --apt-update réactive apt update (+ upgrade, sauf --no-upgrade).
do_update = args.apt_update
do_upgrade = args.apt_update and not args.no_upgrade
lines.append(f"package_update: {'true' if do_update else 'false'}")
lines.append(f"package_upgrade: {'true' if do_upgrade else 'false'}")
# openssh-server : garantit un serveur SSH sur toutes les distros (les
# images Debian genericcloud notamment ne l'ont pas toujours activé).
@ -1102,6 +1108,13 @@ def build_parser() -> argparse.ArgumentParser:
action="store_true",
help="N'exécute pas package_upgrade au premier boot.",
)
g_cloud.add_argument(
"--apt-update",
action="store_true",
help="Exécute « apt update » au 1er boot (package_update). Désactivé "
"par défaut : évite que cloud-init se bloque sur un miroir lent/"
"injoignable (SSH est déjà présent dans les images cloud).",
)
g_run = p.add_argument_group("Exécution")
g_run.add_argument(

View file

@ -197,21 +197,33 @@ class TODO:
def prompt_execute(self):
help_info = f"""{self._menu_header()}
[1] {t("Automation - Demonstration of developed features")}
[2] {t("Code - Developer tools")}
[3] {t("Config - Configuration file management")}
[4] {t("Database - Database tools")}
[5] {t("Doc - Documentation search")}
[6] {t("Git - Git tools")}
[7] {t("GPT code - AI assistant tools")}
[8] {t("Language - Change language / Changer la langue")}
[9] {t("Network - Network tools")}
[10] {t("Process - Execution tools")}
[11] {t("Run - Execute and install an instance")}
[12] {t("Security - Dependency security audit")}
[13] {t("Test - Test an Odoo module")}
[14] {t("Update - Update all developed staging source code")}
[15] {t("Deploy - Deploy ERPLibre locally")}
{t("Development")}
[1] {t("Code - Developer tools")}
[2] {t("Config - Configuration file management")}
[3] {t("Run - Execute and install an instance")}
[4] {t("Test - Test an Odoo module")}
[5] {t("Process - Execution tools")}
{t("Data")}
[6] {t("Database - Database tools")}
{t("Sources & documentation")}
[7] {t("Git - Git tools")}
[8] {t("Update - Update all developed staging source code")}
[9] {t("Doc - Documentation search")}
{t("AI & automation")}
[10] {t("GPT code - AI assistant tools")}
[11] {t("Automation - Demonstration of developed features")}
{t("Deployment, network & security")}
[12] {t("Deploy - Deploy ERPLibre locally")}
[13] {t("Network - Network tools")}
[14] {t("Security - Dependency security audit")}
{t("Preferences")}
[15] {t("Language - Change language / Changer la langue")}
[0] {t("Back")}
"""
while True:
@ -220,65 +232,65 @@ class TODO:
if status == "0":
return
elif status == "1":
status = self.prompt_execute_function()
if status is not False:
return
elif status == "2":
status = self.prompt_execute_code()
if status is not False:
return
elif status == "3":
elif status == "2":
status = self.prompt_execute_config()
if status is not False:
return
elif status == "4":
status = self.prompt_execute_database()
if status is not False:
return
elif status == "5":
status = self.prompt_execute_doc()
if status is not False:
return
elif status == "6":
status = self.prompt_execute_git()
if status is not False:
return
elif status == "7":
status = self.prompt_execute_gpt_code()
if status is not False:
return
elif status == "8":
status = self._change_language()
if status is not False:
return
elif status == "9":
status = self.prompt_execute_network()
if status is not False:
return
elif status == "10":
status = self.prompt_execute_process()
if status is not False:
return
elif status == "11":
elif status == "3":
status = self.prompt_execute_instance()
if status is not False:
return
elif status == "12":
status = self.prompt_execute_security()
if status is not False:
return
elif status == "13":
elif status == "4":
status = self.prompt_execute_test()
if status is not False:
return
elif status == "14":
elif status == "5":
status = self.prompt_execute_process()
if status is not False:
return
elif status == "6":
status = self.prompt_execute_database()
if status is not False:
return
elif status == "7":
status = self.prompt_execute_git()
if status is not False:
return
elif status == "8":
status = self.prompt_execute_update()
if status is not False:
return
elif status == "15":
elif status == "9":
status = self.prompt_execute_doc()
if status is not False:
return
elif status == "10":
status = self.prompt_execute_gpt_code()
if status is not False:
return
elif status == "11":
status = self.prompt_execute_function()
if status is not False:
return
elif status == "12":
status = self.prompt_execute_deploy()
if status is not False:
return
elif status == "13":
status = self.prompt_execute_network()
if status is not False:
return
elif status == "14":
status = self.prompt_execute_security()
if status is not False:
return
elif status == "15":
status = self._change_language()
if status is not False:
return
else:
print(t("Command not found !"))
@ -854,6 +866,7 @@ class TODO:
print(f"{t('QEMU deploy script not found: ')}{script_path}")
return False
choices = [
{"section": t("Deployment")},
{"prompt_description": t("Deploy a new VM")},
{
"prompt_description": t(
@ -861,15 +874,18 @@ class TODO:
)
},
{"prompt_description": t("Download a cloud image only")},
{"prompt_description": t("List VMs (virsh list --all)")},
{"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)"
)
},
{"section": t("Manage")},
{"prompt_description": t("List VMs (virsh list --all)")},
{"prompt_description": t("Show a VM IP address")},
{"prompt_description": t("Open the console on a VM")},
{"prompt_description": t("Delete VM(s)")},
{"section": t("Catalog")},
{"prompt_description": t("List available images and specs")},
]
config_entries = self.config_file.get_config("qemu_from_makefile")
if config_entries:
@ -888,23 +904,27 @@ class TODO:
elif status == "3":
self._qemu_download_image()
elif status == "4":
self._qemu_list_vms()
elif status == "5":
self._qemu_show_ip()
elif status == "6":
self._qemu_list_images()
elif status == "7":
self._qemu_deploy_infra()
elif status == "5":
self._qemu_list_vms()
elif status == "6":
self._qemu_show_ip()
elif status == "7":
self._qemu_console()
elif status == "8":
self._qemu_delete_vm()
elif status == "9":
self._qemu_list_images()
else:
cmd_no_found = True
try:
int_cmd = int(status)
if 0 < int_cmd <= len(choices):
# Ignore les entrées de section pour mapper le numéro
# affiché sur la bonne commande (config incluse).
real = [c for c in choices if not c.get("section")]
if 0 < int_cmd <= len(real):
cmd_no_found = False
instance = choices[int_cmd - 1]
self.execute_from_configuration(instance)
self.execute_from_configuration(real[int_cmd - 1])
except ValueError:
pass
if cmd_no_found:
@ -934,11 +954,15 @@ class TODO:
# sous sudo (HOME=/root) et ne pourrait pas déduire ~ correctement.
ssh_key = os.path.expanduser(ssh_key)
use_password = False
if not ssh_key:
ans = input(t("No SSH key found. Set a password instead? (Y/n): "))
# Défaut oui : tout sauf une réponse négative explicite vaut oui.
use_password = not self._is_no(ans)
# 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:
@ -958,8 +982,8 @@ class TODO:
parts += ["--disk-size", disk_size]
if ssh_key:
parts += ["--ssh-key", ssh_key]
if use_password:
parts.append("--ask-password")
if password:
parts += ["--password", password]
if force:
parts.append("--force")
if dry_run:
@ -969,8 +993,14 @@ class TODO:
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}")
self.execute.exec_command_live(cmd, source_erplibre=False)
# Proposer d'enregistrer la VM dans ~/.ssh/config (après le 1er boot).
if not dry_run:
self._qemu_offer_ssh_config(name, "erplibre")
def _qemu_download_image(self):
script_path = self._qemu_script_path()
distro = self._qemu_prompt_distro()
@ -998,17 +1028,81 @@ class TODO:
def _qemu_show_ip(self):
# Affiche d'abord les VM (avec leur ID) pour que l'utilisateur sache
# quel nom/ID saisir, puis demande lequel.
# quel nom/ID saisir, puis demande lequel (ou « all » pour toutes).
self._qemu_list_vms()
print()
name = input(t("VM name or ID (or 'all'): ")).strip()
if not name:
print(t("VM name is required!"))
return
if name.lower() in ("all", "tous", "*"):
targets = self._qemu_list_domains()
if not targets:
print(t("No VM found."))
return
else:
targets = [name]
for tgt in targets:
cmd = f"sudo virsh domifaddr {shlex.quote(tgt)} --source lease"
print(f"\n{t('Will execute:')} {cmd}")
self.execute.exec_command_live(cmd, source_erplibre=False)
def _qemu_console(self):
# Liste les VM, demande laquelle, rappelle comment quitter (Ctrl+])
# puis ouvre la console série interactive.
self._qemu_list_vms()
print()
name = input(t("VM name or ID: ")).strip()
if not name:
print(t("VM name is required!"))
return
cmd = f"sudo virsh domifaddr {shlex.quote(name)} --source lease"
print(f"\n💡 {t('To leave the console, press Ctrl+] (then Enter).')}")
print(
f"👤 {t('Default login (if set at deploy): erplibre / erplibre')}"
)
cmd = f"sudo virsh console {shlex.quote(name)}"
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")
os.makedirs(os.path.dirname(cfg), exist_ok=True)
existing = ""
if os.path.exists(cfg):
with open(cfg, encoding="utf-8") as fh:
existing = fh.read()
# Retire un ancien bloc du même Host (jusqu'au prochain Host / EOF).
pattern = re.compile(
rf"(?ms)^[ \t]*Host[ \t]+{re.escape(host)}[ \t]*\n"
r"(?:[ \t]+.*\n?)*"
)
existing = pattern.sub("", existing).rstrip("\n")
block = (
f"Host {host}\n"
f" HostName {ip}\n"
f" User {user}\n"
# IP DHCP réutilisées entre VM -> on évite l'erreur de clé d'hôte.
f" StrictHostKeyChecking no\n"
f" UserKnownHostsFile /dev/null\n"
)
content = (existing + "\n\n" + block) if existing else block
with open(cfg, "w", encoding="utf-8") as fh:
fh.write(content)
os.chmod(cfg, 0o600)
print(f"{t('Added to ~/.ssh/config:')} ssh {host}")
def _qemu_list_domains(self):
"""Noms des VM libvirt définies (via virsh)."""
try:
@ -1335,7 +1429,21 @@ class TODO:
if self._is_yes(ans):
install_branch = self._qemu_pick_branch()
# 5) Confirmation puis déploiement séquentiel.
add_ssh_config = self._is_yes(
input(t("Add each VM to ~/.ssh/config? (y/N): "))
)
# Nombre de déploiements en parallèle (défaut : min(nb VM, 4)).
default_par = min(len(selected), 4)
raw = input(
f"{t('Parallel deployments (default:')} {default_par}): "
).strip()
try:
parallelism = max(1, int(raw)) if raw else default_par
except ValueError:
parallelism = default_par
# 5) Confirmation puis déploiement (en parallèle).
ans = input(f"\n{t('Deploy these VMs now? (y/N): ')}")
if not self._is_yes(ans):
print(t("Cancelled."))
@ -1343,10 +1451,11 @@ class TODO:
script_path = self._qemu_script_path()
deployed = []
jobs = [] # (name, parts) des VM à créer
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.')}")
print(f"{name}: {t('already exists, skipped.')}")
deployed.append(name)
continue
parts = [
@ -1358,24 +1467,64 @@ class TODO:
v,
"--name",
name,
# 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 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)
jobs.append((name, parts))
# 6) Installation ERPLibre (clone) si demandée.
if jobs:
from concurrent.futures import (
ThreadPoolExecutor,
as_completed,
)
workers = min(parallelism, len(jobs))
print(
f"\n{t('Deploying')} {len(jobs)} VM "
f"({t('parallel jobs:')} {workers})…"
)
def _run(job):
jname, jparts = job
res = subprocess.run(jparts, capture_output=True, text=True)
out = (res.stdout or "") + (res.stderr or "")
return jname, res.returncode, out
with ThreadPoolExecutor(max_workers=workers) as pool:
futures = [pool.submit(_run, j) for j in jobs]
for fut in as_completed(futures):
jname, rc, out = fut.result()
mark = "" if rc == 0 else ""
print(f"\n{mark} {jname} (rc={rc})")
tail = [ln for ln in out.strip().splitlines() if ln][-4:]
for ln in tail:
print(f" {ln}")
if rc == 0:
deployed.append(jname)
# 6) ~/.ssh/config (une fois les VM démarrées et l'IP attribuée).
if add_ssh_config:
for name in deployed:
ip = self._qemu_vm_ip(name)
if ip:
self._write_ssh_config_entry(name, "erplibre", ip)
# 7) 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('Default login:')} erplibre / erplibre")
print(f" {t('Manage with:')} sudo virsh list --all")
def _deploy_clone_erplibre(self):
@ -2140,13 +2289,16 @@ class TODO:
def prompt_execute_database(self):
print(f"🤖 {t('Make changes to databases!')}")
choices = [
{"section": t("Backup")},
{"prompt_description": t("Create backup (.zip)")},
{
"prompt_description": t(
"Download database to create backup (.zip)"
)
},
{"section": t("Restore")},
{"prompt_description": t("Restore from backup (.zip)")},
{"prompt_description": t("Create backup (.zip)")},
{"section": t("Danger zone")},
{"prompt_description": t("Erase a database")},
]
help_info = self.fill_help_info(choices)
@ -2157,11 +2309,11 @@ class TODO:
if status == "0":
return False
elif status == "1":
self.db_manager.download_database_backup_cli()
elif status == "2":
self.db_manager.restore_from_database()
elif status == "3":
self.db_manager.create_backup_from_database()
elif status == "2":
self.db_manager.download_database_backup_cli()
elif status == "3":
self.db_manager.restore_from_database()
elif status == "4":
self.db_manager.drop_database()
else:
@ -2199,12 +2351,15 @@ class TODO:
f"🤖 {t('Manage RTK (Rust Token Killer) for token optimization!')}"
)
choices = [
{"section": t("Setup")},
{"prompt_description": t("Install RTK")},
{"prompt_description": t("Check RTK version")},
{"prompt_description": t("Show cumulative token savings")},
{"prompt_description": t("Discover optimization opportunities")},
{"prompt_description": t("Initialize global auto-rewrite hook")},
{"section": t("Status")},
{"prompt_description": t("Check RTK version")},
{"prompt_description": t("Check RTK status")},
{"prompt_description": t("Show cumulative token savings")},
{"section": t("Optimize")},
{"prompt_description": t("Discover optimization opportunities")},
]
help_info = self.fill_help_info(choices)
@ -2216,15 +2371,15 @@ class TODO:
elif status == "1":
self.rtk_install()
elif status == "2":
self.rtk_check_version()
elif status == "3":
self.rtk_show_gain()
elif status == "4":
self.rtk_discover()
elif status == "5":
self.rtk_init_global()
elif status == "6":
elif status == "3":
self.rtk_check_version()
elif status == "4":
self.rtk_check_status()
elif status == "5":
self.rtk_show_gain()
elif status == "6":
self.rtk_discover()
else:
print(t("Command not found !"))
@ -2313,10 +2468,12 @@ class TODO:
def prompt_execute_config(self):
print(f"🤖 {t('Manage ERPLibre and Odoo configuration!')}")
choices = [
{"section": t("Generate")},
{"prompt_description": t("Generate all configuration")},
{"prompt_description": t("Generate from pre-configuration")},
{"prompt_description": t("Generate from backup file")},
{"prompt_description": t("Generate from database")},
{"section": t("Advanced")},
{"prompt_description": t("Setup queue job for parallelism")},
]
help_info = self.fill_help_info(choices)

View file

@ -153,6 +153,119 @@ TRANSLATIONS = {
"fr": "Virtualisation & notifications",
"en": "Virtualization & notifications",
},
"Development": {
"fr": "Développement",
"en": "Development",
},
"Data": {
"fr": "Données",
"en": "Data",
},
"Sources & documentation": {
"fr": "Sources & documentation",
"en": "Sources & documentation",
},
"AI & automation": {
"fr": "IA & automatisation",
"en": "AI & automation",
},
"Deployment, network & security": {
"fr": "Déploiement, réseau & sécurité",
"en": "Deployment, network & security",
},
"Preferences": {
"fr": "Préférences",
"en": "Preferences",
},
"Deployment": {
"fr": "Déploiement",
"en": "Deployment",
},
"Manage": {
"fr": "Gérer",
"en": "Manage",
},
"Catalog": {
"fr": "Catalogue",
"en": "Catalog",
},
"Open the console on a VM": {
"fr": "Ouvrir la console d'une VM",
"en": "Open the console on a VM",
},
"Backup": {
"fr": "Sauvegarde",
"en": "Backup",
},
"Restore": {
"fr": "Restauration",
"en": "Restore",
},
"Danger zone": {
"fr": "Zone dangereuse",
"en": "Danger zone",
},
"Setup": {
"fr": "Installation",
"en": "Setup",
},
"Status": {
"fr": "Statut",
"en": "Status",
},
"Optimize": {
"fr": "Optimisation",
"en": "Optimize",
},
"Generate": {
"fr": "Génération",
"en": "Generate",
},
"Advanced": {
"fr": "Avancé",
"en": "Advanced",
},
"To leave the console, press Ctrl+] (then Enter).": {
"fr": "Pour quitter la console, appuyez sur Ctrl+] (puis Entrée).",
"en": "To leave the console, press Ctrl+] (then Enter).",
},
"Default login (if set at deploy): erplibre / erplibre": {
"fr": "Login par défaut (si défini au déploiement) : "
"erplibre / erplibre",
"en": "Default login (if set at deploy): erplibre / erplibre",
},
"Console password for 'erplibre' (default: erplibre): ": {
"fr": "Mot de passe console pour « erplibre » (défaut : erplibre) : ",
"en": "Console password for 'erplibre' (default: erplibre): ",
},
"Console/SSH login:": {
"fr": "Connexion console/SSH :",
"en": "Console/SSH login:",
},
"Default login:": {
"fr": "Login par défaut :",
"en": "Default login:",
},
"Add this VM to ~/.ssh/config? (y/N): ": {
"fr": "Ajouter cette VM à ~/.ssh/config ? (o/N) : ",
"en": "Add this VM to ~/.ssh/config? (y/N): ",
},
"Add each VM to ~/.ssh/config? (y/N): ": {
"fr": "Ajouter chaque VM à ~/.ssh/config ? (o/N) : ",
"en": "Add each VM to ~/.ssh/config? (y/N): ",
},
"Waiting for the VM IP (DHCP lease)...": {
"fr": "Attente de l'IP de la VM (bail DHCP)...",
"en": "Waiting for the VM IP (DHCP lease)...",
},
"No IP yet; add it later once the VM has booted.": {
"fr": "Pas encore d'IP ; à ajouter plus tard une fois la VM démarrée.",
"en": "No IP yet; add it later once the VM has booted.",
},
"Added to ~/.ssh/config:": {
"fr": "Ajouté à ~/.ssh/config :",
"en": "Added to ~/.ssh/config:",
},
"SSH address input method": {
"fr": "Méthode de saisie de l'adresse SSH",
"en": "SSH address input method",
@ -1174,6 +1287,18 @@ TRANSLATIONS = {
"fr": "Clonage d'ERPLibre sur chaque VM",
"en": "Cloning ERPLibre on each VM",
},
"Parallel deployments (default:": {
"fr": "Déploiements en parallèle (défaut :",
"en": "Parallel deployments (default:",
},
"Deploying": {
"fr": "Déploiement de",
"en": "Deploying",
},
"parallel jobs:": {
"fr": "tâches parallèles :",
"en": "parallel jobs:",
},
"ERPLibre infra deployment done.": {
"fr": "Déploiement de l'infra ERPLibre terminé.",
"en": "ERPLibre infra deployment done.",
@ -1222,6 +1347,10 @@ TRANSLATIONS = {
"fr": "Nom ou ID de la VM : ",
"en": "VM name or ID: ",
},
"VM name or ID (or 'all'): ": {
"fr": "Nom ou ID de la VM (ou « all ») : ",
"en": "VM name or ID (or 'all'): ",
},
"RAM in MB (blank = version minimum): ": {
"fr": "RAM en Mo (vide = minimum de la version) : ",
"en": "RAM in MB (blank = version minimum): ",