[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
"""Ce qu'on installe DANS la VM, et qui ne regarde pas l'hyperviseur.
|
|
|
|
|
|
|
|
|
|
Type de VM, production, magasin d'applications, outils de développement,
|
|
|
|
|
fuseau horaire, interpréteur Python : six réglages qui décrivent le système
|
|
|
|
|
invité, pas la machine qui le porte. Ils valent donc mot pour mot sur libvirt
|
|
|
|
|
et sur Proxmox VE — et c'est exactement ce qui avait dérivé. L'écran QEMU/KVM
|
|
|
|
|
les portait tous, l'écran Proxmox trois : une VM créée là-bas naissait sans
|
|
|
|
|
bureau, sans outils et en UTC, sans que rien ne le dise.
|
|
|
|
|
|
|
|
|
|
La duplication était le mécanisme de la dérive, pas son symptôme : chaque
|
|
|
|
|
correctif se posait sur un seul des deux écrans. Ce module met les six ici,
|
|
|
|
|
une seule fois, widgets ET logique. Un formulaire hérite d'`ExtrasMixin`,
|
|
|
|
|
appelle `extras_init(ctx)` puis pose les fragments de `compose_*` où sa mise
|
|
|
|
|
en page les veut.
|
|
|
|
|
|
|
|
|
|
Le contrat côté formulaire, court exprès : l'attribut `vms` (les VM retenues)
|
|
|
|
|
et une case `#f_install`. Rien d'autre — ce module ne connaît ni stockage, ni
|
|
|
|
|
pont, ni domaine libvirt.
|
|
|
|
|
|
|
|
|
|
Chaque accès aux widgets est gardé : un formulaire n'est pas tenu de poser
|
|
|
|
|
tous les fragments, et lire un widget absent ne doit pas casser l'écran.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from script.todo.deploy_form_lib import FREE, t
|
|
|
|
|
|
[REF] déploiement : la branche, le profil et le type se choisissent par VM
Sur Proxmox on déploie le plus souvent un parc MIXTE — un hyperviseur
imbriqué à côté de VM ERPLibre. C'est exactement le cas où un réglage par
machine sert, et c'est le seul écran qui ne l'offrait pas : ses rangées
n'avaient ni branche, ni profil, ni type.
Les trois choix et leur gestionnaire — quatre-vingts lignes — rejoignent le
socle. Les dupliquer aurait remis en place le mécanisme de dérive qu'on vient
d'enlever. L'écran QEMU/KVM perd encore 130 lignes sans qu'un widget, un
modèle ou une spec ne bouge : ancien et nouveau montés dans le même
processus, mêmes rangées, mêmes valeurs après avoir changé une branche, un
type et un profil.
Le déploiement suit : il lisait la seule valeur commune alors que le plan
portait déjà le choix par rangée. Une seule VM qui s'écarte suffit à rendre
la carte nécessaire — « len(set) > 1 » ne l'aurait pas vu.
Un défaut trouvé par un test, pas à l'usage : l'écho du montage se
reconnaissait à sa commande, or quand la commande imposée par le système
n'est pas dans la liste proposée, la liste retombe au rang 0 — et l'écho de
ce rang 0 effaçait l'imposition. Un Proxmox imbriqué reprenait ERPLibre et
Odoo 18. L'écho se reconnaît maintenant au RANG affiché.
--- EN ---
On Proxmox you usually deploy a MIXED fleet — a nested hypervisor next to
ERPLibre VMs. That is exactly where a per-machine setting earns its keep, and
it was the only screen without one: its rows had no branch, no profile, no
type.
The three choices and their handler — eighty lines — move into the shared
foundation. Duplicating them would have restored the very drift mechanism we
just removed. The QEMU/KVM screen loses another 130 lines with no widget,
model or spec moving: old and new mounted in one process, same rows, same
values after changing a branch, a type and a profile.
The deployment follows: it read the single common value while the plan
already carried the per-row choice. One VM that differs is enough to require
the map — "len(set) > 1" would not have seen it.
One defect found by a test, not by use: the mount echo was recognised by its
command, yet when the command imposed by the guest OS is absent from the
offered list, the list falls back to index 0 — and that index-0 echo erased
the imposition. A nested Proxmox took ERPLibre and Odoo 18 back. The echo is
now recognised by the DISPLAYED index.
Assisted-by: Claude Opus 5
2026-08-24 23:31:13 -04:00
|
|
|
# « Serveur » est un CHOIX, pas une absence de choix : lui donner « » le
|
|
|
|
|
# rendrait indistinguable de la sentinelle « rien de sélectionné ».
|
|
|
|
|
SERVER = "__server__"
|
|
|
|
|
|
[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
# Ce que l'écran lit du contexte. Une seule liste, parce que les deux
|
|
|
|
|
# formulaires doivent en recevoir autant : c'est en fournissant un
|
|
|
|
|
# sous-ensemble que l'écran Proxmox avait perdu la moitié des réglages.
|
|
|
|
|
_TABLES = (
|
|
|
|
|
("desktops", list, "desktops"),
|
|
|
|
|
("desktop_disk", int, "desktop_disk_gb"),
|
|
|
|
|
("app_stores", list, "app_stores"),
|
|
|
|
|
("snap_distros", set, "snap_distros"),
|
|
|
|
|
("timezones", list, "timezones"),
|
|
|
|
|
("timezone", str, "timezone"),
|
|
|
|
|
("vm_tools", list, "vm_tools"),
|
[IMP] qemu deploy : pré-configurer la VM, et dire ce qui s'y installe
L'option des outils d'assistance posait trois installateurs amont, puis
laissait tout à retaper : hook global de rtk, zdiff3, hooks git du dépôt,
commandes Claude, activation du venv. Elle les pose, en deux temps parce que
hooks et gabarits VIVENT dans le dépôt ; le complément suit le clone, rend
toujours 0 — un confort ne fait pas échouer une VM — et sans clone la moitié
manquante est nommée. core.editor n'est posé qu'à défaut, l'hôte transmettant
déjà le sien. L'aide « ? » et F1 dit ce que chaque option installe, là où un
libellé de case porte trois des huit poses. Vérifié : 51 tests, les deux
écrans montés sans terminal, « bash -n » sur les fragments distants.
--- EN ---
The AI tools box posed three upstream installers, then left every setting to
retype: rtk's global hook, zdiff3, the checkout's git hooks, the Claude
commands, activating the venv. It poses them, in two phases because hooks and
templates LIVE in the checkout; the complement follows the clone, always
returns 0 — a comfort must not fail a VM — and with no clone the missing half
is named. core.editor is posed only where there is none, the host already
transmitting its own. The `?` and F1 help says what each option installs,
where a checkbox label carries three of this one's eight poses. Checked: 51
tests, both screens mounted headless, `bash -n` on the remote fragments.
Assisted-by: Claude Opus 5
2026-09-04 02:03:52 -04:00
|
|
|
("tool_help", dict, "vm_tool_help"),
|
[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
("tool_disk", dict, "vm_tool_disk"),
|
|
|
|
|
("tool_phases", dict, "vm_tool_phases"),
|
|
|
|
|
("tool_arches", dict, "vm_tool_arches"),
|
|
|
|
|
("tool_desktops", dict, "vm_tool_desktops"),
|
|
|
|
|
("tool_needs_desktop", dict, "vm_tool_needs_desktop"),
|
|
|
|
|
("tool_families", dict, "vm_tool_families"),
|
|
|
|
|
("distro_family", dict, "distro_family"),
|
|
|
|
|
("mise_arches", set, "mise_arches"),
|
|
|
|
|
("defaults", dict, "defaults"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
[IMP] qemu deploy : pré-configurer la VM, et dire ce qui s'y installe
L'option des outils d'assistance posait trois installateurs amont, puis
laissait tout à retaper : hook global de rtk, zdiff3, hooks git du dépôt,
commandes Claude, activation du venv. Elle les pose, en deux temps parce que
hooks et gabarits VIVENT dans le dépôt ; le complément suit le clone, rend
toujours 0 — un confort ne fait pas échouer une VM — et sans clone la moitié
manquante est nommée. core.editor n'est posé qu'à défaut, l'hôte transmettant
déjà le sien. L'aide « ? » et F1 dit ce que chaque option installe, là où un
libellé de case porte trois des huit poses. Vérifié : 51 tests, les deux
écrans montés sans terminal, « bash -n » sur les fragments distants.
--- EN ---
The AI tools box posed three upstream installers, then left every setting to
retype: rtk's global hook, zdiff3, the checkout's git hooks, the Claude
commands, activating the venv. It poses them, in two phases because hooks and
templates LIVE in the checkout; the complement follows the clone, always
returns 0 — a comfort must not fail a VM — and with no clone the missing half
is named. core.editor is posed only where there is none, the host already
transmitting its own. The `?` and F1 help says what each option installs,
where a checkbox label carries three of this one's eight poses. Checked: 51
tests, both screens mounted headless, `bash -n` on the remote fragments.
Assisted-by: Claude Opus 5
2026-09-04 02:03:52 -04:00
|
|
|
# Ce que chaque réglage POSE, pour l'aide « ? ». Une clé de table en tête
|
|
|
|
|
# quand le bloc n'a de sens que si l'écran offre le réglage ; None quand il le
|
|
|
|
|
# porte toujours. Les textes disent l'effet dans la VM, pas la manœuvre à
|
|
|
|
|
# l'écran : le libellé de la case dit déjà où cliquer.
|
|
|
|
|
INSTALL_HELP = (
|
|
|
|
|
(
|
|
|
|
|
None,
|
|
|
|
|
# Le titre de la case, et non « ERPLibre » : elle commande TOUTE
|
|
|
|
|
# installation, l'hyperviseur Proxmox VE compris.
|
|
|
|
|
"Install software in the VM",
|
|
|
|
|
(
|
|
|
|
|
"clone of the checkout, then the make target of the profile",
|
|
|
|
|
"in ~/git/erplibre, or /opt/erplibre in production",
|
|
|
|
|
"unchecked: the VM stays a bare distribution",
|
|
|
|
|
"about +5 Go of disk",
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
None,
|
|
|
|
|
"Production (/opt, confined)",
|
|
|
|
|
(
|
|
|
|
|
"/opt/erplibre instead of ~/git/erplibre",
|
|
|
|
|
"Odoo registered as a systemd service",
|
|
|
|
|
"automatic updates left ON, unlike a dev VM",
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"desktops",
|
|
|
|
|
"VM type",
|
|
|
|
|
(
|
|
|
|
|
"server: SSH only, no graphical session",
|
|
|
|
|
"graphical: the chosen desktop plus its remote access",
|
|
|
|
|
"an IDE needs a desktop, the build tools do not",
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"app_stores",
|
|
|
|
|
"Application store",
|
|
|
|
|
(
|
|
|
|
|
"deb/rpm: packages of the distribution only",
|
|
|
|
|
"snap: snapd kept active where the distribution has it",
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"timezones",
|
|
|
|
|
"Timezone",
|
|
|
|
|
(
|
|
|
|
|
"IANA name written by cloud-init at first boot",
|
|
|
|
|
"a misspelled name is IGNORED: the VM stays in UTC",
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"mise_arches",
|
|
|
|
|
"Python interpreter",
|
|
|
|
|
(
|
|
|
|
|
"mise poses a precompiled CPython, in minutes",
|
|
|
|
|
"pyenv compiles it, which is much longer",
|
|
|
|
|
"mise publishes no binary for every architecture",
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
def extras_tables(ctx) -> dict:
|
|
|
|
|
"""Les tables lues du contexte, chacune ramenée à son type.
|
|
|
|
|
|
|
|
|
|
Ramenées, et non prises telles quelles : une clé absente rend un vide du
|
|
|
|
|
bon type, donc les prédicats plus bas n'ont aucun cas particulier à
|
|
|
|
|
porter. Un formulaire qui n'offre pas les outils passe simplement un
|
|
|
|
|
contexte sans « vm_tools »."""
|
|
|
|
|
ctx = ctx or {}
|
|
|
|
|
return {nom: kind(ctx.get(cle) or kind()) for nom, kind, cle in _TABLES}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def tools_for_vm(vm, tools, tab) -> list:
|
|
|
|
|
"""Outils qu'une VM donnée recevra VRAIMENT.
|
|
|
|
|
|
|
|
|
|
Le même filtre que côté déploiement, et c'est le point : une VM ARM ne
|
|
|
|
|
verra jamais Android Studio, une VM Cinnamon jamais les extensions GNOME,
|
|
|
|
|
un serveur aucun des IDE — mais un serveur reçoit bien la compilation
|
|
|
|
|
mobile, qui n'a rien à afficher, et une distribution sans apt ne la
|
|
|
|
|
reçoit pas, son installateur n'existant que là.
|
|
|
|
|
|
|
|
|
|
Filtrer ici plutôt que dans la commande distante permet d'annoncer
|
|
|
|
|
l'écart AVANT le déploiement, au lieu de le laisser découvrir dans un
|
|
|
|
|
journal d'installation d'une heure."""
|
|
|
|
|
out = []
|
|
|
|
|
for key in tools or ():
|
|
|
|
|
arches = tab["tool_arches"].get(key) or ()
|
|
|
|
|
desks = tab["tool_desktops"].get(key) or ()
|
|
|
|
|
fams = tab["tool_families"].get(key) or ()
|
|
|
|
|
if tab["tool_needs_desktop"].get(key) and not vm.get("desktop"):
|
|
|
|
|
continue
|
|
|
|
|
if arches and vm.get("arch") not in arches:
|
|
|
|
|
continue
|
|
|
|
|
if desks and vm.get("desktop") not in desks:
|
|
|
|
|
continue
|
|
|
|
|
if fams and tab["distro_family"].get(vm.get("distro"), "") not in fams:
|
|
|
|
|
continue
|
|
|
|
|
out.append(key)
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def app_store_needed(vms, tab) -> bool:
|
|
|
|
|
"""Le choix du magasin n'a de sens que pour une VM GRAPHIQUE sur une
|
|
|
|
|
distribution qui livre snapd. Ailleurs, rien ne tire de snap."""
|
|
|
|
|
return any(
|
|
|
|
|
vm.get("desktop") and vm.get("distro") in tab["snap_distros"]
|
|
|
|
|
for vm in vms or ()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def mise_usable(vms, tab) -> bool:
|
|
|
|
|
"""Au moins une VM retenue tourne sur une architecture que mise sert."""
|
|
|
|
|
return any(vm.get("arch") in tab["mise_arches"] for vm in vms or ())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def extras_disk_gb(vm, tools, tab) -> int:
|
|
|
|
|
"""Go que le bureau et les outils ajoutent au disque de CETTE VM.
|
|
|
|
|
|
|
|
|
|
De cette VM et d'elle seule : un supplément commun mentait dès que les
|
|
|
|
|
types différaient d'une machine à l'autre, et gonflait le disque d'un
|
|
|
|
|
serveur pour un bureau qu'il n'aurait pas."""
|
|
|
|
|
gb = tab["desktop_disk"] if vm.get("desktop") else 0
|
|
|
|
|
return gb + sum(
|
|
|
|
|
tab["tool_disk"].get(k, 0) for k in tools_for_vm(vm, tools, tab)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExtrasMixin:
|
|
|
|
|
"""Les six réglages du système invité, widgets et logique.
|
|
|
|
|
|
|
|
|
|
Le formulaire hôte pose les fragments `compose_*` où il veut, appelle
|
|
|
|
|
`render_extras()` après chaque recalcul et `extras_values()` au moment de
|
|
|
|
|
bâtir sa spec."""
|
|
|
|
|
|
[REF] déploiement : la branche, le profil et le type se choisissent par VM
Sur Proxmox on déploie le plus souvent un parc MIXTE — un hyperviseur
imbriqué à côté de VM ERPLibre. C'est exactement le cas où un réglage par
machine sert, et c'est le seul écran qui ne l'offrait pas : ses rangées
n'avaient ni branche, ni profil, ni type.
Les trois choix et leur gestionnaire — quatre-vingts lignes — rejoignent le
socle. Les dupliquer aurait remis en place le mécanisme de dérive qu'on vient
d'enlever. L'écran QEMU/KVM perd encore 130 lignes sans qu'un widget, un
modèle ou une spec ne bouge : ancien et nouveau montés dans le même
processus, mêmes rangées, mêmes valeurs après avoir changé une branche, un
type et un profil.
Le déploiement suit : il lisait la seule valeur commune alors que le plan
portait déjà le choix par rangée. Une seule VM qui s'écarte suffit à rendre
la carte nécessaire — « len(set) > 1 » ne l'aurait pas vu.
Un défaut trouvé par un test, pas à l'usage : l'écho du montage se
reconnaissait à sa commande, or quand la commande imposée par le système
n'est pas dans la liste proposée, la liste retombe au rang 0 — et l'écho de
ce rang 0 effaçait l'imposition. Un Proxmox imbriqué reprenait ERPLibre et
Odoo 18. L'écho se reconnaît maintenant au RANG affiché.
--- EN ---
On Proxmox you usually deploy a MIXED fleet — a nested hypervisor next to
ERPLibre VMs. That is exactly where a per-machine setting earns its keep, and
it was the only screen without one: its rows had no branch, no profile, no
type.
The three choices and their handler — eighty lines — move into the shared
foundation. Duplicating them would have restored the very drift mechanism we
just removed. The QEMU/KVM screen loses another 130 lines with no widget,
model or spec moving: old and new mounted in one process, same rows, same
values after changing a branch, a type and a profile.
The deployment follows: it read the single common value while the plan
already carried the per-row choice. One VM that differs is enough to require
the map — "len(set) > 1" would not have seen it.
One defect found by a test, not by use: the mount echo was recognised by its
command, yet when the command imposed by the guest OS is absent from the
offered list, the list falls back to index 0 — and that index-0 echo erased
the imposition. A nested Proxmox took ERPLibre and Odoo 18 back. The echo is
now recognised by the DISPLAYED index.
Assisted-by: Claude Opus 5
2026-08-24 23:31:13 -04:00
|
|
|
def extras_init(self, ctx, branches=(), profiles=()) -> None:
|
|
|
|
|
"""À appeler dans `__init__`, avant tout `compose`.
|
|
|
|
|
|
|
|
|
|
`branches` et `profiles` viennent du formulaire et non du contexte :
|
|
|
|
|
chacun les ordonne à sa façon (branch_order) et l'écran les propose
|
|
|
|
|
dans cet ordre-là. Ce sont les mêmes listes que portent les rangées."""
|
[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
self._extras = extras_tables(ctx)
|
[REF] déploiement : la branche, le profil et le type se choisissent par VM
Sur Proxmox on déploie le plus souvent un parc MIXTE — un hyperviseur
imbriqué à côté de VM ERPLibre. C'est exactement le cas où un réglage par
machine sert, et c'est le seul écran qui ne l'offrait pas : ses rangées
n'avaient ni branche, ni profil, ni type.
Les trois choix et leur gestionnaire — quatre-vingts lignes — rejoignent le
socle. Les dupliquer aurait remis en place le mécanisme de dérive qu'on vient
d'enlever. L'écran QEMU/KVM perd encore 130 lignes sans qu'un widget, un
modèle ou une spec ne bouge : ancien et nouveau montés dans le même
processus, mêmes rangées, mêmes valeurs après avoir changé une branche, un
type et un profil.
Le déploiement suit : il lisait la seule valeur commune alors que le plan
portait déjà le choix par rangée. Une seule VM qui s'écarte suffit à rendre
la carte nécessaire — « len(set) > 1 » ne l'aurait pas vu.
Un défaut trouvé par un test, pas à l'usage : l'écho du montage se
reconnaissait à sa commande, or quand la commande imposée par le système
n'est pas dans la liste proposée, la liste retombe au rang 0 — et l'écho de
ce rang 0 effaçait l'imposition. Un Proxmox imbriqué reprenait ERPLibre et
Odoo 18. L'écho se reconnaît maintenant au RANG affiché.
--- EN ---
On Proxmox you usually deploy a MIXED fleet — a nested hypervisor next to
ERPLibre VMs. That is exactly where a per-machine setting earns its keep, and
it was the only screen without one: its rows had no branch, no profile, no
type.
The three choices and their handler — eighty lines — move into the shared
foundation. Duplicating them would have restored the very drift mechanism we
just removed. The QEMU/KVM screen loses another 130 lines with no widget,
model or spec moving: old and new mounted in one process, same rows, same
values after changing a branch, a type and a profile.
The deployment follows: it read the single common value while the plan
already carried the per-row choice. One VM that differs is enough to require
the map — "len(set) > 1" would not have seen it.
One defect found by a test, not by use: the mount echo was recognised by its
command, yet when the command imposed by the guest OS is absent from the
offered list, the list falls back to index 0 — and that index-0 echo erased
the imposition. A nested Proxmox took ERPLibre and Odoo 18 back. The echo is
now recognised by the DISPLAYED index.
Assisted-by: Claude Opus 5
2026-08-24 23:31:13 -04:00
|
|
|
self._extras["branches"] = list(branches)
|
|
|
|
|
self._extras["profiles"] = list(profiles)
|
|
|
|
|
self._extras["distro_profiles"] = dict(
|
|
|
|
|
ctx.get("distro_profiles") or {}
|
|
|
|
|
)
|
[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
# Les widgets
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
def compose_vm_type(self):
|
|
|
|
|
"""Type de VM par défaut. Serveur d'abord : c'est ce que sert une
|
|
|
|
|
image cloud, et un bureau ajoute une à deux heures sur une
|
|
|
|
|
architecture émulée. Le plan annonce le surcoût disque."""
|
|
|
|
|
from textual.widgets import RadioButton, RadioSet, Static
|
|
|
|
|
|
|
|
|
|
tab = self._extras
|
|
|
|
|
if not tab["desktops"]:
|
|
|
|
|
return
|
|
|
|
|
yield Static(t("VM type (default):"), classes="grouptitle")
|
|
|
|
|
with RadioSet(id="f_type"):
|
|
|
|
|
yield RadioButton(
|
|
|
|
|
t("Server (no graphical interface)"),
|
|
|
|
|
value=not tab["defaults"].get("desktop", ""),
|
|
|
|
|
)
|
|
|
|
|
for key, label in tab["desktops"]:
|
|
|
|
|
yield RadioButton(
|
|
|
|
|
f"{t('Graphical (server + desktop):')} {label}",
|
|
|
|
|
value=tab["defaults"].get("desktop", "") == key,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def compose_install_extras(self):
|
|
|
|
|
"""Ce qui pend à la case « installer » : production, magasin,
|
|
|
|
|
outils. Posé DANS la section « Installation », après la branche et le
|
|
|
|
|
profil."""
|
|
|
|
|
from textual.widgets import Checkbox, RadioButton, RadioSet, Static
|
|
|
|
|
|
|
|
|
|
tab = self._extras
|
|
|
|
|
yield Checkbox(
|
|
|
|
|
t("Production (/opt, confined)"),
|
|
|
|
|
value=tab["defaults"].get("prod", False),
|
|
|
|
|
id="f_prod",
|
|
|
|
|
)
|
|
|
|
|
if tab["app_stores"]:
|
|
|
|
|
yield Static(
|
|
|
|
|
t("Application store:"), id="t_store", classes="grouptitle"
|
|
|
|
|
)
|
|
|
|
|
with RadioSet(id="f_store"):
|
|
|
|
|
for i, (_k, label) in enumerate(tab["app_stores"]):
|
|
|
|
|
yield RadioButton(label, value=i == 0)
|
|
|
|
|
yield Static("", id="storewarn")
|
|
|
|
|
if tab["vm_tools"]:
|
|
|
|
|
# Une case par outil, et non une liste déroulante : ils sont
|
|
|
|
|
# indépendants, et chacun se prend ou se laisse.
|
|
|
|
|
yield Static(
|
[IMP] qemu deploy : pré-configurer la VM, et dire ce qui s'y installe
L'option des outils d'assistance posait trois installateurs amont, puis
laissait tout à retaper : hook global de rtk, zdiff3, hooks git du dépôt,
commandes Claude, activation du venv. Elle les pose, en deux temps parce que
hooks et gabarits VIVENT dans le dépôt ; le complément suit le clone, rend
toujours 0 — un confort ne fait pas échouer une VM — et sans clone la moitié
manquante est nommée. core.editor n'est posé qu'à défaut, l'hôte transmettant
déjà le sien. L'aide « ? » et F1 dit ce que chaque option installe, là où un
libellé de case porte trois des huit poses. Vérifié : 51 tests, les deux
écrans montés sans terminal, « bash -n » sur les fragments distants.
--- EN ---
The AI tools box posed three upstream installers, then left every setting to
retype: rtk's global hook, zdiff3, the checkout's git hooks, the Claude
commands, activating the venv. It poses them, in two phases because hooks and
templates LIVE in the checkout; the complement follows the clone, always
returns 0 — a comfort must not fail a VM — and with no clone the missing half
is named. core.editor is posed only where there is none, the host already
transmitting its own. The `?` and F1 help says what each option installs,
where a checkbox label carries three of this one's eight poses. Checked: 51
tests, both screens mounted headless, `bash -n` on the remote fragments.
Assisted-by: Claude Opus 5
2026-09-04 02:03:52 -04:00
|
|
|
f"{t('Development tools:')} {t('(F1 or ? : details)')}",
|
|
|
|
|
id="t_tools",
|
|
|
|
|
classes="grouptitle",
|
[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
)
|
|
|
|
|
for key, label, hint in tab["vm_tools"]:
|
|
|
|
|
gb = tab["tool_disk"].get(key, 0)
|
|
|
|
|
yield Checkbox(
|
|
|
|
|
f"{label} +{gb} Go — {hint}",
|
|
|
|
|
value=key in (tab["defaults"].get("tools") or ()),
|
|
|
|
|
id=f"f_tool_{key}",
|
|
|
|
|
)
|
|
|
|
|
yield Static("", id="toolwarn")
|
|
|
|
|
|
|
|
|
|
def compose_timezone(self):
|
|
|
|
|
"""Une liste plutôt qu'une saisie : un nom IANA mal orthographié
|
|
|
|
|
n'est pas refusé par cloud-init, il est IGNORÉ — la VM reste en UTC et
|
|
|
|
|
on ne s'en aperçoit qu'aux horodatages. « libre… » garde la porte
|
|
|
|
|
ouverte aux six cents autres fuseaux de la base."""
|
|
|
|
|
from textual.widgets import Input, Select, Static
|
|
|
|
|
|
|
|
|
|
tab = self._extras
|
|
|
|
|
if not tab["timezones"]:
|
|
|
|
|
return
|
|
|
|
|
null = getattr(Select, "NULL", Select.BLANK)
|
|
|
|
|
yield Static(t("Timezone"), classes="grouptitle")
|
|
|
|
|
yield Select(
|
|
|
|
|
[(z, z) for z in tab["timezones"]] + [(t("free value…"), FREE)],
|
|
|
|
|
value=(tab["timezones"][0] if tab["timezones"] else null),
|
|
|
|
|
allow_blank=False,
|
|
|
|
|
id="f_tz_sel",
|
|
|
|
|
)
|
|
|
|
|
yield Input(
|
|
|
|
|
value=tab["timezone"],
|
|
|
|
|
placeholder=t("Timezone for the VMs"),
|
|
|
|
|
id="f_tz",
|
|
|
|
|
classes="freeval",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def compose_python(self):
|
|
|
|
|
"""mise pose un CPython PRÉCOMPILÉ, pyenv le COMPILE. Grisé quand
|
|
|
|
|
aucune des VM retenues n'est sur une architecture que mise sert."""
|
|
|
|
|
from textual.widgets import RadioButton, RadioSet, Static
|
|
|
|
|
|
|
|
|
|
yield Static(
|
|
|
|
|
t("Python interpreter:"), id="t_python", 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")
|
|
|
|
|
|
[IMP] qemu deploy : pré-configurer la VM, et dire ce qui s'y installe
L'option des outils d'assistance posait trois installateurs amont, puis
laissait tout à retaper : hook global de rtk, zdiff3, hooks git du dépôt,
commandes Claude, activation du venv. Elle les pose, en deux temps parce que
hooks et gabarits VIVENT dans le dépôt ; le complément suit le clone, rend
toujours 0 — un confort ne fait pas échouer une VM — et sans clone la moitié
manquante est nommée. core.editor n'est posé qu'à défaut, l'hôte transmettant
déjà le sien. L'aide « ? » et F1 dit ce que chaque option installe, là où un
libellé de case porte trois des huit poses. Vérifié : 51 tests, les deux
écrans montés sans terminal, « bash -n » sur les fragments distants.
--- EN ---
The AI tools box posed three upstream installers, then left every setting to
retype: rtk's global hook, zdiff3, the checkout's git hooks, the Claude
commands, activating the venv. It poses them, in two phases because hooks and
templates LIVE in the checkout; the complement follows the clone, always
returns 0 — a comfort must not fail a VM — and with no clone the missing half
is named. core.editor is posed only where there is none, the host already
transmitting its own. The `?` and F1 help says what each option installs,
where a checkbox label carries three of this one's eight poses. Checked: 51
tests, both screens mounted headless, `bash -n` on the remote fragments.
Assisted-by: Claude Opus 5
2026-09-04 02:03:52 -04:00
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
# L'aide
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
def extras_help_blocks(self) -> list:
|
|
|
|
|
"""[(titre, lignes)] : ce que chaque option de cet écran installe.
|
|
|
|
|
|
|
|
|
|
Les réglages communs d'abord, dans l'ordre du formulaire, puis un bloc
|
|
|
|
|
par outil OFFERT — un écran qui n'en propose pas n'a rien à en dire.
|
|
|
|
|
L'aide vient de la même table que les cases : une seule autorité, et
|
|
|
|
|
rien à synchroniser.
|
|
|
|
|
|
|
|
|
|
Un outil sans texte d'aide retombe sur son indice, qui tient en une
|
|
|
|
|
ligne : mieux vaut trop court que vide. Ce repli est un filet, pas une
|
|
|
|
|
place où se cacher — un test exige une aide pour chaque outil.
|
|
|
|
|
"""
|
|
|
|
|
tab = self._extras
|
|
|
|
|
blocs = []
|
|
|
|
|
for cle, titre, lignes in INSTALL_HELP:
|
|
|
|
|
if cle and not tab[cle]:
|
|
|
|
|
continue
|
|
|
|
|
blocs.append((t(titre), [t(x) for x in lignes]))
|
|
|
|
|
for key, label, hint in tab["vm_tools"]:
|
|
|
|
|
gb = tab["tool_disk"].get(key, 0)
|
|
|
|
|
lignes = list(tab["tool_help"].get(key) or [hint])
|
|
|
|
|
lignes.append(f"+{gb} Go")
|
|
|
|
|
blocs.append((label, lignes))
|
|
|
|
|
return blocs
|
|
|
|
|
|
|
|
|
|
def action_help(self) -> None:
|
|
|
|
|
"""Ouvre l'aide. Esc ou q la ferme, et rien n'a bougé."""
|
|
|
|
|
from script.todo.deploy_form_plan import help_screen
|
|
|
|
|
|
|
|
|
|
self.push_screen(help_screen()(self.extras_help_blocks()))
|
|
|
|
|
|
[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
# Lire les widgets
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
def _widget(self, selector):
|
|
|
|
|
"""Le widget, ou None s'il n'est pas de cet écran."""
|
|
|
|
|
try:
|
|
|
|
|
return self.query_one(selector)
|
|
|
|
|
except Exception:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def _default_desktop(self) -> str:
|
|
|
|
|
"""« » pour un serveur, sinon la clé de la saveur choisie. Chaque
|
|
|
|
|
rangée du plan peut s'en écarter."""
|
|
|
|
|
desktops = self._extras["desktops"]
|
|
|
|
|
widget = self._widget("#f_type")
|
|
|
|
|
if widget is None:
|
|
|
|
|
return ""
|
|
|
|
|
index = widget.pressed_index
|
|
|
|
|
if index is None or index < 1 or index > len(desktops):
|
|
|
|
|
return ""
|
|
|
|
|
return desktops[index - 1][0]
|
|
|
|
|
|
|
|
|
|
def _install_state(self):
|
|
|
|
|
"""(une installation ?, quelque chose à installer ?).
|
|
|
|
|
|
|
|
|
|
Deux réponses et non une : sans installation mais avec un bureau, il
|
|
|
|
|
se pose encore des paquets — le magasin d'applications et les outils
|
|
|
|
|
de la phase « avant » gardent un effet."""
|
|
|
|
|
widget = self._widget("#f_install")
|
|
|
|
|
installe = True if widget is None else bool(widget.value)
|
|
|
|
|
return installe, bool(installe or self._default_desktop())
|
|
|
|
|
|
|
|
|
|
def _vm_tools(self) -> tuple:
|
|
|
|
|
"""Clés des outils cochés, dans l'ordre de la liste."""
|
|
|
|
|
picked = []
|
|
|
|
|
for key, _label, _hint in self._extras["vm_tools"]:
|
|
|
|
|
widget = self._widget(f"#f_tool_{key}")
|
|
|
|
|
if widget is not None and widget.value:
|
|
|
|
|
picked.append(key)
|
|
|
|
|
return tuple(picked)
|
|
|
|
|
|
|
|
|
|
def _tools_for_vm(self, vm, tools) -> list:
|
|
|
|
|
return tools_for_vm(vm, tools, self._extras)
|
|
|
|
|
|
|
|
|
|
def _app_store_needed(self) -> bool:
|
|
|
|
|
return app_store_needed(self.vms, self._extras)
|
|
|
|
|
|
|
|
|
|
def _app_store(self) -> str:
|
|
|
|
|
"""Magasin retenu. Sans VM concernée, la réponse est « deb » : elle
|
|
|
|
|
ne change rien, et laisser passer « snap » réactiverait snapd pour
|
|
|
|
|
rien."""
|
|
|
|
|
stores = self._extras["app_stores"]
|
|
|
|
|
widget = self._widget("#f_store")
|
|
|
|
|
if not stores or widget is None or not self._app_store_needed():
|
|
|
|
|
return "deb"
|
|
|
|
|
index = widget.pressed_index
|
|
|
|
|
if index is None or not (0 <= index < len(stores)):
|
|
|
|
|
return stores[0][0]
|
|
|
|
|
return stores[index][0]
|
|
|
|
|
|
|
|
|
|
def _mise_usable(self) -> bool:
|
|
|
|
|
return mise_usable(self.vms, self._extras)
|
|
|
|
|
|
|
|
|
|
def _python_provider(self) -> str:
|
|
|
|
|
"""« 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."""
|
|
|
|
|
widget = self._widget("#f_python")
|
|
|
|
|
if widget is None or not self._mise_usable():
|
|
|
|
|
return ""
|
|
|
|
|
return "pyenv" if widget.pressed_index == 1 else "mise"
|
|
|
|
|
|
|
|
|
|
def _timezone(self) -> str:
|
|
|
|
|
"""Le fuseau des VM. Un champ vidé retombe sur celui de l'hôte plutôt
|
|
|
|
|
que sur rien : sans valeur, la VM démarrerait en UTC."""
|
|
|
|
|
widget = self._widget("#f_tz")
|
|
|
|
|
saisi = widget.value.strip() if widget is not None else ""
|
|
|
|
|
return saisi or self._extras["timezone"]
|
|
|
|
|
|
|
|
|
|
def _extras_disk_gb(self, vm, tools) -> int:
|
|
|
|
|
return extras_disk_gb(vm, tools, self._extras)
|
|
|
|
|
|
[REF] déploiement : la branche, le profil et le type se choisissent par VM
Sur Proxmox on déploie le plus souvent un parc MIXTE — un hyperviseur
imbriqué à côté de VM ERPLibre. C'est exactement le cas où un réglage par
machine sert, et c'est le seul écran qui ne l'offrait pas : ses rangées
n'avaient ni branche, ni profil, ni type.
Les trois choix et leur gestionnaire — quatre-vingts lignes — rejoignent le
socle. Les dupliquer aurait remis en place le mécanisme de dérive qu'on vient
d'enlever. L'écran QEMU/KVM perd encore 130 lignes sans qu'un widget, un
modèle ou une spec ne bouge : ancien et nouveau montés dans le même
processus, mêmes rangées, mêmes valeurs après avoir changé une branche, un
type et un profil.
Le déploiement suit : il lisait la seule valeur commune alors que le plan
portait déjà le choix par rangée. Une seule VM qui s'écarte suffit à rendre
la carte nécessaire — « len(set) > 1 » ne l'aurait pas vu.
Un défaut trouvé par un test, pas à l'usage : l'écho du montage se
reconnaissait à sa commande, or quand la commande imposée par le système
n'est pas dans la liste proposée, la liste retombe au rang 0 — et l'écho de
ce rang 0 effaçait l'imposition. Un Proxmox imbriqué reprenait ERPLibre et
Odoo 18. L'écho se reconnaît maintenant au RANG affiché.
--- EN ---
On Proxmox you usually deploy a MIXED fleet — a nested hypervisor next to
ERPLibre VMs. That is exactly where a per-machine setting earns its keep, and
it was the only screen without one: its rows had no branch, no profile, no
type.
The three choices and their handler — eighty lines — move into the shared
foundation. Duplicating them would have restored the very drift mechanism we
just removed. The QEMU/KVM screen loses another 130 lines with no widget,
model or spec moving: old and new mounted in one process, same rows, same
values after changing a branch, a type and a profile.
The deployment follows: it read the single common value while the plan
already carried the per-row choice. One VM that differs is enough to require
the map — "len(set) > 1" would not have seen it.
One defect found by a test, not by use: the mount echo was recognised by its
command, yet when the command imposed by the guest OS is absent from the
offered list, the list falls back to index 0 — and that index-0 echo erased
the imposition. A nested Proxmox took ERPLibre and Odoo 18 back. The echo is
now recognised by the DISPLAYED index.
Assisted-by: Claude Opus 5
2026-08-24 23:31:13 -04:00
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
# Une rangée du plan : branche, profil, type — par VM
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
def _branch(self) -> str:
|
|
|
|
|
"""Branche du formulaire : le défaut de chaque VM."""
|
|
|
|
|
widget = self._widget("#f_branch")
|
|
|
|
|
valeur = widget.value if widget is not None else None
|
|
|
|
|
branches = self._extras["branches"]
|
|
|
|
|
return valeur if isinstance(valeur, str) else (branches or [""])[0]
|
|
|
|
|
|
|
|
|
|
def _profile_cmd(self) -> str:
|
|
|
|
|
"""Commande du profil choisi en haut : le défaut de chaque VM."""
|
|
|
|
|
profiles = self._extras["profiles"]
|
|
|
|
|
widget = self._widget("#f_profile_install")
|
|
|
|
|
if not profiles or widget is None:
|
|
|
|
|
return ""
|
|
|
|
|
index = widget.value
|
|
|
|
|
return profiles[index if isinstance(index, int) else 0][1]
|
|
|
|
|
|
|
|
|
|
def _row_default_cmd(self, i) -> str:
|
|
|
|
|
"""Commande qu'une rangée prend d'elle-même : celle que son système
|
|
|
|
|
impose, sinon le choix commun d'en haut.
|
|
|
|
|
|
|
|
|
|
C'est le défaut CONTRE LEQUEL on compare une saisie : sur une VM
|
|
|
|
|
Proxmox, choisir Odoo 18 est une vraie surcharge même quand c'est
|
|
|
|
|
aussi la valeur commune."""
|
|
|
|
|
if i < len(self.rows):
|
|
|
|
|
impose = self._extras["distro_profiles"].get(
|
|
|
|
|
self.rows[i]["vm"]["distro"]
|
|
|
|
|
)
|
|
|
|
|
if impose:
|
|
|
|
|
return impose[1]
|
|
|
|
|
return self._profile_cmd()
|
|
|
|
|
|
|
|
|
|
def _row_profile_index(self, i) -> int:
|
|
|
|
|
"""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._row_default_cmd(i)
|
|
|
|
|
for k, (_lbl, c) in enumerate(self._extras["profiles"]):
|
|
|
|
|
if c == cmd:
|
|
|
|
|
return k
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
def _type_options(self):
|
|
|
|
|
return [(t("Server"), SERVER)] + [
|
|
|
|
|
(label, key) for key, label in self._extras["desktops"]
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def install_row_widgets(self, i, null=None):
|
|
|
|
|
"""Les trois choix qu'une VM peut prendre à elle seule.
|
|
|
|
|
|
|
|
|
|
Ici et non dans un formulaire : ce sont les mêmes trois des deux
|
|
|
|
|
côtés, et ils manquaient à l'écran Proxmox — où l'on déploie pourtant
|
|
|
|
|
le plus souvent un parc MIXTE, un hyperviseur imbriqué à côté de VM
|
|
|
|
|
ERPLibre."""
|
|
|
|
|
from textual.widgets import Select, Static
|
|
|
|
|
|
|
|
|
|
tab = self._extras
|
|
|
|
|
if null is None:
|
|
|
|
|
null = getattr(Select, "NULL", Select.BLANK)
|
|
|
|
|
vm = self.rows[i]["vm"] if i < len(self.rows) else {}
|
|
|
|
|
widgets = [
|
|
|
|
|
Select(
|
|
|
|
|
[(b, b) for b in tab["branches"]],
|
|
|
|
|
classes="vmbranch",
|
|
|
|
|
# Repli sur la branche du FORMULAIRE, jamais sur branches[0] :
|
|
|
|
|
# les rangées sont remontées dès que le jeu de VM change (une
|
|
|
|
|
# entrée cochée, une copie ajoutée, un renommage), et elles
|
|
|
|
|
# retombaient alors toutes sur « develop » quel que soit le
|
|
|
|
|
# choix commun.
|
|
|
|
|
value=vm.get("branch") or self._branch(),
|
|
|
|
|
allow_blank=False,
|
|
|
|
|
id=f"v{i}_branch",
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
Select(
|
|
|
|
|
[(lbl, k) for k, (lbl, _c) in enumerate(tab["profiles"])],
|
|
|
|
|
value=self._row_profile_index(i),
|
|
|
|
|
allow_blank=False,
|
|
|
|
|
classes="vmprof",
|
|
|
|
|
id=f"v{i}_prof",
|
|
|
|
|
)
|
|
|
|
|
if tab["profiles"]
|
|
|
|
|
else Static("", classes="vmprof")
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
if tab["desktops"]:
|
|
|
|
|
widgets.append(
|
|
|
|
|
Select(
|
|
|
|
|
self._type_options(),
|
|
|
|
|
value=vm.get("desktop") or SERVER,
|
|
|
|
|
allow_blank=False,
|
|
|
|
|
id=f"v{i}_type",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return widgets
|
|
|
|
|
|
|
|
|
|
def extras_on_row_select(self, event, index, field, null=None) -> bool:
|
|
|
|
|
"""Traite branche / profil / type d'UNE rangée. True quand c'est fait.
|
|
|
|
|
|
|
|
|
|
Ce que ces trois partagent, et qui n'est pas évident : poser
|
|
|
|
|
« value= » au montage fait émettre un Changed que Textual délivre
|
|
|
|
|
APRÈS coup, et un verrou temporel ne l'attrape pas — mesuré, les
|
|
|
|
|
champs de chaque VM se retrouvaient surchargés dès l'affichage et le
|
|
|
|
|
profil x1..x4 devenait inopérant. On compare donc à ce que le modèle
|
|
|
|
|
dit DÉJÀ : une valeur identique n'est pas une saisie, c'est l'écho.
|
|
|
|
|
|
|
|
|
|
Cas limite assumé : choisir explicitement la valeur que le profil
|
|
|
|
|
donne déjà n'enregistre pas de surcharge. La VM suivra donc le profil
|
|
|
|
|
s'il change — ce qui est aussi le plus attendu quand on n'a rien
|
|
|
|
|
changé de visible."""
|
|
|
|
|
if field not in ("branch", "prof", "type"):
|
|
|
|
|
return False
|
|
|
|
|
vm_now = self.rows[index]["vm"]
|
|
|
|
|
if field == "prof":
|
|
|
|
|
# L'écho se reconnaît à ceci : la valeur est CELLE QU'ON VIENT
|
|
|
|
|
# D'AFFICHER. Comparer les commandes ne suffisait pas — quand la
|
|
|
|
|
# commande imposée par le système n'est pas dans la liste
|
|
|
|
|
# proposée, la liste retombe sur le rang 0, et l'écho de ce
|
|
|
|
|
# rang 0 effaçait l'imposition. Un Proxmox imbriqué reprenait
|
|
|
|
|
# ainsi ERPLibre et Odoo 18, le défaut qu'on venait de corriger.
|
|
|
|
|
if event.value == self._row_profile_index(index):
|
|
|
|
|
return True
|
|
|
|
|
label, cmd = self._extras["profiles"][event.value]
|
|
|
|
|
defaut = self._row_default_cmd(index)
|
|
|
|
|
if cmd == (vm_now.get("install_cmd") or defaut):
|
|
|
|
|
return True
|
|
|
|
|
meme = cmd == defaut
|
|
|
|
|
self._set_override(index, "install_cmd", "" if meme else cmd)
|
|
|
|
|
self._set_override(index, "install_label", "" if meme else label)
|
|
|
|
|
self._recompute()
|
|
|
|
|
return True
|
|
|
|
|
if field == "branch":
|
|
|
|
|
# « la branche du formulaire » n'est pas une surcharge : la VM
|
|
|
|
|
# doit suivre si on la change en haut.
|
|
|
|
|
if event.value == (vm_now.get("branch") or self._branch()):
|
|
|
|
|
return True
|
|
|
|
|
self._set_override(
|
|
|
|
|
index,
|
|
|
|
|
"branch",
|
|
|
|
|
"" if event.value == self._branch() else event.value,
|
|
|
|
|
)
|
|
|
|
|
self._recompute()
|
|
|
|
|
return True
|
|
|
|
|
nouveau = "" if event.value == SERVER else event.value
|
|
|
|
|
if nouveau == (vm_now.get("desktop") or ""):
|
|
|
|
|
return True
|
|
|
|
|
self._set_override(index, "desktop", nouveau)
|
|
|
|
|
# « Serveur » est un choix légitime, pas un retrait : on le note
|
|
|
|
|
# explicitement pour qu'il tienne face au défaut.
|
|
|
|
|
if event.value == SERVER:
|
|
|
|
|
cle = self._row_key(index)
|
|
|
|
|
if cle is not None:
|
|
|
|
|
self.overrides.setdefault(cle, {})["desktop"] = ""
|
|
|
|
|
self._recompute()
|
|
|
|
|
return True
|
|
|
|
|
|
[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
# Redessiner
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
def _render_store(self) -> None:
|
|
|
|
|
"""Grise le choix quand aucune VM ne le concerne, et dit pourquoi."""
|
|
|
|
|
widget = self._widget("#f_store")
|
|
|
|
|
if widget is None:
|
|
|
|
|
return
|
|
|
|
|
needed = self._app_store_needed()
|
|
|
|
|
_installe, quelque_chose = self._install_state()
|
|
|
|
|
widget.disabled = not (needed and quelque_chose)
|
|
|
|
|
note = self._widget("#storewarn")
|
|
|
|
|
if note is not None:
|
|
|
|
|
note.update(
|
|
|
|
|
""
|
|
|
|
|
if needed
|
|
|
|
|
else f" {t('No graphical VM on a snap-based distro.')}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _render_tools(self) -> None:
|
|
|
|
|
"""Grise chaque case qu'AUCUNE VM retenue ne peut recevoir, et NOMME
|
|
|
|
|
ce qui sera écarté.
|
|
|
|
|
|
|
|
|
|
Une case par outil, et non un blocage en bloc : sur un parc de
|
|
|
|
|
serveurs les IDE se grisent, la compilation mobile reste offerte.
|
|
|
|
|
Cocher Android Studio sur un parc ARM ne produit rien — le dire ici
|
|
|
|
|
évite de le découvrir dans le journal d'installation."""
|
|
|
|
|
tab = self._extras
|
|
|
|
|
if not tab["vm_tools"]:
|
|
|
|
|
return
|
|
|
|
|
installe, quelque_chose = self._install_state()
|
|
|
|
|
for key, _label, _hint in tab["vm_tools"]:
|
|
|
|
|
widget = self._widget(f"#f_tool_{key}")
|
|
|
|
|
if widget is None:
|
|
|
|
|
continue
|
|
|
|
|
usable = any(self._tools_for_vm(vm, (key,)) for vm in self.vms)
|
|
|
|
|
# Un outil de la phase « après » vit DANS le dépôt ERPLibre :
|
|
|
|
|
# sans installation, il n'a rien où s'installer, bureau ou pas.
|
|
|
|
|
offert = (
|
|
|
|
|
installe
|
|
|
|
|
if tab["tool_phases"].get(key) == "after"
|
|
|
|
|
else quelque_chose
|
|
|
|
|
)
|
|
|
|
|
widget.disabled = not (usable and offert)
|
|
|
|
|
picked = self._vm_tools()
|
|
|
|
|
skipped = sorted(
|
|
|
|
|
{
|
|
|
|
|
vm["name"]
|
|
|
|
|
for vm in self.vms
|
|
|
|
|
for k in picked
|
|
|
|
|
if k not in self._tools_for_vm(vm, picked)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
note = self._widget("#toolwarn")
|
|
|
|
|
if note is not None:
|
|
|
|
|
note.update(
|
|
|
|
|
f" ⚠ {t('Partly skipped (arch or desktop):')} "
|
|
|
|
|
f"{', '.join(skipped)}"
|
|
|
|
|
if skipped
|
|
|
|
|
else ""
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _render_mise(self) -> None:
|
|
|
|
|
"""Grise le choix quand aucune VM retenue n'est servie par mise, ou
|
|
|
|
|
quand rien ne s'installe, et NOMME les architectures qui retomberont
|
|
|
|
|
sur pyenv."""
|
|
|
|
|
widget = self._widget("#f_python")
|
|
|
|
|
if widget is None:
|
|
|
|
|
return
|
|
|
|
|
installe, _quelque_chose = self._install_state()
|
|
|
|
|
widget.disabled = not (self._mise_usable() and installe)
|
|
|
|
|
ecartees = sorted(
|
|
|
|
|
{
|
|
|
|
|
vm["arch"]
|
|
|
|
|
for vm in self.vms
|
|
|
|
|
if vm.get("arch") not in self._extras["mise_arches"]
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
note = self._widget("#miswarn")
|
|
|
|
|
if note is not None:
|
|
|
|
|
note.update(
|
|
|
|
|
f" ⚠ {t('mise has no binary for:')} "
|
|
|
|
|
f"{', '.join(ecartees)} — {t('those VMs use pyenv')}"
|
|
|
|
|
if ecartees
|
|
|
|
|
else ""
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def render_extras(self) -> None:
|
|
|
|
|
"""Les trois, dans l'ordre. Appelée à la fin de chaque recalcul."""
|
|
|
|
|
self._render_mise()
|
|
|
|
|
self._render_store()
|
|
|
|
|
self._render_tools()
|
|
|
|
|
|
|
|
|
|
def _sync_install_deps(self) -> None:
|
|
|
|
|
"""Grise ce que le choix d'installation rend SANS EFFET.
|
|
|
|
|
|
|
|
|
|
Trois états et non deux, parce que la commande distante en a trois :
|
|
|
|
|
rien du tout, un bureau seul, ou une installation complète. Sans
|
|
|
|
|
installation MAIS avec un bureau, le magasin d'applications et les
|
|
|
|
|
outils de la phase « avant » servent encore — les griser mentirait
|
|
|
|
|
autant que de laisser actif ce qui ne fait rien. La branche, le
|
|
|
|
|
profil et l'interpréteur Python, eux, ne servent qu'à l'installation.
|
|
|
|
|
|
|
|
|
|
Le type de VM et le suivi ne sont jamais grisés : le premier est
|
|
|
|
|
l'autre moitié de la décision, le second regarde la VM arriver même
|
|
|
|
|
quand rien ne s'installe."""
|
|
|
|
|
installe, quelque_chose = self._install_state()
|
|
|
|
|
for cible, actif in (
|
|
|
|
|
("#f_branch", installe),
|
|
|
|
|
("#f_profile_install", installe),
|
|
|
|
|
("#f_prod", quelque_chose),
|
|
|
|
|
):
|
|
|
|
|
widget = self._widget(cible)
|
|
|
|
|
if widget is not None:
|
|
|
|
|
widget.disabled = not actif
|
|
|
|
|
# Le magasin, les outils et l'interpréteur Python ont leur PROPRE
|
|
|
|
|
# raison de se griser (architecture, bureau, distribution) : ils
|
|
|
|
|
# composent les deux dans « _render_* », qui a le dernier mot.
|
|
|
|
|
# Le titre suit ses champs : une section entière se lit inactive d'un
|
|
|
|
|
# coup d'œil, au lieu de se déduire de trois widgets ternes.
|
|
|
|
|
for cible, actif in (
|
|
|
|
|
("#t_store", quelque_chose),
|
|
|
|
|
("#t_tools", quelque_chose),
|
|
|
|
|
("#t_python", installe),
|
|
|
|
|
):
|
|
|
|
|
widget = self._widget(cible)
|
|
|
|
|
if widget is not None:
|
|
|
|
|
widget.set_class(not actif, "off")
|
|
|
|
|
# Les rangées portent les mêmes choix, par VM.
|
|
|
|
|
for i in range(len(getattr(self, "rows", ()))):
|
|
|
|
|
for cible in (f"#v{i}_branch", f"#v{i}_prof"):
|
|
|
|
|
widget = self._widget(cible)
|
|
|
|
|
if widget is not None:
|
|
|
|
|
widget.disabled = not installe
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
# Les messages, et la spec
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
def extras_on_select(self, event) -> bool:
|
|
|
|
|
"""Traite le sélecteur de fuseau. Rend True quand c'est fait, pour
|
|
|
|
|
que l'appelant s'arrête là.
|
|
|
|
|
|
|
|
|
|
« libre… » révèle la saisie ; un fuseau choisi la referme et y
|
|
|
|
|
recopie le nom, seule valeur que lit `extras_values` — un seul
|
|
|
|
|
endroit porte la réponse."""
|
|
|
|
|
if getattr(event.select, "id", "") != "f_tz_sel":
|
|
|
|
|
return False
|
|
|
|
|
free = event.value is FREE
|
|
|
|
|
field = self._widget("#f_tz")
|
|
|
|
|
if field is None:
|
|
|
|
|
return True
|
|
|
|
|
field.display = free
|
|
|
|
|
field.disabled = not free
|
|
|
|
|
if free:
|
|
|
|
|
field.focus()
|
|
|
|
|
elif isinstance(event.value, str):
|
|
|
|
|
field.value = event.value
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def extras_values(self) -> dict:
|
|
|
|
|
"""Le fragment de spec que ces réglages produisent."""
|
|
|
|
|
return {
|
|
|
|
|
"timezone": self._timezone(),
|
|
|
|
|
"desktop": self._default_desktop(),
|
|
|
|
|
"vm_tools": self._vm_tools(),
|
|
|
|
|
"python_provider": self._python_provider(),
|
|
|
|
|
"app_store": self._app_store(),
|
|
|
|
|
}
|