[IMP] script todo: suivi install — colonne « Odoo » (état :8069)

Nouvelle colonne « Odoo » dans le dashboard : teste par TCP si l'UI web
répond sur le port 8069 de la VM (🟢 up / — sinon). Une fois « up »
détecté, on ne re-teste plus (Odoo ne redescend pas en cours d'install).
Le test (socket, timeout 0,5 s) est fait dans le thread collecteur ->
n'impacte pas la fluidité de l'UI.

Validé headless : port ouvert -> 🟢, fermé -> —.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathieu Benoit 2026-07-30 01:52:55 -04:00
parent 1ebf2b353f
commit 7904c72d71

View file

@ -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."""
@ -645,6 +657,9 @@ 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
# VM dont l'UI Odoo (:8069) répond déjà : une fois détectée « up »,
# on ne re-teste plus (Odoo ne redescend pas en cours d'install).
self._odoo_up = set()
# Debounce du changement de VM : la sélection défile vite au # Debounce du changement de VM : la sélection défile vite au
# clavier ; on ne recharge le log qu'une fois le curseur STABILISÉ. # clavier ; on ne recharge le log qu'une fois le curseur STABILISÉ.
self._pending_sel = None self._pending_sel = None
@ -673,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(
@ -769,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)))
@ -784,19 +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):
# 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()
@ -804,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)