[FIX] install Arch (pacman/base-devel) + attente fin cloud-init (Fedora)

Deux échecs d'install ERPLibre sur VM constatés dans les logs :

Arch — install_arch_linux.sh utilisait « yay » (helper AUR ABSENT d'une
image cloud et qui refuse de tourner en root) et n'installait JAMAIS
base-devel : sur une image cloud fraîche rien ne s'installait, donc pas de
compilateur C -> pyenv : « no acceptable C compiler found » -> build de
Python 3.12 échoué -> pas de venv -> pip/poetry/repo absents. Réécrit avec
pacman (dépôts officiels) : base-devel (gcc/make), deps de build pyenv
(openssl/zlib/xz/tk/…), PostgreSQL (+ initdb, Arch ne l'initialise pas),
Node/npm, deps Odoo en best-effort paquet par paquet (pacman refuse toute
la transaction sur un seul nom inconnu). wkhtmltopdf (AUR) : ignoré
proprement.

Fedora — « Connection closed by remote host » (exit 255) dès le début :
« cloud-init status --wait » tournait dans une session SSH UNIQUE tuée
quand cloud-init régénère les clés d'hôte et REDÉMARRE sshd au 1er boot.
On attend désormais la FIN de cloud-init via des connexions COURTES
successives (chaque tentative survit à un redémarrage de sshd), AVANT de
lancer l'install — dans les deux chemins : _qemu_wait_ssh (streamé) et
_launch_one (détaché/monitoré). On matche sur le TEXTE de « cloud-init
status » (done/disabled/error/degraded) car son code de sortie n'est pas
fiable selon la version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathieu Benoit 2026-07-28 07:19:17 +00:00
parent 482a914ab8
commit 63e7b6af8c
3 changed files with 147 additions and 62 deletions

View file

@ -1,68 +1,122 @@
#!/usr/bin/env bash #!/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() { . ./env_var.sh
local package_name=$1
# Check package is already installed EL_USER=${USER}
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
}
# Odoo installation # pacman résilient : --needed saute ce qui est déjà là, --noconfirm en non
install_package postgis # interactif. On rafraîchit d'abord la base (best-effort : le remote cmd de
install_package postgresql # todo a déjà pu lancer reflector + -Syy).
install_package mariadb PAC="sudo pacman -S --needed --noconfirm"
install_package libev sudo pacman -Sy --noconfirm || true
install_package wkhtmltopdf
install_package freetds
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 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=$? retVal=$?
if [[ $retVal -ne 0 ]]; then if [[ $retVal -ne 0 ]]; then
echo "npm install npm lastest installation error." echo "pacman nodejs 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 exit 1
fi fi
sudo npm install -g rtlcss less || echo "npm rtlcss/less: erreur (optionnel)."
echo -e "\n---- Test tool ----" echo -e "\n---- Test tool ----"
npm install npm install || echo "npm install (prettier/plugin-xml): erreur (optionnel)."
retVal=$?
if [[ $retVal -ne 0 ]]; then # Pour erplibre_devops.
echo "npm install prettier + plugin-xml installation error." ${PAC} sshpass || echo "sshpass non installé (optionnel)."
exit 1
#--------------------------------------------------
# nginx (optionnel)
#--------------------------------------------------
if [ "${EL_INSTALL_NGINX}" = "True" ]; then
echo -e "\n---- Installing nginx ----"
${PAC} nginx || echo "nginx: erreur (optionnel)."
fi fi
# TODO install nginx #--------------------------------------------------
# wkhtmltopdf (optionnel) — uniquement dans l'AUR sur Arch (pas de paquet
# ERPLibre installation # officiel). Sur une image cloud sans helper AUR, on saute proprement.
install_package cmake #--------------------------------------------------
install_package parallel if [ "${EL_INSTALL_WKHTMLTOPDF}" = "True" ]; then
install_package tk if ! command -v wkhtmltopdf >/dev/null 2>&1; then
install_package shfmt echo "wkhtmltopdf : disponible seulement via l'AUR sur Arch, ignoré"
echo " (installez-le manuellement avec un helper AUR si nécessaire)."
# For erplibre_devops else
install_package sshpass echo -e "\n---- Already installed wkhtml ----"
fi
fi
echo -e "\n---- Arch Linux dependency installation done ----"

