[ADD] proxmox : un écran pour déployer sur un hôte distant
Le déploiement Proxmox se faisait par vingt questions, l'une après
l'autre, sans jamais voir le plan entier. Il a maintenant le même écran
que QEMU/KVM — catalogue à gauche, plan à droite, totaux dessous — et une
touche pour revenir aux questions. Il n'en redit rien : 690 lignes contre
1 340, tout le reste vient du socle commun.
Ce que Proxmox a en propre est montré AVANT de lancer : le VMID, parce
que l'hôte ne dit « déjà pris » qu'après avoir téléchargé l'image, et
l'adresse qui s'en déduit sur un pont interne. Le stockage et le pont sont
lus sur l'hôte, jamais devinés. L'icône 🗄 distingue l'entrée du menu de
celle qui déploie ici même.
--- EN ---
Deploying on Proxmox meant twenty questions, one after another, never
seeing the whole plan. It now has the same screen as QEMU/KVM — catalogue
left, plan right, totals below — and a key to fall back to the questions.
It repeats none of it: 690 lines against 1,340, everything else comes
from the shared foundation.
What belongs to Proxmox is shown BEFORE launching: the VMID, because the
host only says "already taken" after downloading the image, and the
address derived from it on an internal bridge. Storage and bridge are read
from the host, never guessed. The 🗄 icon tells the menu entry apart from
the one that deploys right here.
Assisted-by: Claude Opus 5
2026-08-23 18:01:28 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
"""L'écran de déploiement Proxmox VE.
|
|
|
|
|
|
|
|
|
|
Ce que Proxmox a en plus de QEMU/KVM, et qui doit donc être éprouvé ici : le
|
|
|
|
|
VMID choisi AVANT le téléchargement de l'image (l'hôte ne dit « déjà pris »
|
|
|
|
|
qu'après), l'adresse qui s'en déduit sur un pont interne, le stockage et le
|
|
|
|
|
pont lus sur l'hôte, et une VM qui existe déjà et qu'on ne doit surtout pas
|
|
|
|
|
écraser.
|
|
|
|
|
|
|
|
|
|
Le rendu est vérifié sans terminal (`run_test`), sur un contexte synthétique :
|
|
|
|
|
aucun hôte Proxmox n'est joint.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
import sys
|
|
|
|
|
import unittest
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.proxmox_deploy_form import ( # noqa: E402
|
|
|
|
|
assign_vmids,
|
|
|
|
|
build_spec,
|
|
|
|
|
res_label,
|
|
|
|
|
run_proxmox_form,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
import textual # noqa: F401
|
|
|
|
|
|
|
|
|
|
TEXTUAL = True
|
|
|
|
|
except Exception: # pragma: no cover - dépend de l'environnement
|
|
|
|
|
TEXTUAL = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def rangee(nom, etat="new"):
|
|
|
|
|
return {
|
|
|
|
|
"vm": {
|
|
|
|
|
"name": nom,
|
|
|
|
|
"distro": "debian",
|
|
|
|
|
"version": "13",
|
|
|
|
|
"arch": "amd64",
|
|
|
|
|
"vcpus": 2,
|
|
|
|
|
"ram": 2048,
|
|
|
|
|
"disk": "32G",
|
|
|
|
|
},
|
|
|
|
|
"state": etat,
|
|
|
|
|
"note": "",
|
|
|
|
|
"disk_gb": 32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestVmid(unittest.TestCase):
|
|
|
|
|
"""Proxmox refuse un VMID déjà pris, et il le dit APRÈS avoir téléchargé
|
|
|
|
|
l'image : le choix se fait donc avant, d'après ce que l'hôte déclare."""
|
|
|
|
|
|
|
|
|
|
def test_taken_ids_are_skipped(self):
|
|
|
|
|
rows = [rangee("a"), rangee("b")]
|
|
|
|
|
assign_vmids(rows, [100, 101, 103], 100, lambda v: "ip=dhcp")
|
|
|
|
|
self.assertEqual([r["vm"]["vmid"] for r in rows], [102, 104])
|
|
|
|
|
|
|
|
|
|
def test_an_existing_vm_keeps_its_own(self):
|
|
|
|
|
rows = [rangee("a", "exists"), rangee("b")]
|
|
|
|
|
assign_vmids(rows, [], 100, lambda v: "ip=dhcp")
|
|
|
|
|
self.assertNotIn("vmid", rows[0]["vm"])
|
|
|
|
|
self.assertEqual(rows[1]["vm"]["vmid"], 100)
|
|
|
|
|
|
|
|
|
|
def test_the_first_vmid_is_honoured(self):
|
|
|
|
|
rows = [rangee("a")]
|
|
|
|
|
assign_vmids(rows, [], 250, lambda v: "ip=dhcp")
|
|
|
|
|
self.assertEqual(rows[0]["vm"]["vmid"], 250)
|
|
|
|
|
|
|
|
|
|
def test_a_vmid_never_goes_below_100(self):
|
|
|
|
|
# Proxmox réserve les VMID sous 100.
|
|
|
|
|
rows = [rangee("a")]
|
|
|
|
|
assign_vmids(rows, [], 7, lambda v: "ip=dhcp")
|
|
|
|
|
self.assertEqual(rows[0]["vm"]["vmid"], 100)
|
|
|
|
|
|
|
|
|
|
def test_the_address_is_derived_from_the_vmid(self):
|
|
|
|
|
rows = [rangee("a"), rangee("b")]
|
[ADD] todo : dire la place qui reste sous le plan de déploiement
La ligne de totaux annonçait « ~126 G » sans dire sur quoi : la demande
seule ne dit pas si ça rentre, et on l'apprenait au déploiement. Elle dit
maintenant la demande, ce qui reste et la capacité — « ~126 G / 20 G libres
sur 270 G » — et prévient dès que le plan dépasse. Les trois limites (RAM,
disque, cœurs) s'affichent ensemble : n'en montrer qu'une cachait les
autres. Sur Proxmox, la place vient du stockage choisi, que « pvesm status »
donnait déjà.
Deux défauts trouvés en le faisant : la marque de génération, exigée de tous
les widgets, faisait taire chaque réglage commun de l'écran Proxmox, et
« [x1] » disparaissait, lu comme une balise Rich.
--- EN ---
The totals line said "~126 G" without saying out of what: the demand alone
does not tell whether it fits, and one found out at deploy time. It now says
the demand, what is left and the capacity — "~126 G / 20 G free of 270 G" —
and warns as soon as the plan exceeds it. The three limits (RAM, disk,
cores) show together: showing only one hid the others. On Proxmox the room
comes from the chosen storage, which "pvesm status" already gave.
Two defects found on the way: the generation mark, required of every widget,
silenced each common setting on the Proxmox screen, and "[x1]" vanished,
read as a Rich tag.
Assisted-by: Claude Opus 5
2026-08-24 01:11:09 -04:00
|
|
|
assign_vmids(rows, [], 100, lambda v: f"ip=10.10.10.{50 + v % 200}/24")
|
[ADD] proxmox : un écran pour déployer sur un hôte distant
Le déploiement Proxmox se faisait par vingt questions, l'une après
l'autre, sans jamais voir le plan entier. Il a maintenant le même écran
que QEMU/KVM — catalogue à gauche, plan à droite, totaux dessous — et une
touche pour revenir aux questions. Il n'en redit rien : 690 lignes contre
1 340, tout le reste vient du socle commun.
Ce que Proxmox a en propre est montré AVANT de lancer : le VMID, parce
que l'hôte ne dit « déjà pris » qu'après avoir téléchargé l'image, et
l'adresse qui s'en déduit sur un pont interne. Le stockage et le pont sont
lus sur l'hôte, jamais devinés. L'icône 🗄 distingue l'entrée du menu de
celle qui déploie ici même.
--- EN ---
Deploying on Proxmox meant twenty questions, one after another, never
seeing the whole plan. It now has the same screen as QEMU/KVM — catalogue
left, plan right, totals below — and a key to fall back to the questions.
It repeats none of it: 690 lines against 1,340, everything else comes
from the shared foundation.
What belongs to Proxmox is shown BEFORE launching: the VMID, because the
host only says "already taken" after downloading the image, and the
address derived from it on an internal bridge. Storage and bridge are read
from the host, never guessed. The 🗄 icon tells the menu entry apart from
the one that deploys right here.
Assisted-by: Claude Opus 5
2026-08-23 18:01:28 -04:00
|
|
|
self.assertEqual(rows[0]["vm"]["ipconfig"], "ip=10.10.10.150/24")
|
|
|
|
|
self.assertEqual(rows[1]["vm"]["ipconfig"], "ip=10.10.10.151/24")
|
|
|
|
|
|
|
|
|
|
def test_without_a_bridge_rule_it_falls_back_to_dhcp(self):
|
|
|
|
|
rows = [rangee("a")]
|
|
|
|
|
assign_vmids(rows, [], 100, None)
|
|
|
|
|
self.assertEqual(rows[0]["vm"]["ipconfig"], "ip=dhcp")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestSpec(unittest.TestCase):
|
|
|
|
|
def _form(self, **extra):
|
|
|
|
|
base = {
|
|
|
|
|
"host": {"target": "erplibre@10.0.0.5"},
|
|
|
|
|
"storage": "local-lvm",
|
|
|
|
|
"bridge": "vmbr0",
|
|
|
|
|
"res_label": "x1",
|
|
|
|
|
"ssh_key": "/home/x/.ssh/id_ed25519.pub",
|
|
|
|
|
"start": True,
|
|
|
|
|
"add_ssh_config": True,
|
[ADD] todo : dire la place qui reste sous le plan de déploiement
La ligne de totaux annonçait « ~126 G » sans dire sur quoi : la demande
seule ne dit pas si ça rentre, et on l'apprenait au déploiement. Elle dit
maintenant la demande, ce qui reste et la capacité — « ~126 G / 20 G libres
sur 270 G » — et prévient dès que le plan dépasse. Les trois limites (RAM,
disque, cœurs) s'affichent ensemble : n'en montrer qu'une cachait les
autres. Sur Proxmox, la place vient du stockage choisi, que « pvesm status »
donnait déjà.
Deux défauts trouvés en le faisant : la marque de génération, exigée de tous
les widgets, faisait taire chaque réglage commun de l'écran Proxmox, et
« [x1] » disparaissait, lu comme une balise Rich.
--- EN ---
The totals line said "~126 G" without saying out of what: the demand alone
does not tell whether it fits, and one found out at deploy time. It now says
the demand, what is left and the capacity — "~126 G / 20 G free of 270 G" —
and warns as soon as the plan exceeds it. The three limits (RAM, disk,
cores) show together: showing only one hid the others. On Proxmox the room
comes from the chosen storage, which "pvesm status" already gave.
Two defects found on the way: the generation mark, required of every widget,
silenced each common setting on the Proxmox screen, and "[x1]" vanished,
read as a Rich tag.
Assisted-by: Claude Opus 5
2026-08-24 01:11:09 -04:00
|
|
|
"install": {
|
|
|
|
|
"branch": "develop",
|
|
|
|
|
"label": "Odoo 18",
|
|
|
|
|
"cmd": "make",
|
|
|
|
|
},
|
[ADD] proxmox : un écran pour déployer sur un hôte distant
Le déploiement Proxmox se faisait par vingt questions, l'une après
l'autre, sans jamais voir le plan entier. Il a maintenant le même écran
que QEMU/KVM — catalogue à gauche, plan à droite, totaux dessous — et une
touche pour revenir aux questions. Il n'en redit rien : 690 lignes contre
1 340, tout le reste vient du socle commun.
Ce que Proxmox a en propre est montré AVANT de lancer : le VMID, parce
que l'hôte ne dit « déjà pris » qu'après avoir téléchargé l'image, et
l'adresse qui s'en déduit sur un pont interne. Le stockage et le pont sont
lus sur l'hôte, jamais devinés. L'icône 🗄 distingue l'entrée du menu de
celle qui déploie ici même.
--- EN ---
Deploying on Proxmox meant twenty questions, one after another, never
seeing the whole plan. It now has the same screen as QEMU/KVM — catalogue
left, plan right, totals below — and a key to fall back to the questions.
It repeats none of it: 690 lines against 1,340, everything else comes
from the shared foundation.
What belongs to Proxmox is shown BEFORE launching: the VMID, because the
host only says "already taken" after downloading the image, and the
address derived from it on an internal bridge. Storage and bridge are read
from the host, never guessed. The 🗄 icon tells the menu entry apart from
the one that deploys right here.
Assisted-by: Claude Opus 5
2026-08-23 18:01:28 -04:00
|
|
|
"monitor": True,
|
|
|
|
|
"parallelism": 2,
|
|
|
|
|
}
|
|
|
|
|
base.update(extra)
|
|
|
|
|
return base
|
|
|
|
|
|
|
|
|
|
def test_an_existing_vm_is_never_recreated(self):
|
|
|
|
|
vms = [{"name": "a"}, {"name": "b"}]
|
|
|
|
|
spec = build_spec(vms, ["b"], self._form())
|
|
|
|
|
self.assertEqual([v["name"] for v in spec["vms"]], ["a"])
|
|
|
|
|
self.assertEqual(spec["existing"], ["b"])
|
|
|
|
|
|
|
|
|
|
def test_the_user_defaults_to_erplibre(self):
|
|
|
|
|
spec = build_spec([], [], self._form())
|
|
|
|
|
self.assertEqual(spec["user"], "erplibre")
|
|
|
|
|
|
|
|
|
|
def test_the_monitor_choice_reaches_the_spec(self):
|
|
|
|
|
# Le suivi est demandé au NIVEAU DU DÉPLOIEMENT : une VM sans
|
|
|
|
|
# ERPLibre se suit aussi.
|
|
|
|
|
spec = build_spec([], [], self._form(install=None, monitor=True))
|
|
|
|
|
self.assertIsNone(spec["install"])
|
|
|
|
|
self.assertTrue(spec["monitor"])
|
|
|
|
|
|
|
|
|
|
def test_the_resource_label_names_the_common_setting(self):
|
|
|
|
|
self.assertEqual(res_label("3"), "x3")
|
|
|
|
|
self.assertNotEqual(res_label("custom"), "xcustom")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def contexte():
|
|
|
|
|
def entree(distro, version, arch="amd64"):
|
|
|
|
|
return {
|
|
|
|
|
"name": f"erplibre-{distro}-{version}",
|
|
|
|
|
"distro": distro,
|
|
|
|
|
"version": version,
|
|
|
|
|
"arch": arch,
|
|
|
|
|
"ram": 2048,
|
|
|
|
|
"disk": "32G",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
[ADD] todo : dire la place qui reste sous le plan de déploiement
La ligne de totaux annonçait « ~126 G » sans dire sur quoi : la demande
seule ne dit pas si ça rentre, et on l'apprenait au déploiement. Elle dit
maintenant la demande, ce qui reste et la capacité — « ~126 G / 20 G libres
sur 270 G » — et prévient dès que le plan dépasse. Les trois limites (RAM,
disque, cœurs) s'affichent ensemble : n'en montrer qu'une cachait les
autres. Sur Proxmox, la place vient du stockage choisi, que « pvesm status »
donnait déjà.
Deux défauts trouvés en le faisant : la marque de génération, exigée de tous
les widgets, faisait taire chaque réglage commun de l'écran Proxmox, et
« [x1] » disparaissait, lu comme une balise Rich.
--- EN ---
The totals line said "~126 G" without saying out of what: the demand alone
does not tell whether it fits, and one found out at deploy time. It now says
the demand, what is left and the capacity — "~126 G / 20 G free of 270 G" —
and warns as soon as the plan exceeds it. The three limits (RAM, disk,
cores) show together: showing only one hid the others. On Proxmox the room
comes from the chosen storage, which "pvesm status" already gave.
Two defects found on the way: the generation mark, required of every widget,
silenced each common setting on the Proxmox screen, and "[x1]" vanished,
read as a Rich tag.
Assisted-by: Claude Opus 5
2026-08-24 01:11:09 -04:00
|
|
|
"host": {
|
|
|
|
|
"target": "erplibre@10.0.0.5",
|
|
|
|
|
"sudo": "sudo ",
|
|
|
|
|
"label": "pve",
|
|
|
|
|
},
|
[ADD] proxmox : un écran pour déployer sur un hôte distant
Le déploiement Proxmox se faisait par vingt questions, l'une après
l'autre, sans jamais voir le plan entier. Il a maintenant le même écran
que QEMU/KVM — catalogue à gauche, plan à droite, totaux dessous — et une
touche pour revenir aux questions. Il n'en redit rien : 690 lignes contre
1 340, tout le reste vient du socle commun.
Ce que Proxmox a en propre est montré AVANT de lancer : le VMID, parce
que l'hôte ne dit « déjà pris » qu'après avoir téléchargé l'image, et
l'adresse qui s'en déduit sur un pont interne. Le stockage et le pont sont
lus sur l'hôte, jamais devinés. L'icône 🗄 distingue l'entrée du menu de
celle qui déploie ici même.
--- EN ---
Deploying on Proxmox meant twenty questions, one after another, never
seeing the whole plan. It now has the same screen as QEMU/KVM — catalogue
left, plan right, totals below — and a key to fall back to the questions.
It repeats none of it: 690 lines against 1,340, everything else comes
from the shared foundation.
What belongs to Proxmox is shown BEFORE launching: the VMID, because the
host only says "already taken" after downloading the image, and the
address derived from it on an internal bridge. Storage and bridge are read
from the host, never guessed. The 🗄 icon tells the menu entry apart from
the one that deploys right here.
Assisted-by: Claude Opus 5
2026-08-23 18:01:28 -04:00
|
|
|
"node": "pve1",
|
|
|
|
|
"catalog": {
|
|
|
|
|
"amd64": [
|
|
|
|
|
entree("ubuntu", "26.04"),
|
|
|
|
|
entree("debian", "13"),
|
|
|
|
|
entree("fedora", "44"),
|
[FIX] qemu : ne pas poser ERPLibre sur une VM Proxmox VE
Choisir « Proxmox VE » comme système, c'est demander qu'il soit installé.
L'invite en ligne le savait ; le formulaire posait « ERPLibre + Odoo 18 »,
lui ajoutait les 5 Go réservés au dépôt et annonçait une cible make dans le
guide de la VM. La règle vit maintenant en un seul endroit, les deux
chemins la lisent, et un choix explicite l'emporte toujours sur elle.
Trois défauts trouvés derrière. Une commande par VM n'était retenue que si
DEUX VM différaient : déployée seule, la VM Proxmox retombait sur la
commande commune. Le disque choisi à la main disparaissait quand rien
n'était installé — 60 G demandés, 20 G créés. Et une taille absente des
préréglages marquait la rangée ✎ dès son montage.
--- EN ---
Choosing "Proxmox VE" as the system means asking for it to be installed.
The command-line prompt knew that; the form set "ERPLibre + Odoo 18", added
the 5 GB meant for the repository and advertised a make target in the VM's
guide. The rule now lives in one place, both paths read it, and an explicit
choice always wins over it.
Three defects behind it. A per-VM command was only used when TWO VMs
differed: deployed alone, the Proxmox VM fell back to the common command.
A hand-picked disk size vanished when nothing was installed — 60 G asked,
20 G created. And a size absent from the presets marked its row ✎ on mount.
Assisted-by: Claude Opus 5
2026-08-24 01:34:10 -04:00
|
|
|
entree("proxmox", "9"),
|
[ADD] proxmox : un écran pour déployer sur un hôte distant
Le déploiement Proxmox se faisait par vingt questions, l'une après
l'autre, sans jamais voir le plan entier. Il a maintenant le même écran
que QEMU/KVM — catalogue à gauche, plan à droite, totaux dessous — et une
touche pour revenir aux questions. Il n'en redit rien : 690 lignes contre
1 340, tout le reste vient du socle commun.
Ce que Proxmox a en propre est montré AVANT de lancer : le VMID, parce
que l'hôte ne dit « déjà pris » qu'après avoir téléchargé l'image, et
l'adresse qui s'en déduit sur un pont interne. Le stockage et le pont sont
lus sur l'hôte, jamais devinés. L'icône 🗄 distingue l'entrée du menu de
celle qui déploie ici même.
--- EN ---
Deploying on Proxmox meant twenty questions, one after another, never
seeing the whole plan. It now has the same screen as QEMU/KVM — catalogue
left, plan right, totals below — and a key to fall back to the questions.
It repeats none of it: 690 lines against 1,340, everything else comes
from the shared foundation.
What belongs to Proxmox is shown BEFORE launching: the VMID, because the
host only says "already taken" after downloading the image, and the
address derived from it on an internal bridge. Storage and bridge are read
from the host, never guessed. The 🗄 icon tells the menu entry apart from
the one that deploys right here.
Assisted-by: Claude Opus 5
2026-08-23 18:01:28 -04:00
|
|
|
],
|
|
|
|
|
"arm64": [entree("debian", "13", "arm64")],
|
|
|
|
|
},
|
|
|
|
|
"arches": ["amd64", "arm64"],
|
|
|
|
|
"native": "amd64",
|
|
|
|
|
"names": ["erplibre-debian-13"],
|
|
|
|
|
"vmids": [100, 101],
|
|
|
|
|
"next_vmid": 102,
|
|
|
|
|
"storages": ["local-lvm", "local"],
|
|
|
|
|
"storage": "local-lvm",
|
[ADD] todo : dire la place qui reste sous le plan de déploiement
La ligne de totaux annonçait « ~126 G » sans dire sur quoi : la demande
seule ne dit pas si ça rentre, et on l'apprenait au déploiement. Elle dit
maintenant la demande, ce qui reste et la capacité — « ~126 G / 20 G libres
sur 270 G » — et prévient dès que le plan dépasse. Les trois limites (RAM,
disque, cœurs) s'affichent ensemble : n'en montrer qu'une cachait les
autres. Sur Proxmox, la place vient du stockage choisi, que « pvesm status »
donnait déjà.
Deux défauts trouvés en le faisant : la marque de génération, exigée de tous
les widgets, faisait taire chaque réglage commun de l'écran Proxmox, et
« [x1] » disparaissait, lu comme une balise Rich.
--- EN ---
The totals line said "~126 G" without saying out of what: the demand alone
does not tell whether it fits, and one found out at deploy time. It now says
the demand, what is left and the capacity — "~126 G / 20 G free of 270 G" —
and warns as soon as the plan exceeds it. The three limits (RAM, disk,
cores) show together: showing only one hid the others. On Proxmox the room
comes from the chosen storage, which "pvesm status" already gave.
Two defects found on the way: the generation mark, required of every widget,
silenced each common setting on the Proxmox screen, and "[x1]" vanished,
read as a Rich tag.
Assisted-by: Claude Opus 5
2026-08-24 01:11:09 -04:00
|
|
|
"storage_avail": {
|
|
|
|
|
"local-lvm": 90 * (1 << 30),
|
|
|
|
|
"local": 12 * (1 << 30),
|
|
|
|
|
},
|
[ADD] proxmox : un écran pour déployer sur un hôte distant
Le déploiement Proxmox se faisait par vingt questions, l'une après
l'autre, sans jamais voir le plan entier. Il a maintenant le même écran
que QEMU/KVM — catalogue à gauche, plan à droite, totaux dessous — et une
touche pour revenir aux questions. Il n'en redit rien : 690 lignes contre
1 340, tout le reste vient du socle commun.
Ce que Proxmox a en propre est montré AVANT de lancer : le VMID, parce
que l'hôte ne dit « déjà pris » qu'après avoir téléchargé l'image, et
l'adresse qui s'en déduit sur un pont interne. Le stockage et le pont sont
lus sur l'hôte, jamais devinés. L'icône 🗄 distingue l'entrée du menu de
celle qui déploie ici même.
--- EN ---
Deploying on Proxmox meant twenty questions, one after another, never
seeing the whole plan. It now has the same screen as QEMU/KVM — catalogue
left, plan right, totals below — and a key to fall back to the questions.
It repeats none of it: 690 lines against 1,340, everything else comes
from the shared foundation.
What belongs to Proxmox is shown BEFORE launching: the VMID, because the
host only says "already taken" after downloading the image, and the
address derived from it on an internal bridge. Storage and bridge are read
from the host, never guessed. The 🗄 icon tells the menu entry apart from
the one that deploys right here.
Assisted-by: Claude Opus 5
2026-08-23 18:01:28 -04:00
|
|
|
"bridges": ["vmbr0"],
|
|
|
|
|
"bridge": "vmbr0",
|
|
|
|
|
"ipconfig": lambda pont, vmid: f"ip=10.10.10.{50 + vmid % 200}/24",
|
|
|
|
|
"build_command": lambda vm, spec: [f"qm create {vm['vmid']}"],
|
|
|
|
|
"branches": ["develop", "master"],
|
|
|
|
|
"install_profiles": [("ERPLibre + Odoo 18", "make install_odoo_18")],
|
[FIX] qemu : ne pas poser ERPLibre sur une VM Proxmox VE
Choisir « Proxmox VE » comme système, c'est demander qu'il soit installé.
L'invite en ligne le savait ; le formulaire posait « ERPLibre + Odoo 18 »,
lui ajoutait les 5 Go réservés au dépôt et annonçait une cible make dans le
guide de la VM. La règle vit maintenant en un seul endroit, les deux
chemins la lisent, et un choix explicite l'emporte toujours sur elle.
Trois défauts trouvés derrière. Une commande par VM n'était retenue que si
DEUX VM différaient : déployée seule, la VM Proxmox retombait sur la
commande commune. Le disque choisi à la main disparaissait quand rien
n'était installé — 60 G demandés, 20 G créés. Et une taille absente des
préréglages marquait la rangée ✎ dès son montage.
--- EN ---
Choosing "Proxmox VE" as the system means asking for it to be installed.
The command-line prompt knew that; the form set "ERPLibre + Odoo 18", added
the 5 GB meant for the repository and advertised a make target in the VM's
guide. The rule now lives in one place, both paths read it, and an explicit
choice always wins over it.
Three defects behind it. A per-VM command was only used when TWO VMs
differed: deployed alone, the Proxmox VM fell back to the common command.
A hand-picked disk size vanished when nothing was installed — 60 G asked,
20 G created. And a size absent from the presets marked its row ✎ on mount.
Assisted-by: Claude Opus 5
2026-08-24 01:34:10 -04:00
|
|
|
"distro_profiles": {
|
|
|
|
|
"proxmox": (
|
|
|
|
|
"Hyperviseur Proxmox VE (sans Odoo)",
|
|
|
|
|
"./script/proxmox/install_proxmox.sh",
|
|
|
|
|
)
|
|
|
|
|
},
|
[ADD] proxmox : un écran pour déployer sur un hôte distant
Le déploiement Proxmox se faisait par vingt questions, l'une après
l'autre, sans jamais voir le plan entier. Il a maintenant le même écran
que QEMU/KVM — catalogue à gauche, plan à droite, totaux dessous — et une
touche pour revenir aux questions. Il n'en redit rien : 690 lignes contre
1 340, tout le reste vient du socle commun.
Ce que Proxmox a en propre est montré AVANT de lancer : le VMID, parce
que l'hôte ne dit « déjà pris » qu'après avoir téléchargé l'image, et
l'adresse qui s'en déduit sur un pont interne. Le stockage et le pont sont
lus sur l'hôte, jamais devinés. L'icône 🗄 distingue l'entrée du menu de
celle qui déploie ici même.
--- EN ---
Deploying on Proxmox meant twenty questions, one after another, never
seeing the whole plan. It now has the same screen as QEMU/KVM — catalogue
left, plan right, totals below — and a key to fall back to the questions.
It repeats none of it: 690 lines against 1,340, everything else comes
from the shared foundation.
What belongs to Proxmox is shown BEFORE launching: the VMID, because the
host only says "already taken" after downloading the image, and the
address derived from it on an internal bridge. Storage and bridge are read
from the host, never guessed. The 🗄 icon tells the menu entry apart from
the one that deploys right here.
Assisted-by: Claude Opus 5
2026-08-23 18:01:28 -04:00
|
|
|
"ssh_key": "/home/x/.ssh/id_ed25519.pub",
|
|
|
|
|
"cpu_presets": [2, 4, 8],
|
|
|
|
|
"ram_presets": [2048, 4096, 8192],
|
|
|
|
|
"disk_presets": ["32G", "64G"],
|
|
|
|
|
"base_vcpus": 2,
|
|
|
|
|
"host_cpu": 8,
|
|
|
|
|
"free_ram": 12000,
|
|
|
|
|
"extra_disk_gb": 10,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@unittest.skipUnless(TEXTUAL, "Textual absent")
|
|
|
|
|
class TestEcran(unittest.TestCase):
|
|
|
|
|
"""Le formulaire, monté sans terminal."""
|
|
|
|
|
|
|
|
|
|
def _rendu(self, gestes):
|
|
|
|
|
ctx = contexte()
|
|
|
|
|
resultat = {}
|
|
|
|
|
|
|
|
|
|
async def scenario():
|
|
|
|
|
from textual.widgets import SelectionList
|
|
|
|
|
|
|
|
|
|
app = run_proxmox_form(ctx, run_app=False)
|
|
|
|
|
async with app.run_test(size=(200, 50)) as pilote:
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
liste = app.query_one(SelectionList)
|
|
|
|
|
for i in range(3):
|
|
|
|
|
liste.select(liste.get_option_at_index(i).value)
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
await gestes(app, pilote)
|
[ADD] todo : dire la place qui reste sous le plan de déploiement
La ligne de totaux annonçait « ~126 G » sans dire sur quoi : la demande
seule ne dit pas si ça rentre, et on l'apprenait au déploiement. Elle dit
maintenant la demande, ce qui reste et la capacité — « ~126 G / 20 G libres
sur 270 G » — et prévient dès que le plan dépasse. Les trois limites (RAM,
disque, cœurs) s'affichent ensemble : n'en montrer qu'une cachait les
autres. Sur Proxmox, la place vient du stockage choisi, que « pvesm status »
donnait déjà.
Deux défauts trouvés en le faisant : la marque de génération, exigée de tous
les widgets, faisait taire chaque réglage commun de l'écran Proxmox, et
« [x1] » disparaissait, lu comme une balise Rich.
--- EN ---
The totals line said "~126 G" without saying out of what: the demand alone
does not tell whether it fits, and one found out at deploy time. It now says
the demand, what is left and the capacity — "~126 G / 20 G free of 270 G" —
and warns as soon as the plan exceeds it. The three limits (RAM, disk,
cores) show together: showing only one hid the others. On Proxmox the room
comes from the chosen storage, which "pvesm status" already gave.
Two defects found on the way: the generation mark, required of every widget,
silenced each common setting on the Proxmox screen, and "[x1]" vanished,
read as a Rich tag.
Assisted-by: Claude Opus 5
2026-08-24 01:11:09 -04:00
|
|
|
# Relevé AVANT la sortie du contexte : `run_test` démonte
|
|
|
|
|
# l'écran, et « #totals » n'existe plus après.
|
|
|
|
|
from textual.widgets import Static
|
|
|
|
|
|
|
|
|
|
widget = app.query_one("#totals", Static)
|
|
|
|
|
app.ligne_totaux = str(
|
|
|
|
|
getattr(widget, "_content", "") or widget.render()
|
|
|
|
|
)
|
[ADD] proxmox : un écran pour déployer sur un hôte distant
Le déploiement Proxmox se faisait par vingt questions, l'une après
l'autre, sans jamais voir le plan entier. Il a maintenant le même écran
que QEMU/KVM — catalogue à gauche, plan à droite, totaux dessous — et une
touche pour revenir aux questions. Il n'en redit rien : 690 lignes contre
1 340, tout le reste vient du socle commun.
Ce que Proxmox a en propre est montré AVANT de lancer : le VMID, parce
que l'hôte ne dit « déjà pris » qu'après avoir téléchargé l'image, et
l'adresse qui s'en déduit sur un pont interne. Le stockage et le pont sont
lus sur l'hôte, jamais devinés. L'icône 🗄 distingue l'entrée du menu de
celle qui déploie ici même.
--- EN ---
Deploying on Proxmox meant twenty questions, one after another, never
seeing the whole plan. It now has the same screen as QEMU/KVM — catalogue
left, plan right, totals below — and a key to fall back to the questions.
It repeats none of it: 690 lines against 1,340, everything else comes
from the shared foundation.
What belongs to Proxmox is shown BEFORE launching: the VMID, because the
host only says "already taken" after downloading the image, and the
address derived from it on an internal bridge. Storage and bridge are read
from the host, never guessed. The 🗄 icon tells the menu entry apart from
the one that deploys right here.
Assisted-by: Claude Opus 5
2026-08-23 18:01:28 -04:00
|
|
|
resultat["app"] = app
|
|
|
|
|
|
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
return resultat["app"]
|
|
|
|
|
|
|
|
|
|
def test_the_plan_shows_a_row_per_selected_system(self):
|
|
|
|
|
async def rien(app, pilote):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
app = self._rendu(rien)
|
|
|
|
|
self.assertEqual(len(app.rows), 3)
|
|
|
|
|
|
|
|
|
|
def test_the_head_line_carries_the_vmid_and_the_address(self):
|
|
|
|
|
async def rien(app, pilote):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
app = self._rendu(rien)
|
|
|
|
|
tete = app._row_head(0, app.rows[0])
|
|
|
|
|
self.assertIn("VMID", tete)
|
|
|
|
|
self.assertIn("10.10.10.", tete)
|
|
|
|
|
|
|
|
|
|
def test_an_existing_vm_is_marked_and_gets_no_vmid(self):
|
|
|
|
|
async def rien(app, pilote):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
app = self._rendu(rien)
|
|
|
|
|
deja = [r for r in app.rows if r["state"] == "exists"]
|
|
|
|
|
self.assertEqual(len(deja), 1)
|
|
|
|
|
self.assertNotIn("VMID", app._row_head(1, deja[0]))
|
|
|
|
|
|
|
|
|
|
def test_mounting_does_not_mark_every_row_as_custom(self):
|
|
|
|
|
# Poser « value= » sur un Select fait émettre un Changed : pris pour
|
|
|
|
|
# une saisie, il surchargeait les trois champs de CHAQUE VM et toutes
|
|
|
|
|
# les rangées portaient la marque ✎ avant qu'on ne touche à rien.
|
|
|
|
|
async def rien(app, pilote):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
app = self._rendu(rien)
|
|
|
|
|
self.assertEqual(app.overrides, {})
|
|
|
|
|
self.assertNotIn("✎", app._row_head(0, app.rows[0]))
|
|
|
|
|
|
|
|
|
|
def test_a_lock_survives_a_common_setting(self):
|
|
|
|
|
async def gestes(app, pilote):
|
|
|
|
|
app._set_lock(0, True)
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
app.custom["ram"] = 8192
|
|
|
|
|
app.profile = "custom"
|
|
|
|
|
app._clear_overrides(("ram",))
|
|
|
|
|
app._recompute()
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
|
|
|
|
|
app = self._rendu(gestes)
|
|
|
|
|
self.assertEqual(app.rows[0]["vm"]["ram"], 2048)
|
|
|
|
|
|
|
|
|
|
def test_a_copy_adds_a_vm_with_its_own_vmid(self):
|
|
|
|
|
async def gestes(app, pilote):
|
|
|
|
|
app._add_copy(0, 1)
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
|
|
|
|
|
app = self._rendu(gestes)
|
|
|
|
|
self.assertEqual(len(app.rows), 4)
|
[ADD] todo : dire la place qui reste sous le plan de déploiement
La ligne de totaux annonçait « ~126 G » sans dire sur quoi : la demande
seule ne dit pas si ça rentre, et on l'apprenait au déploiement. Elle dit
maintenant la demande, ce qui reste et la capacité — « ~126 G / 20 G libres
sur 270 G » — et prévient dès que le plan dépasse. Les trois limites (RAM,
disque, cœurs) s'affichent ensemble : n'en montrer qu'une cachait les
autres. Sur Proxmox, la place vient du stockage choisi, que « pvesm status »
donnait déjà.
Deux défauts trouvés en le faisant : la marque de génération, exigée de tous
les widgets, faisait taire chaque réglage commun de l'écran Proxmox, et
« [x1] » disparaissait, lu comme une balise Rich.
--- EN ---
The totals line said "~126 G" without saying out of what: the demand alone
does not tell whether it fits, and one found out at deploy time. It now says
the demand, what is left and the capacity — "~126 G / 20 G free of 270 G" —
and warns as soon as the plan exceeds it. The three limits (RAM, disk,
cores) show together: showing only one hid the others. On Proxmox the room
comes from the chosen storage, which "pvesm status" already gave.
Two defects found on the way: the generation mark, required of every widget,
silenced each common setting on the Proxmox screen, and "[x1]" vanished,
read as a Rich tag.
Assisted-by: Claude Opus 5
2026-08-24 01:11:09 -04:00
|
|
|
vmids = [r["vm"]["vmid"] for r in app.rows if r["state"] != "exists"]
|
[ADD] proxmox : un écran pour déployer sur un hôte distant
Le déploiement Proxmox se faisait par vingt questions, l'une après
l'autre, sans jamais voir le plan entier. Il a maintenant le même écran
que QEMU/KVM — catalogue à gauche, plan à droite, totaux dessous — et une
touche pour revenir aux questions. Il n'en redit rien : 690 lignes contre
1 340, tout le reste vient du socle commun.
Ce que Proxmox a en propre est montré AVANT de lancer : le VMID, parce
que l'hôte ne dit « déjà pris » qu'après avoir téléchargé l'image, et
l'adresse qui s'en déduit sur un pont interne. Le stockage et le pont sont
lus sur l'hôte, jamais devinés. L'icône 🗄 distingue l'entrée du menu de
celle qui déploie ici même.
--- EN ---
Deploying on Proxmox meant twenty questions, one after another, never
seeing the whole plan. It now has the same screen as QEMU/KVM — catalogue
left, plan right, totals below — and a key to fall back to the questions.
It repeats none of it: 690 lines against 1,340, everything else comes
from the shared foundation.
What belongs to Proxmox is shown BEFORE launching: the VMID, because the
host only says "already taken" after downloading the image, and the
address derived from it on an internal bridge. Storage and bridge are read
from the host, never guessed. The 🗄 icon tells the menu entry apart from
the one that deploys right here.
Assisted-by: Claude Opus 5
2026-08-23 18:01:28 -04:00
|
|
|
self.assertEqual(len(vmids), len(set(vmids)))
|
|
|
|
|
|
|
|
|
|
def test_deploying_yields_a_spec_the_engine_can_run(self):
|
|
|
|
|
async def gestes(app, pilote):
|
|
|
|
|
app.action_deploy()
|
|
|
|
|
|
|
|
|
|
app = self._rendu(gestes)
|
|
|
|
|
spec = app.result
|
|
|
|
|
self.assertEqual(len(spec["vms"]), 2)
|
|
|
|
|
self.assertEqual(spec["existing"], ["erplibre-debian-13"])
|
|
|
|
|
self.assertEqual(spec["storage"], "local-lvm")
|
|
|
|
|
self.assertEqual(spec["bridge"], "vmbr0")
|
|
|
|
|
for vm in spec["vms"]:
|
|
|
|
|
self.assertIn("vmid", vm)
|
|
|
|
|
self.assertIn("ip=", vm["ipconfig"])
|
|
|
|
|
self.assertEqual(spec["install"]["branch"], "develop")
|
|
|
|
|
|
[ADD] todo : dire la place qui reste sous le plan de déploiement
La ligne de totaux annonçait « ~126 G » sans dire sur quoi : la demande
seule ne dit pas si ça rentre, et on l'apprenait au déploiement. Elle dit
maintenant la demande, ce qui reste et la capacité — « ~126 G / 20 G libres
sur 270 G » — et prévient dès que le plan dépasse. Les trois limites (RAM,
disque, cœurs) s'affichent ensemble : n'en montrer qu'une cachait les
autres. Sur Proxmox, la place vient du stockage choisi, que « pvesm status »
donnait déjà.
Deux défauts trouvés en le faisant : la marque de génération, exigée de tous
les widgets, faisait taire chaque réglage commun de l'écran Proxmox, et
« [x1] » disparaissait, lu comme une balise Rich.
--- EN ---
The totals line said "~126 G" without saying out of what: the demand alone
does not tell whether it fits, and one found out at deploy time. It now says
the demand, what is left and the capacity — "~126 G / 20 G free of 270 G" —
and warns as soon as the plan exceeds it. The three limits (RAM, disk,
cores) show together: showing only one hid the others. On Proxmox the room
comes from the chosen storage, which "pvesm status" already gave.
Two defects found on the way: the generation mark, required of every widget,
silenced each common setting on the Proxmox screen, and "[x1]" vanished,
read as a Rich tag.
Assisted-by: Claude Opus 5
2026-08-24 01:11:09 -04:00
|
|
|
def _totaux(self, app):
|
|
|
|
|
return app.ligne_totaux
|
|
|
|
|
|
|
|
|
|
def test_the_totals_line_shows_the_room_left_on_the_storage(self):
|
|
|
|
|
async def rien(app, pilote):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
ligne = self._totaux(self._rendu(rien))
|
|
|
|
|
# « pvesm status » donne déjà la place : la demande du plan s'affiche
|
|
|
|
|
# donc à côté d'elle, sans un aller-retour de plus vers l'hôte.
|
|
|
|
|
self.assertIn("/ 90 G", ligne)
|
|
|
|
|
self.assertIn("local-lvm", ligne)
|
|
|
|
|
|
|
|
|
|
def test_changing_the_storage_changes_the_room(self):
|
|
|
|
|
# La marque de génération ne vaut que pour les widgets de RANGÉE :
|
|
|
|
|
# l'exiger des widgets globaux faisait taire tous les réglages
|
|
|
|
|
# communs, stockage compris.
|
|
|
|
|
async def gestes(app, pilote):
|
|
|
|
|
from textual.widgets import Select
|
|
|
|
|
|
|
|
|
|
app.query_one("#f_storage", Select).value = "local"
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
|
|
|
|
|
ligne = self._totaux(self._rendu(gestes))
|
|
|
|
|
self.assertIn("/ 12 G", ligne)
|
|
|
|
|
self.assertIn("local", ligne)
|
|
|
|
|
|
|
|
|
|
def test_a_plan_bigger_than_the_storage_is_flagged(self):
|
|
|
|
|
async def gestes(app, pilote):
|
|
|
|
|
from textual.widgets import Select
|
|
|
|
|
|
|
|
|
|
app.query_one("#f_storage", Select).value = "local"
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
|
|
|
|
|
self.assertIn("⚠", self._totaux(self._rendu(gestes)))
|
|
|
|
|
|
|
|
|
|
def test_a_common_setting_reaches_every_vm(self):
|
|
|
|
|
async def gestes(app, pilote):
|
|
|
|
|
# « value = True » sur le bouton : action_next_button() ne
|
|
|
|
|
# déplace que la surbrillance et n'émet aucun message.
|
|
|
|
|
list(app.query("#f_profile RadioButton"))[2].value = True
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
|
|
|
|
|
app = self._rendu(gestes)
|
|
|
|
|
self.assertEqual(app.profile, "3")
|
|
|
|
|
self.assertTrue(all(r["vm"]["ram"] == 6144 for r in app.rows))
|
|
|
|
|
|
|
|
|
|
def test_the_resource_label_survives_the_markup(self):
|
|
|
|
|
# « [x1] » se faisait manger : Static lit le balisage Rich, et une
|
|
|
|
|
# balise inconnue disparaît avec son contenu.
|
|
|
|
|
async def rien(app, pilote):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
self.assertIn("x1", self._totaux(self._rendu(rien)))
|
|
|
|
|
|
[FIX] qemu : ne pas poser ERPLibre sur une VM Proxmox VE
Choisir « Proxmox VE » comme système, c'est demander qu'il soit installé.
L'invite en ligne le savait ; le formulaire posait « ERPLibre + Odoo 18 »,
lui ajoutait les 5 Go réservés au dépôt et annonçait une cible make dans le
guide de la VM. La règle vit maintenant en un seul endroit, les deux
chemins la lisent, et un choix explicite l'emporte toujours sur elle.
Trois défauts trouvés derrière. Une commande par VM n'était retenue que si
DEUX VM différaient : déployée seule, la VM Proxmox retombait sur la
commande commune. Le disque choisi à la main disparaissait quand rien
n'était installé — 60 G demandés, 20 G créés. Et une taille absente des
préréglages marquait la rangée ✎ dès son montage.
--- EN ---
Choosing "Proxmox VE" as the system means asking for it to be installed.
The command-line prompt knew that; the form set "ERPLibre + Odoo 18", added
the 5 GB meant for the repository and advertised a make target in the VM's
guide. The rule now lives in one place, both paths read it, and an explicit
choice always wins over it.
Three defects behind it. A per-VM command was only used when TWO VMs
differed: deployed alone, the Proxmox VM fell back to the common command.
A hand-picked disk size vanished when nothing was installed — 60 G asked,
20 G created. And a size absent from the presets marked its row ✎ on mount.
Assisted-by: Claude Opus 5
2026-08-24 01:34:10 -04:00
|
|
|
def test_a_nested_proxmox_guest_installs_its_hypervisor(self):
|
|
|
|
|
# Même défaut que sur l'écran QEMU/KVM avant correction : un Proxmox
|
|
|
|
|
# imbriqué recevait ERPLibre et Odoo 18.
|
|
|
|
|
async def gestes(app, pilote):
|
|
|
|
|
from textual.widgets import SelectionList
|
|
|
|
|
|
|
|
|
|
liste = app.query_one(SelectionList)
|
|
|
|
|
liste.select(liste.get_option_at_index(3).value)
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
|
|
|
|
|
app = self._rendu(gestes)
|
|
|
|
|
par = {r["vm"]["distro"]: r for r in app.rows}
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
par["proxmox"]["vm"]["install_cmd"],
|
|
|
|
|
"./script/proxmox/install_proxmox.sh",
|
|
|
|
|
)
|
|
|
|
|
# Et ses voisines gardent le choix commun.
|
|
|
|
|
self.assertEqual(par["ubuntu"]["vm"]["install_cmd"], "")
|
|
|
|
|
# Cinq gigaoctets pour un dépôt qu'elle ne clonera pas.
|
|
|
|
|
self.assertEqual(par["proxmox"]["disk_gb"], 32)
|
|
|
|
|
self.assertEqual(par["ubuntu"]["disk_gb"], 42)
|
|
|
|
|
|
[ADD] proxmox : un écran pour déployer sur un hôte distant
Le déploiement Proxmox se faisait par vingt questions, l'une après
l'autre, sans jamais voir le plan entier. Il a maintenant le même écran
que QEMU/KVM — catalogue à gauche, plan à droite, totaux dessous — et une
touche pour revenir aux questions. Il n'en redit rien : 690 lignes contre
1 340, tout le reste vient du socle commun.
Ce que Proxmox a en propre est montré AVANT de lancer : le VMID, parce
que l'hôte ne dit « déjà pris » qu'après avoir téléchargé l'image, et
l'adresse qui s'en déduit sur un pont interne. Le stockage et le pont sont
lus sur l'hôte, jamais devinés. L'icône 🗄 distingue l'entrée du menu de
celle qui déploie ici même.
--- EN ---
Deploying on Proxmox meant twenty questions, one after another, never
seeing the whole plan. It now has the same screen as QEMU/KVM — catalogue
left, plan right, totals below — and a key to fall back to the questions.
It repeats none of it: 690 lines against 1,340, everything else comes
from the shared foundation.
What belongs to Proxmox is shown BEFORE launching: the VMID, because the
host only says "already taken" after downloading the image, and the
address derived from it on an internal bridge. Storage and bridge are read
from the host, never guessed. The 🗄 icon tells the menu entry apart from
the one that deploys right here.
Assisted-by: Claude Opus 5
2026-08-23 18:01:28 -04:00
|
|
|
def test_text_prompts_are_not_a_cancellation(self):
|
|
|
|
|
# {} n'est pas None : l'appelant distingue « annulé » de
|
|
|
|
|
# « pose-moi les questions à l'ancienne ».
|
|
|
|
|
async def gestes(app, pilote):
|
|
|
|
|
from textual.widgets import Button
|
|
|
|
|
|
|
|
|
|
app.on_button_pressed(
|
|
|
|
|
type("E", (), {"button": Button("x", id="prompts")})()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
app = self._rendu(gestes)
|
|
|
|
|
self.assertEqual(app.result, {})
|
|
|
|
|
|
|
|
|
|
def test_cancelling_yields_nothing(self):
|
|
|
|
|
async def gestes(app, pilote):
|
|
|
|
|
app.action_cancel()
|
|
|
|
|
|
|
|
|
|
self.assertIsNone(self._rendu(gestes).result)
|
|
|
|
|
|
|
|
|
|
|
[ADD] proxmox : créer le pont manquant depuis l'écran
Sans pont, « qm create » est impossible — et l'écran refusait de déployer
« aucun pont sur l'hôte » sans offrir le moindre moyen d'en avoir un. Une
Proxmox installée SUR Debian n'en a jamais : l'ISO en crée un, pas la
procédure sur Debian.
Deux moments, donc. Avant l'écran, la question se pose dans le terminal, où
l'on peut expliquer les deux voies et montrer ce qui s'exécute. Dans l'écran,
le sélecteur porte « ➕ créer un pont interne vmbr0 (10.10.10.1/24) + NAT » :
la création part dans un fil, l'affichage reste vivant, et le pont créé se
sélectionne tout seul. Elle ne demande rien parce qu'un pont interne ne
touche à aucune interface physique ; un pont sur le LAN déplace l'adresse de
l'hôte et coupe la session, donc il reste manuel.
--- EN ---
With no bridge, "qm create" is impossible — and the screen refused to deploy
"no bridge on the host" without offering any way to get one. A Proxmox
installed ON Debian never has one: the ISO creates it, the Debian procedure
does not.
Two moments, then. Before the screen, the question is asked in the terminal,
where both ways can be explained and the commands shown. In the screen, the
selector carries "➕ create an internal vmbr0 (10.10.10.1/24) + NAT": creation
runs in a thread, the display stays alive, and the new bridge selects itself.
It asks nothing because an internal bridge touches no physical NIC; a bridge
on the LAN moves the host's address and cuts the session, so it stays manual.
Assisted-by: Claude Opus 5
2026-08-24 04:59:13 -04:00
|
|
|
@unittest.skipUnless(TEXTUAL, "Textual absent")
|
|
|
|
|
class TestCreerUnPont(unittest.TestCase):
|
|
|
|
|
"""Sans pont, « qm create » est impossible — et l'écran refusait de
|
|
|
|
|
déployer sans offrir le moindre moyen d'en avoir un. Rapporté.
|
|
|
|
|
|
|
|
|
|
Le pont INTERNE se crée depuis l'écran parce qu'il ne touche à aucune
|
|
|
|
|
interface physique : il n'y a rien à faire arbitrer. Un pont sur le LAN
|
|
|
|
|
déplace l'adresse de l'hôte et coupe la session : il reste manuel.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def _ecran(self, fabrique, ponts=()):
|
|
|
|
|
from script.todo.proxmox_deploy_form import (
|
|
|
|
|
CREER_PONT,
|
|
|
|
|
run_proxmox_form,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
ctx = contexte()
|
|
|
|
|
ctx["bridges"] = list(ponts)
|
|
|
|
|
ctx["bridge"] = ponts[0] if ponts else ""
|
|
|
|
|
ctx["make_bridge"] = fabrique
|
|
|
|
|
ctx["internal_bridge"] = ("vmbr0", "10.10.10.1/24")
|
|
|
|
|
vu = {}
|
|
|
|
|
|
|
|
|
|
async def scenario():
|
|
|
|
|
from textual.widgets import Select
|
|
|
|
|
|
|
|
|
|
app = run_proxmox_form(ctx, run_app=False)
|
|
|
|
|
async with app.run_test(size=(200, 55)) as pilote:
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
selecteur = app.query_one("#f_bridge", Select)
|
|
|
|
|
vu["choix_avant"] = [str(o[1]) for o in selecteur._options]
|
|
|
|
|
selecteur.value = CREER_PONT
|
|
|
|
|
for _ in range(30):
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
if vu.get("fait"):
|
|
|
|
|
break
|
|
|
|
|
vu["fait"] = bool(app._ponts) and app._bridge()
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
vu["pont"] = app._bridge()
|
|
|
|
|
vu["choix_apres"] = [str(o[1]) for o in selecteur._options]
|
|
|
|
|
|
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
return vu
|
|
|
|
|
|
|
|
|
|
def test_the_entry_is_offered_when_no_bridge_exists(self):
|
|
|
|
|
vu = self._ecran(lambda: ("vmbr0", ""))
|
|
|
|
|
self.assertIn("__creer_pont__", vu["choix_avant"])
|
|
|
|
|
|
|
|
|
|
def test_choosing_it_creates_the_bridge_and_selects_it(self):
|
|
|
|
|
vu = self._ecran(lambda: ("vmbr0", ""))
|
|
|
|
|
self.assertEqual(vu["pont"], "vmbr0")
|
|
|
|
|
self.assertIn("vmbr0", vu["choix_apres"])
|
|
|
|
|
|
|
|
|
|
def test_a_failure_leaves_no_bridge_selected(self):
|
|
|
|
|
# Laissé sur « créer », le sélecteur ferait déployer une VM sur
|
|
|
|
|
# « __creer_pont__ » — un nom que « qm create » refuserait.
|
|
|
|
|
vu = self._ecran(lambda: ("", "Operation not supported"))
|
|
|
|
|
self.assertEqual(vu["pont"], "")
|
|
|
|
|
|
|
|
|
|
def test_the_entry_stays_offered_when_a_bridge_exists(self):
|
|
|
|
|
# Un hôte avec un seul pont sur le LAN : on peut vouloir un réseau
|
|
|
|
|
# interne pour un parc d'essai.
|
|
|
|
|
vu = self._ecran(lambda: ("vmbr0", ""), ponts=("vmbr9",))
|
|
|
|
|
self.assertIn("__creer_pont__", vu["choix_avant"])
|
|
|
|
|
self.assertIn("vmbr9", vu["choix_avant"])
|
|
|
|
|
|
|
|
|
|
|
[FIX] suivi : pas de poubelle avant d'en être sûr, et mise sur Proxmox
Rapporté : une VM Arch à peine déployée sur Proxmox s'affichait 🗑 dès le
premier tour. « Effacée » est un état TERMINAL — la ligne gèle et ne revient
jamais — et il se déduisait d'UN relevé manquant. Or l'hôte peut être occupé,
la VM en train de naître, le relevé en cache d'avant sa création. On distingue
désormais « l'hôte n'a pas répondu » (on ne sait rien) de « l'hôte a répondu
sans elle » (on compte, trois fois), et la case part de « - » plutôt que d'un
sablier qui affirmerait qu'on attend quelque chose.
L'écran Proxmox n'offrait pas le choix de l'interpréteur Python : il envoyait
donc toujours « automatique », et comme mise n'est jamais installé d'office,
c'était pyenv — qui COMPILE Python depuis le tar.xz. Le choix existe
maintenant des deux côtés, avec le même garde-fou : rien n'est imposé quand
aucune architecture retenue n'est servie par mise.
--- EN ---
Reported: an Arch VM barely deployed on Proxmox showed 🗑 on the very first
pass. "Deleted" is a TERMINAL state — the row freezes and never comes back —
and it was inferred from ONE missing reading. Yet the host may be busy, the VM
may be starting, the reading may be cached from before it existed. We now tell
"the host did not answer" (we know nothing) from "the host answered without
it" (count, three times), and the cell starts at "-" rather than an hourglass
claiming we await something.
The Proxmox screen offered no Python interpreter choice: it therefore always
sent "automatic", and since mise is never installed by default, that meant
pyenv — which COMPILES Python from the tar.xz. The choice now exists on both
sides, with the same guard: nothing is imposed when no selected architecture
is served by mise.
Assisted-by: Claude Opus 5
2026-08-24 07:53:54 -04:00
|
|
|
@unittest.skipUnless(TEXTUAL, "Textual absent")
|
|
|
|
|
class TestLInterpretePython(unittest.TestCase):
|
|
|
|
|
"""L'écran Proxmox n'offrait pas le choix, donc envoyait toujours
|
|
|
|
|
« automatique » — et comme mise n'est jamais installé d'office, c'était
|
|
|
|
|
pyenv, qui COMPILE Python. Rapporté sur une VM Arch : « il utilise le
|
|
|
|
|
tar.xz pour le compiler »."""
|
|
|
|
|
|
|
|
|
|
def _ecran(self, gestes=None, mise_arches=("amd64", "arm64")):
|
|
|
|
|
from script.todo.proxmox_deploy_form import run_proxmox_form
|
|
|
|
|
|
|
|
|
|
ctx = contexte()
|
|
|
|
|
ctx["mise_arches"] = mise_arches
|
|
|
|
|
vu = {}
|
|
|
|
|
|
|
|
|
|
async def scenario():
|
|
|
|
|
from textual.widgets import SelectionList
|
|
|
|
|
|
|
|
|
|
app = run_proxmox_form(ctx, run_app=False)
|
|
|
|
|
async with app.run_test(size=(200, 60)) 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["choix"] = app._python_provider()
|
|
|
|
|
app.action_deploy()
|
|
|
|
|
vu["spec"] = app.result or {}
|
|
|
|
|
|
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
return vu
|
|
|
|
|
|
|
|
|
|
def test_mise_is_offered_by_default(self):
|
|
|
|
|
# Un CPython précompilé plutôt qu'une compilation de trois minutes.
|
|
|
|
|
self.assertEqual(self._ecran()["choix"], "mise")
|
|
|
|
|
|
|
|
|
|
def test_the_choice_reaches_the_spec(self):
|
|
|
|
|
async def gestes(app, pilote):
|
|
|
|
|
list(app.query("#f_python RadioButton"))[1].value = True
|
|
|
|
|
await pilote.pause()
|
|
|
|
|
|
|
|
|
|
vu = self._ecran(gestes)
|
|
|
|
|
self.assertEqual(vu["choix"], "pyenv")
|
|
|
|
|
self.assertEqual(vu["spec"].get("python_provider"), "pyenv")
|
|
|
|
|
|
|
|
|
|
def test_an_arch_mise_does_not_serve_yields_nothing(self):
|
|
|
|
|
# « mise indisponible » ne veut pas dire « l'utilisateur exige
|
|
|
|
|
# pyenv » : un choix explicite écarterait le Python de la distro.
|
|
|
|
|
self.assertEqual(self._ecran(mise_arches=("s390x",))["choix"], "")
|
|
|
|
|
|
|
|
|
|
|
[FIX] proxmox : sh: 1: Syntax error: "(" unexpected
Rapporté. La chaîne, en trois maillons : sur un hôte sans pont, « ip -o link
show type bridge » ne rend RIEN, la sortie ne contient donc que
l'avertissement de ssh sur la clé d'hôte — que le lecteur a pris pour un nom
de pont. « (ED25519) » s'est retrouvé dans « --net0 virtio,bridge=… », enrobé
de « sudo sh -c », et dash a répondu ce que l'utilisateur a lu. Le bruit de
ssh est maintenant retiré à la source, et un pont doit avoir la forme d'un
lien pour en être un.
Éprouvé sur l'hôte réel, VM créée puis détruite : le pont ne montait pas
(ifupdown2 accuse « another instance » quand /run/network manque — un
mensonge), le noyau Debian n'a ni module bridge ni table NAT, et une VM en
adresse fixe n'avait aucun résolveur. Le déploiement écrit désormais un
journal par VM sous ~/.erplibre/proxmox-deploy et en donne le chemin.
--- EN ---
Reported. The chain, in three links: on a host with no bridge, "ip -o link
show type bridge" returns NOTHING, so the output holds only ssh's host-key
warning — which the parser took for a bridge name. "(ED25519)" landed in
"--net0 virtio,bridge=…", wrapped in "sudo sh -c", and dash answered what the
user read. Ssh's noise is now stripped at the source, and a bridge must have
the shape of a link to be one.
Proven on the real host, VM created then destroyed: the bridge would not come
up (ifupdown2 claims "another instance" when /run/network is missing — a lie),
the Debian kernel has neither the bridge module nor the NAT table, and a
statically addressed VM had no resolver at all. Deployment now writes one log
per VM under ~/.erplibre/proxmox-deploy and prints its path.
Assisted-by: Claude Opus 5
2026-08-24 04:17:36 -04:00
|
|
|
class TestLeSuivi(unittest.TestCase):
|
|
|
|
|
"""La case « Suivre l'installation » doit commander quelque chose.
|
|
|
|
|
|
|
|
|
|
Elle ne commandait rien : décochée, le tableau de bord s'ouvrait quand
|
|
|
|
|
même ; cochée sans rien à installer, il ne s'ouvrait jamais. Le suivi
|
|
|
|
|
vient du DÉPLOIEMENT, pas de l'installation — c'est la règle déjà tirée du
|
|
|
|
|
côté QEMU/KVM après le même rapport.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def _apres_creation(self, install, monitor):
|
|
|
|
|
"""Rejoue l'épilogue du déploiement et dit quelle voie a été prise."""
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO
|
|
|
|
|
|
|
|
|
|
todo = TODO.__new__(TODO)
|
|
|
|
|
vus = {}
|
|
|
|
|
todo._qemu_install_erplibre_monitored = lambda *a, **k: vus.setdefault(
|
|
|
|
|
"tableau", a
|
|
|
|
|
)
|
|
|
|
|
todo._qemu_install_erplibre_vm = lambda *a, **k: vus.setdefault(
|
|
|
|
|
"serie", a
|
|
|
|
|
)
|
|
|
|
|
todo._write_ssh_config_entry = lambda *a, **k: None
|
|
|
|
|
todo._ssh_private_key = lambda k: None
|
|
|
|
|
todo._pve_guest_ip = lambda vmid, attente=120: ""
|
|
|
|
|
spec = {
|
|
|
|
|
"host": {"target": "pve1"},
|
|
|
|
|
"vms": [
|
|
|
|
|
{
|
|
|
|
|
"name": "vm-a",
|
|
|
|
|
"vmid": 100,
|
|
|
|
|
"ipconfig": "ip=10.10.10.150/24,gw=10.10.10.1",
|
|
|
|
|
"install_cmd": "",
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
"add_ssh_config": False,
|
|
|
|
|
"user": "erplibre",
|
|
|
|
|
"install": install,
|
|
|
|
|
"monitor": monitor,
|
|
|
|
|
}
|
|
|
|
|
import contextlib
|
|
|
|
|
import io
|
|
|
|
|
|
|
|
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
|
|
|
todo._pve_after_create(spec["host"], spec, ["vm-a"], "")
|
|
|
|
|
return vus
|
|
|
|
|
|
|
|
|
|
def test_ticked_without_anything_to_install_still_opens_it(self):
|
|
|
|
|
# La commande distante regarde alors la VM ARRIVER : c'est justement
|
|
|
|
|
# ce qu'on veut voir sur une VM déployée nue.
|
|
|
|
|
vus = self._apres_creation(install=None, monitor=True)
|
|
|
|
|
self.assertIn("tableau", vus)
|
|
|
|
|
self.assertNotIn("serie", vus)
|
|
|
|
|
|
|
|
|
|
def test_unticked_installs_without_the_dashboard(self):
|
|
|
|
|
vus = self._apres_creation(
|
|
|
|
|
install={"branch": "develop", "cmd": "make x", "label": "X"},
|
|
|
|
|
monitor=False,
|
|
|
|
|
)
|
|
|
|
|
self.assertIn("serie", vus)
|
|
|
|
|
self.assertNotIn("tableau", vus)
|
|
|
|
|
|
|
|
|
|
def test_unticked_and_nothing_to_install_does_nothing(self):
|
|
|
|
|
self.assertEqual(self._apres_creation(install=None, monitor=False), {})
|
|
|
|
|
|
[FIX] proxmox : le rebond est le seul chemin vers la VM
Une VM créée sur l'hôte Proxmox vit derrière lui, sur un pont interne : son
adresse n'est pas routable d'ici, et seule l'entrée ~/.ssh/config avec son
ProxyJump y mène. Décocher « ajouter une entrée » tout en demandant une
installation laissait donc le suivi frapper à une porte qui n'existe pas.
L'entrée est maintenant écrite quand une installation ou le suivi la
réclame, et on le dit. Sans rien à faire dans la VM, le choix est respecté.
Trouvé par l'audit du découpage, pas par l'exécution : c'est un cas que
personne n'avait joué.
--- EN ---
A VM created on the Proxmox host lives behind it, on an internal bridge: its
address is not routable from here, and only the ~/.ssh/config entry with its
ProxyJump leads there. Unticking "add an entry" while asking for an install
therefore left the monitor knocking at a door that does not exist. The entry
is now written whenever an install or the dashboard needs it, and we say so.
With nothing to do inside the VM, the choice stands.
Found by the split's audit, not by running it: nobody had played that case.
Assisted-by: Claude Opus 5
2026-08-24 04:21:45 -04:00
|
|
|
def test_the_ssh_entry_is_written_when_the_install_needs_it(self):
|
|
|
|
|
"""La VM est derrière l'hôte : le rebond de ~/.ssh/config est le SEUL
|
|
|
|
|
chemin. Décoché alors qu'une installation est demandée, le suivi ne
|
|
|
|
|
pouvait pas entrer dans la VM."""
|
|
|
|
|
import contextlib
|
|
|
|
|
import io
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO
|
|
|
|
|
|
|
|
|
|
def essai(add_ssh_config, install, monitor):
|
|
|
|
|
todo = TODO.__new__(TODO)
|
|
|
|
|
ecrites = []
|
|
|
|
|
todo._write_ssh_config_entry = lambda nom, *a, **k: ecrites.append(
|
|
|
|
|
nom
|
|
|
|
|
)
|
|
|
|
|
todo._ssh_private_key = lambda k: None
|
|
|
|
|
todo._pve_guest_ip = lambda vmid, attente=120: ""
|
|
|
|
|
todo._qemu_install_erplibre_monitored = lambda *a, **k: None
|
|
|
|
|
todo._qemu_install_erplibre_vm = lambda *a, **k: None
|
|
|
|
|
spec = {
|
|
|
|
|
"host": {"target": "pve1"},
|
|
|
|
|
"vms": [
|
|
|
|
|
{
|
|
|
|
|
"name": "vm-a",
|
|
|
|
|
"vmid": 100,
|
|
|
|
|
"ipconfig": "ip=10.10.10.150/24,gw=10.10.10.1",
|
|
|
|
|
"install_cmd": "",
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
"add_ssh_config": add_ssh_config,
|
|
|
|
|
"user": "erplibre",
|
|
|
|
|
"install": install,
|
|
|
|
|
"monitor": monitor,
|
|
|
|
|
}
|
|
|
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
|
|
|
todo._pve_after_create(spec["host"], spec, ["vm-a"], "")
|
|
|
|
|
return ecrites
|
|
|
|
|
|
|
|
|
|
cmd = {"branch": "develop", "cmd": "make x", "label": "X"}
|
[FIX] proxmox : l'installation partait sur la mauvaise machine
Rapporté, et c'est le plus grave de la série. Une VM déployée sur Proxmox
sous le nom « erplibre-ubuntu-2604 » — nom déjà porté par un domaine LOCAL —
a vu son installation d'ERPLibre + Odoo partir sur la VM locale. Deux causes
enchaînées : l'entrée ~/.ssh/config volait l'alias de la locale, et le
lanceur détaché ré-résout l'adresse par virsh à chaque tour, qui a répondu
avec le domaine homonyme. Le journal l'écrivait — « → 192.168.123.118 » —
sans que rien n'alerte.
Une VM distante n'est plus ré-résolue : son alias est la seule vérité,
puisqu'il porte le rebond. Et son alias suit la convention des VM imbriquées,
« hôte+vm », le nom court n'étant ajouté que s'il est libre — l'écran le dit.
S'y ajoute le sommaire final qui manquait, à l'image de QEMU/KVM : ce qui
existe, son adresse, sa commande ssh, son journal.
--- EN ---
Reported, and the worst of the series. A VM deployed on Proxmox under the
name "erplibre-ubuntu-2604" — a name already held by a LOCAL domain — had its
ERPLibre + Odoo install land on the local VM. Two chained causes: the
~/.ssh/config entry stole the local one's alias, and the detached launcher
re-resolves the address through virsh on every pass, which answered with the
homonymous domain. The log said so — "→ 192.168.123.118" — with nothing to
raise an alarm.
A remote VM is no longer re-resolved: its alias is the only truth, since it
carries the jump. And its alias follows the nested-VM convention, "host+vm",
the short name being added only when free — the screen says so. Plus the
final summary that was missing, mirroring QEMU/KVM: what exists, its address,
its ssh command, its log.
Assisted-by: Claude Opus 5
2026-08-24 06:37:56 -04:00
|
|
|
# Deux noms : le chaîné « hôte+vm », qui dit où la machine vit, et le
|
|
|
|
|
# nom court quand aucun domaine local ne le porte déjà.
|
|
|
|
|
self.assertEqual(essai(False, cmd, False), [["pve1+vm-a", "vm-a"]])
|
[FIX] proxmox : le rebond est le seul chemin vers la VM
Une VM créée sur l'hôte Proxmox vit derrière lui, sur un pont interne : son
adresse n'est pas routable d'ici, et seule l'entrée ~/.ssh/config avec son
ProxyJump y mène. Décocher « ajouter une entrée » tout en demandant une
installation laissait donc le suivi frapper à une porte qui n'existe pas.
L'entrée est maintenant écrite quand une installation ou le suivi la
réclame, et on le dit. Sans rien à faire dans la VM, le choix est respecté.
Trouvé par l'audit du découpage, pas par l'exécution : c'est un cas que
personne n'avait joué.
--- EN ---
A VM created on the Proxmox host lives behind it, on an internal bridge: its
address is not routable from here, and only the ~/.ssh/config entry with its
ProxyJump leads there. Unticking "add an entry" while asking for an install
therefore left the monitor knocking at a door that does not exist. The entry
is now written whenever an install or the dashboard needs it, and we say so.
With nothing to do inside the VM, the choice stands.
Found by the split's audit, not by running it: nobody had played that case.
Assisted-by: Claude Opus 5
2026-08-24 04:21:45 -04:00
|
|
|
# Décoché, suivi demandé : le suivi entre aussi par le rebond.
|
[FIX] proxmox : l'installation partait sur la mauvaise machine
Rapporté, et c'est le plus grave de la série. Une VM déployée sur Proxmox
sous le nom « erplibre-ubuntu-2604 » — nom déjà porté par un domaine LOCAL —
a vu son installation d'ERPLibre + Odoo partir sur la VM locale. Deux causes
enchaînées : l'entrée ~/.ssh/config volait l'alias de la locale, et le
lanceur détaché ré-résout l'adresse par virsh à chaque tour, qui a répondu
avec le domaine homonyme. Le journal l'écrivait — « → 192.168.123.118 » —
sans que rien n'alerte.
Une VM distante n'est plus ré-résolue : son alias est la seule vérité,
puisqu'il porte le rebond. Et son alias suit la convention des VM imbriquées,
« hôte+vm », le nom court n'étant ajouté que s'il est libre — l'écran le dit.
S'y ajoute le sommaire final qui manquait, à l'image de QEMU/KVM : ce qui
existe, son adresse, sa commande ssh, son journal.
--- EN ---
Reported, and the worst of the series. A VM deployed on Proxmox under the
name "erplibre-ubuntu-2604" — a name already held by a LOCAL domain — had its
ERPLibre + Odoo install land on the local VM. Two chained causes: the
~/.ssh/config entry stole the local one's alias, and the detached launcher
re-resolves the address through virsh on every pass, which answered with the
homonymous domain. The log said so — "→ 192.168.123.118" — with nothing to
raise an alarm.
A remote VM is no longer re-resolved: its alias is the only truth, since it
carries the jump. And its alias follows the nested-VM convention, "host+vm",
the short name being added only when free — the screen says so. Plus the
final summary that was missing, mirroring QEMU/KVM: what exists, its address,
its ssh command, its log.
Assisted-by: Claude Opus 5
2026-08-24 06:37:56 -04:00
|
|
|
self.assertEqual(essai(False, None, True), [["pve1+vm-a", "vm-a"]])
|
[FIX] proxmox : le rebond est le seul chemin vers la VM
Une VM créée sur l'hôte Proxmox vit derrière lui, sur un pont interne : son
adresse n'est pas routable d'ici, et seule l'entrée ~/.ssh/config avec son
ProxyJump y mène. Décocher « ajouter une entrée » tout en demandant une
installation laissait donc le suivi frapper à une porte qui n'existe pas.
L'entrée est maintenant écrite quand une installation ou le suivi la
réclame, et on le dit. Sans rien à faire dans la VM, le choix est respecté.
Trouvé par l'audit du découpage, pas par l'exécution : c'est un cas que
personne n'avait joué.
--- EN ---
A VM created on the Proxmox host lives behind it, on an internal bridge: its
address is not routable from here, and only the ~/.ssh/config entry with its
ProxyJump leads there. Unticking "add an entry" while asking for an install
therefore left the monitor knocking at a door that does not exist. The entry
is now written whenever an install or the dashboard needs it, and we say so.
With nothing to do inside the VM, the choice stands.
Found by the split's audit, not by running it: nobody had played that case.
Assisted-by: Claude Opus 5
2026-08-24 04:21:45 -04:00
|
|
|
# Décoché et rien à faire dans la VM : le choix est respecté.
|
|
|
|
|
self.assertEqual(essai(False, None, False), [])
|
|
|
|
|
# Coché : écrite, évidemment.
|
[FIX] proxmox : l'installation partait sur la mauvaise machine
Rapporté, et c'est le plus grave de la série. Une VM déployée sur Proxmox
sous le nom « erplibre-ubuntu-2604 » — nom déjà porté par un domaine LOCAL —
a vu son installation d'ERPLibre + Odoo partir sur la VM locale. Deux causes
enchaînées : l'entrée ~/.ssh/config volait l'alias de la locale, et le
lanceur détaché ré-résout l'adresse par virsh à chaque tour, qui a répondu
avec le domaine homonyme. Le journal l'écrivait — « → 192.168.123.118 » —
sans que rien n'alerte.
Une VM distante n'est plus ré-résolue : son alias est la seule vérité,
puisqu'il porte le rebond. Et son alias suit la convention des VM imbriquées,
« hôte+vm », le nom court n'étant ajouté que s'il est libre — l'écran le dit.
S'y ajoute le sommaire final qui manquait, à l'image de QEMU/KVM : ce qui
existe, son adresse, sa commande ssh, son journal.
--- EN ---
Reported, and the worst of the series. A VM deployed on Proxmox under the
name "erplibre-ubuntu-2604" — a name already held by a LOCAL domain — had its
ERPLibre + Odoo install land on the local VM. Two chained causes: the
~/.ssh/config entry stole the local one's alias, and the detached launcher
re-resolves the address through virsh on every pass, which answered with the
homonymous domain. The log said so — "→ 192.168.123.118" — with nothing to
raise an alarm.
A remote VM is no longer re-resolved: its alias is the only truth, since it
carries the jump. And its alias follows the nested-VM convention, "host+vm",
the short name being added only when free — the screen says so. Plus the
final summary that was missing, mirroring QEMU/KVM: what exists, its address,
its ssh command, its log.
Assisted-by: Claude Opus 5
2026-08-24 06:37:56 -04:00
|
|
|
self.assertEqual(essai(True, None, False), [["pve1+vm-a", "vm-a"]])
|
|
|
|
|
|
|
|
|
|
def test_a_local_vm_of_the_same_name_keeps_its_alias(self):
|
|
|
|
|
"""Le piège qui a fait installer ERPLibre sur la MAUVAISE machine.
|
|
|
|
|
|
|
|
|
|
Une VM déployée sur Proxmox sous un nom déjà porté par un domaine
|
|
|
|
|
LOCAL volait son alias ~/.ssh/config, et le suivi — qui ré-résolvait
|
|
|
|
|
l'adresse par virsh — partait installer sur la locale."""
|
|
|
|
|
import contextlib
|
|
|
|
|
import io
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO
|
|
|
|
|
|
|
|
|
|
todo = TODO.__new__(TODO)
|
|
|
|
|
ecrites = []
|
|
|
|
|
todo._qemu_list_domains = lambda: ["vm-a"]
|
|
|
|
|
todo._write_ssh_config_entry = lambda noms, *a, **k: ecrites.append(
|
|
|
|
|
noms
|
|
|
|
|
)
|
|
|
|
|
todo._ssh_private_key = lambda k: None
|
|
|
|
|
todo._pve_guest_ip = lambda vmid, attente=120: ""
|
|
|
|
|
vus = {}
|
|
|
|
|
todo._qemu_install_erplibre_monitored = (
|
|
|
|
|
lambda noms, br, ipmap, cmd, **k: vus.update(ipmap=ipmap)
|
|
|
|
|
)
|
|
|
|
|
spec = {
|
|
|
|
|
"host": {"target": "erplibre@pve1"},
|
|
|
|
|
"vms": [
|
|
|
|
|
{
|
|
|
|
|
"name": "vm-a",
|
|
|
|
|
"vmid": 100,
|
|
|
|
|
"ipconfig": "ip=10.10.10.150/24,gw=10.10.10.1",
|
|
|
|
|
"install_cmd": "",
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
"add_ssh_config": True,
|
|
|
|
|
"user": "erplibre",
|
|
|
|
|
"install": None,
|
|
|
|
|
"monitor": True,
|
|
|
|
|
}
|
|
|
|
|
with contextlib.redirect_stdout(io.StringIO()) as sortie:
|
|
|
|
|
todo._pve_after_create(spec["host"], spec, ["vm-a"], "")
|
|
|
|
|
# SEUL le nom chaîné est écrit : l'alias court reste à la VM locale.
|
|
|
|
|
self.assertEqual(ecrites, [["pve1+vm-a"]])
|
|
|
|
|
# Et le suivi passe par ce nom-là, jamais par « vm-a ».
|
|
|
|
|
self.assertEqual(vus["ipmap"], {"vm-a": "pve1+vm-a"})
|
|
|
|
|
self.assertIn("pve1+vm-a", sortie.getvalue())
|
[FIX] proxmox : le rebond est le seul chemin vers la VM
Une VM créée sur l'hôte Proxmox vit derrière lui, sur un pont interne : son
adresse n'est pas routable d'ici, et seule l'entrée ~/.ssh/config avec son
ProxyJump y mène. Décocher « ajouter une entrée » tout en demandant une
installation laissait donc le suivi frapper à une porte qui n'existe pas.
L'entrée est maintenant écrite quand une installation ou le suivi la
réclame, et on le dit. Sans rien à faire dans la VM, le choix est respecté.
Trouvé par l'audit du découpage, pas par l'exécution : c'est un cas que
personne n'avait joué.
--- EN ---
A VM created on the Proxmox host lives behind it, on an internal bridge: its
address is not routable from here, and only the ~/.ssh/config entry with its
ProxyJump leads there. Unticking "add an entry" while asking for an install
therefore left the monitor knocking at a door that does not exist. The entry
is now written whenever an install or the dashboard needs it, and we say so.
With nothing to do inside the VM, the choice stands.
Found by the split's audit, not by running it: nobody had played that case.
Assisted-by: Claude Opus 5
2026-08-24 04:21:45 -04:00
|
|
|
|
[FIX] proxmox : sh: 1: Syntax error: "(" unexpected
Rapporté. La chaîne, en trois maillons : sur un hôte sans pont, « ip -o link
show type bridge » ne rend RIEN, la sortie ne contient donc que
l'avertissement de ssh sur la clé d'hôte — que le lecteur a pris pour un nom
de pont. « (ED25519) » s'est retrouvé dans « --net0 virtio,bridge=… », enrobé
de « sudo sh -c », et dash a répondu ce que l'utilisateur a lu. Le bruit de
ssh est maintenant retiré à la source, et un pont doit avoir la forme d'un
lien pour en être un.
Éprouvé sur l'hôte réel, VM créée puis détruite : le pont ne montait pas
(ifupdown2 accuse « another instance » quand /run/network manque — un
mensonge), le noyau Debian n'a ni module bridge ni table NAT, et une VM en
adresse fixe n'avait aucun résolveur. Le déploiement écrit désormais un
journal par VM sous ~/.erplibre/proxmox-deploy et en donne le chemin.
--- EN ---
Reported. The chain, in three links: on a host with no bridge, "ip -o link
show type bridge" returns NOTHING, so the output holds only ssh's host-key
warning — which the parser took for a bridge name. "(ED25519)" landed in
"--net0 virtio,bridge=…", wrapped in "sudo sh -c", and dash answered what the
user read. Ssh's noise is now stripped at the source, and a bridge must have
the shape of a link to be one.
Proven on the real host, VM created then destroyed: the bridge would not come
up (ifupdown2 claims "another instance" when /run/network is missing — a lie),
the Debian kernel has neither the bridge module nor the NAT table, and a
statically addressed VM had no resolver at all. Deployment now writes one log
per VM under ~/.erplibre/proxmox-deploy and prints its path.
Assisted-by: Claude Opus 5
2026-08-24 04:17:36 -04:00
|
|
|
def test_ticked_with_an_install_opens_it(self):
|
|
|
|
|
vus = self._apres_creation(
|
|
|
|
|
install={"branch": "develop", "cmd": "make x", "label": "X"},
|
|
|
|
|
monitor=True,
|
|
|
|
|
)
|
|
|
|
|
self.assertIn("tableau", vus)
|
|
|
|
|
|
|
|
|
|
|
[ADD] proxmox : un écran pour déployer sur un hôte distant
Le déploiement Proxmox se faisait par vingt questions, l'une après
l'autre, sans jamais voir le plan entier. Il a maintenant le même écran
que QEMU/KVM — catalogue à gauche, plan à droite, totaux dessous — et une
touche pour revenir aux questions. Il n'en redit rien : 690 lignes contre
1 340, tout le reste vient du socle commun.
Ce que Proxmox a en propre est montré AVANT de lancer : le VMID, parce
que l'hôte ne dit « déjà pris » qu'après avoir téléchargé l'image, et
l'adresse qui s'en déduit sur un pont interne. Le stockage et le pont sont
lus sur l'hôte, jamais devinés. L'icône 🗄 distingue l'entrée du menu de
celle qui déploie ici même.
--- EN ---
Deploying on Proxmox meant twenty questions, one after another, never
seeing the whole plan. It now has the same screen as QEMU/KVM — catalogue
left, plan right, totals below — and a key to fall back to the questions.
It repeats none of it: 690 lines against 1,340, everything else comes
from the shared foundation.
What belongs to Proxmox is shown BEFORE launching: the VMID, because the
host only says "already taken" after downloading the image, and the
address derived from it on an internal bridge. Storage and bridge are read
from the host, never guessed. The 🗄 icon tells the menu entry apart from
the one that deploys right here.
Assisted-by: Claude Opus 5
2026-08-23 18:01:28 -04:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main(verbosity=2)
|