Compare commits

..

No commits in common. "5a408f10612dafa24437ff9a1342a85864250143" and "2b7e20493a9e707ebabe2b9cdd30ef26bca423d1" have entirely different histories.

17 changed files with 194 additions and 2221 deletions

View file

@ -33,7 +33,3 @@ EL_GITHUB_TOKEN=""
EL_MANIFEST_PROD="./default.xml"
EL_MANIFEST_DEV="./manifest/git_manifest_erplibre.xml"
EL_LANG="fr"
# Verbosité de l'installation (poetry install, repo sync, git daemon).
# 0 = silencieux (défaut), 1 = logs détaillés. On respecte une valeur déjà
# passée en environnement : EL_VERBOSE=1 make install_odoo_18
EL_VERBOSE="${EL_VERBOSE:-0}"

View file

@ -24,7 +24,6 @@ openai
humanize
requests
urwid
textual
python-dotenv
python-dateutil
unidecode

View file

@ -1,126 +1,68 @@
#!/usr/bin/env bash
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
#
# Dépendances système ERPLibre pour Arch Linux (pacman). Réécrit pour les
# images cloud Arch : l'ancien script utilisait « yay » (helper AUR ABSENT
# d'une image cloud et qui refuse de tourner en root) et n'installait jamais
# « base-devel » -> pas de compilateur C -> échec de la compilation de Python
# par pyenv. On passe tout par pacman (dépôts officiels) ; l'AUR (wkhtmltopdf)
# est best-effort et ne bloque pas.
. ./env_var.sh
install_package() {
local package_name=$1
EL_USER=${USER}
# Check package is already installed
if pacman -Qs "$package_name" >/dev/null; then
echo "$package_name is already installed."
else
echo "Installation of package $package_name..."
yes | yay -S "$package_name"
fi
}
# pacman résilient : --needed saute ce qui est déjà là, --noconfirm en non
# interactif.
PAC="sudo pacman -S --needed --noconfirm"
# Mise à jour COMPLÈTE obligatoire : Arch est en rolling release et NE SUPPORTE
# PAS les mises à jour partielles. Sur une image cloud dont la glibc est
# ancienne, un « pacman -S <paquet récent> » lie le binaire à une glibc plus
# récente que celle installée -> « /usr/bin/postgres: GLIBC_2.44 not found ».
# « -Syu » met à jour glibc (et tout le système) pour rester cohérent.
sudo pacman -Syu --noconfirm || true
# Odoo installation
install_package postgis
install_package postgresql
install_package mariadb
install_package libev
install_package wkhtmltopdf
install_package freetds
#--------------------------------------------------
# Outils de compilation — CRITIQUE (build Python via pyenv, extensions Python)
# base-devel fournit gcc, make, patch, pkgconf, fakeroot, etc.
#--------------------------------------------------
echo -e "\n---- Groupe base-devel (compilateur C, make…) ----"
${PAC} base-devel
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "pacman base-devel installation error."
exit 1
fi
echo "Need password to create symbolic link, create postgres user and install npm :"
sudo ln -fs /usr/lib/libldap.so /usr/lib/libldap_r.so
#--------------------------------------------------
# Dépendances de build pyenv (compilation de CPython) + outils de base
#--------------------------------------------------
echo -e "\n---- Dépendances pyenv (compilation Python) + outils ----"
${PAC} git wget curl openssl zlib xz tk bzip2 readline sqlite libffi
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "pacman pyenv dependencies installation error."
exit 1
fi
#--------------------------------------------------
# PostgreSQL (+ PostGIS) — Arch n'initialise pas le cluster automatiquement
#--------------------------------------------------
echo -e "\n---- Install PostgreSQL Server ----"
${PAC} postgresql
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "pacman postgresql installation error."
exit 1
fi
# Initialisation du cluster (chemin Arch : /var/lib/postgres/data).
if [ ! -f /var/lib/postgres/data/PG_VERSION ]; then
echo -e "\n---- Initialisation du cluster PostgreSQL ----"
sudo -u postgres initdb --locale=C.UTF-8 --encoding=UTF8 \
-D /var/lib/postgres/data || true
fi
sudo systemctl enable --now postgresql 2>/dev/null || true
# PostGIS : optionnel (géospatial), ne bloque pas.
${PAC} postgis || echo "PostGIS non installé (optionnel)."
echo -e "\n---- Creating the ERPLibre PostgreSQL User ----"
sudo su - postgres -c "createuser -s ${EL_USER}" 2>/dev/null || true
#--------------------------------------------------
# Dépendances Odoo / ERPLibre (extensions Python, bindings)
#--------------------------------------------------
echo -e "\n---- Installing arch dependency ----"
# best-effort paquet par paquet : pacman refuse TOUTE la transaction si UN
# seul nom est inconnu (pas de --skip-unavailable). Ces deps ne sont pas
# critiques pour le build Python -> on ne bloque pas sur un nom absent.
for _pkg in libxslt libzip libldap libsasl cmake parallel swig portaudio \
cups xmlsec freetds libev mariadb-libs shfmt; do
${PAC} "${_pkg}" || echo " ${_pkg} : non installé (optionnel), on continue."
done
# libldap_r : Odoo/python-ldap cherche parfois libldap_r.so (supprimé des
# versions récentes d'OpenLDAP) -> lien vers libldap.so.
sudo ln -fs /usr/lib/libldap.so /usr/lib/libldap_r.so 2>/dev/null || true
echo -e "\n---- Update NPM ----"
install_package npm
#--------------------------------------------------
# Node.js + npm (rtlcss, less)
#--------------------------------------------------
echo -e "\n---- Installing nodeJS NPM and rtlcss ----"
${PAC} nodejs npm
sudo npm install npm@latest -g
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "pacman nodejs installation error."
echo "npm install npm lastest installation error."
exit 1
fi
sudo npm install -g rtlcss
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "npm install rtlcss installation error."
exit 1
fi
sudo npm install -g less
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "npm install less installation error."
exit 1
fi
sudo npm install -g rtlcss less || echo "npm rtlcss/less: erreur (optionnel)."
echo -e "\n---- Test tool ----"
npm install || echo "npm install (prettier/plugin-xml): erreur (optionnel)."
# Pour erplibre_devops.
${PAC} sshpass || echo "sshpass non installé (optionnel)."
#--------------------------------------------------
# nginx (optionnel)
#--------------------------------------------------
if [ "${EL_INSTALL_NGINX}" = "True" ]; then
echo -e "\n---- Installing nginx ----"
${PAC} nginx || echo "nginx: erreur (optionnel)."
npm install
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "npm install prettier + plugin-xml installation error."
exit 1
fi
#--------------------------------------------------
# wkhtmltopdf (optionnel) — uniquement dans l'AUR sur Arch (pas de paquet
# officiel). Sur une image cloud sans helper AUR, on saute proprement.
#--------------------------------------------------
if [ "${EL_INSTALL_WKHTMLTOPDF}" = "True" ]; then
if ! command -v wkhtmltopdf >/dev/null 2>&1; then
echo "wkhtmltopdf : disponible seulement via l'AUR sur Arch, ignoré"
echo " (installez-le manuellement avec un helper AUR si nécessaire)."
else
echo -e "\n---- Already installed wkhtml ----"
fi
fi
# TODO install nginx
# ERPLibre installation
install_package cmake
install_package parallel
install_package tk
install_package shfmt
# For erplibre_devops
install_package sshpass
echo -e "\n---- Arch Linux dependency installation done ----"

View file

