[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
This commit is contained in:
Mathieu Benoit 2026-08-24 14:06:25 -04:00
parent 2011bfecfe
commit 9b5d7db2a7
3 changed files with 195 additions and 0 deletions

View file

@ -1080,6 +1080,94 @@ class ProxmoxMenuMixin:
fh.write("\n".join(entete) + "\n")
return chemin
def _pve_write_guide(self, cible, vm, spec, mod):
"""Pose le guide de connexion et l'identité git DANS la VM.
La voie libvirt les livre par le « write_files » de cloud-init ;
« qm set » n'offre pas cela, donc une VM Proxmox n'avait AUCUN guide —
quelle que soit sa distribution. Rapporté sur Arch.
Même contenu, livrée par ssh une fois la VM debout : `guide_files` est
la source unique, comme sa docstring le promet. Un seul appel, tous les
fichiers.
"""
import types
from script.todo.todo_i18n import get_lang
install = spec.get("install") or {}
cmd_install = vm.get("install_cmd") or install.get("cmd") or ""
args = types.SimpleNamespace(
distro=vm.get("distro") or "",
version=vm.get("version") or "",
arch=vm.get("arch") or "amd64",
lang=get_lang(),
# La section ERPLibre n'apparaît que si ERPLibre y sera : un guide
# qui annonce un dépôt absent est un guide qui ment.
erplibre_dir=(
self._qemu_guide_dir(False)
if self._qemu_installs_erplibre(
install.get("branch"), cmd_install
)
else ""
),
erplibre_make=self._qemu_make_target(cmd_install),
desktop=bool(vm.get("desktop")),
no_git_identity=False,
user=spec.get("user") or "erplibre",
)
try:
fichiers = mod.guide_files(args)
except Exception as exc: # pragma: no cover - dépend du module
print(f" ⚠ {t('guide not written')} : {exc}")
return False
morceaux = []
for chemin, mode, contenu, proprio in fichiers:
q = shlex.quote(chemin)
morceaux.append(
f"printf '%s' {shlex.quote(contenu)} | sudo tee {q} "
f">/dev/null && sudo chmod {mode} {q}"
)
if proprio:
morceaux.append(f"sudo chown {shlex.quote(proprio)}: {q}")
code, _o = self._pve_ssh(cible, " && ".join(morceaux))
if code:
print(f" ⚠ {t('guide not written')} ({code})")
return False
print(f" ✓ {t('connection guide written')}")
return True
@staticmethod
def _pve_ssh(cible, remote, timeout=60):
"""(code, sortie) d'une commande exécutée DANS la VM, par son alias.
Par l'alias et non par l'adresse : lui seul porte le rebond vers le
réseau interne de l'hôte."""
from script.proxmox import proxmox_deploy as pve
argv = [
"ssh",
"-o",
"BatchMode=yes",
"-o",
"StrictHostKeyChecking=no",
"-o",
"UserKnownHostsFile=/dev/null",
"-o",
"ConnectTimeout=10",
cible,
remote,
]
try:
res = subprocess.run(
argv, capture_output=True, text=True, timeout=timeout
)
except (OSError, subprocess.SubprocessError) as exc:
return 255, str(exc)
return res.returncode, pve.strip_ssh_noise(
(res.stdout or "") + (res.stderr or "")
)
def _pve_print_summary(self, spec, joignables, session):
"""Sommaire final : ce qui existe, où, et comment y entrer.
@ -1144,6 +1232,10 @@ class ProxmoxMenuMixin:
# Les domaines LOCAUX : un nom partagé avec l'un d'eux fait dérailler
# l'alias ssh et le suivi d'installation.
locaux = set(self._qemu_list_domains())
try:
mod_qemu = self._qemu_import_module()
except Exception:
mod_qemu = None
def alias_chaine(nom):
"""« hôte+vm », la convention déjà utilisée pour les VM
@ -1208,6 +1300,10 @@ class ProxmoxMenuMixin:
print(f" ✓ ~/.ssh/config : ssh {noms_alias[-1]}")
vm["adresse"] = ip
vm["alias"] = alias.get(vm["name"], vm["name"])
# Le guide AVANT l'installation : il doit être là même si rien ne
# s'installe, et l'installation ne le touche pas.
if vm["alias"] and mod_qemu:
self._pve_write_guide(vm["alias"], vm, spec, mod_qemu)
joignables.append(vm)
install = spec.get("install")
# Rendu à l'appelant pour son sommaire : lui seul sait ce qui a été

View file

@ -3986,6 +3986,14 @@ TRANSLATIONS = {
"fr": "Fichiers orphelins :",
"en": "Orphan files:",
},
"connection guide written": {
"fr": "guide de connexion écrit (/etc/motd)",
"en": "connection guide written (/etc/motd)",
},
"guide not written": {
"fr": "guide non écrit",
"en": "guide not written",
},
"Quit (q) to start the ERPLibre install": {
"fr": "Quitter (q) pour lancer l'installation d'ERPLibre",
"en": "Quit (q) to start the ERPLibre install",

View file

@ -519,6 +519,97 @@ class TestLInterpretePython(unittest.TestCase):
self.assertEqual(self._ecran(mise_arches=("s390x",))["choix"], "")
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())
class TestLeSuivi(unittest.TestCase):
"""La case « Suivre l'installation » doit commander quelque chose.