View file

@ -37,11 +37,25 @@ def session_dir() -> Path:
def _launch_one(ip: str, remote_cmd: str, log_path: str) -> None: 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 """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.""" 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'à ~12 min).
# 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"
)
wrapper = ( wrapper = (
# Attente du sshd (jusqu'à ~5 min) pour éviter « Connection refused ». f"for i in $(seq 1 144); do "
f"for i in $(seq 1 60); do " f"st=$(ssh {SSH_OPTS} -o BatchMode=yes erplibre@{ip} "
f"ssh {SSH_OPTS} -o BatchMode=yes erplibre@{ip} true " f"{shlex.quote(ci_probe)} 2>/dev/null); "
f">/dev/null 2>&1 && break; sleep 5; done; " f'case "$st" in '
f"*done*|*disabled*|*error*|*degraded*|*nocloudinit*) break;; "
f"esac; sleep 5; done; "
f"ssh {SSH_OPTS} erplibre@{ip} {shlex.quote(remote_cmd)} " f"ssh {SSH_OPTS} erplibre@{ip} {shlex.quote(remote_cmd)} "
f"> {shlex.quote(log_path)} 2>&1; " f"> {shlex.quote(log_path)} 2>&1; "
f'echo "{EXIT_MARKER} $?" >> {shlex.quote(log_path)}' f'echo "{EXIT_MARKER} $?" >> {shlex.quote(log_path)}'

View file

@ -1677,28 +1677,45 @@ class TODO:
ERPLIBRE_EXTRA_DISK_GB = 5 ERPLIBRE_EXTRA_DISK_GB = 5
@staticmethod @staticmethod
def _qemu_wait_ssh(ip, user="erplibre", timeout=180): def _qemu_wait_ssh(ip, user="erplibre", timeout=720):
"""Attend que le SSH réponde (le sshd peut mettre du temps à démarrer """Attend que sshd réponde ET que cloud-init soit TERMINÉ, via des
après le boot, surtout sur Fedora). True si joignable.""" connexions COURTES successives. Au 1er boot, cloud-init régénère les
clés d'hôte et REDÉMARRE sshd : attendre la fin de cloud-init AVANT de
lancer l'install évite qu'une session longue soit tuée (« Connection
closed by remote host », exit 255 cas Fedora). True si prête."""
opts = ( opts = (
"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null " "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
"-o ConnectTimeout=8 -o BatchMode=yes" "-o ConnectTimeout=8 -o BatchMode=yes"
) )
# On imprime toujours l'état (|| true) pour matcher sur le TEXTE :
# « status: running » n'a pas de code de sortie fiable selon la version.
probe = (
"if command -v cloud-init >/dev/null 2>&1; then "
"cloud-init status 2>/dev/null || true; else echo nocloudinit; fi"
)
ready = ("done", "disabled", "error", "degraded", "nocloudinit")
deadline = time.time() + timeout deadline = time.time() + timeout
ssh_up = False
while time.time() < deadline: while time.time() < deadline:
try: try:
res = subprocess.run( res = subprocess.run(
f"ssh {opts} {user}@{ip} true", f"ssh {opts} {user}@{ip} {shlex.quote(probe)}",
shell=True, shell=True,
capture_output=True, capture_output=True,
timeout=15, timeout=20,
text=True,
) )
out = res.stdout or ""
except (OSError, subprocess.SubprocessError): except (OSError, subprocess.SubprocessError):
res = None out = ""
if res is not None and res.returncode == 0: if out.strip():
ssh_up = True # sshd a répondu
if any(k in out for k in ready):
return True return True
time.sleep(5) time.sleep(5)
return False # cloud-init pas confirmé fini dans le délai : on tente quand même si
# sshd répondait au moins (mieux qu'un abandon silencieux).
return ssh_up
def _qemu_erplibre_remote_cmd(self, branch): def _qemu_erplibre_remote_cmd(self, branch):
"""Script d'installation ERPLibre exécuté DANS la VM (curl/git/make """Script d'installation ERPLibre exécuté DANS la VM (curl/git/make