[IMP] qemu: table lisible/scrollable, guest-exec autorisé, service Odoo, choix navigateur

Suivi d'installation (qemu_install_monitor.py) :
- Table VM/État LISIBLE : largeurs de colonnes fixes (VM 26, ⚠ 4, État 12,
  Durée 7, Disque 8) -> l'État n'est plus tronqué à 3-4 caractères
  («  effacée », « ⏸ en pause » lisibles) ; table défilable (overflow-x,
  height 1fr) pour les longs noms et les gros parcs.
- « w » web : offre désormais la LISTE des navigateurs CLI installés
  (_choose_browser) pour choisir lequel utiliser, + option [i] installer.

Déploiement (deploy_qemu.py) :
- guest-exec AUTORISÉ : on vide la liste de blocage de qemu-ga
  (block-rpcs/blacklist vides — pas allow-rpcs qui est une liste BLANCHE et
  casserait les autres RPC), + neutralise /etc/sysconfig/qemu-ga (Fedora).

Installation (todo.py) :
- Profils AVEC Odoo (install_odoo*) UNIQUEMENT : Odoo est enregistré comme
  service systemd (erplibre.service, inspiré de script/systemd/
  install_daemon.sh) puis enable --now. Pas pour ERPLibre seul / mobile /
  Déploiement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathieu Benoit 2026-07-29 12:06:29 +00:00
parent ab8bfec113
commit 8278c38f46
3 changed files with 98 additions and 17 deletions

View file

@ -978,8 +978,23 @@ def build_cloud_config(
" qemu-guest-agent) || (command -v dnf >/dev/null && dnf install -y" " qemu-guest-agent) || (command -v dnf >/dev/null && dnf install -y"
" qemu-guest-agent) || (command -v pacman >/dev/null && pacman -S" " qemu-guest-agent) || (command -v pacman >/dev/null && pacman -S"
" --noconfirm qemu-guest-agent) || true", " --noconfirm qemu-guest-agent) || true",
" - systemctl enable --now qemu-guest-agent 2>/dev/null" # Autorise guest-exec (bloqué par défaut sur certaines distros).
" || systemctl enable --now qemu-ga 2>/dev/null || true", # On VIDE la liste de blocage (block-rpcs/blacklist) plutôt que
# d'utiliser allow-rpcs (liste BLANCHE : casserait les autres RPC).
# Les deux clés couvrent les versions anciennes (blacklist) et
# récentes (block-rpcs) de qemu-ga.
" - mkdir -p /etc/qemu && printf '[general]\\nblock-rpcs=\\n"
"blacklist=\\n' > /etc/qemu/qemu-ga.conf || true",
# Fedora/RHEL bloquent guest-exec via /etc/sysconfig/qemu-ga.
" - [ -f /etc/sysconfig/qemu-ga ] && sed -i"
" 's/^BLACKLIST_RPC=.*/BLACKLIST_RPC=/'"
" /etc/sysconfig/qemu-ga || true",
" - systemctl enable qemu-guest-agent 2>/dev/null"
" || systemctl enable qemu-ga 2>/dev/null || true",
# restart (et non enable --now) : recharge la config si l'agent était
# déjà démarré par l'image cloud.
" - systemctl restart qemu-guest-agent 2>/dev/null"
" || systemctl restart qemu-ga 2>/dev/null || true",
] ]
return "\n".join(lines) + "\n" return "\n".join(lines) + "\n"

View file

