[ADD] tui qemu: an Odoo version per VM

A profile list per row, beside the branch: "ERPLibre + Odoo 18",
"Odoo 17", "ERPLibre alone". The form's profile stays the default; picking
another for a VM creates an override, going back clears it.

It reaches down to execution, like the branch and the type before it.
"final_cmd" now takes either a string for the whole fleet or a
{name: command} map, and the remote command is built per machine as soon
as profiles differ. A VM can install Odoo 18 while another validates
Odoo 17, in the same deployment.

Both lists are widened -- branch to 24 columns, profile to 28. Truncated,
"develop" and "ERPLibre + Odoo 18" no longer showed what had been picked,
and that is precisely what one wants to re-read before deploying.

Two more defects: the global branch and Odoo choices reached nothing at
all, and the summary announced the global profile for every VM, then named
a version nothing would install.

--- FR ---

Une liste de profils par rangée, à côté de la branche : « ERPLibre +
Odoo 18 », « Odoo 17 », « ERPLibre seul ». Le profil du formulaire reste
le défaut ; en choisir un autre pour une VM crée une surcharge, y revenir
l'efface.

Il descend jusqu'à l'exécution, comme la branche et le type avant lui.
« final_cmd » accepte maintenant une chaîne pour tout le parc ou une carte
{nom: commande}, et la commande distante est bâtie par machine dès que les
profils diffèrent. Une VM peut donc installer Odoo 18 pendant qu'une autre
valide Odoo 17, dans le même déploiement.

Les deux listes sont élargies : la branche passe à 24 colonnes, le profil
à 28. Tronqués, « develop » et « ERPLibre + Odoo 18 » ne laissaient plus
voir ce qu'on avait choisi — et c'est précisément ce qu'on veut relire
avant de déployer.

Deux autres défauts : les choix globaux de branche et d'Odoo n'atteignaient
rien, et le sommaire annonçait le profil global pour toutes les VM, puis
nommait une version que rien n'installait.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-08-13 01:15:13 -04:00
parent 2eee924759
commit 8a946a0b89
2 changed files with 131 additions and 9 deletions

View file

