[ADD] qemu diagnostic : le device vidéo reçu par le QEMU en cours

Le relevé disait ce que la définition DEMANDE, jamais ce que la VM a
REÇU ; libvirt peut retirer l'accélération entre les deux sans le
signaler. Or « egl-headless » paraît sur la ligne de commande que la 3D
soit active ou non : le seul témoin est le suffixe « -gl » du device.
Chercher « virgl » n'aide pas — cette forme ne s'écrit plus sur les QEMU
récents, et son absence sur une VM accélérée fait conclure à un réglage
manquant qui n'existe pas.

La sonde lit /proc/<pid>/cmdline, sans privilège, et trie sur argv[0] :
sur la ligne entière elle se compterait elle-même.

--- EN ---

The report told what the definition ASKS for, never what the VM GOT;
libvirt can drop acceleration in between without saying so. And
« egl-headless » shows on the command line whether 3D is live or not:
the only witness is the device's « -gl » suffix. Grepping for « virgl »
does not help — that spelling is gone from recent QEMU, and its absence
on an accelerated VM suggests a missing setting that does not exist.

The probe reads /proc/<pid>/cmdline, unprivileged, and sorts on argv[0]:
on the whole line it would count itself.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-09-03 03:19:23 -04:00
parent 6f7ad39616
commit 9370bff319
2 changed files with 128 additions and 0 deletions

View file

@ -109,6 +109,33 @@ def ssh_orphans(blocs, juge, prefixe="erplibre-"):
return gardes, orphelines
# Programme de la sonde « video du qemu en cours » du diagnostic. Il vit
# ici, et non dans un littéral au milieu de la table des sondes : un
# programme Python cité dans une commande shell citée dans une chaîne
# Python porte trois niveaux d'échappement, et le premier guillemet le
# casse. `shlex.quote` s'occupe du seul niveau qui reste.
_DIAG_VIDEO_PY = """
import glob
MOTS = ("vga", "virtio-gpu", "egl", "virgl", "rendernode")
for chemin in sorted(glob.glob("/proc/[0-9]*/cmdline")):
try:
with open(chemin, "rb") as fh:
argv = fh.read().decode("utf-8", "replace").split(chr(0))
except OSError:
continue
# Le tri se fait sur argv[0] : sur la ligne entière, la sonde se
# verrait elle-même, son propre motif contenant « qemu-system ».
if not argv or "qemu-system" not in argv[0]:
continue
noms = [a[6:].split(",")[0] for a in argv if a.startswith("guest=")]
print(noms[0] if noms else chemin.split("/")[2])
for arg in argv:
if any(mot in arg.lower() for mot in MOTS):
print(" " + arg)
"""
class QemuManageMixin:
"""Menu QEMU/KVM : g\u00e9rer les VM existantes.\n\nLe cycle de vie apr\u00e8s la cr\u00e9ation : lister, allumer et \u00e9teindre, r\u00e9gler le\nmat\u00e9riel, redimensionner (et r\u00e9tr\u00e9cir, ce qui demande de traverser le syst\u00e8me\nde fichiers invit\u00e9 par nbd), effacer, nettoyer les restes, retrouver une\nadresse IP, rouvrir le suivi d'une installation.\n\nC'est le fichier qui appelle \u00ab virsh \u00bb le plus souvent : les helpers qui le\nfont (domstate, dumpxml, c_env) vivent donc ici."""
@ -462,6 +489,19 @@ class QemuManageMixin:
"utilisateur des VM",
"ps -o user=,comm= -C qemu-system-x86_64 2>&1 | sort -u",
),
# Ce que la VM a REÇU, et non ce que sa définition demande : entre
# les deux, libvirt peut avoir retiré l'accélération sans le dire.
# « virtio-vga-gl » est le device 3D des QEMU récents ; l'ancienne
# forme « virtio-vga,virgl=on » ne s'écrit plus, si bien qu'un grep
# sur « virgl » ne rend rien sur un QEMU pourtant accéléré et fait
# conclure à tort qu'il manque un réglage. Le suffixe « -gl » est ce
# qui distingue le device 3D de celui du rendu logiciel, et c'est la
# seule pièce que la ligne de commande dise sans ambiguïté :
# « egl-headless » s'y trouve dans les deux cas.
(
"video du qemu en cours",
"python3 -c " + shlex.quote(_DIAG_VIDEO_PY) + " 2>&1",
),
("stockage", "df -h /var/lib/libvirt/images 2>&1"),
("groupes", "id"),
)

