[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
|
|
|
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
"""Les deux écrans de déploiement offrent les MÊMES réglages du 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 valent donc mot pour mot sur libvirt et sur
|
|
|
|
|
Proxmox VE.
|
|
|
|
|
|
|
|
|
|
C'est ce qui avait dérivé. L'écran QEMU/KVM les portait tous les six, l'écran
|
|
|
|
|
Proxmox trois : une VM créée là-bas naissait serveur nu, sans outils, en UTC —
|
|
|
|
|
et rien ne le disait. 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 fichier teste donc la PARITÉ elle-même, et pas six comportements. Ajouter
|
|
|
|
|
un réglage à un seul écran le fait échouer, quel que soit ce réglage."""
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
import sys
|
|
|
|
|
import unittest
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO # noqa: E402
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
import textual # noqa: F401
|
|
|
|
|
|
|
|
|
|
TEXTUAL = True
|
|
|
|
|
except Exception: # pragma: no cover - dépend de l'environnement
|
|
|
|
|
TEXTUAL = False
|
|
|
|
|
|
|
|
|
|
# Ce que porte le socle partagé. Les identifiants, parce qu'ils sont le
|
|
|
|
|
# contrat : c'est par eux que la spec est lue.
|
|
|
|
|
REGLAGES = ("f_type", "f_prod", "f_store", "f_tools", "f_tz", "f_python")
|
|
|
|
|
|
|
|
|
|
CATALOGUE = [
|
|
|
|
|
{
|
|
|
|
|
"distro": "ubuntu",
|
|
|
|
|
"version": "26.04",
|
|
|
|
|
"arch": "amd64",
|
|
|
|
|
"name": "erplibre-ubuntu-2604",
|
|
|
|
|
"ram": 2048,
|
|
|
|
|
"disk": "20G",
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def todo_muet():
|
|
|
|
|
todo = TODO.__new__(TODO)
|
|
|
|
|
todo._qemu_list_domains = lambda: []
|
|
|
|
|
todo._qemu_branch_list = lambda: ["develop", "master"]
|
|
|
|
|
return todo
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def contexte_proxmox(todo):
|
|
|
|
|
"""Le contexte de l'écran Proxmox, hôte simulé — aucune VM n'est créée."""
|
|
|
|
|
return dict(
|
|
|
|
|
host={"target": "pve", "label": "pve"},
|
|
|
|
|
node="pve",
|
|
|
|
|
arches=["amd64"],
|
|
|
|
|
native="amd64",
|
|
|
|
|
catalog={"amd64": list(CATALOGUE)},
|
|
|
|
|
names=[],
|
|
|
|
|
vmids=[],
|
|
|
|
|
next_vmid=100,
|
|
|
|
|
storages=["local-lvm"],
|
|
|
|
|
storage="local-lvm",
|
|
|
|
|
storage_avail={"local-lvm": 500 << 30},
|
|
|
|
|
bridges=["vmbr0"],
|
|
|
|
|
bridge="vmbr0",
|
|
|
|
|
ipconfig=lambda _p, _v: "ip=dhcp",
|
|
|
|
|
nameservers=["1.1.1.1"],
|
|
|
|
|
branches=["develop", "master"],
|
|
|
|
|
branch_current="develop",
|
|
|
|
|
install_profiles=[
|
|
|
|
|
("ERPLibre + Odoo 18", "make install_os && make install_odoo_18")
|
|
|
|
|
],
|
|
|
|
|
distro_profiles={},
|
|
|
|
|
ssh_key="~/.ssh/id_ed25519.pub",
|
|
|
|
|
cpu_presets=(1, 2, 4),
|
|
|
|
|
ram_presets=(2048, 4096),
|
|
|
|
|
disk_presets=(20, 40),
|
|
|
|
|
base_vcpus=2,
|
|
|
|
|
host_cpu=8,
|
|
|
|
|
free_ram=16000,
|
|
|
|
|
extra_disk_gb=5,
|
|
|
|
|
**todo._qemu_guest_context(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def releve(fabrique, ctx, gestes=None):
|
|
|
|
|
"""Monte l'écran, choisit la première entrée, et relève ce qu'il porte."""
|
|
|
|
|
vu = {}
|
|
|
|
|
|
|
|
|
|
async def scenario():
|
|
|
|
|
from textual.widgets import SelectionList
|
|
|
|
|
|
|
|
|
|
app = fabrique(ctx, run_app=False)
|
|
|
|
|
async with app.run_test(size=(220, 70)) as pilote:
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
liste = app.query_one(SelectionList)
|
|
|
|
|
liste.select(liste.get_option_at_index(0).value)
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
if gestes:
|
|
|
|
|
await gestes(app, pilote)
|
|
|
|
|
vu["ids"] = {w.id for w in app.query("#fields *") if w.id}
|
|
|
|
|
vu["spec"] = app._form_values()
|
|
|
|
|
vu["noms"] = [r["vm"]["name"] for r in app.rows]
|
[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
|
|
|
vu["rangee"] = [w.id for w in app.query("#plan *") if w.id]
|
[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
|
|
|
vu["disques"] = [r["disk_gb"] for r in app.rows]
|
|
|
|
|
|
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
return vu
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@unittest.skipUnless(TEXTUAL, "Textual absent")
|
|
|
|
|
class TestLesDeuxEcrans(unittest.TestCase):
|
|
|
|
|
@classmethod
|
|
|
|
|
def setUpClass(cls):
|
|
|
|
|
from script.todo.proxmox_deploy_form import run_proxmox_form
|
|
|
|
|
from script.todo.qemu_deploy_form import run_deploy_form
|
|
|
|
|
|
|
|
|
|
todo = todo_muet()
|
|
|
|
|
mod = todo._qemu_import_module()
|
|
|
|
|
cls.qemu = releve(run_deploy_form, todo._qemu_form_context(mod))
|
|
|
|
|
cls.pve = releve(run_proxmox_form, contexte_proxmox(todo))
|
|
|
|
|
|
|
|
|
|
def _porte(self, vu, ident):
|
|
|
|
|
"""Un réglage est là s'il a son widget — les outils sont une case par
|
|
|
|
|
outil, donc un préfixe."""
|
|
|
|
|
if ident == "f_tools":
|
|
|
|
|
return any(i.startswith("f_tool_") for i in vu["ids"])
|
|
|
|
|
return ident in vu["ids"]
|
|
|
|
|
|
|
|
|
|
def test_both_screens_offer_the_same_guest_settings(self):
|
|
|
|
|
for ident in REGLAGES:
|
|
|
|
|
with self.subTest(reglage=ident):
|
|
|
|
|
self.assertTrue(self._porte(self.qemu, ident), "QEMU/KVM")
|
|
|
|
|
self.assertTrue(self._porte(self.pve, ident), "Proxmox")
|
|
|
|
|
|
|
|
|
|
def test_both_specs_carry_the_same_guest_keys(self):
|
|
|
|
|
from script.todo.deploy_form_extras import ExtrasMixin
|
|
|
|
|
|
|
|
|
|
attendues = set(ExtrasMixin.extras_values(self._faux()))
|
|
|
|
|
for nom, vu in (("QEMU/KVM", self.qemu), ("Proxmox", self.pve)):
|
|
|
|
|
with self.subTest(ecran=nom):
|
|
|
|
|
self.assertTrue(attendues <= set(vu["spec"]), nom)
|
|
|
|
|
|
|
|
|
|
def _faux(self):
|
|
|
|
|
"""Un porteur du socle sans écran : la liste des clés ne dépend pas
|
|
|
|
|
des widgets, et c'est justement ce qu'on veut vérifier."""
|
|
|
|
|
from script.todo.deploy_form_extras import ExtrasMixin
|
|
|
|
|
|
|
|
|
|
class Vide(ExtrasMixin):
|
|
|
|
|
vms = ()
|
|
|
|
|
rows = ()
|
|
|
|
|
|
|
|
|
|
def query_one(self, _s, *_a):
|
|
|
|
|
raise LookupError
|
|
|
|
|
|
|
|
|
|
vide = Vide()
|
|
|
|
|
vide.extras_init({})
|
|
|
|
|
return vide
|
|
|
|
|
|
[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 test_both_screens_offer_the_same_per_vm_choices(self):
|
|
|
|
|
"""Branche, profil, type : ce qu'une VM peut prendre à elle seule.
|
|
|
|
|
|
|
|
|
|
L'écran Proxmox n'en offrait aucun, alors qu'on y déploie le plus
|
|
|
|
|
souvent un parc MIXTE — un hyperviseur imbriqué à côté de VM
|
|
|
|
|
ERPLibre — c'est-à-dire le cas où ils servent le plus."""
|
|
|
|
|
for ident in ("v0_branch", "v0_prof", "v0_type"):
|
|
|
|
|
with self.subTest(reglage=ident):
|
|
|
|
|
self.assertIn(ident, self.qemu["rangee"], "QEMU/KVM")
|
|
|
|
|
self.assertIn(ident, self.pve["rangee"], "Proxmox")
|
|
|
|
|
|
[FIX] proxmox : quatre écrans qui parlaient d'une machine locale
La confirmation de suppression promettait à TOUTE VM « son disque qcow2
EFFACÉ », puis nommait /var/lib/libvirt/images/<nom>.qcow2. Sur une VM
Proxmox ce fichier n'existe pas — au mieux, au pire c'est celui d'une autre
VM du même nom. C'est la peur exacte qui avait fait remonter le nettoyage.
Elle nomme désormais l'hôte, le VMID et « qm destroy ».
« Console de l'hyperviseur » lisait le port par « virsh vncdisplay ». Un
Proxmox n'a pas de libvirt : l'échec se lisait « écran fermé » et on
conseillait « sudo virsh edit » sur une machine sans ce binaire. Ce n'est pas
un écran fermé, c'est la mauvaise question — Proxmox sert le sien par un
ticket. Les deux vrais chemins sont nommés : la console série, l'interface
web par tunnel.
La colonne Odoo était un 🟢 acquis pour toujours : « Odoo ne redescend pas en
cours d'install » est faux — le service redémarre au moins une fois, et il
lui arrive de mourir. Elle est relue, gratuitement sur Proxmox, toutes les
trente secondes ailleurs. Un hôte muet reste distinct d'un Odoo tombé.
Enfin « Versions principales » (F7) manquait à l'écran Proxmox, qui affiche
pourtant le même catalogue. Les trois gestes du catalogue vivent maintenant
dans le socle du plan, où ils ne peuvent plus diverger.
--- EN ---
The delete confirmation promised EVERY VM "its qcow2 disk ERASED", then named
/var/lib/libvirt/images/<name>.qcow2. On a Proxmox VM that file does not
exist — at best; at worst it is another VM's, of the same name. That is the
very fear that surfaced the cleanup report. It now names the host, the VMID
and "qm destroy".
"Hypervisor console" read the port through "virsh vncdisplay". Proxmox has no
libvirt: the failure read as "screen closed" and we advised "sudo virsh edit"
on a machine without that binary. It is not a closed screen, it is the wrong
question — Proxmox serves its own by ticket. Both real paths are named: the
serial console, the web interface through a tunnel.
The Odoo column was a 🟢 acquired forever: "Odoo does not go back down during
the install" is false — the service restarts at least once, and it does die.
It is re-read, free on Proxmox, every thirty seconds elsewhere. A silent host
stays distinct from a dead Odoo.
Finally "Main versions" (F7) was missing from the Proxmox screen, which shows
the same catalog. The catalog's three gestures now live in the plan
foundation, where they can no longer diverge.
Assisted-by: Claude Opus 5
2026-08-24 23:07:55 -04:00
|
|
|
def test_both_screens_bind_the_same_catalog_shortcuts(self):
|
|
|
|
|
# « Versions principales » (F7) manquait à l'écran Proxmox, qui
|
|
|
|
|
# affiche pourtant le même catalogue, drapeau « default » compris.
|
|
|
|
|
from script.todo.proxmox_deploy_form import run_proxmox_form
|
|
|
|
|
from script.todo.qemu_deploy_form import run_deploy_form
|
|
|
|
|
|
|
|
|
|
todo = todo_muet()
|
|
|
|
|
mod = todo._qemu_import_module()
|
|
|
|
|
touches = {}
|
|
|
|
|
for nom, app in (
|
|
|
|
|
("QEMU/KVM", run_deploy_form(todo._qemu_form_context(mod), False)),
|
|
|
|
|
("Proxmox", run_proxmox_form(contexte_proxmox(todo), False)),
|
|
|
|
|
):
|
|
|
|
|
touches[nom] = {
|
|
|
|
|
b[0]: b[1] for b in app.BINDINGS if b[1].startswith("select_")
|
|
|
|
|
}
|
|
|
|
|
self.assertEqual(touches["QEMU/KVM"], touches["Proxmox"])
|
|
|
|
|
self.assertIn("select_main", touches["Proxmox"].values())
|
|
|
|
|
|
[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 test_parallelism_follows_the_host_on_both_screens(self):
|
|
|
|
|
# L'écran Proxmox plafonnait à quatre choix et en proposait UN, quel
|
|
|
|
|
# que soit le nombre de cœurs de l'hôte.
|
|
|
|
|
for nom, vu in (("QEMU/KVM", self.qemu), ("Proxmox", self.pve)):
|
|
|
|
|
with self.subTest(ecran=nom):
|
|
|
|
|
self.assertIn("f_par_all", vu["ids"], nom)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@unittest.skipUnless(TEXTUAL, "Textual absent")
|
|
|
|
|
class TestCeQueLeBureauChange(unittest.TestCase):
|
|
|
|
|
"""Choisir un bureau doit se voir sur les DEUX écrans, et de la même
|
|
|
|
|
façon : le nom prend un suffixe, le disque grossit."""
|
|
|
|
|
|
|
|
|
|
def _bureau(self):
|
|
|
|
|
async def gestes(app, pilote):
|
|
|
|
|
from textual.widgets import Checkbox
|
|
|
|
|
|
|
|
|
|
list(app.query("#f_type RadioButton"))[1].value = True
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
app.query_one("#f_tool_pycharm", Checkbox).value = True
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
|
|
|
|
|
return gestes
|
|
|
|
|
|
|
|
|
|
def test_the_name_gets_the_desktop_suffix(self):
|
|
|
|
|
from script.todo.proxmox_deploy_form import run_proxmox_form
|
|
|
|
|
|
|
|
|
|
todo = todo_muet()
|
|
|
|
|
vu = releve(
|
|
|
|
|
run_proxmox_form, contexte_proxmox(todo), gestes=self._bureau()
|
|
|
|
|
)
|
|
|
|
|
# Sans suffixe, une VM graphique et sa jumelle serveur portent le même
|
|
|
|
|
# nom : la seconde est signalée « existe déjà » et ignorée.
|
|
|
|
|
self.assertTrue(vu["noms"][0].endswith("-gnome"), vu["noms"])
|
|
|
|
|
|
|
|
|
|
def test_the_disk_grows_by_the_desktop_and_the_tools(self):
|
|
|
|
|
from script.todo.proxmox_deploy_form import run_proxmox_form
|
|
|
|
|
|
|
|
|
|
todo = todo_muet()
|
|
|
|
|
ctx = contexte_proxmox(todo)
|
|
|
|
|
nu = releve(run_proxmox_form, ctx)
|
|
|
|
|
avec = releve(run_proxmox_form, ctx, gestes=self._bureau())
|
|
|
|
|
attendu = ctx["desktop_disk_gb"] + ctx["vm_tool_disk"]["pycharm"]
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
avec["disques"][0] - nu["disques"][0],
|
|
|
|
|
attendu,
|
|
|
|
|
"le plan doit annoncer le surcoût AVANT de déployer",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_the_created_disk_matches_what_the_plan_announced(self):
|
|
|
|
|
# C'est le pont entre l'écran et « qm resize » : le plan annonçait
|
|
|
|
|
# 36 Go et la commande en demandait 20.
|
|
|
|
|
todo = todo_muet()
|
|
|
|
|
taille = todo._pve_disk_with_margin(
|
|
|
|
|
{"disk": "20G", "arch": "amd64", "distro": "ubuntu"},
|
|
|
|
|
{
|
|
|
|
|
"install": {
|
|
|
|
|
"branch": "develop",
|
|
|
|
|
"cmd": "make install_os && make install_odoo_18",
|
|
|
|
|
},
|
|
|
|
|
"desktop": "gnome",
|
|
|
|
|
"vm_tools": ("pycharm",),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(taille, "36G")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCeQueSontCesVm(unittest.TestCase):
|
|
|
|
|
"""L'architecture d'une VM Proxmox venait de « virsh », qui ne connaît que
|
|
|
|
|
les domaines d'ICI.
|
|
|
|
|
|
|
|
|
|
Elle décide des outils : une VM ARM prise pour x86_64 recevait Android
|
|
|
|
|
Studio, que Google ne publie pas pour elle — l'installation s'arrête, une
|
|
|
|
|
heure plus tard. Même famille que le « s » qui partait vers la machine
|
|
|
|
|
interne : on jugeait sur le nom."""
|
|
|
|
|
|
|
|
|
|
def _outils(self, meta):
|
|
|
|
|
vu = {}
|
|
|
|
|
todo = TODO.__new__(TODO)
|
|
|
|
|
todo._qemu_import_module = lambda: object()
|
|
|
|
|
todo._qemu_resolve_ips = lambda noms: {n: "10.0.0.1" for n in noms}
|
|
|
|
|
todo._qemu_vm_meta = lambda nom, mod: ("ubuntu", "26.04", "amd64")
|
|
|
|
|
todo._qemu_erplibre_remote_cmd = lambda *a, **kw: ""
|
|
|
|
|
|
|
|
|
|
def capture(vms, branche, remote):
|
|
|
|
|
vu["archs"] = [v["arch"] for v in vms]
|
|
|
|
|
raise SystemExit
|
|
|
|
|
|
|
|
|
|
import script.todo.qemu_install_monitor as mon
|
|
|
|
|
|
|
|
|
|
vrai = mon.launch_installs
|
|
|
|
|
mon.launch_installs = capture
|
|
|
|
|
try:
|
|
|
|
|
todo._qemu_install_erplibre_monitored(
|
|
|
|
|
["vm-arm"], "develop", {"vm-arm": "10.0.0.1"}, meta=meta
|
|
|
|
|
)
|
|
|
|
|
except SystemExit:
|
|
|
|
|
pass
|
|
|
|
|
finally:
|
|
|
|
|
mon.launch_installs = vrai
|
|
|
|
|
return vu.get("archs")
|
|
|
|
|
|
|
|
|
|
def test_the_caller_knows_better_than_virsh(self):
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
self._outils({"vm-arm": ("ubuntu", "26.04", "arm64")}), ["arm64"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_without_it_virsh_still_answers(self):
|
|
|
|
|
# La voie libvirt ne régresse pas : ses domaines sont bien ici.
|
|
|
|
|
self.assertEqual(self._outils(None), ["amd64"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestLeFuseauDUneVmProxmox(unittest.TestCase):
|
|
|
|
|
"""« qm set » ne pose pas de fuseau : le cloud-init de Proxmox ne règle
|
|
|
|
|
que l'utilisateur, la clé et le réseau. Une VM créée là restait en UTC, et
|
|
|
|
|
on ne s'en aperçoit qu'aux horodatages."""
|
|
|
|
|
|
|
|
|
|
def _pose(self, spec):
|
|
|
|
|
vu = {}
|
|
|
|
|
todo = TODO.__new__(TODO)
|
|
|
|
|
todo._pve_ssh = lambda cible, cmd, **kw: (
|
|
|
|
|
vu.setdefault("cmd", cmd),
|
|
|
|
|
(0, ""),
|
|
|
|
|
)[1]
|
|
|
|
|
todo._pve_set_timezone("vm", spec)
|
|
|
|
|
return vu.get("cmd", "")
|
|
|
|
|
|
|
|
|
|
def test_the_timezone_reaches_the_vm(self):
|
|
|
|
|
self.assertIn(
|
|
|
|
|
"sudo timedatectl set-timezone America/Montreal",
|
|
|
|
|
self._pose({"timezone": "America/Montreal"}),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_a_free_value_is_quoted(self):
|
|
|
|
|
# La liste laisse la saisie libre : ce qui en sort part dans une
|
|
|
|
|
# commande distante, donc il est cité.
|
|
|
|
|
self.assertIn(
|
|
|
|
|
"'a b; rm -rf /'", self._pose({"timezone": "a b; rm -rf /"})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_no_timezone_means_no_command(self):
|
|
|
|
|
self.assertEqual(self._pose({}), "")
|
|
|
|
|
|
|
|
|
|
def test_the_prompt_path_still_gets_one(self):
|
|
|
|
|
# La voie par questions ne demande pas le fuseau ; sans défaut, elle
|
|
|
|
|
# laissait la VM en UTC alors que la voie libvirt reprend celui de
|
|
|
|
|
# l'hôte depuis toujours.
|
|
|
|
|
import re
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
src = Path("script/todo/proxmox_menu.py").read_text(encoding="utf-8")
|
|
|
|
|
bloc = src[src.index("def _pve_deploy_prompts") :]
|
|
|
|
|
bloc = bloc[: bloc.index("_pve_after_create")]
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
re.search(r'"timezone":\s*self\._qemu_host_timezone\(\)', bloc),
|
|
|
|
|
"le spec des invites doit porter un fuseau",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main(verbosity=2)
|