@ -169,6 +169,12 @@ def apply_profile(
# Branche ERPLibre. Même raison : « » signifie « celle du
# formulaire », et une surcharge la remplace pour cette VM.
"branch": "",
# Profil d'installation (« ERPLibre + Odoo 18 »). Même
# convention : « » = celui du formulaire.
"install_cmd": "",
# Libelle du profil, pour que le recapitulatif puisse dire
# « Odoo 18 » sans connaitre la liste des profils.
"install_label": "",
}
)
return out
@ -449,7 +455,10 @@ def run_deploy_form(ctx, run_app: bool = True):
#body { height: 1fr; }
#fields { width: 62; border: solid $accent; overflow-y: auto; }
#right { width: 1fr; }
#plan { height: 1fr; border: solid $accent; }
#plan {
height: 1fr; border: solid $accent;
overflow-x: auto; scrollbar-size-horizontal: 1;
}
#totals { height: auto; color: $text-muted; padding: 0 1; }
.grouptitle { color: $accent; text-style: bold; padding: 1 0 0 0; }
SelectionList { height: 10; border: solid $panel; }
@ -473,12 +482,22 @@ def run_deploy_form(ctx, run_app: bool = True):
/* La branche porte des noms longs (« 1.6.0 », « develop »,
« feature/xyz ») : trop étroite, la liste les tronque et on ne sait
plus ce qu'on a choisi. */
.vmbranch { width: 18; }
.vmcopy { width: 5; min-width: 5; }
.vmhead { height: 1; }
.vmrow { height: 3; align-vertical: middle; }
/* « width: auto » et le défilement du plan : sans eux, une rangée
plus large que le panneau est COUPÉE au lieu d'être atteignable. */
.vmrow { height: 3; width: auto; align-vertical: middle; }
.vmrow Select { width: 15; }
.vmrow Input { width: 11; }
/* Ces deux règles portent « .vmrow Select » EN PLUS de leur classe :
« .vmrow Select » (une classe + un type) l'emporte sur « .vmbranch »
(une classe) par spécificité CSS. Écrites simplement, elles étaient
silencieusement écrasées à 15 — et le test, qui ne vérifiait que la
présence de la classe, passait sans rien prouver. */
.vmrow Select.vmbranch { width: 34; }
.vmrow Select.vmprof { width: 40; }
#reslabel { color: $text-muted; }
RenameScreen { align: center middle; }
#renbox {
@ -864,6 +883,24 @@ def run_deploy_form(ctx, run_app: bool = True):
def _mise_usable(self):
return any(vm["arch"] in mise_arches for vm in self.vms)
def _profile_cmd(self):
"""Commande du profil choisi en haut : le défaut de chaque VM."""
if not profiles:
return ""
index = self.query_one("#f_profile_install", Select).value
return profiles[index if isinstance(index, int) else 0][1]
def _row_profile_index(self, i):
"""Rang du profil que la rangée doit AFFICHER."""
cmd = ""
if i < len(self.rows):
cmd = self.rows[i]["vm"].get("install_cmd") or ""
cmd = cmd or self._profile_cmd()
for k, (_lbl, c) in enumerate(profiles):
if c == cmd:
return k
return 0
def _branch(self):
"""Branche du formulaire : le défaut de chaque VM."""
value = self.query_one("#f_branch", Select).value
@ -1005,6 +1042,17 @@ def run_deploy_form(ctx, run_app: bool = True):
allow_blank=False,
id=f"v{i}_branch",
),
(
Select(
[(lbl, i) for i, (lbl, _c) in enumerate(profiles)],
value=self._row_profile_index(i),
allow_blank=False,
classes="vmprof",
id=f"v{i}_prof",
)
if profiles
else Static("", classes="vmprof")
),
Select(
self._type_options(),
value=vm.get("desktop") or SERVER,
@ -1093,6 +1141,7 @@ def run_deploy_form(ctx, run_app: bool = True):
for wid, value in (
(f"#v{i}_type", vm.get("desktop") or SERVER),
(f"#v{i}_branch", vm.get("branch") or self._branch()),
(f"#v{i}_prof", self._row_profile_index(i)),
):
try:
self.query_one(wid, Select).value = value
@ -1262,6 +1311,9 @@ def run_deploy_form(ctx, run_app: bool = True):
continue
for field in fields:
changed |= self.overrides[key].pop(field, None) is not None
if field == "install_cmd":
# Le libelle n'a pas de sens sans sa commande.
self.overrides[key].pop("install_label", None)
if not self.overrides[key]:
self.overrides.pop(key, None)
if changed:
@ -1336,6 +1388,21 @@ def run_deploy_form(ctx, run_app: bool = True):
# suivra donc le profil s'il change — ce qui est aussi le plus
# attendu quand on n'a rien changé de visible.
vm_now = self.rows[index]["vm"]
if field == "prof":
label, cmd = profiles[event.value]
if cmd == (
vm_now.get("install_cmd") or self._profile_cmd()
):
return
same = cmd == self._profile_cmd()
self._set_override(
index, "install_cmd", "" if same else cmd
)
self._set_override(
index, "install_label", "" if same else label
)
self._recompute()
return
if field == "branch":
# « la branche du formulaire » n'est pas une surcharge :
# la VM doit suivre si on la change en haut.
@ -1472,6 +1539,14 @@ def run_deploy_form(ctx, run_app: bool = True):
"install_cmd": (
vm.get("install_cmd") or self._profile_cmd()
),
"install_label": (
vm.get("install_label")
or (
profiles[self._row_profile_index(index)][0]
if profiles
else ""
)
),
}
else:
self.locked.discard(key)

View file

