Compare commits
37 commits
2b7e20493a
...
5a408f1061
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a408f1061 | |||
| b49e107b83 | |||
| 7bd44d21a9 | |||
| 939e26ba83 | |||
| b395e7c3ed | |||
| 50617e3909 | |||
| bd94c277d3 | |||
| 0930f30cd3 | |||
| 88dc1d5409 | |||
| 8652c30507 | |||
| 3bf8fc2972 | |||
| 374373cc7c | |||
| 63e7b6af8c | |||
| 482a914ab8 | |||
| 37a999b911 | |||
| deddaeb6a9 | |||
| 137f4245ec | |||
| 7a001a9b84 | |||
| 69d87f93c3 | |||
| ea3f9c00cf | |||
| 18d3f4461c | |||
| 44d64fe87a | |||
| 3cb6f45269 | |||
| 7def76eb62 | |||
| 84e383a3b1 | |||
| 4a821226fb | |||
| 0830c895eb | |||
| d4d3d801c3 | |||
| 62bf80d9b1 | |||
| 8d8d0750b0 | |||
| 83a28ed9e8 | |||
| 83f71561f8 | |||
| 0aa84ed1e3 | |||
| 65a0d6f907 | |||
| 6ac7624950 | |||
| c0ceb6bea7 | |||
| d83d244805 |
17 changed files with 2225 additions and 198 deletions
|
|
@ -33,3 +33,7 @@ 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}"
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ openai
|
|||
humanize
|
||||
requests
|
||||
urwid
|
||||
textual
|
||||
python-dotenv
|
||||
python-dateutil
|
||||
unidecode
|
||||
|
|
|
|||
|
|
@ -1,68 +1,126 @@
|
|||
#!/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.
|
||||
|
||||
install_package() {
|
||||
local package_name=$1
|
||||
. ./env_var.sh
|
||||
|
||||
# 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
|
||||
}
|
||||
EL_USER=${USER}
|
||||
|
||||
# Odoo installation
|
||||
install_package postgis
|
||||
install_package postgresql
|
||||
install_package mariadb
|
||||
install_package libev
|
||||
install_package wkhtmltopdf
|
||||
install_package freetds
|
||||
# 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
|
||||
|
||||
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
|
||||
#--------------------------------------------------
|
||||
# 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
|
||||
|
||||
#--------------------------------------------------
|
||||
# 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
|
||||
|
||||
echo -e "\n---- Update NPM ----"
|
||||
install_package npm
|
||||
#--------------------------------------------------
|
||||
# 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
|
||||
|
||||
sudo npm install npm@latest -g
|
||||
#--------------------------------------------------
|
||||
# Node.js + npm (rtlcss, less)
|
||||
#--------------------------------------------------
|
||||
echo -e "\n---- Installing nodeJS NPM and rtlcss ----"
|
||||
${PAC} nodejs npm
|
||||
retVal=$?
|
||||
if [[ $retVal -ne 0 ]]; then
|
||||
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."
|
||||
echo "pacman 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
|
||||
retVal=$?
|
||||
if [[ $retVal -ne 0 ]]; then
|
||||
echo "npm install prettier + plugin-xml installation error."
|
||||
exit 1
|
||||
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)."
|
||||
fi
|
||||
|
||||
# TODO install nginx
|
||||
|
||||
# ERPLibre installation
|
||||
install_package cmake
|
||||
install_package parallel
|
||||
install_package tk
|
||||
install_package shfmt
|
||||
|
||||
# For erplibre_devops
|
||||
install_package sshpass
|
||||
#--------------------------------------------------
|
||||
# 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
|
||||
|
||||
echo -e "\n---- Arch Linux dependency installation done ----"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
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,
|
||||
|
|
@ -55,25 +60,30 @@ echo -e "\n---- Update Server ----"
|
|||
|
||||
if [ "18.04" == "${UBUNTU_VERSION}" ]; then
|
||||
# add-apt-repository can install add-apt-repository Ubuntu 18.x
|
||||
sudo apt-get install software-properties-common curl -y
|
||||
${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"
|
||||
sudo apt-get update
|
||||
sudo apt-get upgrade -y
|
||||
${APT_GET} update
|
||||
${APT_GET} upgrade -y
|
||||
fi
|
||||
|
||||
#--------------------------------------------------
|
||||
# Install PostgreSQL Server
|
||||
#--------------------------------------------------
|
||||
echo -e "\n---- Install PostgreSQL Server ----"
|
||||
sudo apt-get install postgresql libpq-dev postgis -y
|
||||
${APT_GET} install postgresql postgresql-contrib libpq-dev -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
|
||||
|
|
@ -82,20 +92,20 @@ sudo su - postgres -c "createuser -s ${EL_USER}" 2>/dev/null || true
|
|||
# Install Dependencies
|
||||
#--------------------------------------------------
|
||||
echo -e "\n--- Installing debian dependency --"
|
||||
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
|
||||
${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
|
||||
sudo apt-get install libmariadbd-dev freetds-dev -y
|
||||
${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
|
||||
sudo apt-get install libpng12-0 -y
|
||||
${APT_GET} install libpng12-0 -y
|
||||
retVal=$?
|
||||
if [[ $retVal -ne 0 ]]; then
|
||||
echo "apt-get libpng installation error."
|
||||
|
|
@ -103,14 +113,14 @@ if [ "18.04" == "${UBUNTU_VERSION}" ]; then
|
|||
fi
|
||||
fi
|
||||
# Dependencies for pyenv
|
||||
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
|
||||
${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
|
||||
sudo apt-get install libcairo2-dev python3-dev pkg-config libxt-dev libgirepository1.0-dev -y
|
||||
${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."
|
||||
|
|
@ -118,8 +128,8 @@ if [[ $retVal -ne 0 ]]; then
|
|||
fi
|
||||
|
||||
echo -e "\n---- Installing nodeJS NPM and rtlcss for LTR support ----"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ca-certificates curl gnupg
|
||||
${APT_GET} update
|
||||
${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
|
||||
|
||||
|
|
@ -132,8 +142,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
|
||||
sudo apt-get update
|
||||
sudo apt-get install nodejs -y
|
||||
${APT_GET} update
|
||||
${APT_GET} install nodejs -y
|
||||
|
||||
sudo npm install npm@latest -g
|
||||
retVal=$?
|
||||
|
|
@ -177,7 +187,12 @@ fi
|
|||
#--------------------------------------------------
|
||||
# Install Wkhtmltopdf if needed
|
||||
#--------------------------------------------------
|
||||
if [ ${EL_INSTALL_WKHTMLTOPDF} = "True" ]; then
|
||||
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
|
||||
echo -e "\n---- Installing wkhtml ----"
|
||||
INSTALLED=$(dpkg -s wkhtmltox | grep installed)
|
||||
if [ "" == "${INSTALLED}" ]; then
|
||||
|
|
|
|||
|
|
@ -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" ]]; 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
|
||||
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 : ${VERSION_ID}"
|
||||
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}"
|
||||
fi
|
||||
elif [[ "${ID}" == "linuxmint" ]]; then
|
||||
if [[ "${VERSION_ID}" == "22.3" ]]; then
|
||||
|
|
@ -20,9 +20,12 @@ 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 ."
|
||||
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."
|
||||
fi
|
||||
elif [[ "${OSTYPE}" == "darwin"* ]]; then
|
||||
echo "\n---- Darwin installation process started ----"
|
||||
|
|
|
|||
129
script/install/install_fedora_dependency.sh
Executable file
129
script/install/install_fedora_dependency.sh
Executable file
|
|
@ -0,0 +1,129 @@
|
|||
#!/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 ----"
|
||||
|
|
@ -22,6 +22,14 @@ 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
|
||||
|
|
@ -83,7 +91,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 -vvv
|
||||
poetry install --no-root ${POETRY_VERBOSE}
|
||||
fi
|
||||
retVal=$?
|
||||
if [[ $retVal -ne 0 ]]; then
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
. ./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"
|
||||
|
||||
|
|
@ -10,4 +18,4 @@
|
|||
|
||||
# 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 -v
|
||||
.venv.erplibre/bin/repo sync ${REPO_VERBOSE}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
#!/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 --verbose &
|
||||
git daemon --base-path=. --export-all --reuseaddr --informative-errors ${DAEMON_VERBOSE} &
|
||||
DAEMON_PID=$!
|
||||
|
||||
if [ -L "$EL_MANIFEST_DEV" ]; then
|
||||
|
|
@ -24,6 +32,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" -v -m ${MANIFEST_TARGET}
|
||||
.venv.erplibre/bin/repo sync -c -j "$JOBS" ${REPO_VERBOSE} -m ${MANIFEST_TARGET}
|
||||
|
||||
kill ${DAEMON_PID}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,19 @@
|
|||
|
||||
. ./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 --verbose &
|
||||
git daemon --base-path=. --export-all --reuseaddr --informative-errors ${DAEMON_VERBOSE} &
|
||||
DAEMON_PID=$!
|
||||
|
||||
if [ -L "$EL_MANIFEST_DEV" ]; then
|
||||
|
|
@ -25,6 +33,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" -v -m ${MANIFEST_TARGET}
|
||||
.venv.erplibre/bin/repo sync -c -j "$JOBS" ${REPO_VERBOSE} -m ${MANIFEST_TARGET}
|
||||
|
||||
kill ${DAEMON_PID}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
#!/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 --verbose &
|
||||
git daemon --base-path=. --export-all --reuseaddr --informative-errors ${DAEMON_VERBOSE} &
|
||||
DAEMON_PID=$!
|
||||
|
||||
if [ -L "$EL_MANIFEST_DEV" ]; then
|
||||
|
|
@ -24,6 +32,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" -v -m ${MANIFEST_TARGET}
|
||||
.venv.erplibre/bin/repo sync -c -j "$JOBS" ${REPO_VERBOSE} -m ${MANIFEST_TARGET}
|
||||
|
||||
kill ${DAEMON_PID}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,19 @@
|
|||
|
||||
. ./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 --verbose &
|
||||
git daemon --base-path=. --export-all --reuseaddr --informative-errors ${DAEMON_VERBOSE} &
|
||||
DAEMON_PID=$!
|
||||
|
||||
if [ -L "$EL_MANIFEST_DEV" ]; then
|
||||
|
|
@ -25,6 +33,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" -v -m ${MANIFEST_TARGET}
|
||||
.venv.erplibre/bin/repo sync -c -j "$JOBS" ${REPO_VERBOSE} -m ${MANIFEST_TARGET}
|
||||
|
||||
kill ${DAEMON_PID}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
. ./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"
|
||||
|
||||
|
|
@ -16,4 +24,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 -v -j "$JOBS"
|
||||
.venv.erplibre/bin/repo sync ${REPO_VERBOSE} -j "$JOBS"
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import hashlib
|
|||
import os
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
|
@ -90,19 +91,60 @@ 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
|
||||
|
|
@ -159,6 +201,9 @@ 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}")
|
||||
|
||||
|
||||
|
|
@ -193,6 +238,8 @@ 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"
|
||||
|
||||
|
||||
|
|
@ -375,6 +422,44 @@ 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[
|
||||
|
|
@ -583,6 +668,64 @@ 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
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
|
@ -623,6 +766,15 @@ 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(
|
||||
|
|
@ -790,15 +942,19 @@ 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'}")
|
||||
|
||||
# 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é
|
||||
# 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]
|
||||
# Active et démarre SSH quel que soit le nom du service (ssh sur
|
||||
# Debian/Ubuntu, sshd sur Fedora) — sans quoi la VM peut booter sans SSH.
|
||||
# Debian/Ubuntu, sshd sur Fedora/Arch) — sans quoi la VM peut booter
|
||||
# sans SSH accessible.
|
||||
lines += [
|
||||
"runcmd:",
|
||||
" - systemctl enable --now ssh 2>/dev/null"
|
||||
|
|
@ -810,10 +966,13 @@ 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"
|
||||
" primary:\n"
|
||||
" eth0:\n"
|
||||
" match:\n"
|
||||
' name: "e*"\n'
|
||||
" dhcp4: true\n"
|
||||
|
|
@ -957,6 +1116,11 @@ 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",
|
||||
|
|
@ -973,6 +1137,7 @@ 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",
|
||||
|
|
@ -982,21 +1147,64 @@ def virt_install(
|
|||
"--graphics",
|
||||
args.graphics,
|
||||
"--console",
|
||||
"pty,target_type=serial",
|
||||
f"pty,target_type={console_target}",
|
||||
]
|
||||
# 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 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"]
|
||||
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."""
|
||||
"""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 »)."""
|
||||
base = (["sudo"] if use_sudo else []) + [
|
||||
"virsh",
|
||||
"domifaddr",
|
||||
|
|
@ -1005,13 +1213,15 @@ 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)
|
||||
m = re.search(r"(\d+\.\d+\.\d+\.\d+)", res.stdout)
|
||||
if m:
|
||||
return m.group(1)
|
||||
ips = re.findall(r"(\d+\.\d+\.\d+\.\d+)", res.stdout)
|
||||
for ip in ips:
|
||||
if _ip_reachable(ip):
|
||||
return ip
|
||||
time.sleep(3)
|
||||
return None
|
||||
return ips[-1] if ips else None
|
||||
|
||||
|
||||
def ssh_command(user: str, ip: str, has_key: bool) -> str:
|
||||
|
|
@ -1070,7 +1280,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
g_img.add_argument(
|
||||
"--arch",
|
||||
default="amd64",
|
||||
help="Architecture de l'image (défaut : 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).",
|
||||
)
|
||||
g_img.add_argument(
|
||||
"--image-dir",
|
||||
|
|
@ -1306,6 +1518,14 @@ 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
|
||||
|
|
@ -1315,6 +1535,10 @@ 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
|
||||
)
|
||||
|
|
@ -1373,6 +1597,18 @@ 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)
|
||||
|
|
|
|||
315
script/todo/qemu_install_monitor.py
Normal file
315
script/todo/qemu_install_monitor.py
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
#!/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()
|
||||
1091
script/todo/todo.py
1091
script/todo/todo.py
File diff suppressed because it is too large
Load diff
|
|
@ -1162,6 +1162,139 @@ 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):",
|
||||
|
|
@ -1214,6 +1347,102 @@ 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)",
|
||||
|
|
@ -1234,6 +1463,14 @@ 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): ",
|
||||
|
|
@ -1287,6 +1524,70 @@ 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:",
|
||||
|
|
@ -1359,9 +1660,17 @@ TRANSLATIONS = {
|
|||
"fr": "vCPU (défaut : 2) : ",
|
||||
"en": "vCPUs (default: 2): ",
|
||||
},
|
||||
"Disk size (blank = version minimum): ": {
|
||||
"fr": "Taille du disque (vide = minimum de la version) : ",
|
||||
"en": "Disk size (blank = version minimum): ",
|
||||
"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).",
|
||||
},
|
||||
"SSH public key path": {
|
||||
"fr": "Chemin de la clé publique SSH",
|
||||
|
|
|
|||
Loading…
Reference in a new issue