@ -5,11 +5,6 @@
EL_USER=${USER}
#EL_INSTALL_WKHTMLTOPDF="True"
# apt-get qui ATTEND le verrou (jusqu'à 10 min) : sur une image cloud fraîche,
# cloud-init / unattended-upgrades tiennent souvent le verrou apt au 1er boot
# (« Could not get lock » -> échec de l'install). DPkg::Lock::Timeout patiente.
APT_GET="sudo apt-get -o DPkg::Lock::Timeout=600"
##
### WKHTMLTOPDF download links
## === Ubuntu Focal x64 === (for other distributions please replace these two links,
@ -60,30 +55,25 @@ echo -e "\n---- Update Server ----"
if [ "18.04" == "${UBUNTU_VERSION}" ]; then
# add-apt-repository can install add-apt-repository Ubuntu 18.x
${APT_GET} install software-properties-common curl -y
sudo apt-get install software-properties-common curl -y
# universe package is for Ubuntu 18.x
sudo add-apt-repository universe
# libpng12-0 dependency for wkhtmltopdf
sudo add-apt-repository "deb http://mirrors.kernel.org/ubuntu/ xenial main"
${APT_GET} update
${APT_GET} upgrade -y
sudo apt-get update
sudo apt-get upgrade -y
fi
#--------------------------------------------------
# Install PostgreSQL Server
#--------------------------------------------------
echo -e "\n---- Install PostgreSQL Server ----"
${APT_GET} install postgresql postgresql-contrib libpq-dev -y
sudo apt-get install postgresql libpq-dev postgis -y
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "apt-get install postgresql installation error."
exit 1
fi
# PostGIS : optionnel (géospatial). Le nom du paquet « postgis » n'existe pas
# sur toutes les versions (Ubuntu 24.04) -> best-effort, ne bloque pas.
${APT_GET} install postgis -y \
|| ${APT_GET} install postgresql-postgis -y \
|| echo "PostGIS non installé (optionnel)."
echo -e "\n---- Creating the ERPLibre PostgreSQL User ----"
sudo su - postgres -c "createuser -s ${EL_USER}" 2>/dev/null || true
@ -92,20 +82,20 @@ sudo su - postgres -c "createuser -s ${EL_USER}" 2>/dev/null || true
# Install Dependencies
#--------------------------------------------------
echo -e "\n--- Installing debian dependency --"
${APT_GET} install git build-essential wget libxslt-dev libzip-dev libldap2-dev libsasl2-dev gdebi-core libffi-dev libbz2-dev parallel pysassc swig cmake portaudio19-dev libcups2-dev shfmt xmlsec1 -y
sudo apt-get install git build-essential wget libxslt-dev libzip-dev libldap2-dev libsasl2-dev gdebi-core libffi-dev libbz2-dev parallel pysassc swig cmake portaudio19-dev libcups2-dev shfmt xmlsec1 -y
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "apt-get debian tool installation error."
exit 1
fi
${APT_GET} install libmariadbd-dev freetds-dev -y
sudo apt-get install libmariadbd-dev freetds-dev -y
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "apt-get libmariadb installation error."
exit 1
fi
if [ "18.04" == "${UBUNTU_VERSION}" ]; then
${APT_GET} install libpng12-0 -y
sudo apt-get install libpng12-0 -y
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "apt-get libpng installation error."
@ -113,14 +103,14 @@ if [ "18.04" == "${UBUNTU_VERSION}" ]; then
fi
fi
# Dependencies for pyenv
${APT_GET} install make libssl-dev zlib1g-dev libreadline-dev libsqlite3-dev curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev liblzma-dev -y
sudo apt-get install make libssl-dev zlib1g-dev libreadline-dev libsqlite3-dev curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev liblzma-dev -y
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "apt-get pyenv dependencies installation error."
exit 1
fi
# Dependencies for selenium
${APT_GET} install libcairo2-dev python3-dev pkg-config libxt-dev libgirepository1.0-dev -y
sudo apt-get install libcairo2-dev python3-dev pkg-config libxt-dev libgirepository1.0-dev -y
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "apt-get selenium dependencies installation error."
@ -128,8 +118,8 @@ if [[ $retVal -ne 0 ]]; then
fi
echo -e "\n---- Installing nodeJS NPM and rtlcss for LTR support ----"
${APT_GET} update
${APT_GET} install -y ca-certificates curl gnupg
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
@ -142,8 +132,8 @@ else
fi
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
${APT_GET} update
${APT_GET} install nodejs -y
sudo apt-get update
sudo apt-get install nodejs -y
sudo npm install npm@latest -g
retVal=$?
@ -187,12 +177,7 @@ fi
#--------------------------------------------------
# Install Wkhtmltopdf if needed
#--------------------------------------------------
if [ "$(uname -m)" != "x86_64" ]; then
# WKHTMLTOX_X64 pointe vers un .deb amd64 : sur toute autre architecture
# (s390x, arm64/aarch64…) gdebi échouerait. On saute proprement plutôt que
# d'avorter tout l'install (wkhtmltopdf est optionnel).
echo "wkhtmltopdf : pas de build pour $(uname -m), ignoré (optionnel)."
elif [ ${EL_INSTALL_WKHTMLTOPDF} = "True" ]; then
if [ ${EL_INSTALL_WKHTMLTOPDF} = "True" ]; then
echo -e "\n---- Installing wkhtml ----"
INSTALLED=$(dpkg -s wkhtmltox | grep installed)
if [ "" == "${INSTALLED}" ]; then

View file

@ -3,11 +3,11 @@
if [[ "${OSTYPE}" == "linux-gnu" ]]; then
source /etc/os-release
if [[ "${ID}" == "ubuntu" ]]; then
if [[ "${VERSION_ID}" == "18.04" || "${VERSION_ID}" == "20.04" || "${VERSION_ID}" == "22.04" || "${VERSION_ID}" == "22.10" || "${VERSION_ID}" == "23.04" || "${VERSION_ID}" == "23.10" || "${VERSION_ID}" == "24.04" || "${VERSION_ID}" == "25.04" || "${VERSION_ID}" == "25.10" || "${VERSION_ID}" == "26.04" ]]; then
if [[ "${VERSION_ID}" == "18.04" || "${VERSION_ID}" == "20.04" || "${VERSION_ID}" == "22.04" || "${VERSION_ID}" == "22.10" || "${VERSION_ID}" == "23.04" || "${VERSION_ID}" == "23.10" || "${VERSION_ID}" == "24.04" || "${VERSION_ID}" == "25.04" || "${VERSION_ID}" == "25.10" ]]; then
echo "\n---- linux-gnu installation process started ----"
./script/install/install_debian_dependency.sh
else
echo "Your version is not supported, only support 18.04, 20.04 and 22.04 - 24.04, 25.04, 25.10, 26.04 : ${VERSION_ID}"
echo "Your version is not supported, only support 18.04, 20.04 and 22.04 - 24.04, 25.04, 25.10 : ${VERSION_ID}"
fi
elif [[ "${ID}" == "linuxmint" ]]; then
if [[ "${VERSION_ID}" == "22.3" ]]; then
@ -20,12 +20,9 @@ if [[ "${OSTYPE}" == "linux-gnu" ]]; then
./script/install/install_debian_dependency.sh
elif [[ "${ID}" == "arch" ]]; then
./script/install/install_arch_linux.sh
elif [[ "${ID}" == "fedora" || "${ID_LIKE}" == *"fedora"* || "${ID_LIKE}" == *"rhel"* ]]; then
echo "\n---- Fedora installation process started ----"
./script/install/install_fedora_dependency.sh
else
./script/install/install_debian_dependency.sh
echo "Your Linux system is not supported, only support Ubuntu 18.04 or Ubuntu 20.04 or Ubuntu 22.04 - Ubuntu 23.10 - Ubuntu 24.04, Ubuntu 25.04, Ubuntu 25.10, Debian, Fedora, Arch."
echo "Your Linux system is not supported, only support Ubuntu 18.04 or Ubuntu 20.04 or Ubuntu 22.04 - Ubuntu 23.10 - Ubuntu 24.04, Ubuntu 25.04, Ubuntu 25.10 ."
fi
elif [[ "${OSTYPE}" == "darwin"* ]]; then
echo "\n---- Darwin installation process started ----"

View file

@ -1,129 +0,0 @@
#!/usr/bin/env bash
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
#
# Dépendances système ERPLibre pour Fedora (dnf). Équivalent Fedora de
# install_debian_dependency.sh. « --skip-unavailable » tolère un nom de paquet
# absent (dnf5) au lieu d'échouer sur tout le lot.
. ./env_var.sh
EL_USER=${USER}
# dnf résilient : rafraîchit le cache (évite « checksum doesn't match » /
# signature après un cache périmé) et saute les paquets introuvables.
DNF="sudo dnf install -y --refresh --skip-unavailable"
#--------------------------------------------------
# Outils de compilation (build Python via pyenv, extensions Python)
#--------------------------------------------------
echo -e "\n---- Groupe outils de développement ----"
# Groupes par ID (le nom affiché « C Development Tools... » n'est pas matché).
sudo dnf group install -y --skip-unavailable development-tools c-development \
|| sudo dnf install -y gcc gcc-c++ make automake patch
#--------------------------------------------------
# PostgreSQL
#--------------------------------------------------
echo -e "\n---- Install PostgreSQL Server ----"
${DNF} postgresql-server postgresql-contrib libpq-devel
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "dnf install postgresql installation error."
exit 1
fi
# Initialisation du cluster (Fedora ne le fait pas automatiquement).
if [ ! -f /var/lib/pgsql/data/PG_VERSION ]; then
echo -e "\n---- Initialisation du cluster PostgreSQL ----"
# Nettoie un init partiel et FORCE une locale valide : les images cloud
# Fedora n'ont pas de LANG défini -> « initdb: invalid locale settings ».
sudo rm -rf /var/lib/pgsql/data
sudo PGSETUP_INITDB_OPTIONS="--locale=C.UTF-8 --encoding=UTF8" \
postgresql-setup --initdb || true
fi
sudo systemctl enable --now postgresql 2>/dev/null || true
# PostGIS : optionnel (géospatial), ne bloque pas.
${DNF} postgis || echo "PostGIS non installé (optionnel)."
echo -e "\n---- Creating the ERPLibre PostgreSQL User ----"
sudo su - postgres -c "createuser -s ${EL_USER}" 2>/dev/null || true
#--------------------------------------------------
# Dépendances de build (extensions Python, Odoo)
#--------------------------------------------------
echo -e "\n--- Installing fedora dependency --"
# git-daemon : sur Fedora la sous-commande « git daemon » N'EST PAS dans le
# paquet « git » de base (contrairement à Debian/Ubuntu/Arch). ERPLibre sert
# son manifeste via un « git daemon » local (git://127.0.0.1:9418/) pendant
# « repo sync » -> sans ce paquet : « git: 'daemon' is not a git command »
# puis « Connection refused » et l'échec de la synchro du manifeste.
${DNF} \
git git-daemon wget libxslt-devel libzip-devel openldap-devel \
cyrus-sasl-devel \
libffi-devel bzip2-devel parallel swig cmake portaudio-devel \
cups-devel xmlsec1 xmlsec1-openssl mariadb-connector-c-devel freetds-devel
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "dnf fedora tool installation error."
exit 1
fi
# Dépendances de build pour pyenv (compilation de CPython) — CRITIQUE.
echo -e "\n---- Dépendances pyenv (compilation Python) ----"
${DNF} \
make gcc zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel \
openssl-devel tk-devel libffi-devel xz-devel patch findutils
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "dnf pyenv dependencies installation error."
exit 1
fi
# Dépendances selenium / bindings.
${DNF} \
cairo-devel python3-devel pkgconf-pkg-config gobject-introspection-devel \
libXt-devel || echo "Dépendances selenium partielles (optionnel)."
#--------------------------------------------------
# Node.js + npm (rtlcss, less)
#--------------------------------------------------
echo -e "\n---- Installing nodeJS NPM and rtlcss ----"
${DNF} nodejs npm
retVal=$?
if [[ $retVal -ne 0 ]]; then
echo "dnf nodejs installation error."
exit 1
fi
sudo npm install -g rtlcss less || echo "npm rtlcss/less: erreur (optionnel)."
echo -e "\n---- Test tool ----"
npm install || echo "npm install (prettier/plugin-xml): erreur (optionnel)."
sudo ln -fs /usr/local/bin/lessc /usr/bin/lessc 2>/dev/null || true
#--------------------------------------------------
# nginx (optionnel)
#--------------------------------------------------
if [ "${EL_INSTALL_NGINX}" = "True" ]; then
echo -e "\n---- Installing nginx ----"
${DNF} nginx || echo "nginx: erreur (optionnel)."
fi
#--------------------------------------------------
# wkhtmltopdf (optionnel) — paquet RPM officiel wkhtmltopdf
#--------------------------------------------------
if [ "${EL_INSTALL_WKHTMLTOPDF}" = "True" ]; then
if ! command -v wkhtmltopdf >/dev/null 2>&1; then
echo -e "\n---- Installing wkhtml (best-effort) ----"
# wkhtmltopdf ne publie plus de build « fedora-* » ; le RPM AlmaLinux 9
# (EL9) est compatible Fedora (testé sur F42 : dnf résout les deps).
# Repli AlmaLinux 8 au besoin.
_base="https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3"
sudo dnf install -y "${_base}/wkhtmltox-0.12.6.1-3.almalinux9.x86_64.rpm" \
|| sudo dnf install -y "${_base}/wkhtmltox-0.12.6.1-3.almalinux8.x86_64.rpm" \
|| echo "wkhtmltopdf non installé (optionnel)."
else
echo -e "\n---- Already installed wkhtml ----"
fi
fi
echo -e "\n---- Fedora dependency installation done ----"

View file

@ -22,14 +22,6 @@ VENV_ODOO_PATH=".venv.${EL_ERPLIBRE_VERSION}"
POETRY_ODOO_PATH=${VENV_ERPLIBRE_PATH}/bin/poetry
export WITH_POETRY_INSTALLATION=1
# Verbosité de l'installation Poetry : silencieuse (-q) par défaut, les logs
# détaillés (-vvv) reviennent avec la variable d'environnement EL_VERBOSE=1.
if [[ "${EL_VERBOSE:-0}" == "1" ]]; then
POETRY_VERBOSE="-vvv"
else
POETRY_VERBOSE="-q"
fi
# EL_PHASE controls which steps to execute. Used by install_locally_dev.sh
# for parallel installation — do not set manually unless you know what you do.
# all (default) – full install: setup + poetry phases
@ -91,7 +83,7 @@ if [[ "${EL_PHASE}" != "setup" ]]; then
# To fix keyring problem when installation is blocked, use
export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring
if [[ ${WITH_POETRY_INSTALLATION} -ne 0 ]]; then
poetry install --no-root ${POETRY_VERBOSE}
poetry install --no-root -vvv
fi
retVal=$?
if [[ $retVal -ne 0 ]]; then

View file

@ -2,14 +2,6 @@
. ./env_var.sh
# Verbosité de l'installation (git-repo). Silencieuse par défaut ;
# EL_VERBOSE=1 rétablit les logs détaillés (repo sync, git daemon).
if [ "${EL_VERBOSE:-0}" = "1" ]; then
REPO_VERBOSE="-v"; DAEMON_VERBOSE="--verbose"
else
REPO_VERBOSE="-q"; DAEMON_VERBOSE=""
fi
#EL_MANIFEST_PROD="./default.xml"
#EL_MANIFEST_DEV="./manifest/default.dev.xml"
@ -18,4 +10,4 @@ fi
# Update git-repo
.venv.erplibre/bin/repo init -u https://github.com/ERPLibre/ERPLibre -b $(git rev-parse --verify HEAD) -m ${EL_MANIFEST_DEV}
.venv.erplibre/bin/repo sync ${REPO_VERBOSE}
.venv.erplibre/bin/repo sync -v

View file

@ -1,19 +1,11 @@
#!/usr/bin/env bash
. ./env_var.sh
# Verbosité de l'installation (git-repo). Silencieuse par défaut ;
# EL_VERBOSE=1 rétablit les logs détaillés (repo sync, git daemon).
if [ "${EL_VERBOSE:-0}" = "1" ]; then
REPO_VERBOSE="-v"; DAEMON_VERBOSE="--verbose"
else
REPO_VERBOSE="-q"; DAEMON_VERBOSE=""
fi
#EL_MANIFEST_PROD="./default.xml"
#EL_MANIFEST_DEV="./manifest/default.dev.xml"
# Update git-repo
git daemon --base-path=. --export-all --reuseaddr --informative-errors ${DAEMON_VERBOSE} &
git daemon --base-path=. --export-all --reuseaddr --informative-errors --verbose &
DAEMON_PID=$!
if [ -L "$EL_MANIFEST_DEV" ]; then
@ -32,6 +24,6 @@ fi
.venv.erplibre/bin/python ./script/git/git_merge_repo_manifest.py --output .repo/local_manifests/erplibre_manifest.xml --with_OCA
.venv.erplibre/bin/repo init -u git://127.0.0.1:9418/ -b $(git rev-parse --verify HEAD) -m ${MANIFEST_TARGET} "$@"
.venv.erplibre/bin/repo sync -c -j "$JOBS" ${REPO_VERBOSE} -m ${MANIFEST_TARGET}
.venv.erplibre/bin/repo sync -c -j "$JOBS" -v -m ${MANIFEST_TARGET}
kill ${DAEMON_PID}

View file

@ -2,19 +2,11 @@
. ./env_var.sh
# Verbosité de l'installation (git-repo). Silencieuse par défaut ;
# EL_VERBOSE=1 rétablit les logs détaillés (repo sync, git daemon).
if [ "${EL_VERBOSE:-0}" = "1" ]; then
REPO_VERBOSE="-v"; DAEMON_VERBOSE="--verbose"
else
REPO_VERBOSE="-q"; DAEMON_VERBOSE=""
fi
#EL_MANIFEST_PROD="./default.xml"
#EL_MANIFEST_DEV="./manifest/default.dev.xml"
# Update git-repo
git daemon --base-path=. --export-all --reuseaddr --informative-errors ${DAEMON_VERBOSE} &
git daemon --base-path=. --export-all --reuseaddr --informative-errors --verbose &
DAEMON_PID=$!
if [ -L "$EL_MANIFEST_DEV" ]; then
@ -33,6 +25,6 @@ fi
.venv.erplibre/bin/python ./script/git/git_merge_repo_manifest.py --output .repo/local_manifests/erplibre_manifest.xml --with_OCA
.venv.erplibre/bin/repo init -u git://127.0.0.1:9418/ -b $(git rev-parse --verify HEAD) -m ${MANIFEST_TARGET} -g base,code_generator
.venv.erplibre/bin/repo sync -c -j "$JOBS" ${REPO_VERBOSE} -m ${MANIFEST_TARGET}
.venv.erplibre/bin/repo sync -c -j "$JOBS" -v -m ${MANIFEST_TARGET}
kill ${DAEMON_PID}

View file

@ -1,19 +1,11 @@
#!/usr/bin/env bash
. ./env_var.sh
# Verbosité de l'installation (git-repo). Silencieuse par défaut ;
# EL_VERBOSE=1 rétablit les logs détaillés (repo sync, git daemon).
if [ "${EL_VERBOSE:-0}" = "1" ]; then
REPO_VERBOSE="-v"; DAEMON_VERBOSE="--verbose"
else
REPO_VERBOSE="-q"; DAEMON_VERBOSE=""
fi
#EL_MANIFEST_PROD="./default.xml"
#EL_MANIFEST_DEV="./manifest/default.dev.xml"
# Update git-repo
git daemon --base-path=. --export-all --reuseaddr --informative-errors ${DAEMON_VERBOSE} &
git daemon --base-path=. --export-all --reuseaddr --informative-errors --verbose &
DAEMON_PID=$!
if [ -L "$EL_MANIFEST_DEV" ]; then
@ -32,6 +24,6 @@ fi
.venv.erplibre/bin/python ./script/git/git_merge_repo_manifest.py --output .repo/local_manifests/erplibre_manifest.xml --with_mobile
.venv.erplibre/bin/repo init -u git://127.0.0.1:9418/ -b $(git rev-parse --verify HEAD) -m ${MANIFEST_TARGET} "$@"
.venv.erplibre/bin/repo sync -c -j "$JOBS" ${REPO_VERBOSE} -m ${MANIFEST_TARGET}
.venv.erplibre/bin/repo sync -c -j "$JOBS" -v -m ${MANIFEST_TARGET}
kill ${DAEMON_PID}

