Compare commits

..

No commits in common. "4205c0c0e3e109da448d9cb11d523fbf9ad6914a" and "e687b874d672e67b287159e48da0b12fe7c66b5b" have entirely different histories.

2 changed files with 39 additions and 133 deletions

View file

@ -1455,36 +1455,6 @@ class TODO:
# fichier. Le schéma est le même depuis des années (virt-manager 5.1 # fichier. Le schéma est le même depuis des années (virt-manager 5.1
# inclus) ; on LIT d'abord, et on n'écrit que si la lecture a marché. # inclus) ; on LIT d'abord, et on n'écrit que si la lecture a marché.
_VIRT_MANAGER_SCHEMA = "org.virt-manager.virt-manager.connections" _VIRT_MANAGER_SCHEMA = "org.virt-manager.virt-manager.connections"
# Schéma RELOCATABLE d'UNE connexion : il porte son nom affiché.
_VIRT_MANAGER_CONN_SCHEMA = "org.virt-manager.virt-manager.connection"
@staticmethod
def _virt_manager_conn_path(uri):
"""Chemin dconf des réglages d'une connexion.
virt-manager ne fait aucun échappement : il retire simplement TOUS
les « / » de l'URI et s'en sert comme segment unique
(virtManager/config.py, _make_perconn_key). Le « :», le « @» et le
« + » restent donc tels quels."""
return f"/org/virt-manager/virt-manager/conns/{uri.replace('/', '')}/"
def _virt_manager_set_label(self, uri, label):
"""Fixe le nom affiché d'une connexion. Sans lui, virt-manager
fabrique un libellé à partir de l'URI, où l'imbrication se lit mal."""
target = (
f"{self._VIRT_MANAGER_CONN_SCHEMA}:"
f"{self._virt_manager_conn_path(uri)}"
)
try:
res = subprocess.run(
["gsettings", "set", target, "pretty-name", label],
capture_output=True,
text=True,
timeout=15,
)
except (OSError, subprocess.SubprocessError):
return False
return res.returncode == 0
def _virt_manager_uris(self): def _virt_manager_uris(self):
"""URI déjà connues de virt-manager, ou None s'il n'est pas là.""" """URI déjà connues de virt-manager, ou None s'il n'est pas là."""
@ -1552,14 +1522,12 @@ class TODO:
On passe par l'ALIAS SSH et non par l'IP : le transport qemu+ssh On passe par l'ALIAS SSH et non par l'IP : le transport qemu+ssh
utilise le binaire ssh, donc ~/.ssh/config — l'alias porte déjà utilise le binaire ssh, donc ~/.ssh/config — l'alias porte déjà
l'adresse ET le ProxyJump, ce qu'une IP brute ne saurait pas faire l'adresse ET le ProxyJump, ce qu'une IP brute ne saurait pas faire
pour une VM imbriquée. `hosts` contient les noms CHAÎNÉS, de sorte pour une VM imbriquée."""
que l'imbrication se lise aussi dans l'interface graphique."""
if self._virt_manager_uris() is None: if self._virt_manager_uris() is None:
return return
labels = { uris = ["qemu:///system"] + [
f"qemu+ssh://erplibre@{alias}/system": alias for alias in hosts f"qemu+ssh://erplibre@{alias}/system" for alias in hosts
} ]
uris = ["qemu:///system"] + list(labels)
print(f"\n🖥 {t('virt-manager detected')}") print(f"\n🖥 {t('virt-manager detected')}")
for uri in uris: for uri in uris:
print(f" {uri}") print(f" {uri}")
@ -1567,17 +1535,11 @@ class TODO:
input(t("Add the missing connections to virt-manager? (Y/n): ")) input(t("Add the missing connections to virt-manager? (Y/n): "))
): ):
return return
# Le nom affiché est posé AVANT l'ajout à la liste : virt-manager le
# lit au moment d'afficher la connexion, pas à l'inscription.
for uri, label in labels.items():
self._virt_manager_set_label(uri, label)
added = self._virt_manager_add(uris) added = self._virt_manager_add(uris)
if added: if added:
# Le NOM est relu à chaud (virt-manager écoute /pretty-name), # virt-manager réécrit ses réglages en quittant : s'il tourne, il
# mais la liste des connexions est lue au démarrage : une # écraserait ce qu'on vient d'ajouter.
# nouvelle entrée n'apparaît qu'au prochain lancement. print(f" ⚠ {t('Restart virt-manager if it is open.')}")
note = t("Restart virt-manager to see the new connections")
print(f" ℹ️ {note} ({t('the names apply live')})")
def _qemu_ssh_probe_remote(self, alias): def _qemu_ssh_probe_remote(self, alias):
"""Sonde `alias` : (libvirt_présent, [(nom, ip)]), ou None si """Sonde `alias` : (libvirt_présent, [(nom, ip)]), ou None si
@ -1651,10 +1613,8 @@ class TODO:
# Niveau 0 : les VM locales, jointes directement. # Niveau 0 : les VM locales, jointes directement.
ip_map = self._qemu_resolve_ips(roots, timeout=60) ip_map = self._qemu_resolve_ips(roots, timeout=60)
entries = [] # un enregistrement par machine écrite aliases = {} # alias -> (nom, ip, parent|None)
taken = set() # tous les noms d'hôte déjà attribués hosts_libvirt = [] # machines qui font tourner libvirt
chain_of = {} # alias -> nom chaîné « parent+enfant »
hosts_libvirt = [] # machines qui font tourner QEMU
frontier = [] frontier = []
for name in roots: for name in roots:
ip = ip_map.get(name) ip = ip_map.get(name)
@ -1664,11 +1624,7 @@ class TODO:
self._write_ssh_config_entry( self._write_ssh_config_entry(
name, "erplibre", ip, identity_file=identity name, "erplibre", ip, identity_file=identity
) )
entries.append( aliases[name] = (name, ip, None)
{"names": [name], "ip": ip, "parent": None, "chain": name}
)
taken.add(name)
chain_of[name] = name
frontier.append(name) frontier.append(name)
for depth in range(1, max_depth + 1): for depth in range(1, max_depth + 1):
@ -1703,47 +1659,33 @@ class TODO:
if not ip: if not ip:
print(f" ⏭ {parent} › {child}: {t('no IP')}") print(f" ⏭ {parent} › {child}: {t('no IP')}")
continue continue
# Le nom CHAÎNÉ dit où vit la VM ; le nom court est plus # Le nom court est plus agréable à taper ; on ne le
# agréable à taper. UN SEUL bloc porte les deux — ssh # préfixe du parent qu'en cas de collision, pour ne
# accepte plusieurs noms sur la ligne Host — et le court # jamais écraser l'entrée d'une autre machine.
# n'est retenu que s'il est libre, pour ne jamais masquer alias = child
# une autre machine. if alias in aliases:
chain = f"{chain_of[parent]}+{child}" alias = f"{parent}+{child}"
if chain in taken: if alias in aliases:
continue # déjà vu (cycle) continue # déjà vu (cycle)
names = [chain] if child in taken else [child, chain]
self._write_ssh_config_entry( self._write_ssh_config_entry(
names, alias,
"erplibre", "erplibre",
ip, ip,
proxy_jump=parent, proxy_jump=parent,
identity_file=identity, identity_file=identity,
) )
entries.append( aliases[alias] = (child, ip, parent)
{ next_frontier.append(alias)
"names": names,
"ip": ip,
"parent": parent,
"chain": chain,
}
)
taken.update(names)
chain_of[names[0]] = chain
next_frontier.append(names[0])
frontier = next_frontier frontier = next_frontier
print(f"\n── {t('SSH hosts written')} ──") print(f"\n── {t('SSH hosts written')} ──")
for item in entries: for alias, (name, ip, parent) in aliases.items():
via = f" ({t('via')} {item['parent']})" if item["parent"] else "" via = f" ({t('via')} {parent})" if parent else ""
print(f" ssh {' '.join(item['names']):<44} {item['ip']}{via}") print(f" ssh {alias:<34} {ip}{via}")
# Les machines qui hébergent QEMU sont celles qui valent d'être # Les machines qui hébergent des VM sont celles qui valent d'être
# ajoutées à virt-manager : c'est de là qu'on pilote leurs invitées. # ajoutées à virt-manager : c'est de là qu'on pilote leurs invitées.
# On y présente le nom CHAÎNÉ, pour que l'imbrication se lise aussi self._virt_manager_offer(hosts_libvirt)
# dans l'interface graphique et pas seulement dans ~/.ssh/config.
self._virt_manager_offer(
[chain_of.get(alias, alias) for alias in hosts_libvirt]
)
def _qemu_stats(self): def _qemu_stats(self):
"""Statistiques d'utilisation de QEMU, et remise à zéro. """Statistiques d'utilisation de QEMU, et remise à zéro.
@ -3042,50 +2984,11 @@ class TODO:
priv = pub_path[:-4] if pub_path.endswith(".pub") else pub_path priv = pub_path[:-4] if pub_path.endswith(".pub") else pub_path
return priv if os.path.exists(os.path.expanduser(priv)) else "" return priv if os.path.exists(os.path.expanduser(priv)) else ""
@staticmethod
def _ssh_config_drop_hosts(content, names):
"""Retire les blocs « Host … » qui déclarent l'un de `names`.
On découpe en blocs plutôt que de substituer par expression
régulière : une ligne Host peut porter PLUSIEURS noms, et il faut
alors retirer le bloc entier dès qu'un seul de ses noms est repris —
sinon le même nom se retrouverait défini deux fois, et ssh
appliquerait la première définition rencontrée."""
drop = set(names)
out, block, block_names = [], [], set()
def flush():
if block and not (block_names & drop):
out.extend(block)
for line in content.splitlines(keepends=True):
if re.match(r"^[ \t]*Host[ \t]+", line):
flush()
block = [line]
block_names = set(line.split()[1:])
elif block:
# Une ligne non indentée et non vide clôt le bloc (Match,
# directive globale…) : elle n'appartient à personne.
if line.strip() and not line[:1].isspace():
flush()
block, block_names = [], set()
out.append(line)
else:
block.append(line)
else:
out.append(line)
flush()
return "".join(out)
def _write_ssh_config_entry( def _write_ssh_config_entry(
self, host, user, ip, proxy_jump=None, identity_file=None self, host, user, ip, proxy_jump=None, identity_file=None
): ):
"""Écrit/remplace un bloc « Host <host> » dans ~/.ssh/config. """Écrit/remplace un bloc « Host <host> » dans ~/.ssh/config.
`host` peut être une liste de noms : ils partagent alors un seul bloc.
Sert aux VM imbriquées, joignables par leur nom court ET par leur nom
chaîné « parent+enfant », qui montre où elles vivent.
`proxy_jump` : alias du rebond pour une VM imbriquée, dont l'IP n'est `proxy_jump` : alias du rebond pour une VM imbriquée, dont l'IP n'est
joignable que depuis son hôte. OpenSSH enchaîne les ProxyJump tout joignable que depuis son hôte. OpenSSH enchaîne les ProxyJump tout
seul dès que le parent a lui-même le sien. seul dès que le parent a lui-même le sien.
@ -3093,16 +2996,23 @@ class TODO:
`identity_file` : clé PRIVÉE à présenter. Sans elle, ssh propose `identity_file` : clé PRIVÉE à présenter. Sans elle, ssh propose
toutes les identités de l'agent et un parc un peu fourni déclenche toutes les identités de l'agent et un parc un peu fourni déclenche
« Too many authentication failures » avant d'arriver à la bonne.""" « Too many authentication failures » avant d'arriver à la bonne."""
names = [host] if isinstance(host, str) else list(host)
cfg = os.path.expanduser("~/.ssh/config") cfg = os.path.expanduser("~/.ssh/config")
os.makedirs(os.path.dirname(cfg), exist_ok=True) os.makedirs(os.path.dirname(cfg), exist_ok=True)
existing = "" existing = ""
if os.path.exists(cfg): if os.path.exists(cfg):
with open(cfg, encoding="utf-8") as fh: with open(cfg, encoding="utf-8") as fh:
existing = fh.read() existing = fh.read()
existing = self._ssh_config_drop_hosts(existing, names).rstrip("\n") # Retire un ancien bloc du même Host (ses lignes indentées). Pas de
# drapeau DOTALL : « . » ne doit PAS traverser les sauts de ligne,
# sinon .* engloutirait tout jusqu'à la fin du fichier et effacerait
# les entrées suivantes.
pattern = re.compile(
rf"(?m)^[ \t]*Host[ \t]+{re.escape(host)}[ \t]*\n"
r"(?:[ \t]+[^\n]*\n?)*"
)
existing = pattern.sub("", existing).rstrip("\n")
block = ( block = (
f"Host {' '.join(names)}\n" f"Host {host}\n"
f" HostName {ip}\n" f" HostName {ip}\n"
f" User {user}\n" f" User {user}\n"
# IP DHCP réutilisées entre VM -> on évite l'erreur de clé d'hôte. # IP DHCP réutilisées entre VM -> on évite l'erreur de clé d'hôte.
@ -3123,7 +3033,7 @@ class TODO:
with open(cfg, "w", encoding="utf-8") as fh: with open(cfg, "w", encoding="utf-8") as fh:
fh.write(content) fh.write(content)
os.chmod(cfg, 0o600) os.chmod(cfg, 0o600)
print(f"✅ {t('Added to ~/.ssh/config:')} ssh {names[0]}") print(f"✅ {t('Added to ~/.ssh/config:')} ssh {host}")
def _qemu_list_domains(self): def _qemu_list_domains(self):
"""Noms des VM libvirt définies (via virsh).""" """Noms des VM libvirt définies (via virsh)."""

View file

@ -1504,13 +1504,9 @@ TRANSLATIONS = {
"en": "Add the missing connections to virt-manager? " "en": "Add the missing connections to virt-manager? "
"(Y/n, default: yes): ", "(Y/n, default: yes): ",
}, },
"Restart virt-manager to see the new connections": { "Restart virt-manager if it is open.": {
"fr": "Redémarrer virt-manager pour voir les nouvelles connexions", "fr": "Redémarrer virt-manager s'il est ouvert.",
"en": "Restart virt-manager to see the new connections", "en": "Restart virt-manager if it is open.",
},
"the names apply live": {
"fr": "les noms s'appliquent à chaud",
"en": "the names apply live",
}, },
"SSH hosts written": { "SSH hosts written": {
"fr": "Hôtes SSH écrits", "fr": "Hôtes SSH écrits",