[FIX] todo qemu : la console vise l'hyperviseur, pas la VM
Le choix [3] listait les domaines libvirt de la machine qui execute todo.py. Or on pilote un parc imbrique depuis l'exterieur : cette machine ne connait pas les VM d'un orchestrateur, et la bonne cible n'apparaissait donc jamais dans la liste. L'ecran VNC appartient a QEMU, donc a l'hyperviseur. Tunneler vers l'invite ne trouve rien : le socket n'existe pas de ce cote. La cible est le ProxyJump declare pour l'hote, lu par « ssh -G » — la seule lecture qui couvre Match, wildcards et Include. Le nom compose « saut+vm » n'est qu'un libelle. Le port est lu sur cet hyperviseur, sans sudo d'abord : le groupe libvirt suffit souvent, et « sudo -n » distant echoue faute de TTY. --- EN --- Choice [3] listed the libvirt domains of the machine running todo.py. But a nested fleet is driven from outside: that machine knows nothing of an orchestrator's VMs, so the right target never showed up. The VNC screen belongs to QEMU, hence to the hypervisor. Tunnelling to the guest finds nothing: the socket does not exist on that side. The target is the ProxyJump declared for the host, read via "ssh -G" — the only reading that covers Match, wildcards and Include. The composite "jump+vm" name is just a label. The port is read on that hypervisor, without sudo first: libvirt group membership often suffices, and remote "sudo -n" fails for lack of a TTY. Assisted-by: Claude Opus 5 (cherry picked from commit 2fb846d502cc8026732b58e1f487d7a6726f17eb)
This commit is contained in:
parent
d35d559273
commit
0da6c7fcbe
1 changed files with 86 additions and 36 deletions
|
|
@ -1555,7 +1555,7 @@ class TODO:
|
|||
print(f" [3] {t('Hypervisor console (QEMU screen, no guest server)')}")
|
||||
kind_answer = input(f"{t('Choice')} [1]: ").strip() or "1"
|
||||
if kind_answer == "3":
|
||||
self._qemu_console_tunnel()
|
||||
self._qemu_console_tunnel(name, src)
|
||||
return
|
||||
port, kind = (5901, "VNC") if kind_answer == "2" else (3389, "RDP")
|
||||
local = port + 1
|
||||
|
|
@ -1597,52 +1597,102 @@ class TODO:
|
|||
« listen=none » QEMU n'ouvre AUCUN socket, et aucun tunnel n'y peut
|
||||
rien tant que le domaine n'est pas redéfini.
|
||||
"""
|
||||
names = self._qemu_list_domains()
|
||||
if not names:
|
||||
print(f" {t('No local VM.')}")
|
||||
return
|
||||
for i, name in enumerate(names, 1):
|
||||
print(f" [{i}] {name}")
|
||||
raw = input(f"{t('Which VM?')} [1]: ").strip() or "1"
|
||||
if not raw.isdigit() or not (1 <= int(raw) <= len(names)):
|
||||
print(t("Cancelled."))
|
||||
return
|
||||
name = names[int(raw) - 1]
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["sudo", "virsh", "vncdisplay", name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
res = None
|
||||
# « 127.0.0.1:0 » désigne le port 5900, « :1 » le 5901, etc.
|
||||
port = None
|
||||
if res and res.returncode == 0:
|
||||
disp = res.stdout.strip().rsplit(":", 1)
|
||||
if len(disp) == 2 and disp[1].isdigit():
|
||||
port = 5900 + int(disp[1])
|
||||
if src != "ssh_config":
|
||||
jump, domain = "", name
|
||||
else:
|
||||
# L'écran VNC appartient à QEMU, donc à l'HYPERVISEUR — pas à
|
||||
# l'invité. Tunneler vers la VM elle-même ne trouve rien : le
|
||||
# socket n'existe pas de ce côté. Vécu, et c'est aussi ce qui
|
||||
# rendait le premier jet de ce menu inutile hors machine locale.
|
||||
#
|
||||
# L'hyperviseur est le ProxyJump déclaré dans ssh_config, lu par
|
||||
# « ssh -G » : c'est la seule lecture qui couvre toutes les formes
|
||||
# d'écriture (Host, Match, wildcards, includes). Le nom composé
|
||||
# « saut+vm » n'est qu'un libellé, il ne fait pas autorité.
|
||||
jump = self._ssh_proxyjump(name)
|
||||
domain = name.rsplit("+", 1)[-1]
|
||||
if not jump:
|
||||
print(f"\n ⚠ {t('No ProxyJump for this host in ~/.ssh/config.')}")
|
||||
print(f" {t('Cannot tell which machine runs its QEMU.')}")
|
||||
return
|
||||
port = self._qemu_vnc_port(domain, jump)
|
||||
# Les commandes de réparation se lancent SUR l'hyperviseur : le préfixe
|
||||
# évite de les copier sur la mauvaise machine, l'erreur naturelle ici.
|
||||
pre = f"ssh {jump} " if jump else ""
|
||||
if not port:
|
||||
print(f"\n ⚠ {t('This VM exposes no VNC port.')}")
|
||||
print(f" {t('Its display is likely spice with listen=none:')}")
|
||||
print(f" sudo virsh dumpxml {name} | grep -A2 '<graphics'")
|
||||
print(f" {pre}sudo virsh dumpxml {domain} | grep -A2 '<graphics'")
|
||||
print(f" {t('To open it on the loopback (VM restart required):')}")
|
||||
print(f" sudo virsh destroy {name}")
|
||||
print(f" sudo virsh edit {name} # <graphics type='vnc'"
|
||||
print(f" {pre}sudo virsh destroy {domain}")
|
||||
print(f" {pre}sudo virsh edit {domain} # <graphics type='vnc'"
|
||||
" port='-1' autoport='yes' listen='127.0.0.1'/>")
|
||||
print(f" sudo virsh start {name}")
|
||||
print(f" {pre}sudo virsh start {domain}")
|
||||
print(f"\n {t('New VMs get this by default; see deploy_qemu.')}")
|
||||
return
|
||||
host, from_ssh = self._qemu_self_address()
|
||||
user = os.environ.get("USER", "user")
|
||||
if not from_ssh:
|
||||
print(f" ⚠ {t('Not in an SSH session: check the host address.')}")
|
||||
if jump:
|
||||
target = jump
|
||||
else:
|
||||
host, from_ssh = self._qemu_self_address()
|
||||
user = os.environ.get("USER", "user")
|
||||
if not from_ssh:
|
||||
print(f" ⚠ {t('Not in an SSH session: check the host address.')}")
|
||||
target = f"{user}@{host}"
|
||||
print(f"\n {t('Run this on YOUR workstation:')}")
|
||||
print(f"\n ssh -N -L {port}:127.0.0.1:{port} {user}@{host}\n")
|
||||
print(f"\n ssh -N -L {port}:127.0.0.1:{port} {target}\n")
|
||||
if jump:
|
||||
print(f" {t('Target is the hypervisor')} ({jump}), "
|
||||
f"{t('not the VM: the socket is QEMU-side.')}")
|
||||
print(f" {t('then point your VNC client at')} localhost:{port}")
|
||||
print(f" {t('The tunnel stays open as long as that ssh runs.')}")
|
||||
|
||||
@staticmethod
|
||||
def _ssh_proxyjump(host):
|
||||
"""ProxyJump effectif d'un hôte, tel que ssh le calcule lui-même.
|
||||
|
||||
« ssh -G » rend la configuration RÉSOLUE : Match, wildcards et Include
|
||||
compris. Relire ~/.ssh/config à la main raterait tout cela.
|
||||
"""
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["ssh", "-G", host], capture_output=True, text=True, timeout=10
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return ""
|
||||
for line in res.stdout.splitlines():
|
||||
if line.startswith("proxyjump "):
|
||||
value = line.split(None, 1)[1].strip()
|
||||
return "" if value.lower() == "none" else value
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _qemu_vnc_port(domain, jump=""):
|
||||
"""Port VNC réel d'un domaine, localement ou sur un hyperviseur distant.
|
||||
|
||||
Il ne se devine pas : libvirt l'attribue au démarrage. « virsh
|
||||
vncdisplay » rend « 127.0.0.1:0 », où le suffixe est le NUMÉRO d'écran
|
||||
— 0 vaut 5900, 1 vaut 5901.
|
||||
|
||||
Sans sudo d'abord : l'appartenance au groupe libvirt suffit souvent, et
|
||||
« sudo -n » distant échouerait sur l'absence de TTY. On ne retombe sur
|
||||
« sudo -n » que si le premier essai n'a rien donné.
|
||||
"""
|
||||
base = ["virsh", "--connect", "qemu:///system", "vncdisplay", domain]
|
||||
for argv in (base, ["sudo", "-n"] + base):
|
||||
cmd = (["ssh", "-o", "BatchMode=yes", jump] + argv) if jump else argv
|
||||
try:
|
||||
res = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=25
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
continue
|
||||
if res.returncode != 0:
|
||||
continue
|
||||
disp = res.stdout.strip().rsplit(":", 1)
|
||||
if len(disp) == 2 and disp[1].isdigit():
|
||||
return 5900 + int(disp[1])
|
||||
return 0
|
||||
|
||||
def _qemu_ssh_config_menu(self):
|
||||
"""Écrit les entrées ~/.ssh/config du parc QEMU.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue