[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
|
[FIX] proxmox : le pont NAT s'écrivait avant de savoir si le NAT existe
« Table does not exist » : six lignes d'iptables et « code de retour 1 »,
après avoir déjà posé la strophe dans /etc/network/interfaces. Rien dans ce
bruit ne dit qu'il faut redémarrer.
L'hôte tournait le noyau cloud de Debian, qui est dépouillé de tout netfilter
— aucun module NAT, ni legacy ni nft. Et le cas n'a rien d'exotique : c'est
notre propre install_proxmox.sh qui le produit. Il pose le noyau Proxmox sans
redémarrer, à raison — lancé par ssh, un reboot couperait la session et ferait
passer l'installation pour un échec. Une Proxmox imbriquée fraîchement
installée est donc TOUJOURS dans cet état.
L'avertissement sur le noyau existait déjà, mais à la CONFIRMATION de l'hôte,
et l'hôte est ensuite mémorisé : on revient des jours plus tard créer un pont,
et plus personne ne rappelle rien. Le garde va donc là où la conséquence
tombe, et AVANT toute écriture. Il interroge la table NAT elle-même et non le
NOM du noyau — « -pve » est un indice, pas une preuve — puis nomme le noyau
en cours, celui qui est posé, et la commande qui règle l'affaire.
Le sommaire de déploiement le dit désormais aussi, tant qu'on lit encore
l'écran plutôt qu'au bout d'un journal d'une heure.
--- EN ---
"Table does not exist": six lines of iptables and "exit code 1", after the
stanza had already been written into /etc/network/interfaces. Nothing in that
noise says a reboot is needed.
The host was running Debian's cloud kernel, stripped of all netfilter — no NAT
module, legacy or nft. And the case is not exotic: our own install_proxmox.sh
produces it. It installs the Proxmox kernel without rebooting, rightly — run
over ssh, a reboot would cut the session and make the install look failed. A
freshly installed nested Proxmox is therefore ALWAYS in this state.
The kernel warning already existed, but at host CONFIRMATION, and the host is
then remembered: you come back days later to create a bridge and nothing
reminds you. So the guard moves to where the consequence lands, and BEFORE any
write. It asks the NAT table itself rather than the kernel's NAME — "-pve" is
a hint, not a proof — then names the running kernel, the installed one, and
the command that settles it.
The deployment summary now says it too, while the screen is still being read
rather than at the end of an hour-long log.
Assisted-by: Claude Opus 5
2026-08-25 02:16:54 -04:00
|
|
|
from unittest import mock
|
[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
|
|
|
|
|
|
|
|
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 : six défauts trouvés par un audit, pas à l'usage
Trois autres chemins menaient au 🗑 sur un seul incident, et « effacée » gèle
la ligne pour de bon. Un « virsh list » en échec condamnait TOUT le parc
local. Un statut Proxmox hors des trois attendus — prelaunch, suspended,
internal-error — passait pour une disparition. Et le code de sortie de la
suite distante est celui de son DERNIER maillon : un pvesh en panne se lisait
« l'hôte a répondu sans elle ». Ce qui prouve une réponse, c'est désormais une
liste de ressources analysable.
Le plan annonçait « 25G » quand « qm resize » recevait 20 : la marge
d'ERPLibre se perdait en route, la VM naissait trop petite. « Changer l'état »
choisissait par NOM, or seul le VMID est unique sur un hôte — cocher une VM
en éteignait deux homonymes. L'entrée 13 volait son alias à une VM locale du
même nom. Enfin le déploiement par QUESTIONS avait vieilli seul : il partage
maintenant l'épilogue de l'écran, donc le guide, l'alias protégé, les colonnes
vivantes et le sommaire.
--- EN ---
Three more paths led to 🗑 on a single incident, and "deleted" freezes the row
for good. One failing "virsh list" condemned the WHOLE local fleet. A Proxmox
status outside the three expected ones — prelaunch, suspended, internal-error
— passed for a disappearance. And a remote pipeline's exit code is its LAST
link's: a broken pvesh read as "the host answered without it". Proof of an
answer is now a parsable resource list.
The plan announced "25G" while "qm resize" got 20: ERPLibre's margin was lost
on the way and the VM was born too small. "Change state" selected by NAME,
yet only the VMID is unique on a host — ticking one VM shut down two
namesakes. Menu entry 13 stole its alias from a local VM of the same name.
Finally the QUESTION-driven deployment had aged alone: it now shares the
screen's epilogue — guide, protected alias, live columns and summary.
Assisted-by: Claude Opus 5
2026-08-24 14:15:03 -04:00
|
|
|
class TestLeDisquePromis(unittest.TestCase):
|
|
|
|
|
"""Le plan annonçait « 25G » et « qm resize » recevait 20 G.
|
|
|
|
|
|
|
|
|
|
La voie libvirt ajoute la marge d'ERPLibre à la taille créée ; celle de
|
|
|
|
|
Proxmox la perdait entre l'écran et la commande. La VM naissait cinq
|
|
|
|
|
gigaoctets trop petite pour ce qu'on venait de lui promettre."""
|
|
|
|
|
|
|
|
|
|
def _taille(self, install, cmd_vm=""):
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO
|
|
|
|
|
|
|
|
|
|
todo = TODO.__new__(TODO)
|
|
|
|
|
vm = {"disk": "20G", "install_cmd": cmd_vm}
|
|
|
|
|
return todo._pve_disk_with_margin(vm, {"install": install})
|
|
|
|
|
|
|
|
|
|
def test_the_margin_reaches_the_created_disk(self):
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
self._taille(
|
|
|
|
|
{
|
|
|
|
|
"branch": "develop",
|
|
|
|
|
"cmd": "make install_os && make install_odoo_18",
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
"25G",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_nothing_to_install_means_no_margin(self):
|
|
|
|
|
self.assertEqual(self._taille(None), "20G")
|
|
|
|
|
|
|
|
|
|
def test_a_hypervisor_profile_gets_no_margin(self):
|
|
|
|
|
# Elle est réservée au dépôt ERPLibre, qu'un Proxmox ne clonera pas.
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
self._taille(
|
|
|
|
|
{
|
|
|
|
|
"branch": "develop",
|
|
|
|
|
"cmd": "./script/proxmox/install_proxmox.sh",
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
"20G",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestDeuxVmDuMemeNom(unittest.TestCase):
|
|
|
|
|
"""Sur Proxmox, seul le VMID est unique : deux VM du même hôte peuvent
|
|
|
|
|
porter le même nom. « Changer l'état » les choisissait par NOM — cocher
|
|
|
|
|
l'une éteignait les deux."""
|
|
|
|
|
|
|
|
|
|
def test_selecting_one_twin_takes_only_that_one(self):
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO
|
|
|
|
|
|
|
|
|
|
todo = TODO.__new__(TODO)
|
|
|
|
|
vms = [
|
|
|
|
|
{"vmid": 100, "name": "jumeau", "status": "running"},
|
|
|
|
|
{"vmid": 101, "name": "jumeau", "status": "running"},
|
|
|
|
|
]
|
|
|
|
|
rangs = [str(i) for i in range(1, len(vms) + 1)]
|
|
|
|
|
for choix, attendu in (
|
|
|
|
|
("1", [100]),
|
|
|
|
|
("2", [101]),
|
|
|
|
|
("1,2", [100, 101]),
|
|
|
|
|
):
|
|
|
|
|
voulus = {
|
|
|
|
|
int(r)
|
|
|
|
|
for r in todo._parse_index_selection(choix, rangs)
|
|
|
|
|
if str(r).isdigit()
|
|
|
|
|
}
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
[vm["vmid"] for i, vm in enumerate(vms, 1) if i in voulus],
|
|
|
|
|
attendu,
|
|
|
|
|
choix,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
[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
|
|
|
class TestUnParcMixte(unittest.TestCase):
|
|
|
|
|
"""Le plan porte branche, profil et type PAR RANGÉE — le déploiement
|
|
|
|
|
lisait encore la seule valeur commune.
|
|
|
|
|
|
|
|
|
|
C'est le cas qu'on déploie le plus souvent sur un Proxmox : un
|
|
|
|
|
hyperviseur imbriqué à côté de VM ERPLibre. Une seule VM qui porte sa
|
|
|
|
|
propre valeur suffit à rendre la carte nécessaire — « len(set) > 1 » ne
|
|
|
|
|
l'aurait pas vu, et tout le parc serait retombé sur le commun."""
|
|
|
|
|
|
|
|
|
|
def _capture(self, vms):
|
|
|
|
|
import contextlib
|
|
|
|
|
import io
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO
|
|
|
|
|
|
|
|
|
|
vu = {}
|
|
|
|
|
todo = TODO.__new__(TODO)
|
|
|
|
|
todo._write_ssh_config_entry = lambda *a, **k: None
|
|
|
|
|
todo._ssh_private_key = lambda k: None
|
|
|
|
|
todo._ssh_config_block = lambda nom: {}
|
|
|
|
|
todo._qemu_list_domains = lambda: []
|
|
|
|
|
todo._pve_guest_ip = lambda vmid, attente=120: ""
|
|
|
|
|
todo._pve_write_guide = lambda *a, **k: True
|
|
|
|
|
todo._pve_set_timezone = lambda *a, **k: True
|
|
|
|
|
todo._qemu_import_module = lambda: None
|
|
|
|
|
|
|
|
|
|
def prise(noms, branche, alias, finale, **kw):
|
|
|
|
|
vu.update(branche=branche, finale=finale, kw=kw)
|
|
|
|
|
|
|
|
|
|
todo._qemu_install_erplibre_monitored = prise
|
|
|
|
|
spec = {
|
|
|
|
|
"host": {"target": "pve1"},
|
|
|
|
|
"vms": vms,
|
|
|
|
|
"user": "erplibre",
|
|
|
|
|
"add_ssh_config": True,
|
|
|
|
|
"install": {
|
|
|
|
|
"branch": "develop",
|
|
|
|
|
"cmd": "make install_odoo_18",
|
|
|
|
|
"label": "X",
|
|
|
|
|
},
|
|
|
|
|
"monitor": True,
|
|
|
|
|
"desktop": "",
|
|
|
|
|
}
|
|
|
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
|
|
|
todo._pve_after_create(
|
|
|
|
|
spec["host"], spec, [v["name"] for v in vms], ""
|
|
|
|
|
)
|
|
|
|
|
return vu
|
|
|
|
|
|
|
|
|
|
def _vm(self, nom, **extra):
|
|
|
|
|
base = {
|
|
|
|
|
"name": nom,
|
|
|
|
|
"vmid": 100,
|
|
|
|
|
"ipconfig": "ip=10.10.10.150/24,gw=10.10.10.1",
|
|
|
|
|
"install_cmd": "",
|
|
|
|
|
}
|
|
|
|
|
base.update(extra)
|
|
|
|
|
return base
|
|
|
|
|
|
|
|
|
|
def test_a_single_vm_with_its_own_branch_forces_the_map(self):
|
|
|
|
|
vu = self._capture(
|
|
|
|
|
[
|
|
|
|
|
self._vm("vm-a", branch="master"),
|
|
|
|
|
self._vm("vm-b", vmid=101),
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(vu["branche"], {"vm-a": "master", "vm-b": "develop"})
|
|
|
|
|
|
|
|
|
|
def test_a_uniform_fleet_keeps_the_common_value(self):
|
|
|
|
|
vu = self._capture([self._vm("vm-a"), self._vm("vm-b", vmid=101)])
|
|
|
|
|
self.assertEqual(vu["branche"], "develop")
|
|
|
|
|
|
|
|
|
|
def test_a_per_vm_desktop_reaches_the_install(self):
|
|
|
|
|
vu = self._capture(
|
|
|
|
|
[
|
|
|
|
|
self._vm("vm-a", desktop="gnome"),
|
|
|
|
|
self._vm("vm-b", vmid=101),
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(vu["kw"]["desktop"], {"vm-a": "gnome", "vm-b": ""})
|
|
|
|
|
|
|
|
|
|
def test_a_uniform_fleet_keeps_the_common_desktop(self):
|
|
|
|
|
vu = self._capture([self._vm("vm-a"), self._vm("vm-b", vmid=101)])
|
|
|
|
|
self.assertEqual(vu["kw"]["desktop"], "")
|
|
|
|
|
|
|
|
|
|
|
[ADD] déploiement : la VM clone le dépôt distant, pas ce checkout
« Le problème est revenu » — alors qu'il était corrigé la veille. La VM ne
reçoit pas le checkout d'ici : elle CLONE la branche depuis le dépôt distant.
Tout ce qui tourne dedans — install_proxmox.sh, les scripts d'installation, le
Makefile — vient donc de là.
Vécu deux fois de suite. Le correctif de /etc/hosts était commité ici, absent
du distant : chaque VM déployée ensuite recevait l'ancien script, et le même
défaut revenait à l'identique. Rien ne le disait, et il a fallu comparer les
deux versions du fichier à la main pour comprendre. Soixante-et-onze commits
séparaient les deux.
L'écart est donc dit AVANT de déployer, là où l'on peut encore renoncer : le
nombre, les trois premiers sujets, et « git push ». Sur les deux voies, car
les deux clonent.
Une branche que le distant ne connaît pas n'est pas un écart — c'est une
question qui ne se pose pas. La dire quand même vaudrait un avertissement à
chaque déploiement d'une branche neuve.
--- EN ---
"The problem came back" — though it had been fixed the day before. The VM does
not receive this checkout: it CLONES the branch from the remote. Everything
that runs inside it — install_proxmox.sh, the install scripts, the Makefile —
comes from there.
Twice in a row. The /etc/hosts fix was committed here and absent from the
remote: every VM deployed afterwards got the old script, and the same defect
returned unchanged. Nothing said so, and it took comparing both versions of
the file by hand to understand. Seventy-one commits separated them.
The gap is therefore stated BEFORE deploying, where you can still back out:
the count, the first three subjects, and "git push". On both paths, since both
clone.
A branch the remote does not know is not a gap — it is a question that does
not arise. Saying it anyway would mean a warning on every deployment of a new
branch.
Assisted-by: Claude Opus 5
(cherry picked from commit de27be5e736eb6e9bd01efd292e01c3b2231f91a)
2026-08-25 06:19:21 -04:00
|
|
|
class TestLaVmCloneLeDepotDistant(unittest.TestCase):
|
|
|
|
|
"""« Le problème est revenu » — alors qu'il était corrigé.
|
|
|
|
|
|
|
|
|
|
La VM ne reçoit pas le checkout d'ici : elle CLONE la branche depuis le
|
|
|
|
|
dépôt DISTANT. Tout ce qui tourne dedans — install_proxmox.sh, les
|
|
|
|
|
scripts d'installation, le Makefile — vient donc de là. Un correctif
|
|
|
|
|
commité ici et non poussé lui est invisible.
|
|
|
|
|
|
|
|
|
|
Vécu deux fois de suite : la correction de /etc/hosts était dans le
|
|
|
|
|
checkout depuis la veille, absente du distant, et chaque VM déployée
|
|
|
|
|
ensuite recevait l'ancien script. Il a fallu comparer les deux versions à
|
|
|
|
|
la main pour le voir. Rien ne le disait."""
|
|
|
|
|
|
|
|
|
|
def _todo(self, sortie, code=0):
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO
|
|
|
|
|
|
|
|
|
|
todo = TODO.__new__(TODO)
|
|
|
|
|
faux = mock.Mock(returncode=code, stdout=sortie)
|
|
|
|
|
return todo, faux
|
|
|
|
|
|
|
|
|
|
def test_the_gap_is_counted_and_named(self):
|
|
|
|
|
todo, faux = self._todo(
|
|
|
|
|
"abc1234 [FIX] un correctif\ndef5678 [ADD] autre chose\n"
|
|
|
|
|
)
|
|
|
|
|
with mock.patch("subprocess.run", return_value=faux):
|
|
|
|
|
nombre, sujets = todo._qemu_branch_gap("develop")
|
|
|
|
|
self.assertEqual(nombre, 2)
|
|
|
|
|
self.assertIn("[FIX] un correctif", sujets[0])
|
|
|
|
|
|
|
|
|
|
def test_nothing_to_say_when_the_remote_is_up_to_date(self):
|
|
|
|
|
todo, faux = self._todo("")
|
|
|
|
|
with mock.patch("subprocess.run", return_value=faux):
|
|
|
|
|
self.assertEqual(todo._qemu_branch_gap("develop"), (0, []))
|
|
|
|
|
self.assertEqual(todo._qemu_branch_gap_lines("develop"), [])
|
|
|
|
|
|
|
|
|
|
def test_an_unknown_remote_branch_is_not_a_gap(self):
|
|
|
|
|
# « origin/xyz » inconnu fait échouer git : ce n'est pas un écart à
|
|
|
|
|
# signaler, c'est une question qui ne se pose pas. Le dire quand même
|
|
|
|
|
# serait un avertissement à chaque déploiement d'une branche neuve.
|
|
|
|
|
todo, faux = self._todo("", code=128)
|
|
|
|
|
with mock.patch("subprocess.run", return_value=faux):
|
|
|
|
|
self.assertEqual(todo._qemu_branch_gap("nouvelle"), (0, []))
|
|
|
|
|
|
|
|
|
|
def test_no_branch_asks_nothing(self):
|
|
|
|
|
todo, _faux = self._todo("")
|
|
|
|
|
self.assertEqual(todo._qemu_branch_gap(""), (0, []))
|
|
|
|
|
|
|
|
|
|
def test_the_long_list_is_trimmed_but_counted(self):
|
|
|
|
|
todo, faux = self._todo(
|
|
|
|
|
"\n".join(f"c{i} sujet {i}" for i in range(10))
|
|
|
|
|
)
|
|
|
|
|
with mock.patch("subprocess.run", return_value=faux):
|
|
|
|
|
lignes = todo._qemu_branch_gap_lines("develop", limite=2)
|
|
|
|
|
texte = " ".join(lignes)
|
|
|
|
|
self.assertIn("10", texte, "le nombre TOTAL doit rester lisible")
|
|
|
|
|
self.assertIn("8", texte, "et ce qui n'est pas montré, dit")
|
|
|
|
|
self.assertIn("git push", texte)
|
|
|
|
|
|
|
|
|
|
def test_both_screens_say_it_before_deploying(self):
|
|
|
|
|
# L'avertissement ne vaut que là où on peut encore renoncer.
|
|
|
|
|
import inspect
|
|
|
|
|
|
|
|
|
|
from script.todo.proxmox_menu import ProxmoxMenuMixin
|
|
|
|
|
from script.todo.qemu_deploy import QemuDeployMixin
|
|
|
|
|
|
|
|
|
|
for fn in (
|
|
|
|
|
ProxmoxMenuMixin._pve_confirm_spec,
|
|
|
|
|
QemuDeployMixin._qemu_print_recap,
|
|
|
|
|
):
|
|
|
|
|
with self.subTest(fonction=fn.__name__):
|
|
|
|
|
self.assertIn("_qemu_branch_gap_lines", inspect.getsource(fn))
|
|
|
|
|
|
|
|
|
|
|
[FIX] proxmox : le pont NAT s'écrivait avant de savoir si le NAT existe
« Table does not exist » : six lignes d'iptables et « code de retour 1 »,
après avoir déjà posé la strophe dans /etc/network/interfaces. Rien dans ce
bruit ne dit qu'il faut redémarrer.
L'hôte tournait le noyau cloud de Debian, qui est dépouillé de tout netfilter
— aucun module NAT, ni legacy ni nft. Et le cas n'a rien d'exotique : c'est
notre propre install_proxmox.sh qui le produit. Il pose le noyau Proxmox sans
redémarrer, à raison — lancé par ssh, un reboot couperait la session et ferait
passer l'installation pour un échec. Une Proxmox imbriquée fraîchement
installée est donc TOUJOURS dans cet état.
L'avertissement sur le noyau existait déjà, mais à la CONFIRMATION de l'hôte,
et l'hôte est ensuite mémorisé : on revient des jours plus tard créer un pont,
et plus personne ne rappelle rien. Le garde va donc là où la conséquence
tombe, et AVANT toute écriture. Il interroge la table NAT elle-même et non le
NOM du noyau — « -pve » est un indice, pas une preuve — puis nomme le noyau
en cours, celui qui est posé, et la commande qui règle l'affaire.
Le sommaire de déploiement le dit désormais aussi, tant qu'on lit encore
l'écran plutôt qu'au bout d'un journal d'une heure.
--- EN ---
"Table does not exist": six lines of iptables and "exit code 1", after the
stanza had already been written into /etc/network/interfaces. Nothing in that
noise says a reboot is needed.
The host was running Debian's cloud kernel, stripped of all netfilter — no NAT
module, legacy or nft. And the case is not exotic: our own install_proxmox.sh
produces it. It installs the Proxmox kernel without rebooting, rightly — run
over ssh, a reboot would cut the session and make the install look failed. A
freshly installed nested Proxmox is therefore ALWAYS in this state.
The kernel warning already existed, but at host CONFIRMATION, and the host is
then remembered: you come back days later to create a bridge and nothing
reminds you. So the guard moves to where the consequence lands, and BEFORE any
write. It asks the NAT table itself rather than the kernel's NAME — "-pve" is
a hint, not a proof — then names the running kernel, the installed one, and
the command that settles it.
The deployment summary now says it too, while the screen is still being read
rather than at the end of an hour-long log.
Assisted-by: Claude Opus 5
2026-08-25 02:16:54 -04:00
|
|
|
class TestLePontQuiNeMeneraitNullePart(unittest.TestCase):
|
|
|
|
|
"""Le pont NAT était écrit AVANT qu'on sache si le NAT existe.
|
|
|
|
|
|
|
|
|
|
Résultat rapporté : la strophe posée dans /etc/network/interfaces, le
|
|
|
|
|
pont absent, et six lignes d'iptables qui ne parlent pas de redémarrage.
|
|
|
|
|
L'avertissement sur le noyau existait — mais à la CONFIRMATION de l'hôte,
|
|
|
|
|
et l'hôte est ensuite mémorisé : on revient des jours plus tard créer un
|
|
|
|
|
pont, et plus personne ne rappelle rien."""
|
|
|
|
|
|
|
|
|
|
def _todo(self, sortie):
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO
|
|
|
|
|
|
|
|
|
|
todo = TODO.__new__(TODO)
|
|
|
|
|
vu = []
|
|
|
|
|
todo._pve_host = lambda ask=True: {"target": "pve9", "sudo": ""}
|
|
|
|
|
todo._pve_uplink = lambda: "eth0"
|
|
|
|
|
|
|
|
|
|
def faux_run(host, cmd, timeout=120):
|
|
|
|
|
vu.append(cmd)
|
|
|
|
|
from script.proxmox import proxmox_deploy as pve
|
|
|
|
|
|
|
|
|
|
if cmd == pve.NAT_CHECK_CMD:
|
|
|
|
|
return 0, sortie
|
|
|
|
|
return 0, ""
|
|
|
|
|
|
|
|
|
|
return todo, vu, faux_run
|
|
|
|
|
|
|
|
|
|
def _sortie(self, nat, pve_kernel="7.0.14-14-pve"):
|
|
|
|
|
return (
|
|
|
|
|
f"{'7.0.14-14-pve' if nat else '6.12.101+deb13-cloud-amd64'}\n"
|
|
|
|
|
f"---ERPLIBRE-NAT---\n{'NAT-OK' if nat else 'NAT-KO'}\n"
|
|
|
|
|
f"---ERPLIBRE-PVE-KERNEL---\n{pve_kernel}\n"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_nothing_is_written_when_there_is_no_nat(self):
|
|
|
|
|
todo, vu, faux = self._todo(self._sortie(nat=False))
|
|
|
|
|
with mock.patch("script.proxmox.proxmox_deploy.run", faux):
|
|
|
|
|
nom, raison = todo._pve_make_internal_bridge()
|
|
|
|
|
self.assertEqual(nom, "")
|
|
|
|
|
self.assertTrue(raison)
|
|
|
|
|
# Une seule commande : la sonde. Rien n'a touché au fichier.
|
|
|
|
|
self.assertEqual(len(vu), 1, vu)
|
|
|
|
|
self.assertNotIn(
|
|
|
|
|
"interfaces", " ".join(vu), "la strophe ne doit pas être écrite"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_the_reason_names_the_kernel_to_boot(self):
|
|
|
|
|
todo, _vu, faux = self._todo(self._sortie(nat=False))
|
|
|
|
|
with mock.patch("script.proxmox.proxmox_deploy.run", faux):
|
|
|
|
|
ok, lignes = todo._pve_nat_ready({"target": "pve9", "sudo": ""})
|
|
|
|
|
self.assertFalse(ok)
|
|
|
|
|
texte = " ".join(lignes)
|
|
|
|
|
self.assertIn("6.12.101+deb13-cloud-amd64", texte)
|
|
|
|
|
self.assertIn("7.0.14-14-pve", texte)
|
|
|
|
|
self.assertIn("reboot", texte)
|
|
|
|
|
|
|
|
|
|
def test_an_unfinished_install_says_so_instead(self):
|
|
|
|
|
todo, _vu, faux = self._todo(self._sortie(nat=False, pve_kernel=""))
|
|
|
|
|
with mock.patch("script.proxmox.proxmox_deploy.run", faux):
|
|
|
|
|
_ok, lignes = todo._pve_nat_ready({"target": "pve9", "sudo": ""})
|
|
|
|
|
texte = " ".join(lignes)
|
|
|
|
|
self.assertNotIn("reboot", texte, "rien à redémarrer, rien de posé")
|
|
|
|
|
|
|
|
|
|
def test_a_working_host_goes_through(self):
|
|
|
|
|
todo, vu, faux = self._todo(self._sortie(nat=True))
|
|
|
|
|
with mock.patch("script.proxmox.proxmox_deploy.run", faux):
|
|
|
|
|
todo._pve_make_internal_bridge()
|
|
|
|
|
self.assertGreater(len(vu), 1, "la création doit suivre la sonde")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestUneProxmoxImbriqueeDoitRedemarrer(unittest.TestCase):
|
|
|
|
|
"""Le sommaire ne disait pas qu'une VM qui vient de recevoir Proxmox
|
|
|
|
|
tourne encore le noyau de son image cloud.
|
|
|
|
|
|
|
|
|
|
On le redécouvrait des jours plus tard, en créant un pont, devant six
|
|
|
|
|
lignes d'iptables."""
|
|
|
|
|
|
|
|
|
|
def _juge(self, vm, commun=""):
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO
|
|
|
|
|
|
|
|
|
|
return TODO._pve_installs_proxmox(
|
|
|
|
|
vm, {"install": {"cmd": commun}} if commun else {}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_a_vm_that_gets_the_hypervisor(self):
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
self._juge({"install_cmd": "./script/proxmox/install_proxmox.sh"})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_through_the_common_choice_too(self):
|
|
|
|
|
self.assertTrue(self._juge({}, "./script/proxmox/install_proxmox.sh"))
|
|
|
|
|
|
|
|
|
|
def test_an_erplibre_vm_is_left_alone(self):
|
|
|
|
|
self.assertFalse(
|
|
|
|
|
self._juge({}, "make install_os && make install_odoo_18")
|
|
|
|
|
)
|
|
|
|
|
|
[ADD] suivi : le redémarrage fait partie de l'installation de Proxmox
Proxmox VE n'existe qu'après un redémarrage : tant que la VM tourne le noyau
de son image cloud, elle n'a aucun module netfilter — ni pont NAT, ni invité.
install_proxmox.sh pose le noyau puis s'arrête, à raison, car lancé par ssh un
reboot couperait sa session et ferait passer l'installation pour un échec. On
le découvrait donc des jours plus tard, en créant un pont.
Le redémarrage revient à l'enveloppe de lancement, qui tourne sur NOTRE
machine et survit à celui de la VM : installation, reboot, attente, puis
vérification du noyau. Le ✅ ne s'écrit qu'après, et il veut donc dire
« hyperviseur utilisable ».
Trois choix méritent d'être dits. On ne redémarre qu'après un SUCCÈS —
redémarrer après un échec effacerait la seule machine sur laquelle on pouvait
chercher. On n'attend pas que ssh « revienne » mais que « uname -r » porte le
motif attendu : sshd répond encore une seconde ou deux après l'ordre, et on
lirait l'ancien noyau en croyant avoir la réponse. Et l'absence du noyau
attendu est un vrai ÉCHEC, pas un avertissement.
Le shell est exécuté par les tests, ssh bouchonné, dans les quatre cas — dont
celui où les deux premières lectures rendent l'ancien noyau. Un garde qu'on ne
sait pas éprouver s'ouvre le jour où il casse.
La note du sommaire ne paraît plus que sans suivi, où rien ne redémarre :
réclamer un redémarrage déjà fait est une consigne fausse.
--- EN ---
Proxmox VE only exists after a reboot: while the VM runs its cloud image's
kernel it has no netfilter module — no NAT bridge, no guest.
install_proxmox.sh installs the kernel then stops, rightly, since run over ssh
a reboot would cut its own session and make the install look failed. So you
found out days later, when creating a bridge.
The reboot moves to the launch wrapper, which runs on OUR machine and survives
the VM's: install, reboot, wait, then verify the kernel. The ✅ is written only
after, and therefore means "usable hypervisor".
Three choices worth stating. We reboot only after SUCCESS — rebooting after a
failure would wipe the one machine you could investigate. We do not wait for
ssh to "come back" but for "uname -r" to carry the expected pattern: sshd
answers for another second or two after the order, and we would read the old
kernel believing we had the answer. And a missing expected kernel is a real
FAILURE, not a warning.
The shell is executed by the tests, ssh stubbed, in all four cases — including
the one where the first two reads return the old kernel. A guard you cannot
exercise opens the day it breaks.
The summary note now appears only without monitoring, where nothing reboots:
asking for a reboot already done is a false instruction.
Assisted-by: Claude Opus 5
2026-08-25 02:49:02 -04:00
|
|
|
def test_the_note_only_shows_when_nothing_reboots_it(self):
|
|
|
|
|
"""Avec suivi, l'enveloppe redémarre elle-même : réclamer un
|
|
|
|
|
redémarrage déjà fait est une consigne fausse. Sans suivi, la voie en
|
|
|
|
|
série s'arrête à la fin du script, et la note est la seule chose qui
|
|
|
|
|
dit que l'hyperviseur n'est pas encore utilisable."""
|
|
|
|
|
import contextlib
|
|
|
|
|
import io
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO
|
|
|
|
|
|
|
|
|
|
def sommaire(monitor):
|
|
|
|
|
todo = TODO.__new__(TODO)
|
|
|
|
|
spec = {
|
|
|
|
|
"vms": [{"name": "pve-imbrique"}],
|
|
|
|
|
"install": {
|
|
|
|
|
"cmd": "./script/proxmox/install_proxmox.sh",
|
|
|
|
|
"label": "Proxmox VE",
|
|
|
|
|
"branch": "develop",
|
|
|
|
|
},
|
|
|
|
|
"monitor": monitor,
|
|
|
|
|
"storage": "local",
|
|
|
|
|
"bridge": "vmbr0",
|
|
|
|
|
}
|
|
|
|
|
vm = dict(spec["vms"][0], alias="pve9+pve-imbrique", vmid=102)
|
|
|
|
|
tampon = io.StringIO()
|
|
|
|
|
with contextlib.redirect_stdout(tampon):
|
|
|
|
|
todo._pve_print_summary(spec, [vm], "")
|
|
|
|
|
return tampon.getvalue()
|
|
|
|
|
|
|
|
|
|
self.assertIn("reboot", sommaire(monitor=False))
|
|
|
|
|
self.assertNotIn("reboot", sommaire(monitor=True))
|
|
|
|
|
|
[FIX] proxmox : le pont NAT s'écrivait avant de savoir si le NAT existe
« Table does not exist » : six lignes d'iptables et « code de retour 1 »,
après avoir déjà posé la strophe dans /etc/network/interfaces. Rien dans ce
bruit ne dit qu'il faut redémarrer.
L'hôte tournait le noyau cloud de Debian, qui est dépouillé de tout netfilter
— aucun module NAT, ni legacy ni nft. Et le cas n'a rien d'exotique : c'est
notre propre install_proxmox.sh qui le produit. Il pose le noyau Proxmox sans
redémarrer, à raison — lancé par ssh, un reboot couperait la session et ferait
passer l'installation pour un échec. Une Proxmox imbriquée fraîchement
installée est donc TOUJOURS dans cet état.
L'avertissement sur le noyau existait déjà, mais à la CONFIRMATION de l'hôte,
et l'hôte est ensuite mémorisé : on revient des jours plus tard créer un pont,
et plus personne ne rappelle rien. Le garde va donc là où la conséquence
tombe, et AVANT toute écriture. Il interroge la table NAT elle-même et non le
NOM du noyau — « -pve » est un indice, pas une preuve — puis nomme le noyau
en cours, celui qui est posé, et la commande qui règle l'affaire.
Le sommaire de déploiement le dit désormais aussi, tant qu'on lit encore
l'écran plutôt qu'au bout d'un journal d'une heure.
--- EN ---
"Table does not exist": six lines of iptables and "exit code 1", after the
stanza had already been written into /etc/network/interfaces. Nothing in that
noise says a reboot is needed.
The host was running Debian's cloud kernel, stripped of all netfilter — no NAT
module, legacy or nft. And the case is not exotic: our own install_proxmox.sh
produces it. It installs the Proxmox kernel without rebooting, rightly — run
over ssh, a reboot would cut the session and make the install look failed. A
freshly installed nested Proxmox is therefore ALWAYS in this state.
The kernel warning already existed, but at host CONFIRMATION, and the host is
then remembered: you come back days later to create a bridge and nothing
reminds you. So the guard moves to where the consequence lands, and BEFORE any
write. It asks the NAT table itself rather than the kernel's NAME — "-pve" is
a hint, not a proof — then names the running kernel, the installed one, and
the command that settles it.
The deployment summary now says it too, while the screen is still being read
rather than at the end of an hour-long log.
Assisted-by: Claude Opus 5
2026-08-25 02:16:54 -04:00
|
|
|
def test_a_vm_of_its_own_overrides_the_common_choice(self):
|
|
|
|
|
# Parc mixte : la commande de la VM l'emporte sur celle du parc.
|
|
|
|
|
self.assertFalse(
|
|
|
|
|
self._juge(
|
|
|
|
|
{"install_cmd": "make install_odoo_18"},
|
|
|
|
|
"./script/proxmox/install_proxmox.sh",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
[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
|
|
|
class TestLEcranDUneVmProxmox(unittest.TestCase):
|
|
|
|
|
"""« Console de l'hyperviseur » conseillait des commandes virsh sur une
|
|
|
|
|
machine qui n'a pas libvirt.
|
|
|
|
|
|
|
|
|
|
Le tunnel lit le port VNC par « virsh vncdisplay » sur l'hyperviseur. Un
|
|
|
|
|
Proxmox VE n'a pas de libvirt : la commande échoue, et l'absence de port
|
|
|
|
|
était lue « écran fermé ». On imprimait alors « sudo virsh edit » — sur un
|
|
|
|
|
hôte où le binaire n'existe pas. Ce n'est pas un écran fermé, c'est la
|
|
|
|
|
mauvaise question : Proxmox sert son écran par un ticket, sur son
|
|
|
|
|
interface web."""
|
|
|
|
|
|
|
|
|
|
def _sortie(self, qm_present, port=0):
|
|
|
|
|
import contextlib
|
|
|
|
|
import io
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO
|
|
|
|
|
|
|
|
|
|
todo = TODO.__new__(TODO)
|
|
|
|
|
todo._ssh_proxyjump = lambda nom: "pve9"
|
|
|
|
|
todo._qemu_vnc_port = staticmethod(lambda d, j="": port)
|
|
|
|
|
todo._hypervisor_is_proxmox = lambda jump: qm_present
|
|
|
|
|
tampon = io.StringIO()
|
|
|
|
|
with contextlib.redirect_stdout(tampon):
|
|
|
|
|
todo._qemu_console_tunnel("pve9+vm-a", "ssh_config")
|
|
|
|
|
return tampon.getvalue()
|
|
|
|
|
|
|
|
|
|
def test_a_proxmox_host_is_never_told_to_run_virsh(self):
|
|
|
|
|
sortie = self._sortie(qm_present=True)
|
|
|
|
|
self.assertNotIn("virsh", sortie)
|
|
|
|
|
self.assertIn("qm terminal", sortie)
|
|
|
|
|
self.assertIn("8006", sortie, "l'interface web est le second chemin")
|
|
|
|
|
|
|
|
|
|
def test_a_libvirt_host_keeps_its_repair_commands(self):
|
|
|
|
|
# La voie libvirt ne régresse pas : sans port, ses commandes de
|
|
|
|
|
# réparation restent la bonne réponse.
|
|
|
|
|
sortie = self._sortie(qm_present=False)
|
|
|
|
|
self.assertIn("virsh edit", sortie)
|
|
|
|
|
|
|
|
|
|
def test_a_working_vnc_port_still_wins(self):
|
|
|
|
|
# La sonde ne doit pas s'exécuter quand il y a un port : ce serait un
|
|
|
|
|
# aller-retour ssh pour rien.
|
|
|
|
|
sortie = self._sortie(qm_present=True, port=5901)
|
|
|
|
|
self.assertIn("-L 5901:127.0.0.1:5901", sortie)
|
|
|
|
|
self.assertNotIn("qm terminal", sortie)
|
|
|
|
|
|
|
|
|
|
|
[FIX] proxmox : un seul nom par entrée ~/.ssh/config, et le bon
L'entrée portait deux noms sur sa ligne « Host » — le chaîné « hôte+vm » et
le court : « Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest ». Le second est un doublon dès que le premier suffit.
ssh n'a besoin que d'un nom ; le doubler n'ajoute qu'une façon de plus
d'écrire la même adresse. Rapporté.
Un seul, donc, et choisi : le nom court quand il est LIBRE, c'est celui qu'on
tape ; le chaîné quand il désignerait une autre machine — une VM locale
homonyme, ou la VM d'un autre hôte Proxmox. Ce qui a forcé le changement est
nommé à l'écran plutôt que laissé en surprise.
« Pris » se juge sur le ProxyJump du bloc et non sur sa seule présence. Le
test l'a montré avant l'usage : notre propre entrée, réécrite à chaque
déploiement, se prenait pour une rivale et le nom basculait d'une fois sur
l'autre.
--- EN ---
The entry carried two names on its "Host" line — the chained "host+vm" and
the short one: "Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest". The second is redundant as soon as the first is
enough. ssh needs one name; doubling it only adds another way to write the
same address. Reported.
One name then, and a chosen one: the short one while it is FREE, since that
is what you type; the chained one when it would point at another machine — a
local VM of the same name, or another Proxmox host's VM. Whatever forced the
change is named on screen rather than left as a surprise.
"Taken" is judged on the block's ProxyJump, not on its mere presence. The
test showed it before use: our own entry, rewritten at every deployment, took
itself for a rival and the name flipped from one run to the next.
Assisted-by: Claude Opus 5
2026-08-24 22:55:46 -04:00
|
|
|
class TestUnSeulNomDansSshConfig(unittest.TestCase):
|
[FIX] suivi : un relevé Proxmox jeté, et deux lignes qui montraient une autre machine
Sur trois VM d'un même Proxmox, une seule avait ses colonnes vides — et les
deux autres montraient les chiffres d'une AUTRE machine. Deux fautes, dont
une était le miroir d'un correctif précédent.
Le code de sortie de la suite distante est celui de son DERNIER maillon, la
sonde Odoo. Tant qu'Odoo n'écoute pas — c'est-à-dire pendant TOUTE
l'installation, précisément quand on regarde — la boucle finit en échec et le
relevé, parfait, était jeté. On avait corrigé l'erreur inverse, un code 0 pris
pour une réponse ; exiger 0 était la même faute retournée. Seule une liste de
ressources analysable prouve une réponse.
Pendant ce temps, « virsh domstats » indexe par NOM, et un nom se partage :
les deux VM qui avaient un homonyme LOCAL affichaient ses chiffres. Mesuré —
1,5 Gio de RAM sur 12 et 58 Gio de disque sur 65, quand la vraie tournait avec
3 Gio et 25. Les relevés locaux d'une VM qui vit ailleurs sont donc retirés
AVANT d'ajouter ceux de l'hôte : un hôte muet laisse la colonne VIDE, ce qui
est vrai. Une colonne vide se remarque ; une colonne juste et fausse, non.
L'alias enfin. Prendre le nom court quand il se trouvait libre donnait un parc
incohérent : sur ce même déploiement, deux VM ont reçu « hôte+vm » — leurs
noms étaient pris par des domaines locaux — et la troisième son nom court. Une
convention qui dépend de ce qui traîne dans le fichier n'est pas une
convention. Le nom chaîné est systématique.
--- EN ---
Of three VMs on one Proxmox, only one had empty columns — and the other two
showed ANOTHER machine's figures. Two defects, one the mirror of an earlier
fix.
A remote pipeline's exit code is its LAST link's, the Odoo probe. While Odoo
is not listening — that is, during the WHOLE install, exactly when you are
watching — the loop ends in failure and the reading, perfectly good, was
thrown away. We had fixed the opposite error, a 0 taken for an answer;
demanding 0 was the same mistake reversed. Only a parsable resource list
proves an answer.
Meanwhile "virsh domstats" indexes by NAME, and a name is shared: the two VMs
with a LOCAL namesake displayed its figures. Measured — 1.5 GiB of RAM out of
12 and 58 GiB of disk out of 65, while the real one ran on 3 GiB and 25. Local
readings for a VM that lives elsewhere are therefore dropped BEFORE the host's
are added: a silent host leaves the column EMPTY, which is true. An empty
column gets noticed; a plausible wrong one does not.
The alias, finally. Taking the short name while it happened to be free gave an
inconsistent fleet: in that same deployment two VMs got "host+vm" — their
names were held by local domains — and the third its short name. A convention
that depends on what happens to sit in the file is not a convention. The
chained name is now systematic.
Assisted-by: Claude Opus 5
2026-08-25 00:30:54 -04:00
|
|
|
"""L'entrée portait DEUX noms sur sa ligne « Host », puis le mauvais.
|
[FIX] proxmox : un seul nom par entrée ~/.ssh/config, et le bon
L'entrée portait deux noms sur sa ligne « Host » — le chaîné « hôte+vm » et
le court : « Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest ». Le second est un doublon dès que le premier suffit.
ssh n'a besoin que d'un nom ; le doubler n'ajoute qu'une façon de plus
d'écrire la même adresse. Rapporté.
Un seul, donc, et choisi : le nom court quand il est LIBRE, c'est celui qu'on
tape ; le chaîné quand il désignerait une autre machine — une VM locale
homonyme, ou la VM d'un autre hôte Proxmox. Ce qui a forcé le changement est
nommé à l'écran plutôt que laissé en surprise.
« Pris » se juge sur le ProxyJump du bloc et non sur sa seule présence. Le
test l'a montré avant l'usage : notre propre entrée, réécrite à chaque
déploiement, se prenait pour une rivale et le nom basculait d'une fois sur
l'autre.
--- EN ---
The entry carried two names on its "Host" line — the chained "host+vm" and
the short one: "Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest". The second is redundant as soon as the first is
enough. ssh needs one name; doubling it only adds another way to write the
same address. Reported.
One name then, and a chosen one: the short one while it is FREE, since that
is what you type; the chained one when it would point at another machine — a
local VM of the same name, or another Proxmox host's VM. Whatever forced the
change is named on screen rather than left as a surprise.
"Taken" is judged on the block's ProxyJump, not on its mere presence. The
test showed it before use: our own entry, rewritten at every deployment, took
itself for a rival and the name flipped from one run to the next.
Assisted-by: Claude Opus 5
2026-08-24 22:55:46 -04:00
|
|
|
|
[FIX] suivi : un relevé Proxmox jeté, et deux lignes qui montraient une autre machine
Sur trois VM d'un même Proxmox, une seule avait ses colonnes vides — et les
deux autres montraient les chiffres d'une AUTRE machine. Deux fautes, dont
une était le miroir d'un correctif précédent.
Le code de sortie de la suite distante est celui de son DERNIER maillon, la
sonde Odoo. Tant qu'Odoo n'écoute pas — c'est-à-dire pendant TOUTE
l'installation, précisément quand on regarde — la boucle finit en échec et le
relevé, parfait, était jeté. On avait corrigé l'erreur inverse, un code 0 pris
pour une réponse ; exiger 0 était la même faute retournée. Seule une liste de
ressources analysable prouve une réponse.
Pendant ce temps, « virsh domstats » indexe par NOM, et un nom se partage :
les deux VM qui avaient un homonyme LOCAL affichaient ses chiffres. Mesuré —
1,5 Gio de RAM sur 12 et 58 Gio de disque sur 65, quand la vraie tournait avec
3 Gio et 25. Les relevés locaux d'une VM qui vit ailleurs sont donc retirés
AVANT d'ajouter ceux de l'hôte : un hôte muet laisse la colonne VIDE, ce qui
est vrai. Une colonne vide se remarque ; une colonne juste et fausse, non.
L'alias enfin. Prendre le nom court quand il se trouvait libre donnait un parc
incohérent : sur ce même déploiement, deux VM ont reçu « hôte+vm » — leurs
noms étaient pris par des domaines locaux — et la troisième son nom court. Une
convention qui dépend de ce qui traîne dans le fichier n'est pas une
convention. Le nom chaîné est systématique.
--- EN ---
Of three VMs on one Proxmox, only one had empty columns — and the other two
showed ANOTHER machine's figures. Two defects, one the mirror of an earlier
fix.
A remote pipeline's exit code is its LAST link's, the Odoo probe. While Odoo
is not listening — that is, during the WHOLE install, exactly when you are
watching — the loop ends in failure and the reading, perfectly good, was
thrown away. We had fixed the opposite error, a 0 taken for an answer;
demanding 0 was the same mistake reversed. Only a parsable resource list
proves an answer.
Meanwhile "virsh domstats" indexes by NAME, and a name is shared: the two VMs
with a LOCAL namesake displayed its figures. Measured — 1.5 GiB of RAM out of
12 and 58 GiB of disk out of 65, while the real one ran on 3 GiB and 25. Local
readings for a VM that lives elsewhere are therefore dropped BEFORE the host's
are added: a silent host leaves the column EMPTY, which is true. An empty
column gets noticed; a plausible wrong one does not.
The alias, finally. Taking the short name while it happened to be free gave an
inconsistent fleet: in that same deployment two VMs got "host+vm" — their
names were held by local domains — and the third its short name. A convention
that depends on what happens to sit in the file is not a convention. The
chained name is now systematic.
Assisted-by: Claude Opus 5
2026-08-25 00:30:54 -04:00
|
|
|
D'abord le doublon : « Host erplibre-proxmox-9+erplibre-arch-latest
|
|
|
|
|
erplibre-arch-latest ». ssh n'a besoin que d'un nom, et le second
|
|
|
|
|
n'ajoutait qu'une façon de plus d'écrire la même adresse.
|
[FIX] proxmox : un seul nom par entrée ~/.ssh/config, et le bon
L'entrée portait deux noms sur sa ligne « Host » — le chaîné « hôte+vm » et
le court : « Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest ». Le second est un doublon dès que le premier suffit.
ssh n'a besoin que d'un nom ; le doubler n'ajoute qu'une façon de plus
d'écrire la même adresse. Rapporté.
Un seul, donc, et choisi : le nom court quand il est LIBRE, c'est celui qu'on
tape ; le chaîné quand il désignerait une autre machine — une VM locale
homonyme, ou la VM d'un autre hôte Proxmox. Ce qui a forcé le changement est
nommé à l'écran plutôt que laissé en surprise.
« Pris » se juge sur le ProxyJump du bloc et non sur sa seule présence. Le
test l'a montré avant l'usage : notre propre entrée, réécrite à chaque
déploiement, se prenait pour une rivale et le nom basculait d'une fois sur
l'autre.
--- EN ---
The entry carried two names on its "Host" line — the chained "host+vm" and
the short one: "Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest". The second is redundant as soon as the first is
enough. ssh needs one name; doubling it only adds another way to write the
same address. Reported.
One name then, and a chosen one: the short one while it is FREE, since that
is what you type; the chained one when it would point at another machine — a
local VM of the same name, or another Proxmox host's VM. Whatever forced the
change is named on screen rather than left as a surprise.
"Taken" is judged on the block's ProxyJump, not on its mere presence. The
test showed it before use: our own entry, rewritten at every deployment, took
itself for a rival and the name flipped from one run to the next.
Assisted-by: Claude Opus 5
2026-08-24 22:55:46 -04:00
|
|
|
|
[FIX] suivi : un relevé Proxmox jeté, et deux lignes qui montraient une autre machine
Sur trois VM d'un même Proxmox, une seule avait ses colonnes vides — et les
deux autres montraient les chiffres d'une AUTRE machine. Deux fautes, dont
une était le miroir d'un correctif précédent.
Le code de sortie de la suite distante est celui de son DERNIER maillon, la
sonde Odoo. Tant qu'Odoo n'écoute pas — c'est-à-dire pendant TOUTE
l'installation, précisément quand on regarde — la boucle finit en échec et le
relevé, parfait, était jeté. On avait corrigé l'erreur inverse, un code 0 pris
pour une réponse ; exiger 0 était la même faute retournée. Seule une liste de
ressources analysable prouve une réponse.
Pendant ce temps, « virsh domstats » indexe par NOM, et un nom se partage :
les deux VM qui avaient un homonyme LOCAL affichaient ses chiffres. Mesuré —
1,5 Gio de RAM sur 12 et 58 Gio de disque sur 65, quand la vraie tournait avec
3 Gio et 25. Les relevés locaux d'une VM qui vit ailleurs sont donc retirés
AVANT d'ajouter ceux de l'hôte : un hôte muet laisse la colonne VIDE, ce qui
est vrai. Une colonne vide se remarque ; une colonne juste et fausse, non.
L'alias enfin. Prendre le nom court quand il se trouvait libre donnait un parc
incohérent : sur ce même déploiement, deux VM ont reçu « hôte+vm » — leurs
noms étaient pris par des domaines locaux — et la troisième son nom court. Une
convention qui dépend de ce qui traîne dans le fichier n'est pas une
convention. Le nom chaîné est systématique.
--- EN ---
Of three VMs on one Proxmox, only one had empty columns — and the other two
showed ANOTHER machine's figures. Two defects, one the mirror of an earlier
fix.
A remote pipeline's exit code is its LAST link's, the Odoo probe. While Odoo
is not listening — that is, during the WHOLE install, exactly when you are
watching — the loop ends in failure and the reading, perfectly good, was
thrown away. We had fixed the opposite error, a 0 taken for an answer;
demanding 0 was the same mistake reversed. Only a parsable resource list
proves an answer.
Meanwhile "virsh domstats" indexes by NAME, and a name is shared: the two VMs
with a LOCAL namesake displayed its figures. Measured — 1.5 GiB of RAM out of
12 and 58 GiB of disk out of 65, while the real one ran on 3 GiB and 25. Local
readings for a VM that lives elsewhere are therefore dropped BEFORE the host's
are added: a silent host leaves the column EMPTY, which is true. An empty
column gets noticed; a plausible wrong one does not.
The alias, finally. Taking the short name while it happened to be free gave an
inconsistent fleet: in that same deployment two VMs got "host+vm" — their
names were held by local domains — and the third its short name. A convention
that depends on what happens to sit in the file is not a convention. The
chained name is now systematic.
Assisted-by: Claude Opus 5
2026-08-25 00:30:54 -04:00
|
|
|
Puis le choix. Prendre le nom COURT quand il se trouvait libre donnait un
|
|
|
|
|
parc incohérent : sur un même déploiement de trois VM, deux recevaient
|
|
|
|
|
« hôte+vm » — leurs noms étaient pris par des domaines locaux — et la
|
|
|
|
|
troisième son nom court. Une convention qui dépend de ce qui traîne dans
|
|
|
|
|
le fichier n'est pas une convention. Le chaîné est systématique."""
|
[FIX] proxmox : un seul nom par entrée ~/.ssh/config, et le bon
L'entrée portait deux noms sur sa ligne « Host » — le chaîné « hôte+vm » et
le court : « Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest ». Le second est un doublon dès que le premier suffit.
ssh n'a besoin que d'un nom ; le doubler n'ajoute qu'une façon de plus
d'écrire la même adresse. Rapporté.
Un seul, donc, et choisi : le nom court quand il est LIBRE, c'est celui qu'on
tape ; le chaîné quand il désignerait une autre machine — une VM locale
homonyme, ou la VM d'un autre hôte Proxmox. Ce qui a forcé le changement est
nommé à l'écran plutôt que laissé en surprise.
« Pris » se juge sur le ProxyJump du bloc et non sur sa seule présence. Le
test l'a montré avant l'usage : notre propre entrée, réécrite à chaque
déploiement, se prenait pour une rivale et le nom basculait d'une fois sur
l'autre.
--- EN ---
The entry carried two names on its "Host" line — the chained "host+vm" and
the short one: "Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest". The second is redundant as soon as the first is
enough. ssh needs one name; doubling it only adds another way to write the
same address. Reported.
One name then, and a chosen one: the short one while it is FREE, since that
is what you type; the chained one when it would point at another machine — a
local VM of the same name, or another Proxmox host's VM. Whatever forced the
change is named on screen rather than left as a surprise.
"Taken" is judged on the block's ProxyJump, not on its mere presence. The
test showed it before use: our own entry, rewritten at every deployment, took
itself for a rival and the name flipped from one run to the next.
Assisted-by: Claude Opus 5
2026-08-24 22:55:46 -04:00
|
|
|
|
[FIX] suivi : un relevé Proxmox jeté, et deux lignes qui montraient une autre machine
Sur trois VM d'un même Proxmox, une seule avait ses colonnes vides — et les
deux autres montraient les chiffres d'une AUTRE machine. Deux fautes, dont
une était le miroir d'un correctif précédent.
Le code de sortie de la suite distante est celui de son DERNIER maillon, la
sonde Odoo. Tant qu'Odoo n'écoute pas — c'est-à-dire pendant TOUTE
l'installation, précisément quand on regarde — la boucle finit en échec et le
relevé, parfait, était jeté. On avait corrigé l'erreur inverse, un code 0 pris
pour une réponse ; exiger 0 était la même faute retournée. Seule une liste de
ressources analysable prouve une réponse.
Pendant ce temps, « virsh domstats » indexe par NOM, et un nom se partage :
les deux VM qui avaient un homonyme LOCAL affichaient ses chiffres. Mesuré —
1,5 Gio de RAM sur 12 et 58 Gio de disque sur 65, quand la vraie tournait avec
3 Gio et 25. Les relevés locaux d'une VM qui vit ailleurs sont donc retirés
AVANT d'ajouter ceux de l'hôte : un hôte muet laisse la colonne VIDE, ce qui
est vrai. Une colonne vide se remarque ; une colonne juste et fausse, non.
L'alias enfin. Prendre le nom court quand il se trouvait libre donnait un parc
incohérent : sur ce même déploiement, deux VM ont reçu « hôte+vm » — leurs
noms étaient pris par des domaines locaux — et la troisième son nom court. Une
convention qui dépend de ce qui traîne dans le fichier n'est pas une
convention. Le nom chaîné est systématique.
--- EN ---
Of three VMs on one Proxmox, only one had empty columns — and the other two
showed ANOTHER machine's figures. Two defects, one the mirror of an earlier
fix.
A remote pipeline's exit code is its LAST link's, the Odoo probe. While Odoo
is not listening — that is, during the WHOLE install, exactly when you are
watching — the loop ends in failure and the reading, perfectly good, was
thrown away. We had fixed the opposite error, a 0 taken for an answer;
demanding 0 was the same mistake reversed. Only a parsable resource list
proves an answer.
Meanwhile "virsh domstats" indexes by NAME, and a name is shared: the two VMs
with a LOCAL namesake displayed its figures. Measured — 1.5 GiB of RAM out of
12 and 58 GiB of disk out of 65, while the real one ran on 3 GiB and 25. Local
readings for a VM that lives elsewhere are therefore dropped BEFORE the host's
are added: a silent host leaves the column EMPTY, which is true. An empty
column gets noticed; a plausible wrong one does not.
The alias, finally. Taking the short name while it happened to be free gave an
inconsistent fleet: in that same deployment two VMs got "host+vm" — their
names were held by local domains — and the third its short name. A convention
that depends on what happens to sit in the file is not a convention. The
chained name is now systematic.
Assisted-by: Claude Opus 5
2026-08-25 00:30:54 -04:00
|
|
|
def _choisit(self, nom, locaux=()):
|
[FIX] proxmox : un seul nom par entrée ~/.ssh/config, et le bon
L'entrée portait deux noms sur sa ligne « Host » — le chaîné « hôte+vm » et
le court : « Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest ». Le second est un doublon dès que le premier suffit.
ssh n'a besoin que d'un nom ; le doubler n'ajoute qu'une façon de plus
d'écrire la même adresse. Rapporté.
Un seul, donc, et choisi : le nom court quand il est LIBRE, c'est celui qu'on
tape ; le chaîné quand il désignerait une autre machine — une VM locale
homonyme, ou la VM d'un autre hôte Proxmox. Ce qui a forcé le changement est
nommé à l'écran plutôt que laissé en surprise.
« Pris » se juge sur le ProxyJump du bloc et non sur sa seule présence. Le
test l'a montré avant l'usage : notre propre entrée, réécrite à chaque
déploiement, se prenait pour une rivale et le nom basculait d'une fois sur
l'autre.
--- EN ---
The entry carried two names on its "Host" line — the chained "host+vm" and
the short one: "Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest". The second is redundant as soon as the first is
enough. ssh needs one name; doubling it only adds another way to write the
same address. Reported.
One name then, and a chosen one: the short one while it is FREE, since that
is what you type; the chained one when it would point at another machine — a
local VM of the same name, or another Proxmox host's VM. Whatever forced the
change is named on screen rather than left as a surprise.
"Taken" is judged on the block's ProxyJump, not on its mere presence. The
test showed it before use: our own entry, rewritten at every deployment, took
itself for a rival and the name flipped from one run to the next.
Assisted-by: Claude Opus 5
2026-08-24 22:55:46 -04:00
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO
|
|
|
|
|
|
[FIX] suivi : un relevé Proxmox jeté, et deux lignes qui montraient une autre machine
Sur trois VM d'un même Proxmox, une seule avait ses colonnes vides — et les
deux autres montraient les chiffres d'une AUTRE machine. Deux fautes, dont
une était le miroir d'un correctif précédent.
Le code de sortie de la suite distante est celui de son DERNIER maillon, la
sonde Odoo. Tant qu'Odoo n'écoute pas — c'est-à-dire pendant TOUTE
l'installation, précisément quand on regarde — la boucle finit en échec et le
relevé, parfait, était jeté. On avait corrigé l'erreur inverse, un code 0 pris
pour une réponse ; exiger 0 était la même faute retournée. Seule une liste de
ressources analysable prouve une réponse.
Pendant ce temps, « virsh domstats » indexe par NOM, et un nom se partage :
les deux VM qui avaient un homonyme LOCAL affichaient ses chiffres. Mesuré —
1,5 Gio de RAM sur 12 et 58 Gio de disque sur 65, quand la vraie tournait avec
3 Gio et 25. Les relevés locaux d'une VM qui vit ailleurs sont donc retirés
AVANT d'ajouter ceux de l'hôte : un hôte muet laisse la colonne VIDE, ce qui
est vrai. Une colonne vide se remarque ; une colonne juste et fausse, non.
L'alias enfin. Prendre le nom court quand il se trouvait libre donnait un parc
incohérent : sur ce même déploiement, deux VM ont reçu « hôte+vm » — leurs
noms étaient pris par des domaines locaux — et la troisième son nom court. Une
convention qui dépend de ce qui traîne dans le fichier n'est pas une
convention. Le nom chaîné est systématique.
--- EN ---
Of three VMs on one Proxmox, only one had empty columns — and the other two
showed ANOTHER machine's figures. Two defects, one the mirror of an earlier
fix.
A remote pipeline's exit code is its LAST link's, the Odoo probe. While Odoo
is not listening — that is, during the WHOLE install, exactly when you are
watching — the loop ends in failure and the reading, perfectly good, was
thrown away. We had fixed the opposite error, a 0 taken for an answer;
demanding 0 was the same mistake reversed. Only a parsable resource list
proves an answer.
Meanwhile "virsh domstats" indexes by NAME, and a name is shared: the two VMs
with a LOCAL namesake displayed its figures. Measured — 1.5 GiB of RAM out of
12 and 58 GiB of disk out of 65, while the real one ran on 3 GiB and 25. Local
readings for a VM that lives elsewhere are therefore dropped BEFORE the host's
are added: a silent host leaves the column EMPTY, which is true. An empty
column gets noticed; a plausible wrong one does not.
The alias, finally. Taking the short name while it happened to be free gave an
inconsistent fleet: in that same deployment two VMs got "host+vm" — their
names were held by local domains — and the third its short name. A convention
that depends on what happens to sit in the file is not a convention. The
chained name is now systematic.
Assisted-by: Claude Opus 5
2026-08-25 00:30:54 -04:00
|
|
|
todo = TODO.__new__(TODO)
|
|
|
|
|
return todo._pve_alias_names(nom, f"pve9+{nom}", set(locaux), "pve9")
|
[FIX] proxmox : un seul nom par entrée ~/.ssh/config, et le bon
L'entrée portait deux noms sur sa ligne « Host » — le chaîné « hôte+vm » et
le court : « Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest ». Le second est un doublon dès que le premier suffit.
ssh n'a besoin que d'un nom ; le doubler n'ajoute qu'une façon de plus
d'écrire la même adresse. Rapporté.
Un seul, donc, et choisi : le nom court quand il est LIBRE, c'est celui qu'on
tape ; le chaîné quand il désignerait une autre machine — une VM locale
homonyme, ou la VM d'un autre hôte Proxmox. Ce qui a forcé le changement est
nommé à l'écran plutôt que laissé en surprise.
« Pris » se juge sur le ProxyJump du bloc et non sur sa seule présence. Le
test l'a montré avant l'usage : notre propre entrée, réécrite à chaque
déploiement, se prenait pour une rivale et le nom basculait d'une fois sur
l'autre.
--- EN ---
The entry carried two names on its "Host" line — the chained "host+vm" and
the short one: "Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest". The second is redundant as soon as the first is
enough. ssh needs one name; doubling it only adds another way to write the
same address. Reported.
One name then, and a chosen one: the short one while it is FREE, since that
is what you type; the chained one when it would point at another machine — a
local VM of the same name, or another Proxmox host's VM. Whatever forced the
change is named on screen rather than left as a surprise.
"Taken" is judged on the block's ProxyJump, not on its mere presence. The
test showed it before use: our own entry, rewritten at every deployment, took
itself for a rival and the name flipped from one run to the next.
Assisted-by: Claude Opus 5
2026-08-24 22:55:46 -04:00
|
|
|
|
[FIX] suivi : un relevé Proxmox jeté, et deux lignes qui montraient une autre machine
Sur trois VM d'un même Proxmox, une seule avait ses colonnes vides — et les
deux autres montraient les chiffres d'une AUTRE machine. Deux fautes, dont
une était le miroir d'un correctif précédent.
Le code de sortie de la suite distante est celui de son DERNIER maillon, la
sonde Odoo. Tant qu'Odoo n'écoute pas — c'est-à-dire pendant TOUTE
l'installation, précisément quand on regarde — la boucle finit en échec et le
relevé, parfait, était jeté. On avait corrigé l'erreur inverse, un code 0 pris
pour une réponse ; exiger 0 était la même faute retournée. Seule une liste de
ressources analysable prouve une réponse.
Pendant ce temps, « virsh domstats » indexe par NOM, et un nom se partage :
les deux VM qui avaient un homonyme LOCAL affichaient ses chiffres. Mesuré —
1,5 Gio de RAM sur 12 et 58 Gio de disque sur 65, quand la vraie tournait avec
3 Gio et 25. Les relevés locaux d'une VM qui vit ailleurs sont donc retirés
AVANT d'ajouter ceux de l'hôte : un hôte muet laisse la colonne VIDE, ce qui
est vrai. Une colonne vide se remarque ; une colonne juste et fausse, non.
L'alias enfin. Prendre le nom court quand il se trouvait libre donnait un parc
incohérent : sur ce même déploiement, deux VM ont reçu « hôte+vm » — leurs
noms étaient pris par des domaines locaux — et la troisième son nom court. Une
convention qui dépend de ce qui traîne dans le fichier n'est pas une
convention. Le nom chaîné est systématique.
--- EN ---
Of three VMs on one Proxmox, only one had empty columns — and the other two
showed ANOTHER machine's figures. Two defects, one the mirror of an earlier
fix.
A remote pipeline's exit code is its LAST link's, the Odoo probe. While Odoo
is not listening — that is, during the WHOLE install, exactly when you are
watching — the loop ends in failure and the reading, perfectly good, was
thrown away. We had fixed the opposite error, a 0 taken for an answer;
demanding 0 was the same mistake reversed. Only a parsable resource list
proves an answer.
Meanwhile "virsh domstats" indexes by NAME, and a name is shared: the two VMs
with a LOCAL namesake displayed its figures. Measured — 1.5 GiB of RAM out of
12 and 58 GiB of disk out of 65, while the real one ran on 3 GiB and 25. Local
readings for a VM that lives elsewhere are therefore dropped BEFORE the host's
are added: a silent host leaves the column EMPTY, which is true. An empty
column gets noticed; a plausible wrong one does not.
The alias, finally. Taking the short name while it happened to be free gave an
inconsistent fleet: in that same deployment two VMs got "host+vm" — their
names were held by local domains — and the third its short name. A convention
that depends on what happens to sit in the file is not a convention. The
chained name is now systematic.
Assisted-by: Claude Opus 5
2026-08-25 00:30:54 -04:00
|
|
|
def test_one_name_and_it_is_the_chained_one(self):
|
[FIX] proxmox : un seul nom par entrée ~/.ssh/config, et le bon
L'entrée portait deux noms sur sa ligne « Host » — le chaîné « hôte+vm » et
le court : « Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest ». Le second est un doublon dès que le premier suffit.
ssh n'a besoin que d'un nom ; le doubler n'ajoute qu'une façon de plus
d'écrire la même adresse. Rapporté.
Un seul, donc, et choisi : le nom court quand il est LIBRE, c'est celui qu'on
tape ; le chaîné quand il désignerait une autre machine — une VM locale
homonyme, ou la VM d'un autre hôte Proxmox. Ce qui a forcé le changement est
nommé à l'écran plutôt que laissé en surprise.
« Pris » se juge sur le ProxyJump du bloc et non sur sa seule présence. Le
test l'a montré avant l'usage : notre propre entrée, réécrite à chaque
déploiement, se prenait pour une rivale et le nom basculait d'une fois sur
l'autre.
--- EN ---
The entry carried two names on its "Host" line — the chained "host+vm" and
the short one: "Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest". The second is redundant as soon as the first is
enough. ssh needs one name; doubling it only adds another way to write the
same address. Reported.
One name then, and a chosen one: the short one while it is FREE, since that
is what you type; the chained one when it would point at another machine — a
local VM of the same name, or another Proxmox host's VM. Whatever forced the
change is named on screen rather than left as a surprise.
"Taken" is judged on the block's ProxyJump, not on its mere presence. The
test showed it before use: our own entry, rewritten at every deployment, took
itself for a rival and the name flipped from one run to the next.
Assisted-by: Claude Opus 5
2026-08-24 22:55:46 -04:00
|
|
|
noms, vole = self._choisit("erplibre-arch-latest")
|
[FIX] suivi : un relevé Proxmox jeté, et deux lignes qui montraient une autre machine
Sur trois VM d'un même Proxmox, une seule avait ses colonnes vides — et les
deux autres montraient les chiffres d'une AUTRE machine. Deux fautes, dont
une était le miroir d'un correctif précédent.
Le code de sortie de la suite distante est celui de son DERNIER maillon, la
sonde Odoo. Tant qu'Odoo n'écoute pas — c'est-à-dire pendant TOUTE
l'installation, précisément quand on regarde — la boucle finit en échec et le
relevé, parfait, était jeté. On avait corrigé l'erreur inverse, un code 0 pris
pour une réponse ; exiger 0 était la même faute retournée. Seule une liste de
ressources analysable prouve une réponse.
Pendant ce temps, « virsh domstats » indexe par NOM, et un nom se partage :
les deux VM qui avaient un homonyme LOCAL affichaient ses chiffres. Mesuré —
1,5 Gio de RAM sur 12 et 58 Gio de disque sur 65, quand la vraie tournait avec
3 Gio et 25. Les relevés locaux d'une VM qui vit ailleurs sont donc retirés
AVANT d'ajouter ceux de l'hôte : un hôte muet laisse la colonne VIDE, ce qui
est vrai. Une colonne vide se remarque ; une colonne juste et fausse, non.
L'alias enfin. Prendre le nom court quand il se trouvait libre donnait un parc
incohérent : sur ce même déploiement, deux VM ont reçu « hôte+vm » — leurs
noms étaient pris par des domaines locaux — et la troisième son nom court. Une
convention qui dépend de ce qui traîne dans le fichier n'est pas une
convention. Le nom chaîné est systématique.
--- EN ---
Of three VMs on one Proxmox, only one had empty columns — and the other two
showed ANOTHER machine's figures. Two defects, one the mirror of an earlier
fix.
A remote pipeline's exit code is its LAST link's, the Odoo probe. While Odoo
is not listening — that is, during the WHOLE install, exactly when you are
watching — the loop ends in failure and the reading, perfectly good, was
thrown away. We had fixed the opposite error, a 0 taken for an answer;
demanding 0 was the same mistake reversed. Only a parsable resource list
proves an answer.
Meanwhile "virsh domstats" indexes by NAME, and a name is shared: the two VMs
with a LOCAL namesake displayed its figures. Measured — 1.5 GiB of RAM out of
12 and 58 GiB of disk out of 65, while the real one ran on 3 GiB and 25. Local
readings for a VM that lives elsewhere are therefore dropped BEFORE the host's
are added: a silent host leaves the column EMPTY, which is true. An empty
column gets noticed; a plausible wrong one does not.
The alias, finally. Taking the short name while it happened to be free gave an
inconsistent fleet: in that same deployment two VMs got "host+vm" — their
names were held by local domains — and the third its short name. A convention
that depends on what happens to sit in the file is not a convention. The
chained name is now systematic.
Assisted-by: Claude Opus 5
2026-08-25 00:30:54 -04:00
|
|
|
self.assertEqual(noms, ["pve9+erplibre-arch-latest"])
|
[FIX] proxmox : un seul nom par entrée ~/.ssh/config, et le bon
L'entrée portait deux noms sur sa ligne « Host » — le chaîné « hôte+vm » et
le court : « Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest ». Le second est un doublon dès que le premier suffit.
ssh n'a besoin que d'un nom ; le doubler n'ajoute qu'une façon de plus
d'écrire la même adresse. Rapporté.
Un seul, donc, et choisi : le nom court quand il est LIBRE, c'est celui qu'on
tape ; le chaîné quand il désignerait une autre machine — une VM locale
homonyme, ou la VM d'un autre hôte Proxmox. Ce qui a forcé le changement est
nommé à l'écran plutôt que laissé en surprise.
« Pris » se juge sur le ProxyJump du bloc et non sur sa seule présence. Le
test l'a montré avant l'usage : notre propre entrée, réécrite à chaque
déploiement, se prenait pour une rivale et le nom basculait d'une fois sur
l'autre.
--- EN ---
The entry carried two names on its "Host" line — the chained "host+vm" and
the short one: "Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest". The second is redundant as soon as the first is
enough. ssh needs one name; doubling it only adds another way to write the
same address. Reported.
One name then, and a chosen one: the short one while it is FREE, since that
is what you type; the chained one when it would point at another machine — a
local VM of the same name, or another Proxmox host's VM. Whatever forced the
change is named on screen rather than left as a surprise.
"Taken" is judged on the block's ProxyJump, not on its mere presence. The
test showed it before use: our own entry, rewritten at every deployment, took
itself for a rival and the name flipped from one run to the next.
Assisted-by: Claude Opus 5
2026-08-24 22:55:46 -04:00
|
|
|
self.assertFalse(vole)
|
|
|
|
|
|
[FIX] suivi : un relevé Proxmox jeté, et deux lignes qui montraient une autre machine
Sur trois VM d'un même Proxmox, une seule avait ses colonnes vides — et les
deux autres montraient les chiffres d'une AUTRE machine. Deux fautes, dont
une était le miroir d'un correctif précédent.
Le code de sortie de la suite distante est celui de son DERNIER maillon, la
sonde Odoo. Tant qu'Odoo n'écoute pas — c'est-à-dire pendant TOUTE
l'installation, précisément quand on regarde — la boucle finit en échec et le
relevé, parfait, était jeté. On avait corrigé l'erreur inverse, un code 0 pris
pour une réponse ; exiger 0 était la même faute retournée. Seule une liste de
ressources analysable prouve une réponse.
Pendant ce temps, « virsh domstats » indexe par NOM, et un nom se partage :
les deux VM qui avaient un homonyme LOCAL affichaient ses chiffres. Mesuré —
1,5 Gio de RAM sur 12 et 58 Gio de disque sur 65, quand la vraie tournait avec
3 Gio et 25. Les relevés locaux d'une VM qui vit ailleurs sont donc retirés
AVANT d'ajouter ceux de l'hôte : un hôte muet laisse la colonne VIDE, ce qui
est vrai. Une colonne vide se remarque ; une colonne juste et fausse, non.
L'alias enfin. Prendre le nom court quand il se trouvait libre donnait un parc
incohérent : sur ce même déploiement, deux VM ont reçu « hôte+vm » — leurs
noms étaient pris par des domaines locaux — et la troisième son nom court. Une
convention qui dépend de ce qui traîne dans le fichier n'est pas une
convention. Le nom chaîné est systématique.
--- EN ---
Of three VMs on one Proxmox, only one had empty columns — and the other two
showed ANOTHER machine's figures. Two defects, one the mirror of an earlier
fix.
A remote pipeline's exit code is its LAST link's, the Odoo probe. While Odoo
is not listening — that is, during the WHOLE install, exactly when you are
watching — the loop ends in failure and the reading, perfectly good, was
thrown away. We had fixed the opposite error, a 0 taken for an answer;
demanding 0 was the same mistake reversed. Only a parsable resource list
proves an answer.
Meanwhile "virsh domstats" indexes by NAME, and a name is shared: the two VMs
with a LOCAL namesake displayed its figures. Measured — 1.5 GiB of RAM out of
12 and 58 GiB of disk out of 65, while the real one ran on 3 GiB and 25. Local
readings for a VM that lives elsewhere are therefore dropped BEFORE the host's
are added: a silent host leaves the column EMPTY, which is true. An empty
column gets noticed; a plausible wrong one does not.
The alias, finally. Taking the short name while it happened to be free gave an
inconsistent fleet: in that same deployment two VMs got "host+vm" — their
names were held by local domains — and the third its short name. A convention
that depends on what happens to sit in the file is not a convention. The
chained name is now systematic.
Assisted-by: Claude Opus 5
2026-08-25 00:30:54 -04:00
|
|
|
def test_a_fleet_gets_one_single_convention(self):
|
|
|
|
|
# Le défaut rapporté : trois VM du même déploiement, deux nommées
|
|
|
|
|
# d'une façon et la troisième d'une autre.
|
|
|
|
|
noms = [
|
|
|
|
|
self._choisit(n, locaux=("erplibre-ubuntu-2604",))[0][0]
|
|
|
|
|
for n in (
|
|
|
|
|
"erplibre-ubuntu-2604",
|
|
|
|
|
"erplibre-arch-latest",
|
|
|
|
|
"erplibre-proxmox-9",
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
all(n.startswith("pve9+") for n in noms),
|
|
|
|
|
f"un parc, une convention : {noms}",
|
|
|
|
|
)
|
[FIX] proxmox : un seul nom par entrée ~/.ssh/config, et le bon
L'entrée portait deux noms sur sa ligne « Host » — le chaîné « hôte+vm » et
le court : « Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest ». Le second est un doublon dès que le premier suffit.
ssh n'a besoin que d'un nom ; le doubler n'ajoute qu'une façon de plus
d'écrire la même adresse. Rapporté.
Un seul, donc, et choisi : le nom court quand il est LIBRE, c'est celui qu'on
tape ; le chaîné quand il désignerait une autre machine — une VM locale
homonyme, ou la VM d'un autre hôte Proxmox. Ce qui a forcé le changement est
nommé à l'écran plutôt que laissé en surprise.
« Pris » se juge sur le ProxyJump du bloc et non sur sa seule présence. Le
test l'a montré avant l'usage : notre propre entrée, réécrite à chaque
déploiement, se prenait pour une rivale et le nom basculait d'une fois sur
l'autre.
--- EN ---
The entry carried two names on its "Host" line — the chained "host+vm" and
the short one: "Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest". The second is redundant as soon as the first is
enough. ssh needs one name; doubling it only adds another way to write the
same address. Reported.
One name then, and a chosen one: the short one while it is FREE, since that
is what you type; the chained one when it would point at another machine — a
local VM of the same name, or another Proxmox host's VM. Whatever forced the
change is named on screen rather than left as a surprise.
"Taken" is judged on the block's ProxyJump, not on its mere presence. The
test showed it before use: our own entry, rewritten at every deployment, took
itself for a rival and the name flipped from one run to the next.
Assisted-by: Claude Opus 5
2026-08-24 22:55:46 -04:00
|
|
|
|
[FIX] suivi : un relevé Proxmox jeté, et deux lignes qui montraient une autre machine
Sur trois VM d'un même Proxmox, une seule avait ses colonnes vides — et les
deux autres montraient les chiffres d'une AUTRE machine. Deux fautes, dont
une était le miroir d'un correctif précédent.
Le code de sortie de la suite distante est celui de son DERNIER maillon, la
sonde Odoo. Tant qu'Odoo n'écoute pas — c'est-à-dire pendant TOUTE
l'installation, précisément quand on regarde — la boucle finit en échec et le
relevé, parfait, était jeté. On avait corrigé l'erreur inverse, un code 0 pris
pour une réponse ; exiger 0 était la même faute retournée. Seule une liste de
ressources analysable prouve une réponse.
Pendant ce temps, « virsh domstats » indexe par NOM, et un nom se partage :
les deux VM qui avaient un homonyme LOCAL affichaient ses chiffres. Mesuré —
1,5 Gio de RAM sur 12 et 58 Gio de disque sur 65, quand la vraie tournait avec
3 Gio et 25. Les relevés locaux d'une VM qui vit ailleurs sont donc retirés
AVANT d'ajouter ceux de l'hôte : un hôte muet laisse la colonne VIDE, ce qui
est vrai. Une colonne vide se remarque ; une colonne juste et fausse, non.
L'alias enfin. Prendre le nom court quand il se trouvait libre donnait un parc
incohérent : sur ce même déploiement, deux VM ont reçu « hôte+vm » — leurs
noms étaient pris par des domaines locaux — et la troisième son nom court. Une
convention qui dépend de ce qui traîne dans le fichier n'est pas une
convention. Le nom chaîné est systématique.
--- EN ---
Of three VMs on one Proxmox, only one had empty columns — and the other two
showed ANOTHER machine's figures. Two defects, one the mirror of an earlier
fix.
A remote pipeline's exit code is its LAST link's, the Odoo probe. While Odoo
is not listening — that is, during the WHOLE install, exactly when you are
watching — the loop ends in failure and the reading, perfectly good, was
thrown away. We had fixed the opposite error, a 0 taken for an answer;
demanding 0 was the same mistake reversed. Only a parsable resource list
proves an answer.
Meanwhile "virsh domstats" indexes by NAME, and a name is shared: the two VMs
with a LOCAL namesake displayed its figures. Measured — 1.5 GiB of RAM out of
12 and 58 GiB of disk out of 65, while the real one ran on 3 GiB and 25. Local
readings for a VM that lives elsewhere are therefore dropped BEFORE the host's
are added: a silent host leaves the column EMPTY, which is true. An empty
column gets noticed; a plausible wrong one does not.
The alias, finally. Taking the short name while it happened to be free gave an
inconsistent fleet: in that same deployment two VMs got "host+vm" — their
names were held by local domains — and the third its short name. A convention
that depends on what happens to sit in the file is not a convention. The
chained name is now systematic.
Assisted-by: Claude Opus 5
2026-08-25 00:30:54 -04:00
|
|
|
def test_a_local_namesake_is_still_named(self):
|
|
|
|
|
# Le nom chaîné ne lui vole rien, mais on le DIT : c'est ce qui
|
|
|
|
|
# explique pourquoi « ssh <nom court> » va ailleurs.
|
|
|
|
|
_noms, vole = self._choisit(
|
[FIX] proxmox : un seul nom par entrée ~/.ssh/config, et le bon
L'entrée portait deux noms sur sa ligne « Host » — le chaîné « hôte+vm » et
le court : « Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest ». Le second est un doublon dès que le premier suffit.
ssh n'a besoin que d'un nom ; le doubler n'ajoute qu'une façon de plus
d'écrire la même adresse. Rapporté.
Un seul, donc, et choisi : le nom court quand il est LIBRE, c'est celui qu'on
tape ; le chaîné quand il désignerait une autre machine — une VM locale
homonyme, ou la VM d'un autre hôte Proxmox. Ce qui a forcé le changement est
nommé à l'écran plutôt que laissé en surprise.
« Pris » se juge sur le ProxyJump du bloc et non sur sa seule présence. Le
test l'a montré avant l'usage : notre propre entrée, réécrite à chaque
déploiement, se prenait pour une rivale et le nom basculait d'une fois sur
l'autre.
--- EN ---
The entry carried two names on its "Host" line — the chained "host+vm" and
the short one: "Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest". The second is redundant as soon as the first is
enough. ssh needs one name; doubling it only adds another way to write the
same address. Reported.
One name then, and a chosen one: the short one while it is FREE, since that
is what you type; the chained one when it would point at another machine — a
local VM of the same name, or another Proxmox host's VM. Whatever forced the
change is named on screen rather than left as a surprise.
"Taken" is judged on the block's ProxyJump, not on its mere presence. The
test showed it before use: our own entry, rewritten at every deployment, took
itself for a rival and the name flipped from one run to the next.
Assisted-by: Claude Opus 5
2026-08-24 22:55:46 -04:00
|
|
|
"erplibre-arch-latest", locaux=("erplibre-arch-latest",)
|
|
|
|
|
)
|
|
|
|
|
self.assertTrue(vole)
|
|
|
|
|
|
|
|
|
|
def test_no_deploy_path_writes_two_names_anymore(self):
|
|
|
|
|
import re
|
|
|
|
|
from pathlib import Path as P
|
|
|
|
|
|
|
|
|
|
src = P("script/todo/proxmox_menu.py").read_text(encoding="utf-8")
|
|
|
|
|
self.assertIsNone(
|
|
|
|
|
re.search(r"noms_alias\.append|noms\.append\(vm\[.name.\]\)", src),
|
|
|
|
|
"le second nom ne doit plus être ajouté",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
[FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh
Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport
s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus
récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que
le processus installait encore. Deux garde-fous : un PID dans le rapport, et
un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire
car une descente déjà lancée a l'ancien module en mémoire.
Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f
deep_proxmox.py » de surveillance donnait deux faux positifs sur trois.
Trois autres, mêmes preuves :
- un rapport vide plus récent masquait celui qui nommait les VM réelles ;
- detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un
dans tous les cas, donc l'avertissement sortait toujours ;
- _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc
au corps non indenté, seule la ligne Host partait et ssh rattachait
« StrictHostKeyChecking no » au bloc précédent — un serveur de production.
Et un bloc partagé (« Host prod-db vm-a ») partait en entier.
Les cinq correctifs meurent sous mutation.
--- EN ---
Adversarial review of the previous commit: it had CREATED a hazard. With the
report now written VM by VM, the RUNNING descent's is the most recent, and
"--detruire" would have picked it — qm destroy --purge on the tree the process
was still installing. Two guards: a PID in the report, and a flat refusal
while another deep_proxmox.py runs. The second is needed because an
already-running descent holds the old module in memory.
Matched by ARGUMENT, not substring: my own monitoring "pgrep -f
deep_proxmox.py" produced two false positives out of three.
Three more, same evidence:
- a newer empty report masked the one naming the real VMs;
- detruire() never credited level 1: the count was off by one in every case,
so the warning always fired;
- _ssh_config_drop_hosts took indentation for syntax. On a block with an
unindented body only the Host line went, and ssh attached
"StrictHostKeyChecking no" to the preceding block — a production server.
And a shared block ("Host prod-db vm-a") went entirely.
All five fixes die under mutation.
Assisted-by: claude-opus-5
(cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-27 06:56:35 -04:00
|
|
|
class TestNeRienPerdreDansSshConfig(unittest.TestCase):
|
|
|
|
|
"""~/.ssh/config contient les entrées PERSONNELLES de l'utilisateur.
|
|
|
|
|
|
|
|
|
|
Ce fichier est réécrit en entier à chaque déploiement de VM. Deux pertes
|
|
|
|
|
de données y ont été constatées, l'une capable de désactiver la
|
|
|
|
|
vérification de clé d'hôte sur un serveur de production."""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO
|
|
|
|
|
|
|
|
|
|
self.retirer = TODO._ssh_config_drop_hosts
|
|
|
|
|
|
|
|
|
|
def test_a_block_with_an_unindented_body_goes_entirely(self):
|
|
|
|
|
"""L'indentation est COSMÉTIQUE dans ce format, et un fichier écrit à
|
|
|
|
|
la main s'en passe souvent. La règle d'avant la prenait pour de la
|
|
|
|
|
syntaxe : seule la ligne « Host » partait."""
|
|
|
|
|
avant = (
|
|
|
|
|
"Host prod\n"
|
|
|
|
|
" HostName prod.example.com\n"
|
|
|
|
|
" User root\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"Host deep-1\n"
|
|
|
|
|
"HostName 10.0.0.1\n"
|
|
|
|
|
"User erplibre\n"
|
|
|
|
|
"StrictHostKeyChecking no\n"
|
|
|
|
|
"UserKnownHostsFile /dev/null\n"
|
|
|
|
|
"IdentityFile ~/.ssh/id_deep\n"
|
|
|
|
|
)
|
|
|
|
|
apres = self.retirer(avant, ["deep-1"])
|
|
|
|
|
# Rien du bloc retiré ne subsiste : sans Host au-dessus, ssh
|
|
|
|
|
# rattacherait ces lignes à « prod » et la production perdrait sa
|
|
|
|
|
# vérification de clé d'hôte.
|
|
|
|
|
for orphelin in (
|
|
|
|
|
"10.0.0.1",
|
|
|
|
|
"StrictHostKeyChecking",
|
|
|
|
|
"UserKnownHostsFile",
|
|
|
|
|
"id_deep",
|
|
|
|
|
):
|
|
|
|
|
self.assertNotIn(orphelin, apres, orphelin)
|
|
|
|
|
# Et le bloc de l'utilisateur est intact.
|
|
|
|
|
self.assertIn("HostName prod.example.com", apres)
|
|
|
|
|
self.assertIn("User root", apres)
|
|
|
|
|
|
|
|
|
|
def test_a_shared_host_line_keeps_the_names_not_dropped(self):
|
|
|
|
|
"""« Host prod-db vm-a » perdait le prod-db de l'utilisateur : le bloc
|
|
|
|
|
partait en entier dès qu'UN de ses noms était repris."""
|
|
|
|
|
avant = (
|
|
|
|
|
"Host prod-db vm-a\n"
|
|
|
|
|
" HostName db.interne\n"
|
|
|
|
|
" ProxyJump pve9\n"
|
|
|
|
|
)
|
|
|
|
|
apres = self.retirer(avant, ["vm-a"])
|
|
|
|
|
self.assertIn("Host prod-db\n", apres)
|
|
|
|
|
self.assertNotIn("vm-a", apres)
|
|
|
|
|
# Le corps suit le nom qui reste : sinon prod-db perd son rebond.
|
|
|
|
|
self.assertIn("HostName db.interne", apres)
|
|
|
|
|
self.assertIn("ProxyJump pve9", apres)
|
|
|
|
|
|
|
|
|
|
def test_a_nickname_added_by_hand_survives_a_redeploy(self):
|
|
|
|
|
avant = "Host pve9+vm-a webtest\n HostName 10.10.10.5\n"
|
|
|
|
|
apres = self.retirer(avant, ["pve9+vm-a"])
|
|
|
|
|
self.assertIn("Host webtest\n", apres)
|
|
|
|
|
self.assertIn("HostName 10.10.10.5", apres)
|
|
|
|
|
|
|
|
|
|
def test_all_names_dropped_removes_the_block(self):
|
|
|
|
|
avant = "Host a b\n HostName 1.2.3.4\n\nHost garde\n User x\n"
|
|
|
|
|
apres = self.retirer(avant, ["a", "b"])
|
|
|
|
|
self.assertNotIn("1.2.3.4", apres)
|
|
|
|
|
self.assertIn("Host garde", apres)
|
|
|
|
|
|
|
|
|
|
def test_a_match_section_is_never_swallowed(self):
|
|
|
|
|
avant = (
|
|
|
|
|
"Host part\n"
|
|
|
|
|
" HostName 10.0.0.9\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"Match host *.interne\n"
|
|
|
|
|
" User admin\n"
|
|
|
|
|
)
|
|
|
|
|
apres = self.retirer(avant, ["part"])
|
|
|
|
|
self.assertIn("Match host *.interne", apres)
|
|
|
|
|
self.assertIn("User admin", apres)
|
|
|
|
|
self.assertNotIn("10.0.0.9", apres)
|
|
|
|
|
|
|
|
|
|
def test_comments_before_the_next_block_are_not_swallowed(self):
|
|
|
|
|
avant = (
|
|
|
|
|
"Host part\n"
|
|
|
|
|
" HostName 10.0.0.9\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"# la machine du client, ne pas toucher\n"
|
|
|
|
|
"Host client\n"
|
|
|
|
|
" HostName 10.0.0.10\n"
|
|
|
|
|
)
|
|
|
|
|
apres = self.retirer(avant, ["part"])
|
|
|
|
|
self.assertIn("# la machine du client, ne pas toucher", apres)
|
|
|
|
|
self.assertIn("Host client", apres)
|
|
|
|
|
|
|
|
|
|
def test_global_directives_above_the_first_host_stay(self):
|
|
|
|
|
avant = "ServerAliveInterval 60\n\nHost part\n HostName 10.0.0.9\n"
|
|
|
|
|
apres = self.retirer(avant, ["part"])
|
|
|
|
|
self.assertIn("ServerAliveInterval 60", apres)
|
|
|
|
|
self.assertNotIn("10.0.0.9", apres)
|
|
|
|
|
|
|
|
|
|
def test_the_keyword_is_read_case_insensitively(self):
|
|
|
|
|
# ssh lit ses mots-clés sans égard à la casse ; nous aussi, sinon un
|
|
|
|
|
# « host » minuscule échappe au retrait et le nom vit deux fois.
|
|
|
|
|
avant = "host part\n HostName 10.0.0.9\n"
|
|
|
|
|
self.assertNotIn("10.0.0.9", self.retirer(avant, ["part"]))
|
|
|
|
|
|
|
|
|
|
def test_hostname_is_not_mistaken_for_a_host_line(self):
|
|
|
|
|
avant = "Host garde\n HostName part\n"
|
|
|
|
|
apres = self.retirer(avant, ["part"])
|
|
|
|
|
self.assertIn("Host garde", apres)
|
|
|
|
|
self.assertIn("HostName part", apres)
|
|
|
|
|
|
|
|
|
|
|
[FIX] proxmox : l'ancienne entrée ssh s'en va avec la convention
Le nom chaîné devient systématique, mais les entrées écrites AVANT portent le
nom court — et rien ne les retirerait : elles ne déclarent pas le nom qu'on
écrit maintenant. Deux blocs mèneraient à la même machine, exactement ce
qu'on venait d'enlever.
Le ProxyJump tranche : un bloc qui rebondit par CET hôte est le nôtre, on le
retire. Celui d'une VM locale homonyme n'en a pas, et on n'y touche jamais ;
celui d'un autre hôte Proxmox non plus.
Le drapeau Odoo gagne son test au passage. Il tombait pour la même raison que
les colonnes vides — la sonde est le dernier maillon de la suite distante, et
un parc où une seule VM n'a pas d'Odoo, un hyperviseur imbriqué par exemple,
finit en échec. Vérifié sur les trois VM : l'hôte rend bien « ODOO » pour les
deux qui écoutent, et le navigateur répondait 303 pendant que la colonne
disait « — ».
--- EN ---
The chained name becomes systematic, but entries written BEFORE carry the
short one — and nothing would retire them: they do not declare the name we
now write. Two blocks would lead to the same machine, exactly what we had
just removed.
The ProxyJump decides: a block hopping through THIS host is ours, so it goes.
A local namesake's has none, and is never touched; another Proxmox host's
neither.
The Odoo flag gains its test along the way. It failed for the same reason as
the empty columns — the probe is the remote pipeline's last link, and a fleet
where a single VM has no Odoo, a nested hypervisor for instance, ends in
failure. Verified on all three VMs: the host does return "ODOO" for the two
that listen, and the browser answered 303 while the column said "—".
Assisted-by: Claude Opus 5
2026-08-25 00:43:10 -04:00
|
|
|
class TestLAncienNomSEnVa(unittest.TestCase):
|
|
|
|
|
"""La convention a changé : les entrées écrites AVANT portent le nom
|
|
|
|
|
court, et rien ne les retirerait — elles ne portent pas le nom qu'on
|
|
|
|
|
écrit maintenant. Deux blocs mèneraient à la même machine, ce qu'on
|
|
|
|
|
venait justement d'enlever."""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO
|
|
|
|
|
|
|
|
|
|
self.maison = tempfile.mkdtemp()
|
|
|
|
|
os.makedirs(os.path.join(self.maison, ".ssh"))
|
|
|
|
|
self._vrai = os.environ.get("HOME")
|
|
|
|
|
os.environ["HOME"] = self.maison
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
|
import os
|
|
|
|
|
import shutil
|
|
|
|
|
|
|
|
|
|
if self._vrai is not None:
|
|
|
|
|
os.environ["HOME"] = self._vrai
|
|
|
|
|
shutil.rmtree(self.maison, ignore_errors=True)
|
|
|
|
|
|
|
|
|
|
def _hosts(self):
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
with open(
|
|
|
|
|
os.path.join(self.maison, ".ssh/config"), encoding="utf-8"
|
|
|
|
|
) as fh:
|
|
|
|
|
return [
|
|
|
|
|
ligne.rstrip() for ligne in fh if ligne.startswith("Host ")
|
|
|
|
|
]
|
|
|
|
|
|
[FIX] LongTest : rapport écrit VM par VM, retrait ssh sans bloc nu
Descente de dix étages arrêtée pendant l'installation du quatrième : quatre
machines réelles restaient, et « --detruire » répondait « aucun rapport : rien
à défaire ». Le rapport ne s'écrivait qu'à la fin, donc le seul enregistrement
du couple (alias du parent, VMID) mourait avec le processus. Il fallait les
retrouver par leur NOM, ce que tout ce fichier s'applique à éviter.
En nettoyant à la main, second défaut : appelé sans nom à écrire — un retrait
pur, légitime, les machines n'existant plus — _write_ssh_config_entry écrivait
« Host » NU suivi d'un « HostName » vide dans le ~/.ssh/config réel, puis
mourait sur IndexError en annonçant l'ajout. Constaté, puis retiré du fichier.
Les deux correctifs meurent sous mutation : 3 tests et 2 tests.
--- EN ---
A ten-level descent stopped during the fourth level's install: four real
machines were left, and "--detruire" answered "no report: nothing to undo".
The report was only written at the end, so the sole record of the (parent
alias, VMID) pair died with the process. They had to be found by NAME, which
this whole file works to avoid.
Cleaning up by hand surfaced a second defect: called with no name to write — a
pure removal, legitimate since the machines are gone — _write_ssh_config_entry
wrote a BARE "Host" followed by an empty "HostName" into the real ~/.ssh/config,
then died on IndexError while announcing the addition. Observed, then removed.
Both fixes die under mutation: 3 tests and 2 tests.
Assisted-by: claude-opus-5
(cherry picked from commit e7c8b540c630b33c6eb1b1a2f51c01497e0b3ca0)
2026-08-27 05:39:08 -04:00
|
|
|
def test_dropping_the_last_entry_writes_no_nameless_block(self):
|
|
|
|
|
"""Retirer sans réécrire est un appel légitime : les machines
|
|
|
|
|
n'existent plus.
|
|
|
|
|
|
|
|
|
|
Constaté dans le vrai ~/.ssh/config de l'utilisateur : l'appel écrivait
|
|
|
|
|
« Host » NU, suivi d'un « HostName » vide, puis mourait sur un
|
|
|
|
|
IndexError en annonçant l'ajout. Le bloc sans nom s'applique à rien et
|
|
|
|
|
brouille la lecture du fichier."""
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
self.todo._write_ssh_config_entry(
|
|
|
|
|
["deep-1"], "erplibre", "10.10.10.150"
|
|
|
|
|
)
|
|
|
|
|
self.todo._write_ssh_config_entry(
|
|
|
|
|
["deep-2"], "erplibre", "10.10.10.151", proxy_jump="deep-1"
|
|
|
|
|
)
|
|
|
|
|
self.todo._write_ssh_config_entry(
|
|
|
|
|
[], "erplibre", "", also_drop=("deep-1", "deep-2")
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(self._hosts(), [])
|
|
|
|
|
with open(
|
|
|
|
|
os.path.join(self.maison, ".ssh/config"), encoding="utf-8"
|
|
|
|
|
) as fh:
|
|
|
|
|
reste = fh.read()
|
|
|
|
|
self.assertNotIn("Host", reste)
|
|
|
|
|
self.assertNotIn("HostName", reste)
|
|
|
|
|
# Et le fichier garde ses droits : ssh refuse un config trop ouvert.
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
oct(os.stat(os.path.join(self.maison, ".ssh/config")).st_mode)[
|
|
|
|
|
-3:
|
|
|
|
|
],
|
|
|
|
|
"600",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_dropping_one_entry_leaves_the_others_untouched(self):
|
|
|
|
|
for nom, ip in (("garde-a", "10.0.0.1"), ("part", "10.0.0.2")):
|
|
|
|
|
self.todo._write_ssh_config_entry([nom], "erplibre", ip)
|
|
|
|
|
self.todo._write_ssh_config_entry(
|
|
|
|
|
[], "erplibre", "", also_drop=("part",)
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(self._hosts(), ["Host garde-a"])
|
|
|
|
|
|
[FIX] proxmox : l'ancienne entrée ssh s'en va avec la convention
Le nom chaîné devient systématique, mais les entrées écrites AVANT portent le
nom court — et rien ne les retirerait : elles ne déclarent pas le nom qu'on
écrit maintenant. Deux blocs mèneraient à la même machine, exactement ce
qu'on venait d'enlever.
Le ProxyJump tranche : un bloc qui rebondit par CET hôte est le nôtre, on le
retire. Celui d'une VM locale homonyme n'en a pas, et on n'y touche jamais ;
celui d'un autre hôte Proxmox non plus.
Le drapeau Odoo gagne son test au passage. Il tombait pour la même raison que
les colonnes vides — la sonde est le dernier maillon de la suite distante, et
un parc où une seule VM n'a pas d'Odoo, un hyperviseur imbriqué par exemple,
finit en échec. Vérifié sur les trois VM : l'hôte rend bien « ODOO » pour les
deux qui écoutent, et le navigateur répondait 303 pendant que la colonne
disait « — ».
--- EN ---
The chained name becomes systematic, but entries written BEFORE carry the
short one — and nothing would retire them: they do not declare the name we
now write. Two blocks would lead to the same machine, exactly what we had
just removed.
The ProxyJump decides: a block hopping through THIS host is ours, so it goes.
A local namesake's has none, and is never touched; another Proxmox host's
neither.
The Odoo flag gains its test along the way. It failed for the same reason as
the empty columns — the probe is the remote pipeline's last link, and a fleet
where a single VM has no Odoo, a nested hypervisor for instance, ends in
failure. Verified on all three VMs: the host does return "ODOO" for the two
that listen, and the browser answered 303 while the column said "—".
Assisted-by: Claude Opus 5
2026-08-25 00:43:10 -04:00
|
|
|
def test_the_old_short_entry_is_retired(self):
|
|
|
|
|
# L'état d'avant : une entrée écrite sous l'ancienne convention.
|
|
|
|
|
self.todo._write_ssh_config_entry(
|
|
|
|
|
["vm-a"], "erplibre", "10.10.10.151", proxy_jump="pve9"
|
|
|
|
|
)
|
|
|
|
|
perime = self.todo._pve_alias_perime("vm-a", "pve9")
|
|
|
|
|
self.assertEqual(perime, ["vm-a"])
|
|
|
|
|
self.todo._write_ssh_config_entry(
|
|
|
|
|
["pve9+vm-a"],
|
|
|
|
|
"erplibre",
|
|
|
|
|
"10.10.10.151",
|
|
|
|
|
proxy_jump="pve9",
|
|
|
|
|
also_drop=perime,
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(self._hosts(), ["Host pve9+vm-a"])
|
|
|
|
|
|
|
|
|
|
def test_a_local_vm_of_the_same_name_is_left_alone(self):
|
|
|
|
|
# Sans ProxyJump vers cet hôte, le bloc n'est pas le nôtre : on n'y
|
|
|
|
|
# touche pas, même s'il porte exactement ce nom.
|
|
|
|
|
self.todo._write_ssh_config_entry(["vm-a"], "erplibre", "192.168.1.9")
|
|
|
|
|
self.assertEqual(self.todo._pve_alias_perime("vm-a", "pve9"), [])
|
|
|
|
|
self.todo._write_ssh_config_entry(
|
|
|
|
|
["pve9+vm-a"],
|
|
|
|
|
"erplibre",
|
|
|
|
|
"10.10.10.151",
|
|
|
|
|
proxy_jump="pve9",
|
|
|
|
|
also_drop=self.todo._pve_alias_perime("vm-a", "pve9"),
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(self._hosts(), ["Host vm-a", "Host pve9+vm-a"])
|
|
|
|
|
|
|
|
|
|
def test_another_hosts_vm_is_left_alone(self):
|
|
|
|
|
self.todo._write_ssh_config_entry(
|
|
|
|
|
["vm-a"], "erplibre", "10.0.0.9", proxy_jump="pve7"
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(self.todo._pve_alias_perime("vm-a", "pve9"), [])
|
|
|
|
|
|
|
|
|
|
|
[ADD] proxmox : le guide de connexion, qui manquait à toute VM distante
Rapporté sur Arch : « pas l'écran de connexion, avec le guide qui dit de
prendre pacman, comme sur ubuntu ». Ce n'était pas Arch — c'était Proxmox.
La voie libvirt livre /etc/motd par le « write_files » de cloud-init, et
« qm set » n'offre pas cela : AUCUNE VM déployée sur un hôte Proxmox n'avait
de guide, quelle que soit sa distribution.
Une troisième voie de livraison, par ssh une fois la VM debout, avec la MÊME
source — « guide_files », qui connaissait déjà pacman. Le guide n'annonce le
dépôt que s'il y sera : un guide qui promet ce qui n'existe pas est pire que
pas de guide. Écrit sur la VM Arch de l'utilisateur pour le vérifier.
--- EN ---
Reported on Arch: "no login screen, with the guide telling you to use pacman,
like on ubuntu". It was not Arch — it was Proxmox. The libvirt path delivers
/etc/motd through cloud-init's "write_files", and "qm set" offers no such
thing: NO VM deployed on a Proxmox host had a guide, whatever its distro.
A third delivery path, over ssh once the VM is up, from the SAME source —
"guide_files", which already knew pacman. The guide only announces the
repository if it will be there: a guide promising what does not exist is worse
than none. Written onto the user's Arch VM to check it.
Assisted-by: Claude Opus 5
2026-08-24 14:06:25 -04:00
|
|
|
class TestLeGuideDeConnexion(unittest.TestCase):
|
|
|
|
|
"""Une VM Proxmox n'avait AUCUN guide, quelle que soit sa distribution.
|
|
|
|
|
|
|
|
|
|
Rapporté sur Arch : « pas l'écran de connexion, avec le guide qui dit de
|
|
|
|
|
prendre pacman, comme sur ubuntu ». La voie libvirt livre /etc/motd par le
|
|
|
|
|
« write_files » de cloud-init ; « qm set » n'offre pas cela. Le contenu
|
|
|
|
|
vient de la MÊME source (`guide_files`) et part par ssh.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def _ecrit(self, vm=None, install=None, distro="arch"):
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO
|
|
|
|
|
|
|
|
|
|
todo = TODO.__new__(TODO)
|
|
|
|
|
vus = {}
|
|
|
|
|
todo._pve_ssh = lambda cible, remote, timeout=60: (
|
|
|
|
|
vus.update(cible=cible, remote=remote) or (0, "")
|
|
|
|
|
)
|
|
|
|
|
mod = todo._qemu_import_module()
|
|
|
|
|
vm = vm or {
|
|
|
|
|
"name": "vm-a",
|
|
|
|
|
"distro": distro,
|
|
|
|
|
"version": "latest",
|
|
|
|
|
"arch": "amd64",
|
|
|
|
|
"desktop": "",
|
|
|
|
|
"install_cmd": "",
|
|
|
|
|
}
|
|
|
|
|
spec = {"user": "erplibre", "install": install}
|
|
|
|
|
import contextlib
|
|
|
|
|
import io
|
|
|
|
|
|
|
|
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
|
|
|
ok = todo._pve_write_guide("hote+vm-a", vm, spec, mod)
|
|
|
|
|
vus["ok"] = ok
|
|
|
|
|
return vus
|
|
|
|
|
|
|
|
|
|
def test_the_guide_goes_to_etc_motd_through_the_alias(self):
|
|
|
|
|
vus = self._ecrit()
|
|
|
|
|
self.assertTrue(vus["ok"])
|
|
|
|
|
# Par l'ALIAS : lui seul porte le rebond vers le réseau interne.
|
|
|
|
|
self.assertEqual(vus["cible"], "hote+vm-a")
|
|
|
|
|
self.assertIn("/etc/motd", vus["remote"])
|
|
|
|
|
self.assertIn("sudo tee", vus["remote"])
|
|
|
|
|
|
|
|
|
|
def test_an_arch_vm_is_told_about_pacman(self):
|
|
|
|
|
self.assertIn("pacman", self._ecrit(distro="arch")["remote"])
|
|
|
|
|
|
|
|
|
|
def test_a_debian_vm_is_told_about_apt(self):
|
|
|
|
|
self.assertIn("apt", self._ecrit(distro="debian")["remote"])
|
|
|
|
|
|
|
|
|
|
def test_without_erplibre_the_guide_does_not_promise_a_repository(self):
|
|
|
|
|
# Un guide qui annonce un dépôt absent est un guide qui mente.
|
|
|
|
|
sans = self._ecrit(install=None)["remote"]
|
|
|
|
|
self.assertNotIn("git/erplibre", sans)
|
|
|
|
|
|
|
|
|
|
def test_with_erplibre_it_says_where_it_lives(self):
|
|
|
|
|
avec = self._ecrit(
|
|
|
|
|
install={
|
|
|
|
|
"branch": "develop",
|
|
|
|
|
"cmd": "make install_os && make install_odoo_18",
|
|
|
|
|
}
|
|
|
|
|
)["remote"]
|
|
|
|
|
self.assertIn("git/erplibre", avec)
|
|
|
|
|
|
|
|
|
|
def test_a_failure_is_said_not_swallowed(self):
|
|
|
|
|
import contextlib
|
|
|
|
|
import io
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO
|
|
|
|
|
|
|
|
|
|
todo = TODO.__new__(TODO)
|
|
|
|
|
todo._pve_ssh = lambda *a, **k: (255, "no route")
|
|
|
|
|
mod = todo._qemu_import_module()
|
|
|
|
|
vm = {
|
|
|
|
|
"name": "vm-a",
|
|
|
|
|
"distro": "arch",
|
|
|
|
|
"version": "latest",
|
|
|
|
|
"arch": "amd64",
|
|
|
|
|
"desktop": "",
|
|
|
|
|
"install_cmd": "",
|
|
|
|
|
}
|
|
|
|
|
with contextlib.redirect_stdout(io.StringIO()) as sortie:
|
|
|
|
|
ok = todo._pve_write_guide("x", vm, {"user": "erplibre"}, mod)
|
|
|
|
|
self.assertFalse(ok)
|
|
|
|
|
self.assertIn("⚠", sortie.getvalue())
|
|
|
|
|
|
|
|
|
|
|
[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
|
[FIX] suivi : un relevé Proxmox jeté, et deux lignes qui montraient une autre machine
Sur trois VM d'un même Proxmox, une seule avait ses colonnes vides — et les
deux autres montraient les chiffres d'une AUTRE machine. Deux fautes, dont
une était le miroir d'un correctif précédent.
Le code de sortie de la suite distante est celui de son DERNIER maillon, la
sonde Odoo. Tant qu'Odoo n'écoute pas — c'est-à-dire pendant TOUTE
l'installation, précisément quand on regarde — la boucle finit en échec et le
relevé, parfait, était jeté. On avait corrigé l'erreur inverse, un code 0 pris
pour une réponse ; exiger 0 était la même faute retournée. Seule une liste de
ressources analysable prouve une réponse.
Pendant ce temps, « virsh domstats » indexe par NOM, et un nom se partage :
les deux VM qui avaient un homonyme LOCAL affichaient ses chiffres. Mesuré —
1,5 Gio de RAM sur 12 et 58 Gio de disque sur 65, quand la vraie tournait avec
3 Gio et 25. Les relevés locaux d'une VM qui vit ailleurs sont donc retirés
AVANT d'ajouter ceux de l'hôte : un hôte muet laisse la colonne VIDE, ce qui
est vrai. Une colonne vide se remarque ; une colonne juste et fausse, non.
L'alias enfin. Prendre le nom court quand il se trouvait libre donnait un parc
incohérent : sur ce même déploiement, deux VM ont reçu « hôte+vm » — leurs
noms étaient pris par des domaines locaux — et la troisième son nom court. Une
convention qui dépend de ce qui traîne dans le fichier n'est pas une
convention. Le nom chaîné est systématique.
--- EN ---
Of three VMs on one Proxmox, only one had empty columns — and the other two
showed ANOTHER machine's figures. Two defects, one the mirror of an earlier
fix.
A remote pipeline's exit code is its LAST link's, the Odoo probe. While Odoo
is not listening — that is, during the WHOLE install, exactly when you are
watching — the loop ends in failure and the reading, perfectly good, was
thrown away. We had fixed the opposite error, a 0 taken for an answer;
demanding 0 was the same mistake reversed. Only a parsable resource list
proves an answer.
Meanwhile "virsh domstats" indexes by NAME, and a name is shared: the two VMs
with a LOCAL namesake displayed its figures. Measured — 1.5 GiB of RAM out of
12 and 58 GiB of disk out of 65, while the real one ran on 3 GiB and 25. Local
readings for a VM that lives elsewhere are therefore dropped BEFORE the host's
are added: a silent host leaves the column EMPTY, which is true. An empty
column gets noticed; a plausible wrong one does not.
The alias, finally. Taking the short name while it happened to be free gave an
inconsistent fleet: in that same deployment two VMs got "host+vm" — their
names were held by local domains — and the third its short name. A convention
that depends on what happens to sit in the file is not a convention. The
chained name is now systematic.
Assisted-by: Claude Opus 5
2026-08-25 00:30:54 -04:00
|
|
|
# Hermétique : le choix du nom lit la liste des domaines
|
|
|
|
|
# locaux. Sans ce bouchon, le test dépendrait de la machine qui
|
|
|
|
|
# le lance.
|
[FIX] proxmox : un seul nom par entrée ~/.ssh/config, et le bon
L'entrée portait deux noms sur sa ligne « Host » — le chaîné « hôte+vm » et
le court : « Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest ». Le second est un doublon dès que le premier suffit.
ssh n'a besoin que d'un nom ; le doubler n'ajoute qu'une façon de plus
d'écrire la même adresse. Rapporté.
Un seul, donc, et choisi : le nom court quand il est LIBRE, c'est celui qu'on
tape ; le chaîné quand il désignerait une autre machine — une VM locale
homonyme, ou la VM d'un autre hôte Proxmox. Ce qui a forcé le changement est
nommé à l'écran plutôt que laissé en surprise.
« Pris » se juge sur le ProxyJump du bloc et non sur sa seule présence. Le
test l'a montré avant l'usage : notre propre entrée, réécrite à chaque
déploiement, se prenait pour une rivale et le nom basculait d'une fois sur
l'autre.
--- EN ---
The entry carried two names on its "Host" line — the chained "host+vm" and
the short one: "Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest". The second is redundant as soon as the first is
enough. ssh needs one name; doubling it only adds another way to write the
same address. Reported.
One name then, and a chosen one: the short one while it is FREE, since that
is what you type; the chained one when it would point at another machine — a
local VM of the same name, or another Proxmox host's VM. Whatever forced the
change is named on screen rather than left as a surprise.
"Taken" is judged on the block's ProxyJump, not on its mere presence. The
test showed it before use: our own entry, rewritten at every deployment, took
itself for a rival and the name flipped from one run to the next.
Assisted-by: Claude Opus 5
2026-08-24 22:55:46 -04:00
|
|
|
todo._qemu_list_domains = lambda: []
|
[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
|
|
|
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] suivi : un relevé Proxmox jeté, et deux lignes qui montraient une autre machine
Sur trois VM d'un même Proxmox, une seule avait ses colonnes vides — et les
deux autres montraient les chiffres d'une AUTRE machine. Deux fautes, dont
une était le miroir d'un correctif précédent.
Le code de sortie de la suite distante est celui de son DERNIER maillon, la
sonde Odoo. Tant qu'Odoo n'écoute pas — c'est-à-dire pendant TOUTE
l'installation, précisément quand on regarde — la boucle finit en échec et le
relevé, parfait, était jeté. On avait corrigé l'erreur inverse, un code 0 pris
pour une réponse ; exiger 0 était la même faute retournée. Seule une liste de
ressources analysable prouve une réponse.
Pendant ce temps, « virsh domstats » indexe par NOM, et un nom se partage :
les deux VM qui avaient un homonyme LOCAL affichaient ses chiffres. Mesuré —
1,5 Gio de RAM sur 12 et 58 Gio de disque sur 65, quand la vraie tournait avec
3 Gio et 25. Les relevés locaux d'une VM qui vit ailleurs sont donc retirés
AVANT d'ajouter ceux de l'hôte : un hôte muet laisse la colonne VIDE, ce qui
est vrai. Une colonne vide se remarque ; une colonne juste et fausse, non.
L'alias enfin. Prendre le nom court quand il se trouvait libre donnait un parc
incohérent : sur ce même déploiement, deux VM ont reçu « hôte+vm » — leurs
noms étaient pris par des domaines locaux — et la troisième son nom court. Une
convention qui dépend de ce qui traîne dans le fichier n'est pas une
convention. Le nom chaîné est systématique.
--- EN ---
Of three VMs on one Proxmox, only one had empty columns — and the other two
showed ANOTHER machine's figures. Two defects, one the mirror of an earlier
fix.
A remote pipeline's exit code is its LAST link's, the Odoo probe. While Odoo
is not listening — that is, during the WHOLE install, exactly when you are
watching — the loop ends in failure and the reading, perfectly good, was
thrown away. We had fixed the opposite error, a 0 taken for an answer;
demanding 0 was the same mistake reversed. Only a parsable resource list
proves an answer.
Meanwhile "virsh domstats" indexes by NAME, and a name is shared: the two VMs
with a LOCAL namesake displayed its figures. Measured — 1.5 GiB of RAM out of
12 and 58 GiB of disk out of 65, while the real one ran on 3 GiB and 25. Local
readings for a VM that lives elsewhere are therefore dropped BEFORE the host's
are added: a silent host leaves the column EMPTY, which is true. An empty
column gets noticed; a plausible wrong one does not.
The alias, finally. Taking the short name while it happened to be free gave an
inconsistent fleet: in that same deployment two VMs got "host+vm" — their
names were held by local domains — and the third its short name. A convention
that depends on what happens to sit in the file is not a convention. The
chained name is now systematic.
Assisted-by: Claude Opus 5
2026-08-25 00:30:54 -04:00
|
|
|
# UN nom, et le chaîné : « hôte+vm » dit où la machine vit et ne
|
|
|
|
|
# dépend pas de ce qui traîne dans ~/.ssh/config (voir
|
[FIX] proxmox : un seul nom par entrée ~/.ssh/config, et le bon
L'entrée portait deux noms sur sa ligne « Host » — le chaîné « hôte+vm » et
le court : « Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest ». Le second est un doublon dès que le premier suffit.
ssh n'a besoin que d'un nom ; le doubler n'ajoute qu'une façon de plus
d'écrire la même adresse. Rapporté.
Un seul, donc, et choisi : le nom court quand il est LIBRE, c'est celui qu'on
tape ; le chaîné quand il désignerait une autre machine — une VM locale
homonyme, ou la VM d'un autre hôte Proxmox. Ce qui a forcé le changement est
nommé à l'écran plutôt que laissé en surprise.
« Pris » se juge sur le ProxyJump du bloc et non sur sa seule présence. Le
test l'a montré avant l'usage : notre propre entrée, réécrite à chaque
déploiement, se prenait pour une rivale et le nom basculait d'une fois sur
l'autre.
--- EN ---
The entry carried two names on its "Host" line — the chained "host+vm" and
the short one: "Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest". The second is redundant as soon as the first is
enough. ssh needs one name; doubling it only adds another way to write the
same address. Reported.
One name then, and a chosen one: the short one while it is FREE, since that
is what you type; the chained one when it would point at another machine — a
local VM of the same name, or another Proxmox host's VM. Whatever forced the
change is named on screen rather than left as a surprise.
"Taken" is judged on the block's ProxyJump, not on its mere presence. The
test showed it before use: our own entry, rewritten at every deployment, took
itself for a rival and the name flipped from one run to the next.
Assisted-by: Claude Opus 5
2026-08-24 22:55:46 -04:00
|
|
|
# TestUnSeulNomDansSshConfig).
|
[FIX] suivi : un relevé Proxmox jeté, et deux lignes qui montraient une autre machine
Sur trois VM d'un même Proxmox, une seule avait ses colonnes vides — et les
deux autres montraient les chiffres d'une AUTRE machine. Deux fautes, dont
une était le miroir d'un correctif précédent.
Le code de sortie de la suite distante est celui de son DERNIER maillon, la
sonde Odoo. Tant qu'Odoo n'écoute pas — c'est-à-dire pendant TOUTE
l'installation, précisément quand on regarde — la boucle finit en échec et le
relevé, parfait, était jeté. On avait corrigé l'erreur inverse, un code 0 pris
pour une réponse ; exiger 0 était la même faute retournée. Seule une liste de
ressources analysable prouve une réponse.
Pendant ce temps, « virsh domstats » indexe par NOM, et un nom se partage :
les deux VM qui avaient un homonyme LOCAL affichaient ses chiffres. Mesuré —
1,5 Gio de RAM sur 12 et 58 Gio de disque sur 65, quand la vraie tournait avec
3 Gio et 25. Les relevés locaux d'une VM qui vit ailleurs sont donc retirés
AVANT d'ajouter ceux de l'hôte : un hôte muet laisse la colonne VIDE, ce qui
est vrai. Une colonne vide se remarque ; une colonne juste et fausse, non.
L'alias enfin. Prendre le nom court quand il se trouvait libre donnait un parc
incohérent : sur ce même déploiement, deux VM ont reçu « hôte+vm » — leurs
noms étaient pris par des domaines locaux — et la troisième son nom court. Une
convention qui dépend de ce qui traîne dans le fichier n'est pas une
convention. Le nom chaîné est systématique.
--- EN ---
Of three VMs on one Proxmox, only one had empty columns — and the other two
showed ANOTHER machine's figures. Two defects, one the mirror of an earlier
fix.
A remote pipeline's exit code is its LAST link's, the Odoo probe. While Odoo
is not listening — that is, during the WHOLE install, exactly when you are
watching — the loop ends in failure and the reading, perfectly good, was
thrown away. We had fixed the opposite error, a 0 taken for an answer;
demanding 0 was the same mistake reversed. Only a parsable resource list
proves an answer.
Meanwhile "virsh domstats" indexes by NAME, and a name is shared: the two VMs
with a LOCAL namesake displayed its figures. Measured — 1.5 GiB of RAM out of
12 and 58 GiB of disk out of 65, while the real one ran on 3 GiB and 25. Local
readings for a VM that lives elsewhere are therefore dropped BEFORE the host's
are added: a silent host leaves the column EMPTY, which is true. An empty
column gets noticed; a plausible wrong one does not.
The alias, finally. Taking the short name while it happened to be free gave an
inconsistent fleet: in that same deployment two VMs got "host+vm" — their
names were held by local domains — and the third its short name. A convention
that depends on what happens to sit in the file is not a convention. The
chained name is now systematic.
Assisted-by: Claude Opus 5
2026-08-25 00:30:54 -04:00
|
|
|
self.assertEqual(essai(False, cmd, False), [["pve1+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] suivi : un relevé Proxmox jeté, et deux lignes qui montraient une autre machine
Sur trois VM d'un même Proxmox, une seule avait ses colonnes vides — et les
deux autres montraient les chiffres d'une AUTRE machine. Deux fautes, dont
une était le miroir d'un correctif précédent.
Le code de sortie de la suite distante est celui de son DERNIER maillon, la
sonde Odoo. Tant qu'Odoo n'écoute pas — c'est-à-dire pendant TOUTE
l'installation, précisément quand on regarde — la boucle finit en échec et le
relevé, parfait, était jeté. On avait corrigé l'erreur inverse, un code 0 pris
pour une réponse ; exiger 0 était la même faute retournée. Seule une liste de
ressources analysable prouve une réponse.
Pendant ce temps, « virsh domstats » indexe par NOM, et un nom se partage :
les deux VM qui avaient un homonyme LOCAL affichaient ses chiffres. Mesuré —
1,5 Gio de RAM sur 12 et 58 Gio de disque sur 65, quand la vraie tournait avec
3 Gio et 25. Les relevés locaux d'une VM qui vit ailleurs sont donc retirés
AVANT d'ajouter ceux de l'hôte : un hôte muet laisse la colonne VIDE, ce qui
est vrai. Une colonne vide se remarque ; une colonne juste et fausse, non.
L'alias enfin. Prendre le nom court quand il se trouvait libre donnait un parc
incohérent : sur ce même déploiement, deux VM ont reçu « hôte+vm » — leurs
noms étaient pris par des domaines locaux — et la troisième son nom court. Une
convention qui dépend de ce qui traîne dans le fichier n'est pas une
convention. Le nom chaîné est systématique.
--- EN ---
Of three VMs on one Proxmox, only one had empty columns — and the other two
showed ANOTHER machine's figures. Two defects, one the mirror of an earlier
fix.
A remote pipeline's exit code is its LAST link's, the Odoo probe. While Odoo
is not listening — that is, during the WHOLE install, exactly when you are
watching — the loop ends in failure and the reading, perfectly good, was
thrown away. We had fixed the opposite error, a 0 taken for an answer;
demanding 0 was the same mistake reversed. Only a parsable resource list
proves an answer.
Meanwhile "virsh domstats" indexes by NAME, and a name is shared: the two VMs
with a LOCAL namesake displayed its figures. Measured — 1.5 GiB of RAM out of
12 and 58 GiB of disk out of 65, while the real one ran on 3 GiB and 25. Local
readings for a VM that lives elsewhere are therefore dropped BEFORE the host's
are added: a silent host leaves the column EMPTY, which is true. An empty
column gets noticed; a plausible wrong one does not.
The alias, finally. Taking the short name while it happened to be free gave an
inconsistent fleet: in that same deployment two VMs got "host+vm" — their
names were held by local domains — and the third its short name. A convention
that depends on what happens to sit in the file is not a convention. The
chained name is now systematic.
Assisted-by: Claude Opus 5
2026-08-25 00:30:54 -04:00
|
|
|
self.assertEqual(essai(False, None, True), [["pve1+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] suivi : un relevé Proxmox jeté, et deux lignes qui montraient une autre machine
Sur trois VM d'un même Proxmox, une seule avait ses colonnes vides — et les
deux autres montraient les chiffres d'une AUTRE machine. Deux fautes, dont
une était le miroir d'un correctif précédent.
Le code de sortie de la suite distante est celui de son DERNIER maillon, la
sonde Odoo. Tant qu'Odoo n'écoute pas — c'est-à-dire pendant TOUTE
l'installation, précisément quand on regarde — la boucle finit en échec et le
relevé, parfait, était jeté. On avait corrigé l'erreur inverse, un code 0 pris
pour une réponse ; exiger 0 était la même faute retournée. Seule une liste de
ressources analysable prouve une réponse.
Pendant ce temps, « virsh domstats » indexe par NOM, et un nom se partage :
les deux VM qui avaient un homonyme LOCAL affichaient ses chiffres. Mesuré —
1,5 Gio de RAM sur 12 et 58 Gio de disque sur 65, quand la vraie tournait avec
3 Gio et 25. Les relevés locaux d'une VM qui vit ailleurs sont donc retirés
AVANT d'ajouter ceux de l'hôte : un hôte muet laisse la colonne VIDE, ce qui
est vrai. Une colonne vide se remarque ; une colonne juste et fausse, non.
L'alias enfin. Prendre le nom court quand il se trouvait libre donnait un parc
incohérent : sur ce même déploiement, deux VM ont reçu « hôte+vm » — leurs
noms étaient pris par des domaines locaux — et la troisième son nom court. Une
convention qui dépend de ce qui traîne dans le fichier n'est pas une
convention. Le nom chaîné est systématique.
--- EN ---
Of three VMs on one Proxmox, only one had empty columns — and the other two
showed ANOTHER machine's figures. Two defects, one the mirror of an earlier
fix.
A remote pipeline's exit code is its LAST link's, the Odoo probe. While Odoo
is not listening — that is, during the WHOLE install, exactly when you are
watching — the loop ends in failure and the reading, perfectly good, was
thrown away. We had fixed the opposite error, a 0 taken for an answer;
demanding 0 was the same mistake reversed. Only a parsable resource list
proves an answer.
Meanwhile "virsh domstats" indexes by NAME, and a name is shared: the two VMs
with a LOCAL namesake displayed its figures. Measured — 1.5 GiB of RAM out of
12 and 58 GiB of disk out of 65, while the real one ran on 3 GiB and 25. Local
readings for a VM that lives elsewhere are therefore dropped BEFORE the host's
are added: a silent host leaves the column EMPTY, which is true. An empty
column gets noticed; a plausible wrong one does not.
The alias, finally. Taking the short name while it happened to be free gave an
inconsistent fleet: in that same deployment two VMs got "host+vm" — their
names were held by local domains — and the third its short name. A convention
that depends on what happens to sit in the file is not a convention. The
chained name is now systematic.
Assisted-by: Claude Opus 5
2026-08-25 00:30:54 -04:00
|
|
|
self.assertEqual(essai(True, None, False), [["pve1+vm-a"]])
|
[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
|
|
|
|
|
|
|
|
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)
|