View file

@ -2,19 +2,11 @@
. ./env_var.sh
# Verbosité de l'installation (git-repo). Silencieuse par défaut ;
# EL_VERBOSE=1 rétablit les logs détaillés (repo sync, git daemon).
if [ "${EL_VERBOSE:-0}" = "1" ]; then
REPO_VERBOSE="-v"; DAEMON_VERBOSE="--verbose"
else
REPO_VERBOSE="-q"; DAEMON_VERBOSE=""
fi
#EL_MANIFEST_PROD="./default.xml"
#EL_MANIFEST_DEV="./manifest/default.dev.xml"
# Update git-repo
git daemon --base-path=. --export-all --reuseaddr --informative-errors ${DAEMON_VERBOSE} &
git daemon --base-path=. --export-all --reuseaddr --informative-errors --verbose &
DAEMON_PID=$!
if [ -L "$EL_MANIFEST_DEV" ]; then
@ -33,6 +25,6 @@ fi
.venv.erplibre/bin/python ./script/git/git_merge_repo_manifest.py --output .repo/local_manifests/erplibre_manifest.xml --with_OCA
.venv.erplibre/bin/repo init -u git://127.0.0.1:9418/ -b $(git rev-parse --verify HEAD) -m ${MANIFEST_TARGET}
.venv.erplibre/bin/repo sync -c -j "$JOBS" ${REPO_VERBOSE} -m ${MANIFEST_TARGET}
.venv.erplibre/bin/repo sync -c -j "$JOBS" -v -m ${MANIFEST_TARGET}
kill ${DAEMON_PID}

View file

@ -2,14 +2,6 @@
. ./env_var.sh
# Verbosité de l'installation (git-repo). Silencieuse par défaut ;
# EL_VERBOSE=1 rétablit les logs détaillés (repo sync, git daemon).
if [ "${EL_VERBOSE:-0}" = "1" ]; then
REPO_VERBOSE="-v"; DAEMON_VERBOSE="--verbose"
else
REPO_VERBOSE="-q"; DAEMON_VERBOSE=""
fi
#EL_MANIFEST_PROD="./default.xml"
#EL_MANIFEST_DEV="./manifest/default.dev.xml"
@ -24,4 +16,4 @@ fi
# Update git-repo
.venv.erplibre/bin/repo init -u https://github.com/ERPLibre/ERPLibre -b $(git rev-parse --verify HEAD)
.venv.erplibre/bin/repo sync ${REPO_VERBOSE} -j "$JOBS"
.venv.erplibre/bin/repo sync -v -j "$JOBS"

View file