View file

@ -509,3 +509,91 @@ class _FauxModule:
if __name__ == "__main__":
unittest.main()
class LaSondeVideo(unittest.TestCase):
"""La 3D d'une VM se lit sur la ligne de commande de son QEMU.
La définition dit ce qui est DEMANDÉ ; la ligne de commande dit ce qui
a été REÇU. Entre les deux, libvirt peut retirer l'accélération sans le
signaler, et « egl-headless » s'affiche dans les deux cas — chercher ce
seul mot fait conclure à tort que la 3D est en place. Le suffixe
« -gl » du device est la pièce qui tranche.
"""
ARGV = (
"/usr/bin/qemu-system-x86_64",
"-name",
"guest=une-vm,debug-threads=on",
"-device",
"%s,id=video0,max_outputs=1,bus=pcie.0",
"-display",
"egl-headless,rendernode=/dev/dri/renderD128",
"-audiodev",
'{"id":"audio1","driver":"none"}',
)
def _sonder(self, device, argv0=None, extra=()):
"""Sortie de la sonde devant un /proc bâti pour l'occasion."""
from script.todo.qemu_manage import _DIAG_VIDEO_PY
import subprocess
with tempfile.TemporaryDirectory() as tmp:
proc = Path(tmp) / "proc" / "4242"
proc.mkdir(parents=True)
argv = list(self.ARGV)
argv[0] = argv0 or argv[0]
argv[4] = argv[4] % device
argv.extend(extra)
(proc / "cmdline").write_bytes("\0".join(argv).encode() + b"\0")
programme = _DIAG_VIDEO_PY.replace(
"/proc/", str(Path(tmp) / "proc") + "/"
).replace('chemin.split("/")[2]', "chemin")
fini = subprocess.run(
[sys.executable, "-c", programme],
capture_output=True,
text=True,
)
self.assertEqual(fini.returncode, 0, fini.stderr)
return fini.stdout
def test_the_accelerated_device_is_reported(self):
sortie = self._sonder("virtio-vga-gl")
self.assertIn("une-vm", sortie)
self.assertIn("virtio-vga-gl", sortie)
def test_software_rendering_is_told_apart(self):
"""L'épreuve du rapport : les deux cas ne doivent PAS se lire pareil.
« egl-headless » est présent des deux côtés ; un rapport qui ne
montrerait que lui laisserait croire la 3D acquise.
"""
accelere = self._sonder("virtio-vga-gl")
logiciel = self._sonder("virtio-vga")
self.assertIn("egl-headless", accelere)
self.assertIn("egl-headless", logiciel)
self.assertNotEqual(accelere, logiciel)
self.assertNotIn("virtio-vga-gl", logiciel)
def test_a_process_merely_naming_qemu_is_ignored(self):
"""Le tri se fait sur argv[0], et non sur la ligne entière.
Un processus quelconque peut porter « qemu-system » dans ses
arguments — un pager ouvert sur un journal, et la sonde elle-même,
dont le programme contient le mot. Les compter ferait naître des
VM qui n'existent pas. Le contre-exemple porte donc le motif
AILLEURS qu'en tête, sans quoi il ne départage rien.
"""
argv = ["/var/log/qemu-system-x86_64.log"]
self.assertTrue(any("qemu-system" in a for a in argv))
self.assertEqual(
self._sonder("virtio-vga-gl", argv0="/usr/bin/less", extra=argv),
"",
)
def test_the_diagnostic_carries_the_probe(self):
from script.todo.qemu_manage import QemuManageMixin
sondes = dict(QemuManageMixin._DIAG_PROBES)
self.assertIn("video du qemu en cours", sondes)