[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
This commit is contained in:
Mathieu Benoit 2026-08-24 04:21:45 -04:00
parent ad72f43daa
commit 215b0aac3f
3 changed files with 64 additions and 1 deletions

View file

@ -987,7 +987,16 @@ class ProxmoxMenuMixin:
)
continue
print(f" ✓ {vm['name']} : {ip}")
if spec.get("add_ssh_config"):
# L'entrée ~/.ssh/config est le SEUL chemin vers cette VM : elle
# est derrière l'hôte Proxmox (pont interne), donc son adresse
# n'est pas routable d'ici et seul le rebond y mène. Décochée
# alors qu'une installation est demandée, l'installation suivie ne
# pouvait pas entrer — elle est donc écrite quand même, et on le
# dit. Sans installation ni suivi, le choix est respecté.
besoin = bool(spec.get("install")) or spec.get("monitor", True)
if not spec.get("add_ssh_config") and besoin:
print(f" → {t('~/.ssh/config written anyway (install)')}")
if spec.get("add_ssh_config") or besoin:
self._write_ssh_config_entry(
vm["name"],
spec.get("user") or "erplibre",

View file

@ -2290,6 +2290,10 @@ TRANSLATIONS = {
"fr": "Aucun affichage ici ; à lancer sur VOTRE poste :",
"en": "No display here; run this on YOUR workstation:",
},
"~/.ssh/config written anyway (install)": {
"fr": "entrée ~/.ssh/config écrite quand même : l'installation passe par le rebond",
"en": "~/.ssh/config written anyway: the install needs the jump",
},
"Still on the distribution kernel:": {
"fr": "Encore sur le noyau de la distribution :",
"en": "Still on the distribution kernel:",

View file

@ -467,6 +467,56 @@ class TestLeSuivi(unittest.TestCase):
def test_unticked_and_nothing_to_install_does_nothing(self):
self.assertEqual(self._apres_creation(install=None, monitor=False), {})
def test_the_ssh_entry_is_written_when_the_install_needs_it(self):
"""La VM est derrière l'hôte : le rebond de ~/.ssh/config est le SEUL
chemin. Décoché alors qu'une installation est demandée, le suivi ne
pouvait pas entrer dans la VM."""
import contextlib
import io
import sys
sys.argv = ["todo.py"]
from script.todo.todo import TODO
def essai(add_ssh_config, install, monitor):
todo = TODO.__new__(TODO)
ecrites = []
todo._write_ssh_config_entry = lambda nom, *a, **k: ecrites.append(
nom
)
todo._ssh_private_key = lambda k: None
todo._pve_guest_ip = lambda vmid, attente=120: ""
todo._qemu_install_erplibre_monitored = lambda *a, **k: None
todo._qemu_install_erplibre_vm = lambda *a, **k: None
spec = {
"host": {"target": "pve1"},
"vms": [
{
"name": "vm-a",
"vmid": 100,
"ipconfig": "ip=10.10.10.150/24,gw=10.10.10.1",
"install_cmd": "",
}
],
"add_ssh_config": add_ssh_config,
"user": "erplibre",
"install": install,
"monitor": monitor,
}
with contextlib.redirect_stdout(io.StringIO()):
todo._pve_after_create(spec["host"], spec, ["vm-a"], "")
return ecrites
cmd = {"branch": "develop", "cmd": "make x", "label": "X"}
# Décoché mais une installation demandée : écrite quand même.
self.assertEqual(essai(False, cmd, False), ["vm-a"])
# Décoché, suivi demandé : le suivi entre aussi par le rebond.
self.assertEqual(essai(False, None, True), ["vm-a"])
# Décoché et rien à faire dans la VM : le choix est respecté.
self.assertEqual(essai(False, None, False), [])
# Coché : écrite, évidemment.
self.assertEqual(essai(True, None, False), ["vm-a"])
def test_ticked_with_an_install_opens_it(self):
vus = self._apres_creation(
install={"branch": "develop", "cmd": "make x", "label": "X"},