[FIX] qemu menu : passer par le groupe libvirt plutôt que par sudo
Appartenir au groupe libvirt suffit à joindre qemu:///system : le sudo
écrit en dur n'y ajoutait aucun droit et réclamait un mot de passe à
chaque entrée de menu. La question se tranche en ESSAYANT, jamais en
lisant /etc/group : les groupes d'un processus sont figés à l'ouverture
de session, donc la table dit le déclaré, l'essai le faisable. C'est la
distinction que porte aussi l'avertissement d'avant-installation. Un
hyperviseur distant garde sudo, ses droits ne se sondant pas d'ici.
Vérifié : 10 tests, rougis par deux mutations — lire /etc/group, et
conclure « pas de sudo » sur un sondage mort.
--- EN ---
Membership of the libvirt group is enough to reach qemu:///system: the
hardcoded sudo added no right there and asked for a password at every
menu entry. The question is settled by TRYING, never by reading
/etc/group: a process's groups are frozen at session start, so the table
states what is declared, the attempt what is doable. The pre-install
warning carries that same distinction. A remote hypervisor keeps sudo,
its rights not being probeable from here.
Checked: 10 tests, turned red by two mutations — reading /etc/group, and
concluding « no sudo » from a dead probe.
Assisted-by: Claude Opus 5
2026-09-03 00:26:01 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
"""Faut-il « sudo » pour parler à libvirt ? Une seule réponse pour tout TODO.
|
|
|
|
|
|
|
|
|
|
Appartenir au groupe libvirt suffit à joindre qemu:///system : préfixer alors
|
|
|
|
|
les commandes de « sudo » ne donne aucun droit de plus et réclame un mot de
|
|
|
|
|
passe pour rien. La question se tranche en ESSAYANT, jamais en lisant
|
|
|
|
|
/etc/group : les groupes d'un processus sont figés à l'ouverture de session,
|
|
|
|
|
donc un utilisateur fraîchement ajouté y figure sans que le shell courant en
|
|
|
|
|
dispose. L'essai dit ce que le shell peut FAIRE, la table dit ce qui a été
|
|
|
|
|
DÉCLARÉ — et c'est l'écart entre les deux qui explique « je suis pourtant
|
|
|
|
|
dans le groupe ».
|
[ADD] qemu diagnostic : un relevé à transmettre, Gérer scindé en trois
Un problème de VM se résout souvent par quelqu'un qui n'a pas accès à la
machine : vingt et une sondes en lecture écrivent un relevé unique —
hôte, hyperviseur, GPU, outils, stockage — chacune bornée dans le temps,
une commande qui pend ne devant pas retenir le rapport. Sur le pilote
NVIDIA propriétaire, trois conditions vivent hors des briques Mesa et
QEMU, dont la liste de périphériques où libvirt n'ajoute jamais les nœuds
de la carte : le relevé les nomme et propose la commande qui installe ce
qui lui manque, affichée avant la question. Gérer se scinde en trois.
--- EN ---
A VM problem is often solved by someone with no access to the machine:
twenty-one read-only probes write one report — host, hypervisor, GPU,
tools, storage — each time-bounded, since a command that hangs must not
hold the report. On the proprietary NVIDIA driver three conditions live
outside the Mesa and QEMU pieces, among them the device list where
libvirt never adds the card's own nodes: the report names them and offers
the command installing what it lacks, shown before the question is put.
The Manage menu splits in three.
Assisted-by: Claude Opus 5
2026-09-03 04:49:23 -04:00
|
|
|
|
|
|
|
|
Le même fichier porte de quoi ATTEINDRE ces outils : le PATH à assainir pour
|
|
|
|
|
qu'un outil de la distribution ne s'amorce pas sur le venv, et la commande
|
|
|
|
|
qui installe un paquet manquant selon le gestionnaire de l'hôte.
|
[FIX] qemu menu : passer par le groupe libvirt plutôt que par sudo
Appartenir au groupe libvirt suffit à joindre qemu:///system : le sudo
écrit en dur n'y ajoutait aucun droit et réclamait un mot de passe à
chaque entrée de menu. La question se tranche en ESSAYANT, jamais en
lisant /etc/group : les groupes d'un processus sont figés à l'ouverture
de session, donc la table dit le déclaré, l'essai le faisable. C'est la
distinction que porte aussi l'avertissement d'avant-installation. Un
hyperviseur distant garde sudo, ses droits ne se sondant pas d'ici.
Vérifié : 10 tests, rougis par deux mutations — lire /etc/group, et
conclure « pas de sudo » sur un sondage mort.
--- EN ---
Membership of the libvirt group is enough to reach qemu:///system: the
hardcoded sudo added no right there and asked for a password at every
menu entry. The question is settled by TRYING, never by reading
/etc/group: a process's groups are frozen at session start, so the table
states what is declared, the attempt what is doable. The pre-install
warning carries that same distinction. A remote hypervisor keeps sudo,
its rights not being probeable from here.
Checked: 10 tests, turned red by two mutations — reading /etc/group, and
concluding « no sudo » from a dead probe.
Assisted-by: Claude Opus 5
2026-09-03 00:26:01 -04:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import grp
|
|
|
|
|
import os
|
|
|
|
|
import shutil
|
|
|
|
|
import subprocess
|
|
|
|
|
|
|
|
|
|
# Réponse du sondage, gardée pour la session : chaque commande du menu la
|
|
|
|
|
# demande, et lancer un virsh par entrée de menu se verrait.
|
|
|
|
|
_CACHE: bool | None = None
|
|
|
|
|
|
|
|
|
|
PROBE = ["virsh", "--connect", "qemu:///system", "list", "--name"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def reset_cache() -> None:
|
|
|
|
|
"""Oublie le sondage. À appeler après un changement de droits."""
|
|
|
|
|
global _CACHE
|
|
|
|
|
_CACHE = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def libvirt_reachable(force: bool = False) -> bool:
|
|
|
|
|
"""qemu:///system répond-il SANS sudo ?
|
|
|
|
|
|
|
|
|
|
Rend faux quand virsh est absent : il n'y a alors rien à joindre, et le
|
|
|
|
|
dire évite de conclure « il faut sudo » sur une machine sans libvirt.
|
|
|
|
|
"""
|
|
|
|
|
global _CACHE
|
|
|
|
|
if _CACHE is not None and not force:
|
|
|
|
|
return _CACHE
|
|
|
|
|
if shutil.which("virsh") is None:
|
|
|
|
|
_CACHE = False
|
|
|
|
|
return _CACHE
|
|
|
|
|
try:
|
|
|
|
|
probe = subprocess.run(
|
|
|
|
|
PROBE, capture_output=True, text=True, timeout=15
|
|
|
|
|
)
|
|
|
|
|
_CACHE = probe.returncode == 0
|
|
|
|
|
except (OSError, subprocess.SubprocessError):
|
|
|
|
|
_CACHE = False
|
|
|
|
|
return _CACHE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def needs_sudo() -> bool:
|
|
|
|
|
"""Faut-il préfixer les commandes libvirt de « sudo » ?
|
|
|
|
|
|
|
|
|
|
Root n'en a jamais besoin. Sans virsh, il n'y a rien à préfixer : rendre
|
|
|
|
|
faux laisse la commande échouer sur « command not found » plutôt que sur
|
|
|
|
|
une invite de mot de passe qui ne mène nulle part.
|
|
|
|
|
"""
|
|
|
|
|
if os.geteuid() == 0:
|
|
|
|
|
return False
|
|
|
|
|
if shutil.which("virsh") is None:
|
|
|
|
|
return False
|
|
|
|
|
return not libvirt_reachable()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def sudo_prefix() -> str:
|
|
|
|
|
"""« sudo » et son espace, ou la chaîne vide. À coller devant virsh."""
|
|
|
|
|
return "sudo " if needs_sudo() else ""
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 00:57:56 -04:00
|
|
|
# L'URI ne se laisse JAMAIS implicite. Pour un utilisateur non root, libvirt
|
|
|
|
|
# choisit « qemu:///session », un hyperviseur séparé où AUCUNE des VM du
|
|
|
|
|
# système n'existe : « virsh list --all » y rend une liste vide, sans erreur
|
|
|
|
|
# et sans avertissement. Appartenir au groupe libvirt donne le DROIT d'accéder
|
|
|
|
|
# à qemu:///system, mais ne change pas l'URI par défaut. Tant que les
|
|
|
|
|
# commandes passaient par sudo, l'URI de root masquait l'omission.
|
|
|
|
|
LIBVIRT_URI = "qemu:///system"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def virsh_argv(*args: str) -> list:
|
|
|
|
|
"""Argv d'un virsh local : sudo si besoin, URI toujours."""
|
|
|
|
|
prefixe = ["sudo"] if needs_sudo() else []
|
|
|
|
|
return prefixe + ["virsh", "--connect", LIBVIRT_URI, *args]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def virsh_cmd(args: str = "") -> str:
|
|
|
|
|
"""Même chose, en chaîne pour un shell. « args » est déjà échappé."""
|
|
|
|
|
base = f"{sudo_prefix()}virsh --connect {LIBVIRT_URI}"
|
|
|
|
|
return f"{base} {args}" if args else base
|
|
|
|
|
|
|
|
|
|
|
[FIX] qemu menu : sortir le venv du PATH des outils système
Le « bin » du venv est en tête du PATH de chaque commande lancée par le
menu, et il contient un python3. Un outil système écrit en Python et
amorcé par « env python3 » s'y amorce donc, dans un interpréteur où les
modules de la distribution n'existent pas : l'import échoue sur un module
que la machine possède pourtant. Sous sudo le piège était invisible, sudo
réinitialisant le PATH ; le retirer là où il ne servait plus l'a mis au
jour.
Vérifié : 6 tests, rougis par trois mutations. La ligne d'amorçage des
outils visés n'a pas été inspectée : le mécanisme est démontré, pas qu'il
soit la cause sur un hôte donné.
--- EN ---
The venv's « bin » leads the PATH of every command the menu launches, and
it holds a python3. A system tool written in Python and started through
« env python3 » therefore boots on that interpreter, where the
distribution's modules do not exist: the import fails on a module the
machine does have. Under sudo the trap was invisible, sudo resetting the
PATH; removing it where it was no longer needed brought it out.
Checked: 6 tests, turned red by three mutations. The shebang of the tools
concerned was not inspected: the mechanism is demonstrated, not that it
is the cause on any given host.
Assisted-by: Claude Opus 5
2026-09-03 01:36:39 -04:00
|
|
|
def system_path(path: str = "") -> str:
|
|
|
|
|
"""PATH débarrassé des répertoires de venv du projet.
|
|
|
|
|
|
|
|
|
|
TODO tourne DANS son venv, dont le « bin » est en TÊTE du PATH : chaque
|
|
|
|
|
commande lancée par le menu le voit en premier, et ce répertoire contient
|
|
|
|
|
un « python3 ». Un outil système écrit en Python et amorcé par
|
|
|
|
|
« #!/usr/bin/env python3 » y trouve donc l'interpréteur du venv, où les
|
|
|
|
|
modules fournis par la distribution — PyGObject, entre autres — n'existent
|
|
|
|
|
pas, et l'import échoue sur un module que le système possède pourtant.
|
|
|
|
|
|
|
|
|
|
Sous sudo le piège ne se voyait pas : sudo réinitialise le PATH par son
|
|
|
|
|
« secure_path ». Le retirer là où il n'était pas nécessaire l'a mis au
|
|
|
|
|
jour, et l'assainissement doit donc être explicite.
|
|
|
|
|
"""
|
|
|
|
|
path = path or os.environ.get("PATH", "")
|
|
|
|
|
venv = os.environ.get("VIRTUAL_ENV", "")
|
|
|
|
|
gardees = []
|
|
|
|
|
for entree in path.split(os.pathsep):
|
|
|
|
|
if not entree:
|
|
|
|
|
continue
|
|
|
|
|
# Un répertoire du venv courant, ou de n'importe quel « .venv* » du
|
|
|
|
|
# dépôt : les deux mènent au même interpréteur de trop.
|
|
|
|
|
if venv and os.path.normpath(entree).startswith(
|
|
|
|
|
os.path.normpath(venv) + os.sep
|
|
|
|
|
):
|
|
|
|
|
continue
|
|
|
|
|
if any(
|
|
|
|
|
part.startswith(".venv")
|
|
|
|
|
for part in os.path.normpath(entree).split(os.sep)
|
|
|
|
|
):
|
|
|
|
|
continue
|
|
|
|
|
gardees.append(entree)
|
|
|
|
|
return os.pathsep.join(gardees)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def system_env(env: dict | None = None) -> dict:
|
|
|
|
|
"""Environnement où un outil système trouve l'interpréteur système."""
|
|
|
|
|
base = dict(env or os.environ)
|
|
|
|
|
base["PATH"] = system_path(base.get("PATH", ""))
|
|
|
|
|
return base
|
|
|
|
|
|
|
|
|
|
|
[ADD] qemu diagnostic : un relevé à transmettre, Gérer scindé en trois
Un problème de VM se résout souvent par quelqu'un qui n'a pas accès à la
machine : vingt et une sondes en lecture écrivent un relevé unique —
hôte, hyperviseur, GPU, outils, stockage — chacune bornée dans le temps,
une commande qui pend ne devant pas retenir le rapport. Sur le pilote
NVIDIA propriétaire, trois conditions vivent hors des briques Mesa et
QEMU, dont la liste de périphériques où libvirt n'ajoute jamais les nœuds
de la carte : le relevé les nomme et propose la commande qui installe ce
qui lui manque, affichée avant la question. Gérer se scinde en trois.
--- EN ---
A VM problem is often solved by someone with no access to the machine:
twenty-one read-only probes write one report — host, hypervisor, GPU,
tools, storage — each time-bounded, since a command that hangs must not
hold the report. On the proprietary NVIDIA driver three conditions live
outside the Mesa and QEMU pieces, among them the device list where
libvirt never adds the card's own nodes: the report names them and offers
the command installing what it lacks, shown before the question is put.
The Manage menu splits in three.
Assisted-by: Claude Opus 5
2026-09-03 04:49:23 -04:00
|
|
|
# (gestionnaire, binaire à détecter, commande d'installation). L'ordre est
|
|
|
|
|
# celui de la détection : le premier binaire présent gagne.
|
|
|
|
|
PKG_INSTALL = (
|
|
|
|
|
("apt", "apt-get", "sudo apt-get install -y"),
|
|
|
|
|
("dnf", "dnf", "sudo dnf install -y"),
|
|
|
|
|
("pacman", "pacman", "sudo pacman -S --needed --noconfirm"),
|
|
|
|
|
("zypper", "zypper", "sudo zypper --non-interactive install"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def install_cmd_for(par_gestionnaire: dict) -> tuple:
|
|
|
|
|
"""(commande, paquet) pour cet hôte, ou (None, None).
|
|
|
|
|
|
|
|
|
|
`par_gestionnaire` donne le nom du paquet POUR CHAQUE gestionnaire : ces
|
|
|
|
|
noms diffèrent d'une distribution à l'autre, et celui de Debian ne marche
|
|
|
|
|
presque jamais ailleurs. Rend None sur un hôte dont le gestionnaire n'est
|
|
|
|
|
pas connu — proposer une commande qui échouera vaut moins que de dire
|
|
|
|
|
qu'on ne sait pas.
|
|
|
|
|
"""
|
|
|
|
|
for cle, binaire, install in PKG_INSTALL:
|
|
|
|
|
if shutil.which(binaire) and par_gestionnaire.get(cle):
|
|
|
|
|
paquet = par_gestionnaire[cle]
|
|
|
|
|
return f"{install} {paquet}", paquet
|
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
|
|
|
[FIX] qemu menu : passer par le groupe libvirt plutôt que par sudo
Appartenir au groupe libvirt suffit à joindre qemu:///system : le sudo
écrit en dur n'y ajoutait aucun droit et réclamait un mot de passe à
chaque entrée de menu. La question se tranche en ESSAYANT, jamais en
lisant /etc/group : les groupes d'un processus sont figés à l'ouverture
de session, donc la table dit le déclaré, l'essai le faisable. C'est la
distinction que porte aussi l'avertissement d'avant-installation. Un
hyperviseur distant garde sudo, ses droits ne se sondant pas d'ici.
Vérifié : 10 tests, rougis par deux mutations — lire /etc/group, et
conclure « pas de sudo » sur un sondage mort.
--- EN ---
Membership of the libvirt group is enough to reach qemu:///system: the
hardcoded sudo added no right there and asked for a password at every
menu entry. The question is settled by TRYING, never by reading
/etc/group: a process's groups are frozen at session start, so the table
states what is declared, the attempt what is doable. The pre-install
warning carries that same distinction. A remote hypervisor keeps sudo,
its rights not being probeable from here.
Checked: 10 tests, turned red by two mutations — reading /etc/group, and
concluding « no sudo » from a dead probe.
Assisted-by: Claude Opus 5
2026-09-03 00:26:01 -04:00
|
|
|
def group_state(user: str = "") -> tuple[bool, bool]:
|
|
|
|
|
"""(déclaré, actif) pour le groupe libvirt.
|
|
|
|
|
|
|
|
|
|
« déclaré » : le nom figure dans le groupe, d'après la base système.
|
|
|
|
|
« actif » : le processus courant PORTE le groupe. Les deux diffèrent tant
|
|
|
|
|
que la session n'a pas été rouverte, et c'est le cas qui déroute le plus.
|
|
|
|
|
Les deux valent faux quand le groupe n'existe pas — libvirt pas encore
|
|
|
|
|
installé.
|
|
|
|
|
"""
|
|
|
|
|
user = user or _current_user()
|
|
|
|
|
try:
|
|
|
|
|
entry = grp.getgrnam("libvirt")
|
|
|
|
|
except KeyError:
|
|
|
|
|
return False, False
|
|
|
|
|
declared = user in entry.gr_mem
|
|
|
|
|
try:
|
|
|
|
|
declared = declared or grp.getgrgid(os.getgid()).gr_name == "libvirt"
|
|
|
|
|
except (KeyError, OSError):
|
|
|
|
|
pass
|
|
|
|
|
return declared, entry.gr_gid in os.getgroups()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _current_user() -> str:
|
|
|
|
|
"""Le nom de l'utilisateur courant, sans lever si l'environnement ment."""
|
|
|
|
|
try:
|
|
|
|
|
import getpass
|
|
|
|
|
|
|
|
|
|
return getpass.getuser()
|
|
|
|
|
except Exception:
|
|
|
|
|
return os.environ.get("USER") or ""
|