@ -4902,9 +4902,13 @@ class TODO:
# une carte {nom: branche} quand elles diffèrent d'une VM à l'autre.
branch_map = branch if isinstance(branch, dict) else {}
branch_def = "" if branch_map else branch
# Idem pour le profil : « ERPLibre + Odoo 18 » peut differer d'une
# machine a l'autre, on valide alors deux versions d'un coup.
cmd_map = final_cmd if isinstance(final_cmd, dict) else {}
cmd_def = None if cmd_map else final_cmd
remote = self._qemu_erplibre_remote_cmd(
branch_def,
final_cmd,
cmd_def,
prod,
"" if desk_map else desktop,
python_provider,
@ -4932,10 +4936,10 @@ class TODO:
"version": v,
"arch": a,
}
if desk_map or branch_map:
if desk_map or branch_map or cmd_map:
entry["remote_cmd"] = self._qemu_erplibre_remote_cmd(
branch_map.get(name, branch_def),
final_cmd,
cmd_map.get(name, cmd_def),
prod,
desk_map.get(name, ""),
python_provider,
@ -5361,10 +5365,33 @@ class TODO:
gigs = self._parse_disk_gb(vm["disk"]) + (
self.ERPLIBRE_EXTRA_DISK_GB if branch else 0
)
# Ce qui S'ECARTE du choix commun se dit sur la ligne de la VM.
# Sans cela le sommaire annoncait le profil general pour tout le
# monde, y compris pour une VM figee sur un autre — on lisait
# « Odoo 15 » avant de deployer une machine en Odoo 18.
apart = []
# Seul ce qui DIFFERE vaut d'etre signale : une VM figee sur la
# meme branche que le global n'a rien de particulier a montrer,
# et repeter la valeur commune sur chaque ligne la noierait.
if install and vm.get("branch") and vm["branch"] != branch:
apart.append(vm["branch"])
if (
vm.get("install_label")
and install
and vm.get("install_cmd") != install.get("cmd")
):
apart.append(vm["install_label"])
if vm.get("desktop") and vm["desktop"] != spec.get("desktop"):
apart.append(
(self._QEMU_DESKTOP.get(vm["desktop"]) or {}).get(
"label", vm["desktop"]
)
)
print(
f" {vm['name']:<30} {vm['distro']} {vm['version']:<7} "
f"[{vm['arch']:<5}] {vm['vcpus']} vCPU RAM {vm['ram']}Mo "
f"{t('disk')} {gigs}G"
+ (f" ⟵ {' · '.join(apart)}" if apart else "")
)
if existing:
print(f" {t('Existing, left untouched:')} {', '.join(existing)}")
@ -5374,9 +5401,22 @@ class TODO:
if install["prod"]
else t("development (~/git)")
)
# « par defaut » : chaque VM peut s'en ecarter, et sa ligne le dit.
# Ce qui sera REELLEMENT pose, pas le defaut du formulaire. Avec
# une seule VM figee sur un autre profil, annoncer le defaut
# revenait a nommer une version que rien n'installe — c'est
# exactement ce qu'on relit ici pour eviter de se tromper.
used_br = {vm.get("branch") or branch for vm in spec["vms"]}
used_lb = {
vm.get("install_label") or install["label"]
for vm in spec["vms"]
}
varies = t("varies, see each line")
br_txt = used_br.pop() if len(used_br) == 1 else varies
lb_txt = used_lb.pop() if len(used_lb) == 1 else varies
print(
f" {t('ERPLibre install:')} {t('branch')} {branch}, "
f"{t('profile')} {install['label']}, {env}"
f" {t('ERPLibre install:')} {t('branch')} {br_txt}, "
f"{t('profile')} {lb_txt}, {env}"
)
else:
print(f" {t('ERPLibre install:')} {t('no')}")
@ -6305,6 +6345,13 @@ class TODO:
for _n in deployed:
branch_map.setdefault(_n, install_branch or "")
branch_multi = len(set(branch_map.values())) > 1
base_cmd = install["cmd"] if install else None
cmd_map = {
vm["name"]: (vm.get("install_cmd") or base_cmd) for vm in pending
}
for _n in deployed:
cmd_map.setdefault(_n, base_cmd)
cmd_multi = len(set(cmd_map.values())) > 1
ssh_key = spec.get("ssh_key")
add_ssh_config = spec["add_ssh_config"]
parallelism = spec["parallelism"]
@ -6376,7 +6423,7 @@ class TODO:
deployed,
branch_map if branch_multi else install_branch,
ip_map,
install["cmd"] if install else None,
cmd_map if cmd_multi else base_cmd,
install["prod"] if install else False,
desktop=desktop_map,
python_provider=python_provider,