@ -51,7 +51,6 @@ import hashlib
import os
import re
import shutil
import socket
import subprocess
import sys
import tempfile
@ -91,60 +90,19 @@ FEDORA_VERSIONS: dict[str, tuple[str, str, int, str]] = {
"43": ("43", "fedora43", 2048, "15G"),
"44": ("44", "fedora44", 2048, "15G"),
}
# Arch est en rolling release : une seule « version » (latest).
ARCH_VERSIONS: dict[str, tuple[str, str, int, str]] = {
"latest": ("latest", "archlinux", 1024, "10G"),
}
# 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"),
"arch": (ARCH_VERSIONS, "latest"),
}
# Traduction de l'arch générique (amd64/arm64) vers le nom propre à la distro.
# s390x s'écrit « s390x » partout (identité), donc aucune entrée n'est requise.
ARCH_ALIASES: dict[str, dict[str, str]] = {
"fedora": {"amd64": "x86_64", "arm64": "aarch64"},
"arch": {"amd64": "x86_64", "arm64": "aarch64"},
}
# Architectures non-x86 nécessitant une machine/un amorçage spécifiques (voir
# virt_install). Émulées en TCG (pas de KVM), donc LENTES, quand elles
# diffèrent de l'architecture de l'hôte.
NON_X86_ARCHES: tuple[str, ...] = ("arm64", "s390x")
# Distros publiant des images cloud par architecture (vérifié juillet 2026) :
# - s390x (IBM Z) : Ubuntu seulement (Debian/Fedora : 404 ; Arch : x86/arm).
# - arm64/aarch64 : Ubuntu, Debian, Fedora (Arch : pas d'image cloud officielle
# aarch64 sur geo.mirror.pkgbuild.com).
S390X_DISTROS: tuple[str, ...] = ("ubuntu",)
ARM64_DISTROS: tuple[str, ...] = ("ubuntu", "debian", "fedora")
# arch générique -> distros la publiant (pour valider --arch tôt).
ARCH_DISTRO_SUPPORT: dict[str, tuple[str, ...]] = {
"s390x": S390X_DISTROS,
"arm64": ARM64_DISTROS,
}
def host_arch() -> str:
"""Architecture de l'hôte en jeton générique (amd64/arm64/s390x)."""
try:
machine = os.uname().machine
except (AttributeError, OSError):
machine = ""
return {
"x86_64": "amd64",
"amd64": "amd64",
"aarch64": "arm64",
"arm64": "arm64",
"s390x": "s390x",
}.get(machine, "amd64")
ARCH_CLOUD_BASE = "https://geo.mirror.pkgbuild.com/images/latest"
CLOUD_IMG_BASE = "https://cloud-images.ubuntu.com"
# Debian : cloud.debian.org est un redirecteur qui, selon le réseau, peut
# renvoyer vers un miroir injoignable. On essaie donc plusieurs bases dans
@ -201,9 +159,6 @@ def image_candidates(
]
if distro == "fedora":
return [resolve_fedora_url(version, arch, dry_run)]
if distro == "arch":
# Rolling release : image « latest » officielle (cloud-init inclus).
return [f"{ARCH_CLOUD_BASE}/Arch-Linux-{a}-cloudimg.qcow2"]
raise ValueError(f"URL indisponible pour la distro {distro!r}")
@ -238,8 +193,6 @@ def default_image_name(distro: str, code: str, arch: str, version: str) -> str:
return f"ubuntu-{version}-server-cloudimg-{a}.img"
if distro == "debian":
return f"debian-{version}-genericcloud-{a}.qcow2"
if distro == "arch":
return f"arch-linux-{a}-cloudimg.qcow2"
return f"fedora-cloud-{version}-{a}.qcow2"
@ -422,44 +375,6 @@ DAEMON_PACKAGES: dict[str, list[str]] = {
"brew": [],
}
# Émulateurs QEMU système pour architectures non-x86, requis pour --arch
# <arch> sur un hôte d'architecture différente (émulation TCG). Par arch puis
# par gestionnaire de paquets. NB apt : qemu-system-misc ne contient PAS s390x
# (alpha/avr/… seulement) -> paquet dédié qemu-system-s390x ; qemu-system-arm
# fournit qemu-system-aarch64 ; l'arm64 exige aussi le firmware UEFI AAVMF.
EMULATOR_PACKAGES: dict[str, dict[str, list[str]]] = {
"s390x": {
"apt": ["qemu-system-s390x"],
"dnf": ["qemu-system-s390x"],
"pacman": ["qemu-emulators-full"],
"zypper": ["qemu-s390"],
"brew": [],
},
"arm64": {
"apt": ["qemu-system-arm", "qemu-efi-aarch64"],
"dnf": ["qemu-system-aarch64", "edk2-aarch64"],
"pacman": ["qemu-emulators-full", "edk2-aarch64"],
"zypper": ["qemu-arm", "qemu-uefi-aarch64"],
"brew": [],
},
}
# Binaire émulateur par arch (détection de présence).
EMULATOR_BINARY: dict[str, str] = {
"s390x": "qemu-system-s390x",
"arm64": "qemu-system-aarch64",
}
# Firmwares UEFI AAVMF possibles (arm64) : au moins un doit exister.
AARCH64_FIRMWARE_PATHS: tuple[str, ...] = (
"/usr/share/AAVMF/AAVMF_CODE.fd",
"/usr/share/AAVMF/AAVMF_CODE.no-secboot.fd",
"/usr/share/edk2/aarch64/QEMU_EFI.fd",
"/usr/share/edk2/aarch64/QEMU_EFI-silent.fd",
"/usr/share/qemu-efi-aarch64/QEMU_EFI.fd",
"/usr/share/edk2-armvirt/aarch64/QEMU_EFI.fd",
)
# Gestionnaires de paquets, dans l'ordre de préférence :
# (clé TOOL_PACKAGES, binaire à détecter, commande d'installation, sudo, refresh)
PKG_MANAGERS: tuple[
@ -668,64 +583,6 @@ def ensure_tools(runner: Runner, assume_yes: bool, no_install: bool) -> None:
ensure_libvirt_service(runner)
def emulator_ready(arch: str) -> bool:
"""Vrai si l'émulateur (et, pour arm64, un firmware UEFI AAVMF) est là."""
if shutil.which(EMULATOR_BINARY[arch]) is None:
return False
if arch == "arm64":
return any(os.path.exists(p) for p in AARCH64_FIRMWARE_PATHS)
return True
def ensure_emulator(
arch: str, runner: Runner, assume_yes: bool, no_install: bool
) -> None:
"""Vérifie l'émulateur QEMU système pour `arch` (et le firmware UEFI pour
arm64), requis quand on émule une architecture différente de l'hôte ;
l'installe au besoin via le gestionnaire de paquets."""
binary = EMULATOR_BINARY[arch]
if emulator_ready(arch):
return
pm = detect_pkg_manager()
if pm is None:
sys.exit(
f" Émulateur {arch} introuvable ({binary}) et gestionnaire de "
"paquets non reconnu.\n Installez-le manuellement."
)
pm_key, install_cmd, use_sudo, refresh = pm
packages = EMULATOR_PACKAGES.get(arch, {}).get(pm_key, [])
if not packages:
sys.exit(
f" Émulateur {arch} introuvable ({binary}) : installez le paquet "
"QEMU système correspondant de votre distribution "
"(+ firmware UEFI pour arm64)."
)
full_cmd = install_cmd + packages
printable = ("sudo " if use_sudo and runner.use_sudo else "") + " ".join(
full_cmd
)
print(f" Émulateur {arch} manquant ({binary}).")
print(f" Commande d'installation : {printable}")
if no_install:
sys.exit(
" Installation automatique désactivée (--no-install-deps).\n"
" Installez manuellement : " + printable
)
if not assume_yes and not prompt_yes_no(
f" Installer l'émulateur {arch} maintenant ?"
):
sys.exit(" Installation refusée.\n Commande manuelle : " + printable)
if refresh:
runner.run(refresh, privileged=use_sudo, check=False)
runner.run(full_cmd, privileged=use_sudo)
if not emulator_ready(arch):
sys.exit(
f" Émulateur {arch} ({binary}) ou firmware toujours absent après "
f"installation.\n Essayez manuellement : {printable}"
)
print(f" Émulateur {arch} installé avec succès.")
# --------------------------------------------------------------------------- #
# Étapes
# --------------------------------------------------------------------------- #
@ -766,15 +623,6 @@ def _download_one(url: str, tmp: Path, timeout: int) -> None:
print(f" {pct:3d}%", flush=True)
if is_tty:
print()
# Vérifie la COMPLÉTUDE : si le serveur a annoncé une taille et qu'on a
# reçu moins (connexion coupée), le .part est TRONQUÉ. Sans ce contrôle,
# il était validé comme « complet » -> qcow2 valide mais VIDE (juste
# l'en-tête) -> VM qui ne boote pas, et cache empoisonné réutilisé ensuite.
if total > 0 and done < total:
raise OSError(
f"téléchargement incomplet : {done}/{total} octets reçus "
"(connexion interrompue)"
)
def download_image(
@ -942,19 +790,15 @@ def build_cloud_config(
lines.append(f"package_update: {'true' if do_update else 'false'}")
lines.append(f"package_upgrade: {'true' if do_upgrade else 'false'}")
# On n'installe AUCUN paquet via cloud-init : cela exige le réseau au 1er
# boot et peut bloquer longtemps (dnf/apt/pacman lents sur réseau lent) —
# cloud-init reste alors « running » et tout ce qui suit attend. sshd est
# DÉJÀ présent dans toutes les images cloud (Ubuntu/Debian/Fedora/Arch) ;
# on l'active seulement (runcmd). Les outils nécessaires (curl/git/make…)
# sont installés — avec dépôts optimisés — par le bootstrap d'installation.
packages = list(dict.fromkeys(args.package)) # seulement --package
if packages:
lines.append("packages:")
lines += [f" - {p}" for p in packages]
# openssh-server : garantit un serveur SSH sur toutes les distros (les
# images Debian genericcloud notamment ne l'ont pas toujours activé).
packages = ["qemu-guest-agent", "openssh-server", *args.package]
lines.append("packages:")
lines += [
f" - {p}" for p in dict.fromkeys(packages)
] # dédoublonne, ordre gardé
# Active et démarre SSH quel que soit le nom du service (ssh sur
# Debian/Ubuntu, sshd sur Fedora/Arch) — sans quoi la VM peut booter
# sans SSH accessible.
# Debian/Ubuntu, sshd sur Fedora) — sans quoi la VM peut booter sans SSH.
lines += [
"runcmd:",
" - systemctl enable --now ssh 2>/dev/null"
@ -966,13 +810,10 @@ def build_cloud_config(
# network-config (cloud-init v2) : DHCP sur toute interface « e* ». Les images
# Debian genericcloud ne configurent pas toujours le réseau sans ça (le NIC
# reste down -> pas d'IP), contrairement à Ubuntu. Inoffensif pour Ubuntu.
# La clé est « eth0 » car le renderer cloud-init d'Arch utilise la CLÉ comme
# nom d'interface (en ignorant « match ») et l'image Arch nomme son NIC eth0 ;
# Debian/Fedora/Ubuntu utilisent bien « match: name: e* » (leur en*).
NETWORK_CONFIG = (
"version: 2\n"
"ethernets:\n"
" eth0:\n"
" primary:\n"
" match:\n"
' name: "e*"\n'
" dhcp4: true\n"
@ -1116,11 +957,6 @@ def virt_install(
osinfo: str,
runner: Runner,
) -> None:
# Émulée (TCG, pas de KVM) si l'arch demandée diffère de celle de l'hôte.
emulated = args.arch != host_arch()
# s390x n'a pas de port série ISA : la console est SCLP (ttysclp0), et non
# ttyS0. Ailleurs (x86/arm64), console série classique.
console_target = "sclp" if args.arch == "s390x" else "serial"
cmd = [
"virt-install",
"--name",
@ -1137,7 +973,6 @@ def virt_install(
# volume « cidata » est visible dès init-local et cloud-init le lit.
# En CD-ROM, l'initramfs Debian ne charge pas sr_mod à temps -> le
# seed n'est pas vu et rien ne s'applique (Ubuntu, lui, tolère le CD).
# Sur s390x, bus=virtio est mappé en virtio-ccw par libvirt.
"--disk",
f"path={seed},readonly=on,bus=virtio",
"--osinfo",
@ -1147,64 +982,21 @@ def virt_install(
"--graphics",
args.graphics,
"--console",
f"pty,target_type={console_target}",
"pty,target_type=serial",
]
if args.arch == "s390x":
# s390x (IBM Z) : machine s390-ccw-virtio, amorçage IPL/zipl depuis le
# disque (ni BIOS ni UEFI/OVMF -> aucun --boot).
cmd += ["--arch", "s390x", "--machine", "s390-ccw-virtio"]
elif args.arch == "arm64":
# arm64/aarch64 : machine « virt » + UEFI (firmware AAVMF). Les images
# cloud arm64 n'ont pas de BIOS -> UEFI obligatoire. Secure Boot
# DÉSACTIVÉ (comme x86) : sinon libvirt sélectionne l'AAVMF « secure »
# à clés Microsoft enrôlées et la VM RESTE FIGÉE au firmware (aucun
# boot, aucune IP). secure-boot=no -> AAVMF non-SB, boot OK.
cmd += [
"--arch",
"aarch64",
"--machine",
"virt",
"--boot",
"uefi,firmware.feature0.name=secure-boot,"
"firmware.feature0.enabled=no",
]
elif not args.bios:
# Boot UEFI par défaut (x86) : Debian 13 (trixie) et les images cloud
# récentes n'embarquent plus le chargeur BIOS/GRUB-pc et partent en
# boucle « Booting... » en SeaBIOS. UEFI (OVMF) fonctionne pour
# Ubuntu/Debian/Fedora. --bios force l'ancien BIOS si OVMF est absent.
# Secure Boot DÉSACTIVÉ : le chargeur d'Arch (GRUB) n'est pas signé et
# OVMF Secure Boot le refuse (« Access Denied » -> pas de boot).
# Ubuntu/Debian/Fedora bootent aussi sans Secure Boot.
cmd += [
"--boot",
"uefi,firmware.feature0.name=secure-boot,"
"firmware.feature0.enabled=no",
]
if emulated:
# Architecture différente de l'hôte -> émulation logicielle TCG
# (pas de KVM). LENT.
cmd += ["--virt-type", "qemu"]
# Boot UEFI par défaut : Debian 13 (trixie) et les images cloud récentes
# n'embarquent plus le chargeur BIOS/GRUB-pc et partent en boucle
# « Booting... » en SeaBIOS. UEFI (OVMF) fonctionne pour Ubuntu/Debian/
# Fedora. --bios force l'ancien BIOS si OVMF est indisponible.
if not args.bios:
cmd += ["--boot", "uefi"]
if not args.attach_console:
cmd.append("--noautoconsole")
runner.run(cmd, privileged=True)
def _ip_reachable(ip: str, port: int = 22, timeout: float = 3) -> bool:
"""Vrai si le port SSH répond (distingue le bail actif du bail périmé)."""
try:
with socket.create_connection((ip, port), timeout=timeout):
return True
except OSError:
return False
def wait_for_ip(name: str, use_sudo: bool, timeout: int) -> str | None:
"""Interroge les baux DHCP libvirt jusqu'à obtenir l'IPv4 de la VM. Une VM
peut avoir PLUSIEURS baux (l'image demande d'abord une IP avec le hostname
par défaut « ubuntu », puis cloud-init fixe le vrai hostname -> 2e bail) :
on renvoie une IP JOIGNABLE (sshd up), sinon la plus récente, jamais
aveuglément la 1re (souvent le bail précoce périmé, « No route to host »)."""
"""Interroge les baux DHCP libvirt jusqu'à obtenir l'IPv4 de la VM."""
base = (["sudo"] if use_sudo else []) + [
"virsh",
"domifaddr",
@ -1213,15 +1005,13 @@ def wait_for_ip(name: str, use_sudo: bool, timeout: int) -> str | None:
"lease",
]
deadline = time.time() + timeout
ips: list[str] = []
while time.time() < deadline:
res = subprocess.run(base, capture_output=True, text=True)
ips = re.findall(r"(\d+\.\d+\.\d+\.\d+)", res.stdout)
for ip in ips:
if _ip_reachable(ip):
return ip
m = re.search(r"(\d+\.\d+\.\d+\.\d+)", res.stdout)
if m:
return m.group(1)
time.sleep(3)
return ips[-1] if ips else None
return None
def ssh_command(user: str, ip: str, has_key: bool) -> str:
@ -1280,9 +1070,7 @@ def build_parser() -> argparse.ArgumentParser:
g_img.add_argument(
"--arch",
default="amd64",
help="Architecture de l'image (défaut : amd64). amd64/x86_64, "
"arm64/aarch64 (Ubuntu/Debian/Fedora) ou s390x (Ubuntu). Toute arch "
"différente de l'hôte est ÉMULÉE (TCG, lente).",
help="Architecture de l'image (défaut : amd64).",
)
g_img.add_argument(
"--image-dir",
@ -1518,14 +1306,6 @@ def main() -> None:
f"Version {args.version!r} inconnue pour {args.distro}. "
f"Choix : {', '.join(versions)} (voir --list-images)."
)
# Certaines architectures ne sont publiées que par une partie des distros
# (voir ARCH_DISTRO_SUPPORT) : on valide tôt plutôt qu'échouer au download.
supported = ARCH_DISTRO_SUPPORT.get(args.arch)
if supported is not None and args.distro not in supported:
sys.exit(
f"Architecture {args.arch} indisponible pour {args.distro!r} : "
f"images cloud publiées seulement pour {', '.join(supported)}."
)
code, default_osinfo, min_ram, min_disk = versions[args.version]
if args.codename:
code = args.codename
@ -1535,10 +1315,6 @@ def main() -> None:
args.memory = min_ram
if args.disk_size is None:
args.disk_size = min_disk
# Un nombre nu (« 30 ») serait pris pour des OCTETS par qemu-img et ferait
# échouer le resize : on suppose des Go par défaut (« 30 » -> « 30G »).
if re.fullmatch(r"\d+", str(args.disk_size)):
args.disk_size = f"{args.disk_size}G"
urls = image_candidates(
args.distro, code, args.arch, args.version, args.dry_run
)
@ -1597,18 +1373,6 @@ def main() -> None:
if not args.dry_run:
ensure_tools(runner, args.assume_yes, args.no_install_deps)
# Émulation requise si l'arch diffère de l'hôte : installe l'émulateur
# QEMU système adéquat (+ firmware UEFI pour arm64) et prévient de la
# lenteur.
if args.arch in EMULATOR_BINARY and args.arch != host_arch():
ensure_emulator(
args.arch, runner, args.assume_yes, args.no_install_deps
)
print(
f" Note : {args.arch} est ÉMULÉ (TCG) sur cet hôte "
f"({host_arch()}) — le boot et l'installation seront "
"nettement plus lents que l'architecture native."
)
print(f"\n== 1/5 Image cloud ({args.distro} {args.version} / {code}) ==")
download_image(urls, args.image_path, args.dry_run)

View file

@ -1,315 +0,0 @@
#!/usr/bin/env python3
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""Dashboard Textual : suivi des installations ERPLibre parallèles sur VM.
Principe « détachable » : chaque installation tourne dans un processus
DÉTACHÉ (setsid) qui écrit un fichier log et, à la fin, un marqueur
« __ERPLIBRE_EXIT__ <code> ». Ce module ne fait que VISUALISER ces fichiers :
quitter le dashboard n'arrête rien, on peut le rouvrir pour ré-attacher.
- launch_installs(...) : lance les process détachés + écrit un manifeste JSON.
- run_monitor(manifest_path) : ouvre le dashboard Textual sur un manifeste.
"""
from __future__ import annotations
import json
import os
import shlex
import subprocess
import time
from pathlib import Path
try:
from script.todo.todo_i18n import t
except Exception: # pragma: no cover - repli si i18n indisponible
def t(key: str) -> str:
return key
EXIT_MARKER = "__ERPLIBRE_EXIT__"
SSH_OPTS = (
"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
"-o ConnectTimeout=8"
)
def session_dir() -> Path:
"""Répertoire des logs/manifestes d'installation (créé au besoin)."""
base = Path(os.path.expanduser("~/.erplibre/qemu-install"))
base.mkdir(parents=True, exist_ok=True)
return base
def _launch_one(ip: str, remote_cmd: str, log_path: str) -> None:
"""Lance une install SSH DÉTACHÉE : attend le sshd, exécute, journalise
la sortie puis écrit le marqueur de fin avec le code de sortie."""
# Sonde de disponibilité : on attend que sshd réponde ET que cloud-init
# soit TERMINÉ, via des connexions COURTES successives (jusqu'à ~20 min :
# une architecture ÉMULÉE, s390x/arm64 sur hôte x86, boote lentement).
# Au 1er boot, cloud-init régénère les clés d'hôte et REDÉMARRE sshd : une
# session SSH longue (ex. « cloud-init status --wait ») serait alors tuée
# (« Connection closed by remote host », exit 255 — cas Fedora). Chaque
# itération étant une connexion neuve, un redémarrage de sshd ne casse que
# la tentative en cours. On imprime toujours l'état (|| true) pour matcher
# sur le TEXTE, « status: running » n'ayant pas de code de sortie fiable.
ci_probe = (
"if command -v cloud-init >/dev/null 2>&1; then "
"cloud-init status 2>/dev/null || true; else echo nocloudinit; fi"
)
log_q = shlex.quote(log_path)
# On écrit un message d'attente + un battement toutes les ~30 s : sinon le
# log reste VIDE pendant tout le boot émulé et paraît « bloqué ».
msg_wait = t("Waiting for the VM to start (boot + cloud-init)")
msg_slow = t("(an emulated architecture can be slow; this is normal)")
msg_ready = t("VM ready - starting the ERPLibre install")
wrapper = (
f"echo {shlex.quote('== ' + msg_wait + ' ==')} >> {log_q}; "
f"echo {shlex.quote(' ' + msg_slow)} >> {log_q}; "
f"for i in $(seq 1 240); do "
f"st=$(ssh {SSH_OPTS} -o BatchMode=yes erplibre@{ip} "
f"{shlex.quote(ci_probe)} 2>/dev/null); "
f'case "$st" in '
f"*done*|*disabled*|*error*|*degraded*|*nocloudinit*) break;; "
f"esac; "
f'if [ $((i % 6)) -eq 0 ]; then echo " ... $((i*5))s" >> {log_q}; fi; '
f"sleep 5; done; "
f"echo {shlex.quote('== ' + msg_ready + ' ==')} >> {log_q}; "
f"ssh {SSH_OPTS} erplibre@{ip} {shlex.quote(remote_cmd)} "
f">> {log_q} 2>&1; "
f'echo "{EXIT_MARKER} $?" >> {log_q}'
)
# setsid -f : le process survit à la fermeture du menu / du dashboard.
subprocess.Popen(
["setsid", "-f", "bash", "-c", wrapper],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
def _log_header(vm: dict, branch: str, when: str) -> str:
"""En-tête du log : date, VM, distribution, version, architecture, branche.
Permet d'identifier l'installation d'un coup d'œil (et de ne jamais laisser
le log vide pendant l'attente du boot)."""
distro = vm.get("distro") or "?"
version = vm.get("version") or ""
arch = vm.get("arch") or "?"
bar = "=" * 64
return (
f"{bar}\n"
f" ERPLibre — {t('installation')}\n"
f" Date : {when}\n"
f" VM : {vm['name']}\n"
f" Distribution : {distro} {version}\n"
f" Architecture : {arch}\n"
f" Branche : {branch}\n"
f" IP : {vm['ip']}\n"
f"{bar}\n\n"
)
def launch_installs(vms: list[dict], branch: str, remote_cmd: str) -> str:
"""vms : [{name, ip, distro?, version?, arch?}]. Lance chaque install
détachée, écrit un manifeste et retourne son chemin. remote_cmd : script
exécuté dans chaque VM."""
sdir = session_dir()
stamp = time.strftime("%Y%m%d-%H%M%S")
when = time.strftime("%Y-%m-%d %H:%M:%S")
logdir = sdir / stamp
logdir.mkdir(parents=True, exist_ok=True)
entries = []
for vm in vms:
log_path = str(logdir / f"{vm['name']}.log")
# En-tête d'emblée (date/distro/version/arch) : le log n'est jamais
# vide, l'utilisateur voit tout de suite QUOI s'installe.
Path(log_path).write_text(_log_header(vm, branch, when))
_launch_one(vm["ip"], remote_cmd, log_path)
entries.append(
{
"name": vm["name"],
"ip": vm["ip"],
"distro": vm.get("distro"),
"version": vm.get("version"),
"arch": vm.get("arch"),
"log": log_path,
"ssh": f"ssh erplibre@{vm['ip']}",
}
)
manifest = {
"branch": branch,
"started": time.time(),
"vms": entries,
}
manifest_path = str(logdir / "session.json")
Path(manifest_path).write_text(json.dumps(manifest, indent=2))
return manifest_path
def read_status(log_path: str) -> tuple[str, int | None]:
"""(état, code) d'un log : pending / running / done / failed."""
try:
data = Path(log_path).read_text(errors="replace")
except OSError:
return "pending", None
if not data.strip():
return "pending", None
for line in reversed(data.splitlines()):
if EXIT_MARKER in line:
try:
code = int(line.split()[-1])
except ValueError:
code = 1
return ("done" if code == 0 else "failed"), code
return "running", None
# --------------------------------------------------------------------------- #
# Dashboard Textual
# --------------------------------------------------------------------------- #
def run_monitor(manifest_path: str) -> None:
"""Ouvre le dashboard Textual sur un manifeste d'installation."""
from textual.app import App, ComposeResult
from textual.containers import Horizontal
from textual.widgets import DataTable, Footer, Header, RichLog, Static
manifest = json.loads(Path(manifest_path).read_text())
started = manifest.get("started", time.time())
vms = manifest["vms"]
ICON = {
"pending": "⏳",
"running": "⏳",
"done": "✅",
"failed": "❌",
}
class Monitor(App):
CSS = """
DataTable { width: 40; border: solid $accent; }
RichLog { border: solid $accent; }
#sshbar { height: 2; color: $text-muted; }
"""
BINDINGS = [
("q", "quit", "Quitter (détaché)"),
("s", "ssh", "SSH"),
("f", "follow", "Suivre"),
("c", "copy_log", "Copier log"),
]
def __init__(self):
super().__init__()
self._offsets = {vm["name"]: 0 for vm in vms}
self._selected = vms[0]["name"] if vms else None
self._follow = True
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
table = DataTable(id="vms", cursor_type="row")
# Clés de colonnes explicites : update_cell() les référence.
table.add_column("VM", key="vm")
table.add_column("État", key="state")
table.add_column("Durée", key="elapsed")
for vm in vms:
table.add_row(vm["name"], "⏳", "00:00", key=vm["name"])
self._log = RichLog(id="log", highlight=False, markup=False)
with Horizontal():
yield table
yield self._log
yield Static("", id="sshbar")
yield Footer()
def on_mount(self) -> None:
self.title = "ERPLibre — suivi d'installation"
self._refresh_ssh()
self._load_selected_log(reset=True)
self.set_interval(1.0, self._tick)
# -- helpers -------------------------------------------------------- #
def _vm_by_name(self, name):
return next((v for v in vms if v["name"] == name), None)
def _refresh_ssh(self):
vm = self._vm_by_name(self._selected)
bar = self.query_one("#sshbar", Static)
if vm:
bar.update(
f" {vm['ssh']} (s = SSH · c = copier le log · "
"Maj+glisser = sélectionner)\n"
f" Log : {vm['log']}"
)
def _load_selected_log(self, reset=False):
vm = self._vm_by_name(self._selected)
if not vm:
return
if reset:
self._log.clear()
self._offsets[vm["name"]] = 0
try:
data = Path(vm["log"]).read_text(errors="replace")
except OSError:
return
off = self._offsets[vm["name"]]
new = data[off:]
if new:
for line in new.splitlines():
if EXIT_MARKER not in line:
self._log.write(line)
self._offsets[vm["name"]] = len(data)
def _tick(self):
table = self.query_one("#vms", DataTable)
for vm in vms:
state, code = read_status(vm["log"])
elapsed = time.time() - started
mm, ss = divmod(int(elapsed), 60)
label = ICON[state]
if state == "failed":
label = f"❌ ({code})"
elif state == "done":
label = "✅"
table.update_cell(vm["name"], "state", label)
if state in ("running", "pending"):
table.update_cell(
vm["name"], "elapsed", f"{mm:02d}:{ss:02d}"
)
if self._follow:
self._load_selected_log()
# -- events --------------------------------------------------------- #
def on_data_table_row_highlighted(self, event) -> None:
name = event.row_key.value
if name and name != self._selected:
self._selected = name
self._refresh_ssh()
self._load_selected_log(reset=True)
def action_follow(self) -> None:
self._follow = not self._follow
def action_ssh(self) -> None:
vm = self._vm_by_name(self._selected)
if not vm:
return
with self.suspend():
os.system(f"ssh {SSH_OPTS} erplibre@{vm['ip']} || true")
def action_copy_log(self) -> None:
"""Copie le log complet de la VM sélectionnée dans le
presse-papiers (OSC 52 ; marche aussi à travers SSH)."""
vm = self._vm_by_name(self._selected)
if not vm:
return
try:
text = Path(vm["log"]).read_text(errors="replace")
except OSError:
return
self.copy_to_clipboard(text)
self.notify(
f"Log de {vm['name']} copié ({len(text)} car.)",
title="Presse-papiers",
)
Monitor().run()

File diff suppressed because it is too large Load diff

View file

@ -1162,139 +1162,6 @@ TRANSLATIONS = {
"fr": "Version pour",
"en": "Version for",
},
"Architecture:": {
"fr": "Architecture :",
"en": "Architecture:",
},
"native": {
"fr": "native",
"en": "native",
},
"IBM Z — emulated, slow on x86; Ubuntu only": {
"fr": "IBM Z — émulé, lent sur x86 ; Ubuntu uniquement",
"en": "IBM Z — emulated, slow on x86; Ubuntu only",
},
"IBM Z — emulated, slow; Ubuntu only": {
"fr": "IBM Z — émulé, lent ; Ubuntu uniquement",
"en": "IBM Z — emulated, slow; Ubuntu only",
},
"ARM 64-bit — emulated, slow": {
"fr": "ARM 64 bits — émulé, lent",
"en": "ARM 64-bit — emulated, slow",
},
"emulated, slow": {
"fr": "émulé, lent",
"en": "emulated, slow",
},
"This architecture is emulated (TCG): boot and install are"
" much slower than the native one.": {
"fr": "Cette architecture est émulée (TCG) : le boot et l'installation"
" sont bien plus lents que l'architecture native.",
"en": "This architecture is emulated (TCG): boot and install are"
" much slower than the native one.",
},
"images for this arch only exist for:": {
"fr": "images pour cette architecture disponibles seulement pour :",
"en": "images for this arch only exist for:",
},
"All supported architectures": {
"fr": "Toutes les architectures supportées",
"en": "All supported architectures",
},
"installation": {
"fr": "installation",
"en": "installation",
},
"Resources per VM (x1 = catalog minimum):": {
"fr": "Ressources par VM (x1 = minimum catalogue) :",
"en": "Resources per VM (x1 = catalog minimum):",
},
"Host:": {
"fr": "Hôte :",
"en": "Host:",
},
"free": {
"fr": "libres",
"en": "free",
},
"free RAM unknown": {
"fr": "RAM libre inconnue",
"en": "free RAM unknown",
},
"> host free RAM": {
"fr": "> RAM libre de l'hôte",
"en": "> host free RAM",
},
"total RAM": {
"fr": "RAM totale",
"en": "total RAM",
},
"Choice (1-4, default 1):": {
"fr": "Choix (1-4, défaut 1) :",
"en": "Choice (1-4, default 1):",
},
"(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)",
},
"Waiting for the VM to start (boot + cloud-init)": {
"fr": "En attente du démarrage de la VM (boot + cloud-init)",
"en": "Waiting for the VM to start (boot + cloud-init)",
},
"(an emulated architecture can be slow; this is normal)": {
"fr": "(une architecture émulée peut être lente ; c'est normal)",
"en": "(an emulated architecture can be slow; this is normal)",
},
"VM ready - starting the ERPLibre install": {
"fr": "VM prête — installation ERPLibre en cours",
"en": "VM ready - starting the ERPLibre install",
},
"Choice (number or name, blank = native):": {
"fr": "Choix (numéro ou nom, vide = native) :",
"en": "Choice (number or name, blank = native):",
},
"Pick exact versions (comma-separated list)": {
"fr": "Choisir des versions précises (liste séparée par des virgules)",
"en": "Pick exact versions (comma-separated list)",
},
"Selection (numbers, 'all', 'principal' or 'granulaire',"
" default: all): ": {
"fr": "Sélection (numéros, « all », « principal » ou « granulaire »,"
" défaut : all) : ",
"en": "Selection (numbers, 'all', 'principal' or 'granulaire',"
" default: all): ",
},
"All versions:": {
"fr": "Toutes les versions :",
"en": "All versions:",
},
"Selection (comma-separated numbers): ": {
"fr": "Sélection (numéros séparés par des virgules) : ",
"en": "Selection (comma-separated numbers): ",
},
"s390x images only exist for:": {
"fr": "images s390x disponibles seulement pour :",
"en": "s390x images only exist for:",
},
"ignored:": {
"fr": "ignoré :",
"en": "ignored:",
},
"IBM Z — emulated, slow on x86": {
"fr": "IBM Z — émulé, lent sur x86",
"en": "IBM Z — emulated, slow on x86",
},
"Choice (number or name, blank = amd64):": {
"fr": "Choix (numéro ou nom, vide = amd64) :",
"en": "Choice (number or name, blank = amd64):",
},
"s390x is emulated (TCG): boot and install are much "
"slower than x86.": {
"fr": "s390x est émulé (TCG) : le boot et l'installation sont bien "
"plus lents que x86.",
"en": "s390x is emulated (TCG): boot and install are much "
"slower than x86.",
},
"Choice (number or version, blank = default):": {
"fr": "Choix (numéro ou version, vide = défaut) :",
"en": "Choice (number or version, blank = default):",
@ -1347,102 +1214,6 @@ TRANSLATIONS = {
"fr": "Effacement terminé.",
"en": "Deletion done.",
},
"Clean up QEMU (orphan files)": {
"fr": "Nettoyer QEMU (fichiers orphelins)",
"en": "Clean up QEMU (orphan files)",
},
"Scanning for orphan QEMU files...": {
"fr": "Recherche des fichiers QEMU orphelins...",
"en": "Scanning for orphan QEMU files...",
},
"orphan disk": {
"fr": "disque orphelin",
"en": "orphan disk",
},
"orphan seed": {
"fr": "seed orphelin",
"en": "orphan seed",
},
"partial download": {
"fr": "téléchargement interrompu",
"en": "partial download",
},
"orphan UEFI nvram": {
"fr": "nvram UEFI orpheline",
"en": "orphan UEFI nvram",
},
"No orphan files found.": {
"fr": "Aucun fichier orphelin trouvé.",
"en": "No orphan files found.",
},
"Orphan files:": {
"fr": "Fichiers orphelins :",
"en": "Orphan files:",
},
"Total:": {
"fr": "Total :",
"en": "Total:",
},
"files": {
"fr": "fichiers",
"en": "files",
},
"Delete these orphan files? (y/N): ": {
"fr": "Effacer ces fichiers orphelins ? (o/N) : ",
"en": "Delete these orphan files? (y/N): ",
},
"Cleanup done.": {
"fr": "Nettoyage terminé.",
"en": "Cleanup done.",
},
"Cached base images (reusable):": {
"fr": "Images de base en cache (réutilisables) :",
"en": "Cached base images (reusable):",
},
"Also delete the cached base images? (y/N): ": {
"fr": "Effacer aussi les images de base en cache ? (o/N) : ",
"en": "Also delete the cached base images? (y/N): ",
},
"All cached base images (reusable):": {
"fr": "Toutes les images de base en cache (réutilisables) :",
"en": "All cached base images (reusable):",
},
"Delete ALL cached base images? (y/N): ": {
"fr": "Effacer TOUTES les images de base en cache ? (o/N) : ",
"en": "Delete ALL cached base images? (y/N): ",
},
"Ghost domains (defined but disk missing):": {
"fr": "Domaines fantômes (définis, disque manquant) :",
"en": "Ghost domains (defined but disk missing):",
},
"Undefine these ghost domains? (y/N): ": {
"fr": "Supprimer la définition de ces domaines ? (o/N) : ",
"en": "Undefine these ghost domains? (y/N): ",
},
"Stale codename-named Ubuntu images (duplicates):": {
"fr": "Images Ubuntu nommées par codename (doublons) :",
"en": "Stale codename-named Ubuntu images (duplicates):",
},
"Delete these duplicate images? (y/N): ": {
"fr": "Effacer ces images en double ? (o/N) : ",
"en": "Delete these duplicate images? (y/N): ",
},
"Orphan ~/.ssh/config entries:": {
"fr": "Entrées ~/.ssh/config orphelines :",
"en": "Orphan ~/.ssh/config entries:",
},
"Remove these ~/.ssh/config entries? (y/N): ": {
"fr": "Retirer ces entrées de ~/.ssh/config ? (o/N) : ",
"en": "Remove these ~/.ssh/config entries? (y/N): ",
},
"Stale DHCP leases (no matching VM):": {
"fr": "Baux DHCP périmés (aucune VM correspondante) :",
"en": "Stale DHCP leases (no matching VM):",
},
"Clear these stale leases? (y/N): ": {
"fr": "Effacer ces baux périmés ? (o/N) : ",
"en": "Clear these stale leases? (y/N): ",
},
"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)",
@ -1463,14 +1234,6 @@ TRANSLATIONS = {
"fr": "Tout le catalogue (chaque version)",
"en": "Whole catalog (every version)",
},
"The main version of each distro (marked *)": {
"fr": "La version principale de chaque distro (marquée *)",
"en": "The main version of each distro (marked *)",
},
"Selection (numbers, 'all' or 'principal', default: all): ": {
"fr": "Sélection (numéros, « all » ou « principal », défaut : all) : ",
"en": "Selection (numbers, 'all' or 'principal', default: all): ",
},
"Selection (numbers, or 'all', default: all): ": {
"fr": "Sélection (numéros, ou « all », défaut : all) : ",
"en": "Selection (numbers, or 'all', default: all): ",
@ -1524,70 +1287,6 @@ TRANSLATIONS = {
"fr": "Clonage d'ERPLibre sur chaque VM",
"en": "Cloning ERPLibre on each VM",
},
"Installing ERPLibre on each VM": {
"fr": "Installation d'ERPLibre sur chaque VM",
"en": "Installing ERPLibre on each VM",
},
"Install ERPLibre into ~/git/erplibre on this VM? (y/N): ": {
"fr": "Installer ERPLibre dans ~/git/erplibre sur cette VM ? (o/N) : ",
"en": "Install ERPLibre into ~/git/erplibre on this VM? (y/N): ",
},
"installing ERPLibre": {
"fr": "installation d'ERPLibre",
"en": "installing ERPLibre",
},
"no IP obtained, ERPLibre install skipped.": {
"fr": "aucune IP obtenue, installation ERPLibre ignorée.",
"en": "no IP obtained, ERPLibre install skipped.",
},
"waiting for SSH...": {
"fr": "attente du SSH...",
"en": "waiting for SSH...",
},
"SSH not reachable, ERPLibre install skipped.": {
"fr": "SSH injoignable, installation ERPLibre ignorée.",
"en": "SSH not reachable, ERPLibre install skipped.",
},
"Interactive monitoring dashboard? (y/N): ": {
"fr": "Suivi interactif (dashboard) ? (o/N) : ",
"en": "Interactive monitoring dashboard? (y/N): ",
},
"resolving IP...": {
"fr": "résolution de l'IP...",
"en": "resolving IP...",
},
"no IP, skipped.": {
"fr": "pas d'IP, ignorée.",
"en": "no IP, skipped.",
},
"No VM to install.": {
"fr": "Aucune VM à installer.",
"en": "No VM to install.",
},
"Opening the interactive monitor...": {
"fr": "Ouverture du suivi interactif...",
"en": "Opening the interactive monitor...",
},
"Monitor closed. Installs keep running in the background.": {
"fr": "Suivi fermé. Les installations continuent en arrière-plan.",
"en": "Monitor closed. Installs keep running in the background.",
},
"Logs:": {
"fr": "Logs :",
"en": "Logs:",
},
"Read the logs:": {
"fr": "Lire les logs :",
"en": "Read the logs:",
},
"Log files:": {
"fr": "Fichiers de log :",
"en": "Log files:",
},
"Install textual for the dashboard (pip).": {
"fr": "Installez textual pour le dashboard (pip).",
"en": "Install textual for the dashboard (pip).",
},
"Parallel deployments (default:": {
"fr": "Déploiements en parallèle (défaut :",
"en": "Parallel deployments (default:",
@ -1660,17 +1359,9 @@ TRANSLATIONS = {
"fr": "vCPU (défaut : 2) : ",
"en": "vCPUs (default: 2): ",
},
"Disk size (blank = version min; e.g. 30G, 1T): ": {
"fr": "Taille du disque (vide = min. version ; ex. 30G, 1T) : ",
"en": "Disk size (blank = version min; e.g. 30G, 1T): ",
},
"VM name (default: ": {
"fr": "Nom de la VM (défaut : ",
"en": "VM name (default: ",
},
"Deployment failed (see the error above).": {
"fr": "Échec du déploiement (voir l'erreur ci-dessus).",
"en": "Deployment failed (see the error above).",
"Disk size (blank = version minimum): ": {
"fr": "Taille du disque (vide = minimum de la version) : ",
"en": "Disk size (blank = version minimum): ",
},
"SSH public key path": {
"fr": "Chemin de la clé publique SSH",