Compare commits
7 commits
bea9a82d75
...
808e37cd59
| Author | SHA1 | Date | |
|---|---|---|---|
| 808e37cd59 | |||
| 43d268e448 | |||
| 64c0bbc698 | |||
| 7904c72d71 | |||
| 1ebf2b353f | |||
| b99db0ea66 | |||
| fd3226a053 |
6 changed files with 270 additions and 146 deletions
|
|
@ -36,10 +36,14 @@ elif [[ "${OS}" == "Linuxmint" ]]; then
|
||||||
WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb
|
WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb
|
||||||
fi
|
fi
|
||||||
elif [[ "${OS}" == "Debian" ]]; then
|
elif [[ "${OS}" == "Debian" ]]; then
|
||||||
if [ "bookworm" == "${DEBIAN_VERSION}" ]; then
|
if [ "bullseye" == "${DEBIAN_VERSION}" ]; then
|
||||||
WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.bookworm_amd64.deb
|
|
||||||
else
|
|
||||||
WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.bullseye_amd64.deb
|
WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.bullseye_amd64.deb
|
||||||
|
else
|
||||||
|
# bookworm (12), trixie (13) et au-delà : wkhtmltopdf ne publie pas de
|
||||||
|
# build au-delà de « bookworm » -> on prend bookworm (le plus récent).
|
||||||
|
# Le build « bullseye » (Debian 11) échouait à s'installer sur trixie
|
||||||
|
# (gdebi : dépendances incompatibles).
|
||||||
|
WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.bookworm_amd64.deb
|
||||||
fi
|
fi
|
||||||
elif [[ "${OS}" == *"Ubuntu"* ]]; then
|
elif [[ "${OS}" == *"Ubuntu"* ]]; then
|
||||||
echo "Your version of Ubuntu is not supported, only support 18.04, 20.04 and 22.04"
|
echo "Your version of Ubuntu is not supported, only support 18.04, 20.04 and 22.04"
|
||||||
|
|
@ -218,11 +222,15 @@ elif [ ${EL_INSTALL_WKHTMLTOPDF} = "True" ]; then
|
||||||
sudo gdebi --n $(basename ${_url})
|
sudo gdebi --n $(basename ${_url})
|
||||||
retVal=$?
|
retVal=$?
|
||||||
if [[ $retVal -ne 0 ]]; then
|
if [[ $retVal -ne 0 ]]; then
|
||||||
echo "gdebi install wkhtmltopdf installation error."
|
# wkhtmltopdf est OPTIONNEL (rapports PDF). NON bloquant : sur Debian
|
||||||
exit 1
|
# 13 (trixie) le .deb dépendait de libssl1.1 (absent) et « exit 1 »
|
||||||
|
# faisait échouer TOUT install_os -> Odoo jamais installé. On avertit
|
||||||
|
# et on continue (comme Arch qui poursuit sans wkhtmltopdf).
|
||||||
|
echo "wkhtmltopdf : installation échouée, ignoré (optionnel — pas de PDF)."
|
||||||
|
else
|
||||||
|
sudo ln -fs /usr/local/bin/wkhtmltopdf /usr/bin
|
||||||
|
sudo ln -fs /usr/local/bin/wkhtmltoimage /usr/bin
|
||||||
fi
|
fi
|
||||||
sudo ln -fs /usr/local/bin/wkhtmltopdf /usr/bin
|
|
||||||
sudo ln -fs /usr/local/bin/wkhtmltoimage /usr/bin
|
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo -e "\n---- Already installed wkhtml ----"
|
echo -e "\n---- Already installed wkhtml ----"
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,12 @@ if [[ "${EL_PHASE}" != "setup" ]]; then
|
||||||
retVal=$?
|
retVal=$?
|
||||||
if [[ $retVal -ne 0 ]]; then
|
if [[ $retVal -ne 0 ]]; then
|
||||||
echo "Poetry installation error with status ${retVal}"
|
echo "Poetry installation error with status ${retVal}"
|
||||||
|
# « -q » masque la CAUSE. On rejoue en « -vvv » (debug) car c'est
|
||||||
|
# le SEUL niveau où Poetry affiche la sortie des sous-processus
|
||||||
|
# (git clone/checkout, build pip) — donc l'erreur réelle d'une
|
||||||
|
# dépendance VCS/build. Capturé dans le log pour diagnostic.
|
||||||
|
echo "---- Poetry: rejeu -vvv pour diagnostic ----"
|
||||||
|
poetry install --no-root -vvv 2>&1 || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,10 @@ DEBIAN_CLOUD_BASES: tuple[str, ...] = (
|
||||||
"https://laotzu.ftp.acc.umu.se/cdimage/cloud",
|
"https://laotzu.ftp.acc.umu.se/cdimage/cloud",
|
||||||
)
|
)
|
||||||
FEDORA_BASE = "https://download.fedoraproject.org/pub/fedora/linux/releases"
|
FEDORA_BASE = "https://download.fedoraproject.org/pub/fedora/linux/releases"
|
||||||
|
# Serveur MAÎTRE (pas de redirection MirrorManager) : repli fiable quand le
|
||||||
|
# redirecteur envoie sur un miroir incomplet (fréquent en déploiement
|
||||||
|
# PARALLÈLE : chaque requête peut tomber sur un miroir différent/désynchronisé).
|
||||||
|
FEDORA_BASE_MASTER = "https://dl.fedoraproject.org/pub/fedora/linux/releases"
|
||||||
|
|
||||||
# Répertoire de cache par défaut des images cloud (cohérent avec --disk-dir /
|
# Répertoire de cache par défaut des images cloud (cohérent avec --disk-dir /
|
||||||
# --seed-dir). L'écriture y nécessite root : le déploiement tourne de toute
|
# --seed-dir). L'écriture y nécessite root : le déploiement tourne de toute
|
||||||
|
|
@ -214,26 +218,38 @@ def image_candidates(
|
||||||
|
|
||||||
def resolve_fedora_url(version: str, arch: str, dry_run: bool) -> str:
|
def resolve_fedora_url(version: str, arch: str, dry_run: bool) -> str:
|
||||||
"""Résout l'URL du qcow2 « Fedora Cloud Base Generic » depuis l'index
|
"""Résout l'URL du qcow2 « Fedora Cloud Base Generic » depuis l'index
|
||||||
HTML des releases (Fedora ne publie pas de lien « latest »)."""
|
HTML des releases (Fedora ne publie pas de lien « latest »). ROBUSTE :
|
||||||
|
plusieurs tentatives sur le redirecteur puis repli sur le serveur maître
|
||||||
|
(dl.fedoraproject.org) — sinon, en déploiement parallèle, une requête
|
||||||
|
tombée sur un miroir incomplet faisait échouer la VM (« image introuvable »)
|
||||||
|
alors que l'image existe bel et bien."""
|
||||||
a = distro_arch("fedora", arch)
|
a = distro_arch("fedora", arch)
|
||||||
index = f"{FEDORA_BASE}/{version}/Cloud/{a}/images/"
|
|
||||||
pattern = re.compile(
|
pattern = re.compile(
|
||||||
rf"Fedora-Cloud-Base-Generic-{version}-[0-9.]+\.{a}\.qcow2"
|
rf"Fedora-Cloud-Base-Generic-{version}-[0-9.]+\.{a}\.qcow2"
|
||||||
)
|
)
|
||||||
if dry_run:
|
if dry_run:
|
||||||
|
index = f"{FEDORA_BASE}/{version}/Cloud/{a}/images/"
|
||||||
return index + f"Fedora-Cloud-Base-Generic-{version}-<build>.{a}.qcow2"
|
return index + f"Fedora-Cloud-Base-Generic-{version}-<build>.{a}.qcow2"
|
||||||
try:
|
# On essaie le redirecteur (2 fois : chaque requête peut viser un miroir
|
||||||
with urllib.request.urlopen(index, timeout=30) as resp: # noqa: S310
|
# différent) puis le serveur maître (toujours complet).
|
||||||
html = resp.read().decode(errors="replace")
|
bases = [FEDORA_BASE, FEDORA_BASE, FEDORA_BASE_MASTER]
|
||||||
except Exception as exc: # pragma: no cover - dépend du réseau
|
last_err = ""
|
||||||
sys.exit(f"Impossible de lister les images Fedora {version} : {exc}")
|
for base in bases:
|
||||||
names = sorted(set(pattern.findall(html)))
|
index = f"{base}/{version}/Cloud/{a}/images/"
|
||||||
if not names:
|
try:
|
||||||
sys.exit(
|
with urllib.request.urlopen(index, timeout=30) as resp: # noqa: S310
|
||||||
"Aucune image « Fedora-Cloud-Base-Generic » trouvée pour "
|
html = resp.read().decode(errors="replace")
|
||||||
f"Fedora {version} ({a}) dans {index}"
|
except Exception as exc: # pragma: no cover - dépend du réseau
|
||||||
)
|
last_err = str(exc)
|
||||||
return index + names[-1]
|
continue
|
||||||
|
names = sorted(set(pattern.findall(html)))
|
||||||
|
if names:
|
||||||
|
return index + names[-1]
|
||||||
|
sys.exit(
|
||||||
|
"Aucune image « Fedora-Cloud-Base-Generic » trouvée pour "
|
||||||
|
f"Fedora {version} ({a}) après plusieurs miroirs"
|
||||||
|
+ (f" (dernière erreur : {last_err})" if last_err else "")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def default_image_name(distro: str, code: str, arch: str, version: str) -> str:
|
def default_image_name(distro: str, code: str, arch: str, version: str) -> str:
|
||||||
|
|
@ -1241,7 +1257,17 @@ def virt_install(
|
||||||
cmd += ["--virt-type", "qemu"]
|
cmd += ["--virt-type", "qemu"]
|
||||||
if not args.attach_console:
|
if not args.attach_console:
|
||||||
cmd.append("--noautoconsole")
|
cmd.append("--noautoconsole")
|
||||||
runner.run(cmd, privileged=True)
|
# virtinst écrit un journal de debug dans ~/.cache/virt-manager ; sous
|
||||||
|
# sudo, HOME/cache peut être inaccessible -> l'écriture échoue et Python
|
||||||
|
# déverse un « Logging error » (le pavé « Fetched capabilities … »). On
|
||||||
|
# force un cache/HOME ÉCRIVABLE via « env VAR=… » (traverse sudo) pour
|
||||||
|
# que le journal s'écrive silencieusement.
|
||||||
|
log_env = [
|
||||||
|
"env",
|
||||||
|
"XDG_CACHE_HOME=/var/tmp/erplibre-virtinst",
|
||||||
|
"HOME=/var/tmp/erplibre-virtinst",
|
||||||
|
]
|
||||||
|
runner.run(log_env + cmd, privileged=True)
|
||||||
|
|
||||||
|
|
||||||
def _ip_reachable(ip: str, port: int = 22, timeout: float = 3) -> bool:
|
def _ip_reachable(ip: str, port: int = 22, timeout: float = 3) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import json
|
||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -280,6 +281,17 @@ def scan_log_errors(log_path: str) -> tuple[int, int]:
|
||||||
return nerr, nwarn
|
return nerr, nwarn
|
||||||
|
|
||||||
|
|
||||||
|
def _port_open(ip: str, port: int = 8069, timeout: float = 0.5) -> bool:
|
||||||
|
"""Vrai si un TCP connect réussit (l'UI web Odoo écoute sur :8069)."""
|
||||||
|
if not ip:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
with socket.create_connection((ip, port), timeout=timeout):
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _read_new(path: str, offset: int) -> tuple[str, int]:
|
def _read_new(path: str, offset: int) -> tuple[str, int]:
|
||||||
"""Lit le log à partir de `offset` (lecture incrémentale). Renvoie
|
"""Lit le log à partir de `offset` (lecture incrémentale). Renvoie
|
||||||
(nouveau_texte, nouvel_offset). Bloquant -> à appeler dans un thread."""
|
(nouveau_texte, nouvel_offset). Bloquant -> à appeler dans un thread."""
|
||||||
|
|
@ -292,6 +304,32 @@ def _read_new(path: str, offset: int) -> tuple[str, int]:
|
||||||
return "", offset
|
return "", offset
|
||||||
|
|
||||||
|
|
||||||
|
def _read_tail(
|
||||||
|
path: str, max_bytes: int = 131072, max_lines: int = 1000
|
||||||
|
) -> tuple[str, int]:
|
||||||
|
"""Lit uniquement la FIN du log (dernier `max_bytes`, tronqué à
|
||||||
|
`max_lines` lignes) et renvoie (texte, TAILLE TOTALE du fichier). Utilisé
|
||||||
|
au CHANGEMENT de VM : lire+réafficher le fichier ENTIER (offset 0) gelait
|
||||||
|
l'UI sur les gros logs (250 Ko / milliers de lignes). L'offset renvoyé =
|
||||||
|
taille totale -> le suivi incrémental (_tick_log) continue depuis la fin."""
|
||||||
|
try:
|
||||||
|
size = os.path.getsize(path)
|
||||||
|
with open(path, "rb") as fh:
|
||||||
|
if size > max_bytes:
|
||||||
|
fh.seek(-max_bytes, os.SEEK_END)
|
||||||
|
raw = fh.read()
|
||||||
|
text = raw.decode(errors="replace")
|
||||||
|
# Si on a coupé au milieu d'une ligne, jeter la 1re ligne partielle.
|
||||||
|
if size > max_bytes and "\n" in text:
|
||||||
|
text = text.split("\n", 1)[1]
|
||||||
|
lines = text.splitlines()
|
||||||
|
if len(lines) > max_lines:
|
||||||
|
lines = lines[-max_lines:]
|
||||||
|
return "\n".join(lines), size
|
||||||
|
except OSError:
|
||||||
|
return "", 0
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
# Télémétrie, historique de durées (ETA), navigateur CLI
|
# Télémétrie, historique de durées (ETA), navigateur CLI
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
@ -576,7 +614,6 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
DataTable { width: 74; height: 1fr; overflow-x: auto; border: solid $accent; }
|
DataTable { width: 74; height: 1fr; overflow-x: auto; border: solid $accent; }
|
||||||
RichLog { border: solid $accent; }
|
RichLog { border: solid $accent; }
|
||||||
#telemetry { height: 1; color: $text-muted; }
|
#telemetry { height: 1; color: $text-muted; }
|
||||||
#autobar { height: 1; color: $warning; }
|
|
||||||
#stats { height: 1; color: $accent; }
|
#stats { height: 1; color: $accent; }
|
||||||
#statsdetail { display: none; height: auto; color: $text-muted; }
|
#statsdetail { display: none; height: auto; color: $text-muted; }
|
||||||
#sshbar { height: 2; color: $text-muted; }
|
#sshbar { height: 2; color: $text-muted; }
|
||||||
|
|
@ -595,8 +632,6 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
("f", "follow", "Suivre"),
|
("f", "follow", "Suivre"),
|
||||||
("c", "copy_log", "Copier log"),
|
("c", "copy_log", "Copier log"),
|
||||||
("d", "details", "Détails erreurs"),
|
("d", "details", "Détails erreurs"),
|
||||||
("a", "toggle_auto", "Auto-refresh on/off"),
|
|
||||||
("r", "refresh_now", "Rafraîchir"),
|
|
||||||
("p", "pause_all", "Pause tout"),
|
("p", "pause_all", "Pause tout"),
|
||||||
("o", "resume_all", "Reprendre tout"),
|
("o", "resume_all", "Reprendre tout"),
|
||||||
]
|
]
|
||||||
|
|
@ -622,11 +657,13 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
self._errcount = {}
|
self._errcount = {}
|
||||||
# Sommaire de stats déplié (clic) ou non.
|
# Sommaire de stats déplié (clic) ou non.
|
||||||
self._stats_open = False
|
self._stats_open = False
|
||||||
# Rafraîchissement automatique COUPÉ par défaut : sur un gros parc,
|
# VM dont l'UI Odoo (:8069) répond déjà : une fois détectée « up »,
|
||||||
# les ticks périodiques (logs/table/domstate) causent du lag quand
|
# on ne re-teste plus (Odoo ne redescend pas en cours d'install).
|
||||||
# on lit les logs. On l'active à la demande (touche « a ») ; sinon
|
self._odoo_up = set()
|
||||||
# « r » rafraîchit une seule fois.
|
# Debounce du changement de VM : la sélection défile vite au
|
||||||
self._auto_refresh = False
|
# clavier ; on ne recharge le log qu'une fois le curseur STABILISÉ.
|
||||||
|
self._pending_sel = None
|
||||||
|
self._sel_timer = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _fmt(secs):
|
def _fmt(secs):
|
||||||
|
|
@ -651,11 +688,13 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
table.add_column("VM", key="vm", width=26)
|
table.add_column("VM", key="vm", width=26)
|
||||||
table.add_column("⚠", key="err", width=4)
|
table.add_column("⚠", key="err", width=4)
|
||||||
table.add_column("État", key="state", width=12)
|
table.add_column("État", key="state", width=12)
|
||||||
|
# « Odoo » : l'UI web répond-elle sur :8069 ? (🟢 up / — down)
|
||||||
|
table.add_column("Odoo", key="odoo", width=6)
|
||||||
table.add_column("Durée", key="elapsed", width=7)
|
table.add_column("Durée", key="elapsed", width=7)
|
||||||
table.add_column("Disque", key="disk", width=8)
|
table.add_column("Disque", key="disk", width=8)
|
||||||
for vm in vms:
|
for vm in vms:
|
||||||
table.add_row(
|
table.add_row(
|
||||||
vm["name"], "", "⏳", "--:--", "-", key=vm["name"]
|
vm["name"], "", "⏳", "—", "--:--", "-", key=vm["name"]
|
||||||
)
|
)
|
||||||
# max_lines borne la mémoire/rendu (un install verbeux × 30 VM).
|
# max_lines borne la mémoire/rendu (un install verbeux × 30 VM).
|
||||||
self._log = RichLog(
|
self._log = RichLog(
|
||||||
|
|
@ -664,8 +703,6 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
with Horizontal():
|
with Horizontal():
|
||||||
yield table
|
yield table
|
||||||
yield self._log
|
yield self._log
|
||||||
# Indicateur d'auto-refresh (a = on/off · r = rafraîchir).
|
|
||||||
yield Static("", id="autobar")
|
|
||||||
# Barre de télémétrie hôte (CPU, disque, ETA parc).
|
# Barre de télémétrie hôte (CPU, disque, ETA parc).
|
||||||
yield Static("", id="telemetry")
|
yield Static("", id="telemetry")
|
||||||
# Sommaire de stats en CHIFFRES (cliquable -> détail).
|
# Sommaire de stats en CHIFFRES (cliquable -> détail).
|
||||||
|
|
@ -681,17 +718,15 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
self.sub_title = f"0/{len(vms)} {t('completed')}"
|
self.sub_title = f"0/{len(vms)} {t('completed')}"
|
||||||
self._refresh_ssh()
|
self._refresh_ssh()
|
||||||
self._load_selected_log(reset=True)
|
self._load_selected_log(reset=True)
|
||||||
self._update_autobar()
|
# Table toutes les 2 s (30 lectures de fin de log), suivi du log
|
||||||
# Ticks périodiques : ils ne font RIEN tant que l'auto-refresh est
|
# sélectionné toutes les 1 s (une seule lecture incrémentale).
|
||||||
# coupé (wrappers gardés). On garde les intervalles en place pour
|
|
||||||
# ne pas avoir à les (re)créer au moment du toggle.
|
|
||||||
self.set_interval(2.0, self._tick_table)
|
self.set_interval(2.0, self._tick_table)
|
||||||
self.set_interval(1.0, self._tick_log)
|
self.set_interval(1.0, self._tick_log)
|
||||||
|
# État libvirt (pause / effacée) : check LENT (appel virsh) toutes
|
||||||
|
# les 10 s ; le tableau applique le cache à chaque tick (2 s).
|
||||||
self.set_interval(10.0, self._tick_domstate)
|
self.set_interval(10.0, self._tick_domstate)
|
||||||
# Un SEUL relevé initial (table + domstate) pour afficher l'état
|
# Premier relevé domstate immédiat (async -> via worker).
|
||||||
# courant à l'ouverture, même auto-refresh coupé.
|
self.run_worker(self._tick_domstate(), exclusive=False)
|
||||||
self.run_worker(self._do_domstate(), exclusive=False)
|
|
||||||
self.run_worker(self._do_table(), exclusive=False)
|
|
||||||
|
|
||||||
# -- helpers -------------------------------------------------------- #
|
# -- helpers -------------------------------------------------------- #
|
||||||
def _vm_by_name(self, name):
|
def _vm_by_name(self, name):
|
||||||
|
|
@ -704,21 +739,27 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
bar.update(
|
bar.update(
|
||||||
f" {vm['ssh']} (s = SSH · w = web :8069 · "
|
f" {vm['ssh']} (s = SSH · w = web :8069 · "
|
||||||
"c = copier le log · d = détails erreurs · "
|
"c = copier le log · d = détails erreurs · "
|
||||||
"a = auto-refresh · r = rafraîchir · "
|
|
||||||
"Maj+glisser = sélectionner)\n"
|
"Maj+glisser = sélectionner)\n"
|
||||||
f" Log : {vm['log']}"
|
f" Log : {vm['log']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _load_selected_log(self, reset=False):
|
def _load_selected_log(self, reset=False):
|
||||||
# Lecture SYNCHRONE (mount / changement de ligne). Le suivi périodique
|
# Au reset (changement de VM), on ne lit QUE LA FIN du log
|
||||||
# passe par _tick_log (async, I/O en thread).
|
# (_read_tail) et on cale l'offset sur la taille totale : le suivi
|
||||||
|
# incrémental (_tick_log) continue depuis la fin. Lire+réafficher
|
||||||
|
# le fichier ENTIER gelait l'UI sur les gros logs.
|
||||||
vm = self._vm_by_name(self._selected)
|
vm = self._vm_by_name(self._selected)
|
||||||
if not vm:
|
if not vm:
|
||||||
return
|
return
|
||||||
name = vm["name"]
|
name = vm["name"]
|
||||||
if reset:
|
if reset:
|
||||||
self._log.clear()
|
self._log.clear()
|
||||||
self._offsets[name] = 0
|
text, size = _read_tail(vm["log"])
|
||||||
|
self._offsets[name] = size
|
||||||
|
for line in text.splitlines():
|
||||||
|
if EXIT_MARKER not in line:
|
||||||
|
self._log.write(line)
|
||||||
|
return
|
||||||
new, off = _read_new(vm["log"], self._offsets.get(name, 0))
|
new, off = _read_new(vm["log"], self._offsets.get(name, 0))
|
||||||
self._offsets[name] = off
|
self._offsets[name] = off
|
||||||
for line in new.splitlines():
|
for line in new.splitlines():
|
||||||
|
|
@ -745,7 +786,7 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
def _collect_table(self):
|
def _collect_table(self):
|
||||||
"""(THREAD) statut + taille disque de chaque VM + télémétrie. AUCUNE
|
"""(THREAD) statut + taille disque de chaque VM + télémétrie. AUCUNE
|
||||||
mise à jour d'UI ici : uniquement des I/O bloquantes déportées."""
|
mise à jour d'UI ici : uniquement des I/O bloquantes déportées."""
|
||||||
disks, status, errors = {}, {}, {}
|
disks, status, errors, odoo = {}, {}, {}, {}
|
||||||
for vm in vms:
|
for vm in vms:
|
||||||
name = vm["name"]
|
name = vm["name"]
|
||||||
disks[name] = _fmt_size(disk_actual_size(vm_disk_path(vm)))
|
disks[name] = _fmt_size(disk_actual_size(vm_disk_path(vm)))
|
||||||
|
|
@ -760,23 +801,27 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
# peut contenir des erreurs passées inaperçues.
|
# peut contenir des erreurs passées inaperçues.
|
||||||
if st[0] in ("done", "failed") and name not in errors:
|
if st[0] in ("done", "failed") and name not in errors:
|
||||||
errors[name] = scan_log_errors(vm["log"])
|
errors[name] = scan_log_errors(vm["log"])
|
||||||
return disks, status, self._collect_tele(), errors
|
# Odoo up ? On ne teste que celles pas encore confirmées up
|
||||||
|
# et non effacées (test TCP court sur :8069).
|
||||||
|
if name not in self._odoo_up and self._domstate.get(
|
||||||
|
name
|
||||||
|
) != "gone":
|
||||||
|
if _port_open(vm.get("ip"), 8069):
|
||||||
|
odoo[name] = True
|
||||||
|
return disks, status, self._collect_tele(), errors, odoo
|
||||||
|
|
||||||
async def _tick_table(self):
|
async def _tick_table(self):
|
||||||
if self._auto_refresh:
|
|
||||||
await self._do_table()
|
|
||||||
|
|
||||||
async def _do_table(self):
|
|
||||||
# I/O (lectures de logs, stat disque, /proc) DÉPORTÉES en thread ->
|
# I/O (lectures de logs, stat disque, /proc) DÉPORTÉES en thread ->
|
||||||
# la boucle d'événements Textual reste fluide même sous forte
|
# la boucle d'événements Textual reste fluide même sous forte
|
||||||
# charge ou disque lent. Les mises à jour d'UI restent sur la boucle.
|
# charge ou disque lent. Les mises à jour d'UI restent sur la boucle.
|
||||||
try:
|
try:
|
||||||
disks, status, tele, errors = await asyncio.to_thread(
|
disks, status, tele, errors, odoo = await asyncio.to_thread(
|
||||||
self._collect_table
|
self._collect_table
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
return
|
||||||
self._errcount.update(errors)
|
self._errcount.update(errors)
|
||||||
|
self._odoo_up.update(odoo)
|
||||||
try:
|
try:
|
||||||
table = self.query_one("#vms", DataTable)
|
table = self.query_one("#vms", DataTable)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
@ -784,6 +829,11 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
for vm in vms:
|
for vm in vms:
|
||||||
name = vm["name"]
|
name = vm["name"]
|
||||||
self._set_cell(table, name, "disk", disks.get(name, "-"))
|
self._set_cell(table, name, "disk", disks.get(name, "-"))
|
||||||
|
# Colonne Odoo : 🟢 dès que :8069 répond, sinon « — ».
|
||||||
|
self._set_cell(
|
||||||
|
table, name, "odoo",
|
||||||
|
"🟢" if name in self._odoo_up else "—",
|
||||||
|
)
|
||||||
if name in self._final:
|
if name in self._final:
|
||||||
continue
|
continue
|
||||||
ds = self._domstate.get(name)
|
ds = self._domstate.get(name)
|
||||||
|
|
@ -914,10 +964,6 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
async def _tick_log(self):
|
async def _tick_log(self):
|
||||||
if self._auto_refresh:
|
|
||||||
await self._do_log()
|
|
||||||
|
|
||||||
async def _do_log(self):
|
|
||||||
if not self._follow:
|
if not self._follow:
|
||||||
return
|
return
|
||||||
vm = self._vm_by_name(self._selected)
|
vm = self._vm_by_name(self._selected)
|
||||||
|
|
@ -936,10 +982,6 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
self._log.write(line)
|
self._log.write(line)
|
||||||
|
|
||||||
async def _tick_domstate(self):
|
async def _tick_domstate(self):
|
||||||
if self._auto_refresh:
|
|
||||||
await self._do_domstate()
|
|
||||||
|
|
||||||
async def _do_domstate(self):
|
|
||||||
# Relevé LENT (subprocess virsh) DÉPORTÉ en thread : ne bloque plus
|
# Relevé LENT (subprocess virsh) DÉPORTÉ en thread : ne bloque plus
|
||||||
# la boucle. Une VM absente de « virsh list » est EFFACÉE (« gone »).
|
# la boucle. Une VM absente de « virsh list » est EFFACÉE (« gone »).
|
||||||
try:
|
try:
|
||||||
|
|
@ -949,40 +991,28 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
for vm in vms:
|
for vm in vms:
|
||||||
self._domstate[vm["name"]] = states.get(vm["name"], "gone")
|
self._domstate[vm["name"]] = states.get(vm["name"], "gone")
|
||||||
|
|
||||||
# -- auto-refresh (a = on/off · r = rafraîchir une fois) ------------- #
|
|
||||||
def _update_autobar(self):
|
|
||||||
if self._auto_refresh:
|
|
||||||
msg = f" ▶ {t('Auto-refresh: ON')} (a = {t('turn off')})"
|
|
||||||
else:
|
|
||||||
msg = (
|
|
||||||
f" ⏸ {t('Auto-refresh: OFF')} "
|
|
||||||
f"(a = {t('turn on')} · r = {t('refresh once')})"
|
|
||||||
)
|
|
||||||
self.query_one("#autobar", Static).update(msg)
|
|
||||||
|
|
||||||
def action_toggle_auto(self) -> None:
|
|
||||||
self._auto_refresh = not self._auto_refresh
|
|
||||||
self._update_autobar()
|
|
||||||
if self._auto_refresh:
|
|
||||||
# Rafraîchit tout de suite en activant.
|
|
||||||
self.run_worker(self._do_domstate(), exclusive=False)
|
|
||||||
self.run_worker(self._do_table(), exclusive=False)
|
|
||||||
self.run_worker(self._do_log(), exclusive=False)
|
|
||||||
|
|
||||||
def action_refresh_now(self) -> None:
|
|
||||||
# Une seule actualisation, même auto-refresh coupé.
|
|
||||||
self.run_worker(self._do_domstate(), exclusive=False)
|
|
||||||
self.run_worker(self._do_table(), exclusive=False)
|
|
||||||
self.run_worker(self._do_log(), exclusive=False)
|
|
||||||
self.notify(t("Refreshed."))
|
|
||||||
|
|
||||||
# -- events --------------------------------------------------------- #
|
# -- events --------------------------------------------------------- #
|
||||||
def on_data_table_row_highlighted(self, event) -> None:
|
def on_data_table_row_highlighted(self, event) -> None:
|
||||||
|
# DEBOUNCE : RowHighlighted se déclenche à CHAQUE mouvement du
|
||||||
|
# curseur. Recharger le log à chaque pas (surtout en maintenant la
|
||||||
|
# flèche) enchaînait les rechargements -> gros lag. On diffère de
|
||||||
|
# 0,25 s et on ne charge que la DERNIÈRE VM sélectionnée.
|
||||||
name = event.row_key.value
|
name = event.row_key.value
|
||||||
if name and name != self._selected:
|
if not name or name == self._selected:
|
||||||
self._selected = name
|
return
|
||||||
self._refresh_ssh()
|
self._pending_sel = name
|
||||||
self._load_selected_log(reset=True)
|
if self._sel_timer is not None:
|
||||||
|
self._sel_timer.stop()
|
||||||
|
self._sel_timer = self.set_timer(0.25, self._apply_pending_sel)
|
||||||
|
|
||||||
|
def _apply_pending_sel(self) -> None:
|
||||||
|
name = self._pending_sel
|
||||||
|
self._sel_timer = None
|
||||||
|
if not name or name == self._selected:
|
||||||
|
return
|
||||||
|
self._selected = name
|
||||||
|
self._refresh_ssh()
|
||||||
|
self._load_selected_log(reset=True)
|
||||||
|
|
||||||
def action_follow(self) -> None:
|
def action_follow(self) -> None:
|
||||||
self._follow = not self._follow
|
self._follow = not self._follow
|
||||||
|
|
@ -1167,10 +1197,8 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
await asyncio.to_thread(self._virsh_bulk, action, targets)
|
await asyncio.to_thread(self._virsh_bulk, action, targets)
|
||||||
verb = t("paused") if action == "suspend" else t("resumed")
|
verb = t("paused") if action == "suspend" else t("resumed")
|
||||||
self.notify(f"{len(targets)} VM {verb}.")
|
self.notify(f"{len(targets)} VM {verb}.")
|
||||||
# Rafraîchit tout de suite l'état libvirt (pause/reprise visible),
|
# Rafraîchit tout de suite l'état libvirt (pause/reprise visible).
|
||||||
# même si l'auto-refresh est coupé -> _do_domstate (forcé).
|
self.run_worker(self._tick_domstate(), exclusive=False)
|
||||||
self.run_worker(self._do_domstate(), exclusive=False)
|
|
||||||
self.run_worker(self._do_table(), exclusive=False)
|
|
||||||
|
|
||||||
app = Monitor()
|
app = Monitor()
|
||||||
if run_app:
|
if run_app:
|
||||||
|
|
|
||||||
|
|
@ -3017,6 +3017,16 @@ class TODO:
|
||||||
"libvirt virt-install libguestfs 2>/dev/null || true)"
|
"libvirt virt-install libguestfs 2>/dev/null || true)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _qemu_ask_prod(self):
|
||||||
|
"""Environnement cible : dev (défaut) ou prod. En PROD : ERPLibre est
|
||||||
|
installé dans /opt/erplibre (au lieu de ~/git/erplibre) et le service
|
||||||
|
systemd reste CONFINÉ par SELinux (pas d'unconfined)."""
|
||||||
|
print(f"\n{t('Target environment?')}")
|
||||||
|
print(f" [1] {t('Development (~/git/erplibre, SELinux relaxed)')} *")
|
||||||
|
print(f" [2] {t('Production (/opt/erplibre, SELinux enforced)')}")
|
||||||
|
sel = input(t("Choice (1-2, default 1): ")).strip()
|
||||||
|
return sel == "2"
|
||||||
|
|
||||||
def _qemu_pick_install_profile(self):
|
def _qemu_pick_install_profile(self):
|
||||||
"""Choix de CE QU'ON installe sur la VM. Renvoie (label, commande
|
"""Choix de CE QU'ON installe sur la VM. Renvoie (label, commande
|
||||||
finale exécutée dans ~/git/erplibre)."""
|
finale exécutée dans ~/git/erplibre)."""
|
||||||
|
|
@ -3059,14 +3069,43 @@ class TODO:
|
||||||
return profiles[0] # défaut : ERPLibre + Odoo 18
|
return profiles[0] # défaut : ERPLibre + Odoo 18
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _qemu_odoo_service_cmd():
|
@staticmethod
|
||||||
|
def _qemu_install_dir(prod):
|
||||||
|
"""Répertoire d'installation ERPLibre dans la VM : /opt/erplibre en
|
||||||
|
PROD (hors /home -> service SELinux confiné possible), sinon
|
||||||
|
~/git/erplibre (dev)."""
|
||||||
|
return "/opt/erplibre" if prod else "$HOME/git/erplibre"
|
||||||
|
|
||||||
|
def _qemu_odoo_service_cmd(self, prod=False):
|
||||||
"""Snippet shell (exécuté dans la VM) qui installe ERPLibre/Odoo comme
|
"""Snippet shell (exécuté dans la VM) qui installe ERPLibre/Odoo comme
|
||||||
service systemd (inspiré de script/systemd/install_daemon.sh) puis
|
service systemd puis l'active. N'est ajouté QUE pour les profils Odoo.
|
||||||
l'active + démarre. N'est ajouté QUE pour les profils avec Odoo."""
|
|
||||||
|
DEV : ERPLibre sous ~/home ; si SELinux est actif (Fedora), on lève le
|
||||||
|
confinement du service (unconfined) — un service système ne peut pas
|
||||||
|
exécuter du user_home_t. Acceptable pour une VM de dev.
|
||||||
|
PROD : ERPLibre sous /opt/erplibre (hors user_home_t) -> le service
|
||||||
|
reste CONFINÉ par SELinux ; on restaure les contextes (restorecon)."""
|
||||||
|
svc_dir = self._qemu_install_dir(prod)
|
||||||
|
if prod:
|
||||||
|
pre = (
|
||||||
|
"command -v restorecon >/dev/null 2>&1 && "
|
||||||
|
"sudo restorecon -R /opt/erplibre >/dev/null 2>&1 || true; "
|
||||||
|
)
|
||||||
|
selinux_shell = 'SELINUX_LINE=""; ' # confiné : pas d'unconfined
|
||||||
|
else:
|
||||||
|
pre = ""
|
||||||
|
selinux_shell = (
|
||||||
|
'SELINUX_LINE=""; '
|
||||||
|
"if command -v getenforce >/dev/null 2>&1 && "
|
||||||
|
'[ "$(getenforce)" != "Disabled" ]; then '
|
||||||
|
'SELINUX_LINE="SELinuxContext=unconfined_u:unconfined_r:'
|
||||||
|
'unconfined_t:s0"; fi; '
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
'SVC_USER=$(whoami); SVC_GROUP=$(id -gn); '
|
f'SVC_USER=$(whoami); SVC_GROUP=$(id -gn); SVC_DIR="{svc_dir}"; '
|
||||||
'SVC_DIR="$HOME/git/erplibre"; '
|
+ pre
|
||||||
"sudo tee /etc/systemd/system/erplibre.service >/dev/null <<UNIT\n"
|
+ selinux_shell
|
||||||
|
+ "sudo tee /etc/systemd/system/erplibre.service >/dev/null <<UNIT\n"
|
||||||
"[Unit]\n"
|
"[Unit]\n"
|
||||||
"Description=ERPLibre\n"
|
"Description=ERPLibre\n"
|
||||||
"Requires=postgresql.service\n"
|
"Requires=postgresql.service\n"
|
||||||
|
|
@ -3081,6 +3120,7 @@ class TODO:
|
||||||
"ExecStart=$SVC_DIR/run.sh\n"
|
"ExecStart=$SVC_DIR/run.sh\n"
|
||||||
"WorkingDirectory=$SVC_DIR\n"
|
"WorkingDirectory=$SVC_DIR\n"
|
||||||
"StandardOutput=journal+console\n"
|
"StandardOutput=journal+console\n"
|
||||||
|
"$SELINUX_LINE\n"
|
||||||
"\n"
|
"\n"
|
||||||
"[Install]\n"
|
"[Install]\n"
|
||||||
"WantedBy=multi-user.target\n"
|
"WantedBy=multi-user.target\n"
|
||||||
|
|
@ -3089,17 +3129,18 @@ class TODO:
|
||||||
"sudo systemctl enable --now erplibre.service"
|
"sudo systemctl enable --now erplibre.service"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _qemu_erplibre_remote_cmd(self, branch, final_cmd=None):
|
def _qemu_erplibre_remote_cmd(self, branch, final_cmd=None, prod=False):
|
||||||
"""Script d'installation ERPLibre exécuté DANS la VM (curl/git/make
|
"""Script d'installation ERPLibre exécuté DANS la VM (curl/git/make
|
||||||
puis clone + `final_cmd` dans ~/git/erplibre). `final_cmd` par défaut :
|
puis clone + `final_cmd`). `final_cmd` par défaut : install_os +
|
||||||
install_os + install_odoo_18."""
|
install_odoo_18. `prod` : installe dans /opt/erplibre (au lieu de
|
||||||
|
~/git/erplibre) + service SELinux confiné."""
|
||||||
if not final_cmd:
|
if not final_cmd:
|
||||||
final_cmd = f"make install_os && make {self.ERPLIBRE_ODOO_TARGET}"
|
final_cmd = f"make install_os && make {self.ERPLIBRE_ODOO_TARGET}"
|
||||||
# Profils AVEC Odoo (install_odoo*) uniquement : après l'install, on
|
# Profils AVEC Odoo (install_odoo*) uniquement : après l'install, on
|
||||||
# enregistre Odoo comme service systemd (enable + start). Pas pour
|
# enregistre Odoo comme service systemd (enable + start). Pas pour
|
||||||
# « ERPLibre seul », « mobile » ni « Déploiement ».
|
# « ERPLibre seul », « mobile » ni « Déploiement ».
|
||||||
if "install_odoo" in final_cmd:
|
if "install_odoo" in final_cmd:
|
||||||
final_cmd = f"{final_cmd} && {self._qemu_odoo_service_cmd()}"
|
final_cmd = f"{final_cmd} && {self._qemu_odoo_service_cmd(prod)}"
|
||||||
return (
|
return (
|
||||||
"set -e; "
|
"set -e; "
|
||||||
# Attendre la FIN de cloud-init : pendant sa phase « paquets » il
|
# Attendre la FIN de cloud-init : pendant sa phase « paquets » il
|
||||||
|
|
@ -3116,11 +3157,15 @@ class TODO:
|
||||||
# (Fedora) et pacman (Arch).
|
# (Fedora) et pacman (Arch).
|
||||||
"PKGS='curl git make'; "
|
"PKGS='curl git make'; "
|
||||||
"if command -v apt-get >/dev/null 2>&1; then "
|
"if command -v apt-get >/dev/null 2>&1; then "
|
||||||
|
# DPkg::Lock::Timeout=600 : au 1er boot, cloud-init (install de
|
||||||
|
# qemu-guest-agent) et/ou apt-daily.service tiennent le verrou apt.
|
||||||
|
# Sans attente, « apt-get install » échouait aussitôt (« Could not
|
||||||
|
# get lock … held by process »). On ATTEND le verrou jusqu'à 10 min.
|
||||||
# update best-effort (|| true) puis install OBLIGATOIRE : sans le
|
# update best-effort (|| true) puis install OBLIGATOIRE : sans le
|
||||||
# « || true », un « apt-get update » en échec (réseau) sautait
|
# « || true », un « apt-get update » en échec (réseau) sautait
|
||||||
# l'install sans erreur (liste &&) -> git/make absents ensuite.
|
# l'install sans erreur (liste &&) -> git/make absents ensuite.
|
||||||
"sudo apt-get update -qq || true; "
|
"sudo apt-get -o DPkg::Lock::Timeout=600 update -qq || true; "
|
||||||
"sudo apt-get install -y $PKGS; "
|
"sudo apt-get -o DPkg::Lock::Timeout=600 install -y $PKGS; "
|
||||||
"elif command -v dnf >/dev/null 2>&1; then "
|
"elif command -v dnf >/dev/null 2>&1; then "
|
||||||
# makecache (dnf5 choisit les miroirs les plus rapides) puis
|
# makecache (dnf5 choisit les miroirs les plus rapides) puis
|
||||||
# install --refresh ; retry avec « clean all » car les images
|
# install --refresh ; retry avec « clean all » car les images
|
||||||
|
|
@ -3151,27 +3196,42 @@ class TODO:
|
||||||
"for t in curl git make; do command -v $t >/dev/null 2>&1 || "
|
"for t in curl git make; do command -v $t >/dev/null 2>&1 || "
|
||||||
'{ echo "Outil manquant apres installation: $t '
|
'{ echo "Outil manquant apres installation: $t '
|
||||||
'(reseau de la VM ?)"; exit 1; }; done; '
|
'(reseau de la VM ?)"; exit 1; }; done; '
|
||||||
"mkdir -p ~/git; "
|
# Clone : /opt/erplibre en PROD (racine, puis chown à l'utilisateur
|
||||||
"if [ ! -d ~/git/erplibre/.git ]; then "
|
# pour que make/venv s'exécutent sans sudo), ~/git/erplibre en dev.
|
||||||
f"git clone --branch {shlex.quote(branch)} "
|
+ (
|
||||||
f"{self.ERPLIBRE_GIT_URL} ~/git/erplibre; fi; "
|
(
|
||||||
# Installation : commande finale selon le profil choisi.
|
"sudo mkdir -p /opt; "
|
||||||
f"cd ~/git/erplibre && {final_cmd}"
|
"if [ ! -d /opt/erplibre/.git ]; then "
|
||||||
|
f"sudo git clone --branch {shlex.quote(branch)} "
|
||||||
|
f"{self.ERPLIBRE_GIT_URL} /opt/erplibre; "
|
||||||
|
"sudo chown -R $(id -un):$(id -gn) /opt/erplibre; fi; "
|
||||||
|
f"cd /opt/erplibre && {final_cmd}"
|
||||||
|
)
|
||||||
|
if prod
|
||||||
|
else (
|
||||||
|
"mkdir -p ~/git; "
|
||||||
|
"if [ ! -d ~/git/erplibre/.git ]; then "
|
||||||
|
f"git clone --branch {shlex.quote(branch)} "
|
||||||
|
f"{self.ERPLIBRE_GIT_URL} ~/git/erplibre; fi; "
|
||||||
|
f"cd ~/git/erplibre && {final_cmd}"
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _qemu_install_erplibre_monitored(
|
def _qemu_install_erplibre_monitored(
|
||||||
self, names, branch, ip_map=None, final_cmd=None
|
self, names, branch, ip_map=None, final_cmd=None, prod=False
|
||||||
):
|
):
|
||||||
"""Lance l'install ERPLibre en parallèle DÉTACHÉE sur les VM et ouvre
|
"""Lance l'install ERPLibre en parallèle DÉTACHÉE sur les VM et ouvre
|
||||||
le dashboard Textual. Quitter le dashboard n'arrête pas les installs.
|
le dashboard Textual. Quitter le dashboard n'arrête pas les installs.
|
||||||
`ip_map` : IP déjà résolues (sinon on résout ici, EN PARALLÈLE).
|
`ip_map` : IP déjà résolues (sinon on résout ici, EN PARALLÈLE).
|
||||||
`final_cmd` : commande d'install selon le profil choisi."""
|
`final_cmd` : commande d'install selon le profil choisi.
|
||||||
|
`prod` : install /opt/erplibre + service SELinux confiné."""
|
||||||
from script.todo.qemu_install_monitor import (
|
from script.todo.qemu_install_monitor import (
|
||||||
launch_installs,
|
launch_installs,
|
||||||
run_monitor,
|
run_monitor,
|
||||||
)
|
)
|
||||||
|
|
||||||
remote = self._qemu_erplibre_remote_cmd(branch, final_cmd)
|
remote = self._qemu_erplibre_remote_cmd(branch, final_cmd, prod)
|
||||||
try:
|
try:
|
||||||
mod = self._qemu_import_module()
|
mod = self._qemu_import_module()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -3224,11 +3284,11 @@ class TODO:
|
||||||
print(f" {t('Read the logs:')} tail -n +1 {logdir}/*.log")
|
print(f" {t('Read the logs:')} tail -n +1 {logdir}/*.log")
|
||||||
|
|
||||||
def _qemu_install_erplibre_vm(
|
def _qemu_install_erplibre_vm(
|
||||||
self, name, ssh_key, branch, ip=None, final_cmd=None
|
self, name, ssh_key, branch, ip=None, final_cmd=None, prod=False
|
||||||
):
|
):
|
||||||
"""Clone ERPLibre (branche donnée) dans ~/git/erplibre de la VM puis
|
"""Clone ERPLibre (branche donnée) dans la VM puis exécute la commande
|
||||||
exécute la commande d'install du profil choisi (streamé). `ip` : IP
|
d'install du profil choisi (streamé). `ip` : IP déjà résolue ;
|
||||||
déjà résolue ; `final_cmd` : commande d'install."""
|
`final_cmd` : commande d'install ; `prod` : /opt + SELinux confiné."""
|
||||||
if ip is None:
|
if ip is None:
|
||||||
ip = self._qemu_vm_ip(name)
|
ip = self._qemu_vm_ip(name)
|
||||||
if not ip:
|
if not ip:
|
||||||
|
|
@ -3245,7 +3305,7 @@ class TODO:
|
||||||
f"{t('SSH not reachable, ERPLibre install skipped.')}"
|
f"{t('SSH not reachable, ERPLibre install skipped.')}"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
remote = self._qemu_erplibre_remote_cmd(branch, final_cmd)
|
remote = self._qemu_erplibre_remote_cmd(branch, final_cmd, prod)
|
||||||
ssh_opts = (
|
ssh_opts = (
|
||||||
"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
|
"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
|
||||||
"-o ConnectTimeout=15"
|
"-o ConnectTimeout=15"
|
||||||
|
|
@ -3559,12 +3619,14 @@ class TODO:
|
||||||
# 4) Option : installer ERPLibre dans ~/git/erplibre de chaque VM.
|
# 4) Option : installer ERPLibre dans ~/git/erplibre de chaque VM.
|
||||||
install_branch = None
|
install_branch = None
|
||||||
install_monitor = False
|
install_monitor = False
|
||||||
|
install_prod = False # dev (~/git, SELinux dev) vs prod (/opt, confiné)
|
||||||
install_cmd = None # commande finale selon le profil choisi
|
install_cmd = None # commande finale selon le profil choisi
|
||||||
ans = input(
|
ans = input(
|
||||||
t("Install ERPLibre into ~/git/erplibre on each VM? (y/N): ")
|
t("Install ERPLibre into ~/git/erplibre on each VM? (y/N): ")
|
||||||
)
|
)
|
||||||
if self._is_yes(ans):
|
if self._is_yes(ans):
|
||||||
install_branch = self._qemu_pick_branch()
|
install_branch = self._qemu_pick_branch()
|
||||||
|
install_prod = self._qemu_ask_prod()
|
||||||
_label, install_cmd = self._qemu_pick_install_profile()
|
_label, install_cmd = self._qemu_pick_install_profile()
|
||||||
install_monitor = self._is_yes_default_yes(
|
install_monitor = self._is_yes_default_yes(
|
||||||
input(t("Interactive monitoring dashboard? (y/N): "))
|
input(t("Interactive monitoring dashboard? (y/N): "))
|
||||||
|
|
@ -3683,7 +3745,8 @@ class TODO:
|
||||||
if install_monitor:
|
if install_monitor:
|
||||||
# Installs détachées en parallèle + dashboard Textual.
|
# Installs détachées en parallèle + dashboard Textual.
|
||||||
self._qemu_install_erplibre_monitored(
|
self._qemu_install_erplibre_monitored(
|
||||||
deployed, install_branch, ip_map, install_cmd
|
deployed, install_branch, ip_map, install_cmd,
|
||||||
|
install_prod,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
|
|
@ -3697,6 +3760,7 @@ class TODO:
|
||||||
install_branch,
|
install_branch,
|
||||||
ip_map.get(name),
|
ip_map.get(name),
|
||||||
install_cmd,
|
install_cmd,
|
||||||
|
install_prod,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sommaire TOTAL (déploiement + résolution IP + ssh_config + install
|
# Sommaire TOTAL (déploiement + résolution IP + ssh_config + install
|
||||||
|
|
|
||||||
|
|
@ -1575,30 +1575,6 @@ TRANSLATIONS = {
|
||||||
"fr": "Aucune erreur détectée.",
|
"fr": "Aucune erreur détectée.",
|
||||||
"en": "No error detected.",
|
"en": "No error detected.",
|
||||||
},
|
},
|
||||||
"Auto-refresh: ON": {
|
|
||||||
"fr": "Rafraîchissement auto : ACTIVÉ",
|
|
||||||
"en": "Auto-refresh: ON",
|
|
||||||
},
|
|
||||||
"Auto-refresh: OFF": {
|
|
||||||
"fr": "Rafraîchissement auto : COUPÉ",
|
|
||||||
"en": "Auto-refresh: OFF",
|
|
||||||
},
|
|
||||||
"turn off": {
|
|
||||||
"fr": "couper",
|
|
||||||
"en": "turn off",
|
|
||||||
},
|
|
||||||
"turn on": {
|
|
||||||
"fr": "activer",
|
|
||||||
"en": "turn on",
|
|
||||||
},
|
|
||||||
"refresh once": {
|
|
||||||
"fr": "rafraîchir",
|
|
||||||
"en": "refresh once",
|
|
||||||
},
|
|
||||||
"Refreshed.": {
|
|
||||||
"fr": "Rafraîchi.",
|
|
||||||
"en": "Refreshed.",
|
|
||||||
},
|
|
||||||
"Test a VM (open Odoo in a CLI browser)": {
|
"Test a VM (open Odoo in a CLI browser)": {
|
||||||
"fr": "🧪 Tester une VM (ouvrir Odoo dans un navigateur CLI)",
|
"fr": "🧪 Tester une VM (ouvrir Odoo dans un navigateur CLI)",
|
||||||
"en": "🧪 Test a VM (open Odoo in a CLI browser)",
|
"en": "🧪 Test a VM (open Odoo in a CLI browser)",
|
||||||
|
|
@ -1818,6 +1794,22 @@ TRANSLATIONS = {
|
||||||
"fr": "État cible :",
|
"fr": "État cible :",
|
||||||
"en": "Target state:",
|
"en": "Target state:",
|
||||||
},
|
},
|
||||||
|
"Target environment?": {
|
||||||
|
"fr": "Environnement cible ?",
|
||||||
|
"en": "Target environment?",
|
||||||
|
},
|
||||||
|
"Development (~/git/erplibre, SELinux relaxed)": {
|
||||||
|
"fr": "Développement (~/git/erplibre, SELinux relâché)",
|
||||||
|
"en": "Development (~/git/erplibre, SELinux relaxed)",
|
||||||
|
},
|
||||||
|
"Production (/opt/erplibre, SELinux enforced)": {
|
||||||
|
"fr": "Production (/opt/erplibre, SELinux confiné)",
|
||||||
|
"en": "Production (/opt/erplibre, SELinux enforced)",
|
||||||
|
},
|
||||||
|
"Choice (1-2, default 1): ": {
|
||||||
|
"fr": "Choix (1-2, défaut 1) : ",
|
||||||
|
"en": "Choice (1-2, default 1): ",
|
||||||
|
},
|
||||||
"Open (start)": {
|
"Open (start)": {
|
||||||
"fr": "Ouvrir (démarrer)",
|
"fr": "Ouvrir (démarrer)",
|
||||||
"en": "Open (start)",
|
"en": "Open (start)",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue