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
197 lines
7.3 KiB
Python
197 lines
7.3 KiB
Python
#!/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 ».
|
|
|
|
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.
|
|
"""
|
|
|
|
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 ""
|
|
|
|
|
|
# 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
|
|
|
|
|
|
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
|
|
|
|
|
|
# (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
|
|
|
|
|
|
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 ""
|