@ -470,7 +470,7 @@ def run_monitor(manifest_path: str, run_app: bool = True):
class Monitor(App): class Monitor(App):
CSS = """ CSS = """
DataTable { width: 58; 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; }
#stats { height: 1; color: $accent; } #stats { height: 1; color: $accent; }
@ -526,11 +526,14 @@ def run_monitor(manifest_path: str, run_app: bool = True):
table = DataTable(id="vms", cursor_type="row") table = DataTable(id="vms", cursor_type="row")
# Clés de colonnes explicites : update_cell() les référence. # Clés de colonnes explicites : update_cell() les référence.
# La colonne « ⚠ » (erreurs détectées) est à GAUCHE d'« État ». # La colonne « ⚠ » (erreurs détectées) est à GAUCHE d'« État ».
table.add_column("VM", key="vm") # Largeurs FIXES pour les colonnes courtes -> l'État n'est plus
table.add_column("", key="err") # tronqué (« ❌ effacée », « ⏸ en pause » lisibles) ; la table
table.add_column("État", key="state") # défile horizontalement (overflow-x) pour les noms de VM longs.
table.add_column("Durée", key="elapsed") table.add_column("VM", key="vm", width=26)
table.add_column("Disque", key="disk") table.add_column("", key="err", width=4)
table.add_column("État", key="state", width=12)
table.add_column("Durée", key="elapsed", width=7)
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"]
@ -835,16 +838,9 @@ def run_monitor(manifest_path: str, run_app: bool = True):
if not vm or not vm.get("ip"): if not vm or not vm.get("ip"):
self.notify("Pas d'IP pour cette VM.", title="Web") self.notify("Pas d'IP pour cette VM.", title="Web")
return return
browser = cli_browser() browser = self._choose_browser()
if not browser: if not browser:
browser = self._install_cli_browser() return
if not browser:
self.notify(
"Aucun navigateur CLI disponible.",
title="Web",
severity="warning",
)
return
url = f"http://{vm['ip']}:8069" url = f"http://{vm['ip']}:8069"
with self.suspend(): with self.suspend():
print(f"{browser} {url}") print(f"{browser} {url}")
@ -863,6 +859,40 @@ def run_monitor(manifest_path: str, run_app: bool = True):
except EOFError: except EOFError:
pass pass
def _choose_browser(self):
"""Offre la LISTE des navigateurs CLI installés et laisse choisir
lequel utiliser pour voir la page. Si aucun n'est installé, propose
d'en installer un. Renvoie le binaire choisi, ou None."""
available = [b for b in CLI_BROWSERS if shutil.which(b)]
if not available:
browser = self._install_cli_browser()
if not browser:
self.notify(
"Aucun navigateur CLI disponible.",
title="Web",
severity="warning",
)
return browser
with self.suspend():
print("Quel navigateur utiliser pour voir la page ?")
for i, b in enumerate(available, 1):
print(f" [{i}] {b}{' *' if i == 1 else ''}")
print(" [i] Installer un autre navigateur")
sel = input(
f"Choix (numéro, vide = {available[0]}) : "
).strip().lower()
if sel == "i":
return self._install_cli_browser()
if not sel:
return available[0]
try:
idx = int(sel) - 1
if 0 <= idx < len(available):
return available[idx]
except ValueError:
pass
return available[0]
def _install_cli_browser(self): def _install_cli_browser(self):
"""Demande QUEL navigateur CLI installer (w3m/lynx/links/elinks), """Demande QUEL navigateur CLI installer (w3m/lynx/links/elinks),
affiche la commande, l'exécute après validation. Renvoie le binaire affiche la commande, l'exécute après validation. Renvoie le binaire

View file

@ -2533,12 +2533,48 @@ class TODO:
pass pass
return profiles[0] # défaut : ERPLibre + Odoo 18 return profiles[0] # défaut : ERPLibre + Odoo 18
@staticmethod
def _qemu_odoo_service_cmd():
"""Snippet shell (exécuté dans la VM) qui installe ERPLibre/Odoo comme
service systemd (inspiré de script/systemd/install_daemon.sh) puis
l'active + démarre. N'est ajouté QUE pour les profils avec Odoo."""
return (
'SVC_USER=$(whoami); SVC_GROUP=$(id -gn); '
'SVC_DIR="$HOME/git/erplibre"; '
"sudo tee /etc/systemd/system/erplibre.service >/dev/null <<UNIT\n"
"[Unit]\n"
"Description=ERPLibre\n"
"Requires=postgresql.service\n"
"After=network.target network-online.target postgresql.service\n"
"\n"
"[Service]\n"
"Type=simple\n"
"User=$SVC_USER\n"
"Group=$SVC_GROUP\n"
"Restart=always\n"
"RestartSec=5\n"
"ExecStart=$SVC_DIR/run.sh\n"
"WorkingDirectory=$SVC_DIR\n"
"StandardOutput=journal+console\n"
"\n"
"[Install]\n"
"WantedBy=multi-user.target\n"
"UNIT\n"
"sudo systemctl daemon-reload; "
"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):
"""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` dans ~/git/erplibre). `final_cmd` par défaut :
install_os + install_odoo_18.""" install_os + install_odoo_18."""
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
# enregistre Odoo comme service systemd (enable + start). Pas pour
# « ERPLibre seul », « mobile » ni « Déploiement ».
if "install_odoo" in final_cmd:
final_cmd = f"{final_cmd} && {self._qemu_odoo_service_cmd()}"
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