[ADD] tui qemu: choose mise or pyenv at deploy time
The interpreter provider is now chosen when creating a VM, in both interfaces. "mise" installs it in the VM and lays down a precompiled CPython; "pyenv" keeps today's build-from-source. The question is only asked where it means something: mise publishes binaries for amd64 and arm64 only. An all-s390x fleet never sees it, a mixed fleet sees it along with the architectures that will fall back to pyenv -- said before deploying, not discovered in a log an hour later. mise goes into /usr/local/bin rather than ~/.local/bin: the remote command runs through "ssh host 'command'", where neither ~/.profile nor ~/.bashrc is read. Choosing "mise" exports "auto", not "mise", so a failed install can still fall back to pyenv. The unavailability notice was translated as "pyenv required", which said the opposite of what happens: pyenv is the fallback, not a demand. --- FR --- Le fournisseur d'interpréteur se choisit désormais à la création d'une VM, dans les deux interfaces. « mise » l'installe dans la VM et pose un CPython précompilé ; « pyenv » garde la compilation d'aujourd'hui. La question n'est posée que là où elle a un sens : mise ne publie de binaires que pour amd64 et arm64. Un parc tout s390x ne la voit jamais, un parc mixte la voit avec les architectures qui retomberont sur pyenv — dit avant de déployer, pas découvert dans un log une heure plus tard. mise va dans /usr/local/bin plutôt que ~/.local/bin : la commande distante passe par « ssh hôte 'commande' », où ni ~/.profile ni ~/.bashrc ne sont lus. Choisir « mise » exporte « auto », pas « mise », pour qu'une installation ratée puisse encore retomber sur pyenv. L'avis d'indisponibilité était traduit par « pyenv exigé », qui disait l'inverse de ce qui se passe : pyenv est le repli, pas une exigence. Assisted-by: Claude Opus 5
This commit is contained in:
parent
70d9ad456c
commit
7f6c18037d
3 changed files with 171 additions and 4 deletions
|
|
@ -178,6 +178,7 @@ def build_spec(vms, domains, form):
|
|||
"ssh_key": form["ssh_key"],
|
||||
"timezone": form.get("timezone", ""),
|
||||
"desktop": form.get("desktop", ""),
|
||||
"python_provider": form.get("python_provider", ""),
|
||||
"install": form["install"],
|
||||
"add_ssh_config": form["add_ssh_config"],
|
||||
"parallelism": form["parallelism"],
|
||||
|
|
@ -245,6 +246,8 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
desktop_disk = ctx.get("desktop_disk_gb") or 0
|
||||
# [(clé, libellé)] — la liste vient de todo.py, source unique.
|
||||
desktops = list(ctx.get("desktops") or [])
|
||||
# Architectures pour lesquelles mise publie un binaire.
|
||||
mise_arches = set(ctx.get("mise_arches") or ())
|
||||
defaults = ctx.get("defaults") or {}
|
||||
result = {"spec": None}
|
||||
|
||||
|
|
@ -508,6 +511,18 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
value=defaults.get("add_ssh_config", True),
|
||||
id="f_sshcfg",
|
||||
)
|
||||
# mise pose un CPython précompilé, pyenv le compile.
|
||||
# Grisé quand AUCUNE des VM retenues n'est sur une
|
||||
# architecture que mise sert.
|
||||
yield Static(
|
||||
t("Python interpreter:"), classes="grouptitle"
|
||||
)
|
||||
with RadioSet(id="f_python"):
|
||||
yield RadioButton(
|
||||
t("mise (precompiled, faster)"), value=True
|
||||
)
|
||||
yield RadioButton(t("pyenv (compiles from source)"))
|
||||
yield Static("", id="miswarn")
|
||||
yield Static(t("Parallelism"), classes="grouptitle")
|
||||
# Cochée, la case donne une exécution PAR installation :
|
||||
# le plafond du nombre de CPU ne s'applique plus. Décochée,
|
||||
|
|
@ -600,6 +615,44 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
grow += desktop_disk
|
||||
self.rows = plan_rows(self.vms, domains, grow)
|
||||
self._render_plan()
|
||||
self._render_mise()
|
||||
|
||||
def _render_mise(self):
|
||||
"""Grise le choix quand aucune VM retenue n'est servie par mise,
|
||||
et nomme les architectures qui retomberont sur pyenv."""
|
||||
usable = self._mise_usable()
|
||||
self.query_one("#f_python", RadioSet).disabled = not usable
|
||||
skipped = sorted(
|
||||
{
|
||||
vm["arch"]
|
||||
for vm in self.vms
|
||||
if vm["arch"] not in mise_arches
|
||||
}
|
||||
)
|
||||
msg = ""
|
||||
if skipped:
|
||||
msg = (
|
||||
f" ⚠ {t('mise has no binary for:')} "
|
||||
f"{', '.join(skipped)} — {t('those VMs use pyenv')}"
|
||||
)
|
||||
self.query_one("#miswarn", Static).update(msg)
|
||||
|
||||
def _python_provider(self):
|
||||
"""« mise », « pyenv », ou rien — c'est-à-dire « automatique ».
|
||||
|
||||
Rien, et surtout pas « pyenv », quand mise n'est servi par aucune
|
||||
architecture retenue. « mise est indisponible » ne veut pas dire
|
||||
« l'utilisateur exige pyenv » : la nuance décide de tout, puisqu'un
|
||||
choix EXPLICITE écarte le Python de la distribution. Sur s390x,
|
||||
renvoyer « pyenv » forçait la compilation de CPython — celle dont
|
||||
gcc 15.2 ne revient pas."""
|
||||
if not self._mise_usable():
|
||||
return ""
|
||||
index = self.query_one("#f_python", RadioSet).pressed_index
|
||||
return "pyenv" if index == 1 else "mise"
|
||||
|
||||
def _mise_usable(self):
|
||||
return any(vm["arch"] in mise_arches for vm in self.vms)
|
||||
|
||||
def _desktop(self):
|
||||
"""« » pour un serveur, sinon la clé de la saveur choisie."""
|
||||
|
|
@ -783,6 +836,7 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
or ctx.get("timezone")
|
||||
or "",
|
||||
"desktop": self._desktop(),
|
||||
"python_provider": self._python_provider(),
|
||||
"install": install,
|
||||
"add_ssh_config": self.query_one("#f_sshcfg", Checkbox).value,
|
||||
# Une exécution par installation : le nombre de VM retenues
|
||||
|
|
|
|||
|
|
@ -4512,8 +4512,51 @@ class TODO:
|
|||
'(vncpasswd puis vncserver :1)"; fi; '
|
||||
)
|
||||
|
||||
# mise ne publie de binaire que pour ces architectures : 46 assets à la
|
||||
# v2026.8.4, aucun s390x — son propre script d'installation refuse cette
|
||||
# plateforme. Ailleurs, le choix « mise » est sans objet et on reste sur
|
||||
# pyenv, ce que le formulaire et l'invite disent avant de déployer.
|
||||
QEMU_MISE_ARCHES = ("amd64", "arm64")
|
||||
|
||||
def _qemu_mise_remote_cmd(self, python_provider):
|
||||
"""Pose mise DANS la VM et fixe EL_PYTHON_PROVIDER pour l'installation.
|
||||
|
||||
mise s'installe par défaut dans ~/.local/bin, qui n'est PAS dans le
|
||||
PATH d'un « ssh hôte 'commande' » : ni ~/.profile ni ~/.bashrc n'y sont
|
||||
lus. On le pose donc dans /usr/local/bin, présent dans le PATH par
|
||||
défaut — même raison que pour cargo et rustc.
|
||||
|
||||
Sans mise utilisable, rien n'est écrit : lib_python_provider.sh
|
||||
retombe alors sur pyenv toute seule."""
|
||||
if python_provider == "pyenv":
|
||||
# Explicite : même si mise se trouvait déjà dans l'image, on ne
|
||||
# l'utilise pas. Sans cela le mode « auto » du dépôt le prendrait.
|
||||
return "export EL_PYTHON_PROVIDER=pyenv; "
|
||||
if python_provider != "mise":
|
||||
return ""
|
||||
return (
|
||||
f'echo "== {t("Installing mise (precompiled Python)")} =="; '
|
||||
"if command -v mise >/dev/null 2>&1; then "
|
||||
'echo " mise: $(mise --version)"; '
|
||||
"else "
|
||||
# La variable est passée À sudo, pas exportée avant : « sudo -E »
|
||||
# dépend de env_reset dans sudoers et n'est pas garanti.
|
||||
"curl -fsSL https://mise.run "
|
||||
"| sudo MISE_INSTALL_PATH=/usr/local/bin/mise sh "
|
||||
'|| echo " mise indisponible ici : pyenv prendra le relais"; '
|
||||
"fi; "
|
||||
# « auto », et non « mise » : si l'installation ci-dessus a échoué,
|
||||
# lib_python_provider.sh doit pouvoir retomber sur pyenv.
|
||||
"export EL_PYTHON_PROVIDER=auto; "
|
||||
)
|
||||
|
||||
def _qemu_erplibre_remote_cmd(
|
||||
self, branch, final_cmd=None, prod=False, desktop=False
|
||||
self,
|
||||
branch,
|
||||
final_cmd=None,
|
||||
prod=False,
|
||||
desktop=False,
|
||||
python_provider="",
|
||||
):
|
||||
"""Script exécuté DANS la VM. `branch` à None n'installe QUE le bureau
|
||||
— le choix graphique ne dépend pas d'ERPLibre, et une VM peut être
|
||||
|
|
@ -4521,7 +4564,9 @@ class TODO:
|
|||
|
||||
`final_cmd` par défaut : install_os + install_odoo_18. `prod` :
|
||||
installe dans /opt/erplibre (au lieu de ~/git/erplibre) + service
|
||||
SELinux confiné. `desktop` : ajoute GNOME et son accès distant."""
|
||||
SELinux confiné. `desktop` : ajoute GNOME et son accès distant.
|
||||
`python_provider` : « mise » pour un CPython précompilé, sinon le
|
||||
comportement par défaut du dépôt (pyenv, qui compile)."""
|
||||
if not branch:
|
||||
# Bureau seul : ni clone ni make, mais on garde le prologue —
|
||||
# attente de cloud-init et coupure des mises à jour automatiques,
|
||||
|
|
@ -4635,6 +4680,7 @@ class TODO:
|
|||
"for t in curl git make; do command -v $t >/dev/null 2>&1 || "
|
||||
'{ echo "Outil manquant apres installation: $t '
|
||||
'(reseau de la VM ?)"; exit 1; }; done; '
|
||||
+ self._qemu_mise_remote_cmd(python_provider)
|
||||
# Clone : /opt/erplibre en PROD (racine, puis chown à l'utilisateur
|
||||
# pour que make/venv s'exécutent sans sudo), ~/git/erplibre en dev.
|
||||
+ (
|
||||
|
|
@ -4665,6 +4711,7 @@ class TODO:
|
|||
final_cmd=None,
|
||||
prod=False,
|
||||
desktop=False,
|
||||
python_provider="",
|
||||
):
|
||||
"""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.
|
||||
|
|
@ -4677,7 +4724,7 @@ class TODO:
|
|||
)
|
||||
|
||||
remote = self._qemu_erplibre_remote_cmd(
|
||||
branch, final_cmd, prod, desktop
|
||||
branch, final_cmd, prod, desktop, python_provider
|
||||
)
|
||||
try:
|
||||
mod = self._qemu_import_module()
|
||||
|
|
@ -4742,6 +4789,7 @@ class TODO:
|
|||
final_cmd=None,
|
||||
prod=False,
|
||||
desktop=False,
|
||||
python_provider="",
|
||||
):
|
||||
"""Clone ERPLibre (branche donnée) dans la VM puis exécute la commande
|
||||
d'install du profil choisi (streamé). `ip` : IP déjà résolue ;
|
||||
|
|
@ -4763,7 +4811,7 @@ class TODO:
|
|||
)
|
||||
return
|
||||
remote = self._qemu_erplibre_remote_cmd(
|
||||
branch, final_cmd, prod, desktop
|
||||
branch, final_cmd, prod, desktop, python_provider
|
||||
)
|
||||
ssh_opts = (
|
||||
"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
|
||||
|
|
@ -5145,6 +5193,9 @@ class TODO:
|
|||
print(
|
||||
f" {t('VM type:')} {t('Graphical (server + desktop):')} {label}"
|
||||
)
|
||||
prov = spec.get("python_provider")
|
||||
if prov:
|
||||
print(f" {t('Python interpreter:')} {prov}")
|
||||
print(f" {t('SSH key:')} {spec.get('ssh_key') or t('none')}")
|
||||
cfg = (
|
||||
t("one entry per VM") if spec["add_ssh_config"] else t("untouched")
|
||||
|
|
@ -5472,6 +5523,7 @@ class TODO:
|
|||
"disk_presets": self._QEMU_DISK_PRESETS,
|
||||
"extra_disk_gb": self.ERPLIBRE_EXTRA_DISK_GB,
|
||||
"desktop_disk_gb": self.QEMU_DESKTOP_EXTRA_DISK_GB,
|
||||
"mise_arches": self.QEMU_MISE_ARCHES,
|
||||
"desktops": [
|
||||
(k, v["label"]) for k, v in self._QEMU_DESKTOP.items()
|
||||
],
|
||||
|
|
@ -5762,6 +5814,32 @@ class TODO:
|
|||
return ""
|
||||
return flavours[index] if 0 <= index < len(flavours) else ""
|
||||
|
||||
def _qemu_ask_python_provider(self, arches):
|
||||
"""mise (CPython précompilé) ou pyenv (compilation).
|
||||
|
||||
`arches` : les architectures du parc à déployer. mise ne publie pas de
|
||||
binaire pour toutes — hors de QEMU_MISE_ARCHES la question n'a pas de
|
||||
sens et on ne la pose pas."""
|
||||
usable = [a for a in arches if a in self.QEMU_MISE_ARCHES]
|
||||
if not usable:
|
||||
# Rien, pas « pyenv » : le mode automatique doit rester libre de
|
||||
# préférer un Python de la distribution. Voir _python_provider()
|
||||
# dans le formulaire, même raisonnement.
|
||||
return ""
|
||||
print(f"\n{t('Python interpreter:')}")
|
||||
print(f" [1] {t('mise (precompiled, faster)')} *")
|
||||
print(f" [2] {t('pyenv (compiles from source)')}")
|
||||
skipped = [a for a in arches if a not in self.QEMU_MISE_ARCHES]
|
||||
if skipped:
|
||||
# Dit AVANT le déploiement plutôt que découvert dans un log.
|
||||
print(
|
||||
f" ⚠ {t('mise has no binary for:')} "
|
||||
f"{', '.join(sorted(set(skipped)))} — "
|
||||
f"{t('those VMs use pyenv')}"
|
||||
)
|
||||
sel = input(t("Choice (number, blank = mise): ")).strip()
|
||||
return "pyenv" if sel == "2" else "mise"
|
||||
|
||||
def _qemu_host_timezone(self):
|
||||
"""Fuseau de l'hôte. Défini une seule fois, dans deploy_qemu.py, qui
|
||||
est aussi ce qui l'écrit dans le cloud-config : l'invite ne peut donc
|
||||
|
|
@ -5821,6 +5899,9 @@ class TODO:
|
|||
timezone = self._qemu_ask_timezone()
|
||||
locale = self._qemu_ask_locale()
|
||||
desktop = self._qemu_ask_desktop()
|
||||
python_provider = self._qemu_ask_python_provider(
|
||||
[vm["arch"] for vm in vms]
|
||||
)
|
||||
|
||||
# 4) Option : installer ERPLibre dans ~/git/erplibre de chaque VM.
|
||||
install = None
|
||||
|
|
@ -5898,6 +5979,7 @@ class TODO:
|
|||
"timezone": timezone,
|
||||
"locale": locale,
|
||||
"desktop": desktop,
|
||||
"python_provider": python_provider,
|
||||
"install": install,
|
||||
"add_ssh_config": add_ssh_config,
|
||||
"parallelism": parallelism,
|
||||
|
|
@ -5966,6 +6048,7 @@ class TODO:
|
|||
install = spec.get("install")
|
||||
install_branch = install["branch"] if install else None
|
||||
desktop = spec.get("desktop") or ""
|
||||
python_provider = spec.get("python_provider") or ""
|
||||
ssh_key = spec.get("ssh_key")
|
||||
add_ssh_config = spec["add_ssh_config"]
|
||||
parallelism = spec["parallelism"]
|
||||
|
|
@ -6040,6 +6123,7 @@ class TODO:
|
|||
install["cmd"] if install else None,
|
||||
install["prod"] if install else False,
|
||||
desktop=desktop,
|
||||
python_provider=python_provider,
|
||||
)
|
||||
else:
|
||||
print(
|
||||
|
|
@ -6055,6 +6139,7 @@ class TODO:
|
|||
install["cmd"],
|
||||
install["prod"],
|
||||
desktop=desktop,
|
||||
python_provider=python_provider,
|
||||
)
|
||||
|
||||
# Sommaire TOTAL (déploiement + résolution IP + ssh_config + install
|
||||
|
|
|
|||
|
|
@ -2023,6 +2023,34 @@ TRANSLATIONS = {
|
|||
"fr": "Choix (numéro, vide = rouvrir) : ",
|
||||
"en": "Choice (number, blank = reopen): ",
|
||||
},
|
||||
"Python interpreter:": {
|
||||
"fr": "Interpreteur Python :",
|
||||
"en": "Python interpreter:",
|
||||
},
|
||||
"mise (precompiled, faster)": {
|
||||
"fr": "mise (precompile, plus rapide)",
|
||||
"en": "mise (precompiled, faster)",
|
||||
},
|
||||
"pyenv (compiles from source)": {
|
||||
"fr": "pyenv (compile depuis les sources)",
|
||||
"en": "pyenv (compiles from source)",
|
||||
},
|
||||
"Choice (number, blank = mise): ": {
|
||||
"fr": "Choix (numero, vide = mise) : ",
|
||||
"en": "Choice (number, blank = mise): ",
|
||||
},
|
||||
"mise has no binary for:": {
|
||||
"fr": "mise ne publie pas de binaire pour :",
|
||||
"en": "mise has no binary for:",
|
||||
},
|
||||
"those VMs use pyenv": {
|
||||
"fr": "ces VM utiliseront pyenv",
|
||||
"en": "those VMs use pyenv",
|
||||
},
|
||||
"Installing mise (precompiled Python)": {
|
||||
"fr": "Installation de mise (Python precompile)",
|
||||
"en": "Installing mise (precompiled Python)",
|
||||
},
|
||||
"VM type:": {
|
||||
"fr": "Type de VM :",
|
||||
"en": "VM type:",
|
||||
|
|
|
|||
Loading…
Reference in a new issue