diff --git a/script/todo/qemu_manage.py b/script/todo/qemu_manage.py index f833cc3..5f4788d 100644 --- a/script/todo/qemu_manage.py +++ b/script/todo/qemu_manage.py @@ -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"), ) diff --git a/test/test_qemu_start_egl.py b/test/test_qemu_start_egl.py index 2f7cc40..948ff79 100644 --- a/test/test_qemu_start_egl.py +++ b/test/test_qemu_start_egl.py @@ -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)