[FIX] qemu ssh: take the login from ~/.ssh/config, not a hardcoded erplibre
Adopting a host from ~/.ssh/config only to write « User erplibre » under its
guests defeats the point: those hosts are not necessarily ERPLibre VMs, and a
guest follows its parent's convention because the parent created it.
The declared User is now read and propagated — to the guest entries, to the
summary line, and to the qemu+ssh URI handed to virt-manager. Only when
nothing is declared does QEMU_VM_USER apply, the cloud-init account of VMs
deployed from here, now a named constant instead of a literal repeated at
each call site.
Resolution follows OpenSSH: the FIRST User among the matching blocks wins,
wildcards included. That is the opposite of what one expects, so it was
checked against ssh -G rather than the manual: with « Host * / User global »
placed first, ssh really does report global even for a host that declares its
own — which is why the manual tells you to put « Host * » last. The
implementation matches on both layouts.
« user@host » is also accepted when typing a raw address, and otherwise the
account is asked with QEMU_VM_USER as default.
ssh-copy-id needed no change: it is given the alias, whose block now carries
the right User.
Verified on a config with a per-host User, a host without one and a trailing
« Host * »: the three resolutions correct, the guest written with mathben and
not erplibre, and virt-manager offered ('hyperviseur', 'mathben').
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d6ec01d289
commit
e8f7e8384a
2 changed files with 105 additions and 15 deletions
|
|
@ -1288,6 +1288,10 @@ class TODO:
|
||||||
if cmd_no_found:
|
if cmd_no_found:
|
||||||
print(t("Command not found !"))
|
print(t("Command not found !"))
|
||||||
|
|
||||||
|
# Compte créé par cloud-init dans les VM déployées ici. Sert de défaut
|
||||||
|
# quand ~/.ssh/config ne déclare aucun `User` pour l'hôte adopté.
|
||||||
|
QEMU_VM_USER = "erplibre"
|
||||||
|
|
||||||
# Profondeur d'exploration par défaut : hôte -> VM -> VM imbriquée. Le
|
# Profondeur d'exploration par défaut : hôte -> VM -> VM imbriquée. Le
|
||||||
# profil « ERPLibre Déploiement (+ QEMU + dev) » installe QEMU DANS la VM,
|
# profil « ERPLibre Déploiement (+ QEMU + dev) » installe QEMU DANS la VM,
|
||||||
# donc un parc à deux niveaux est le cas courant.
|
# donc un parc à deux niveaux est le cas courant.
|
||||||
|
|
@ -1346,26 +1350,48 @@ class TODO:
|
||||||
hosts if not raw else self._parse_index_selection(raw, hosts)
|
hosts if not raw else self._parse_index_selection(raw, hosts)
|
||||||
)
|
)
|
||||||
# Déjà dans ~/.ssh/config : leur adresse y est, rien à réécrire.
|
# Déjà dans ~/.ssh/config : leur adresse y est, rien à réécrire.
|
||||||
return [{"alias": name, "ip": None} for name in chosen or hosts]
|
# Le `User` déclaré est repris tel quel : ces hôtes ne sont pas
|
||||||
|
# forcément des VM ERPLibre, et leurs invitées suivent la même
|
||||||
|
# convention que leur parent.
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"alias": name,
|
||||||
|
"ip": None,
|
||||||
|
"user": self._ssh_config_user(name),
|
||||||
|
}
|
||||||
|
for name in chosen or hosts
|
||||||
|
]
|
||||||
|
|
||||||
if answer == "3":
|
if answer == "3":
|
||||||
target = input(f"{t('Host or IP:')} ").strip()
|
target = input(f"{t('Host or IP:')} ").strip()
|
||||||
if not target:
|
if not target:
|
||||||
return []
|
return []
|
||||||
if target in self._ssh_config_hosts():
|
if target in self._ssh_config_hosts():
|
||||||
return [{"alias": target, "ip": None}]
|
return [
|
||||||
|
{
|
||||||
|
"alias": target,
|
||||||
|
"ip": None,
|
||||||
|
"user": self._ssh_config_user(target),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
# « utilisateur@hôte » est accepté : c'est la forme qu'on tape
|
||||||
|
# naturellement, et elle évite une question de plus.
|
||||||
|
user, _, address = target.rpartition("@")
|
||||||
# Une IP brute n'est pas un alias : on lui en donne un, sinon ni
|
# Une IP brute n'est pas un alias : on lui en donne un, sinon ni
|
||||||
# le ProxyJump des enfants ni virt-manager n'auraient de nom.
|
# le ProxyJump des enfants ni virt-manager n'auraient de nom.
|
||||||
default_alias = "qemu-" + target.replace(".", "-").replace(
|
default_alias = "qemu-" + address.replace(".", "-")
|
||||||
"@", "-"
|
|
||||||
)
|
|
||||||
alias = (
|
alias = (
|
||||||
input(
|
input(
|
||||||
f"{t('Name for ~/.ssh/config')} ({default_alias}): "
|
f"{t('Name for ~/.ssh/config')} ({default_alias}): "
|
||||||
).strip()
|
).strip()
|
||||||
or default_alias
|
or default_alias
|
||||||
)
|
)
|
||||||
return [{"alias": alias, "ip": target}]
|
if not user:
|
||||||
|
user = (
|
||||||
|
input(f"{t('User')} ({self.QEMU_VM_USER}): ").strip()
|
||||||
|
or self.QEMU_VM_USER
|
||||||
|
)
|
||||||
|
return [{"alias": alias, "ip": address, "user": user}]
|
||||||
|
|
||||||
names = self._qemu_pick_domains()
|
names = self._qemu_pick_domains()
|
||||||
if not names:
|
if not names:
|
||||||
|
|
@ -1613,12 +1639,14 @@ 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. `hosts` = [(alias_chaîné, compte)], de sorte
|
||||||
que l'imbrication se lise aussi dans l'interface graphique."""
|
que l'imbrication se lise aussi dans l'interface graphique et que le
|
||||||
|
compte soit celui de ~/.ssh/config, pas un défaut supposé."""
|
||||||
if self._virt_manager_uris() is None:
|
if self._virt_manager_uris() is None:
|
||||||
return
|
return
|
||||||
labels = {
|
labels = {
|
||||||
f"qemu+ssh://erplibre@{alias}/system": alias for alias in hosts
|
f"qemu+ssh://{user or self.QEMU_VM_USER}@{alias}/system": alias
|
||||||
|
for alias, user in hosts
|
||||||
}
|
}
|
||||||
uris = ["qemu:///system"] + list(labels)
|
uris = ["qemu:///system"] + list(labels)
|
||||||
print(f"\n🖥 {t('virt-manager detected')}")
|
print(f"\n🖥 {t('virt-manager detected')}")
|
||||||
|
|
@ -1703,15 +1731,26 @@ class TODO:
|
||||||
entries = [] # un enregistrement par machine écrite
|
entries = [] # un enregistrement par machine écrite
|
||||||
taken = set() # tous les noms d'hôte déjà attribués
|
taken = set() # tous les noms d'hôte déjà attribués
|
||||||
chain_of = {} # alias -> nom chaîné « parent+enfant »
|
chain_of = {} # alias -> nom chaîné « parent+enfant »
|
||||||
|
user_of = {} # alias -> compte de connexion
|
||||||
hosts_libvirt = [] # machines qui font tourner QEMU
|
hosts_libvirt = [] # machines qui font tourner QEMU
|
||||||
frontier = []
|
frontier = []
|
||||||
for root in roots:
|
for root in roots:
|
||||||
alias, ip = root["alias"], root.get("ip")
|
alias, ip = root["alias"], root.get("ip")
|
||||||
|
# Le compte vient de ~/.ssh/config quand il y est déclaré : un
|
||||||
|
# hôte adopté n'est pas forcément une VM ERPLibre.
|
||||||
|
user_of[alias] = root.get("user") or self.QEMU_VM_USER
|
||||||
if ip:
|
if ip:
|
||||||
self._write_ssh_config_entry(
|
self._write_ssh_config_entry(
|
||||||
alias, "erplibre", ip, identity_file=identity
|
alias, user_of[alias], ip, identity_file=identity
|
||||||
|
)
|
||||||
|
entries.append(
|
||||||
|
{
|
||||||
|
"names": [alias],
|
||||||
|
"ip": ip,
|
||||||
|
"parent": None,
|
||||||
|
"user": user_of[alias],
|
||||||
|
}
|
||||||
)
|
)
|
||||||
entries.append({"names": [alias], "ip": ip, "parent": None})
|
|
||||||
taken.add(alias)
|
taken.add(alias)
|
||||||
chain_of[alias] = alias
|
chain_of[alias] = alias
|
||||||
frontier.append(alias)
|
frontier.append(alias)
|
||||||
|
|
@ -1756,15 +1795,23 @@ class TODO:
|
||||||
chain = f"{chain_of[parent]}+{child}"
|
chain = f"{chain_of[parent]}+{child}"
|
||||||
if chain in taken:
|
if chain in taken:
|
||||||
continue # déjà vu (cycle)
|
continue # déjà vu (cycle)
|
||||||
|
# L'invitée hérite du compte de son parent : elle a été
|
||||||
|
# créée par lui, avec la même convention.
|
||||||
|
user_of[chain] = user_of[parent]
|
||||||
self._write_ssh_config_entry(
|
self._write_ssh_config_entry(
|
||||||
chain,
|
chain,
|
||||||
"erplibre",
|
user_of[chain],
|
||||||
ip,
|
ip,
|
||||||
proxy_jump=parent,
|
proxy_jump=parent,
|
||||||
identity_file=identity,
|
identity_file=identity,
|
||||||
)
|
)
|
||||||
entries.append(
|
entries.append(
|
||||||
{"names": [chain], "ip": ip, "parent": parent}
|
{
|
||||||
|
"names": [chain],
|
||||||
|
"ip": ip,
|
||||||
|
"parent": parent,
|
||||||
|
"user": user_of[chain],
|
||||||
|
}
|
||||||
)
|
)
|
||||||
taken.add(chain)
|
taken.add(chain)
|
||||||
chain_of[chain] = chain
|
chain_of[chain] = chain
|
||||||
|
|
@ -1774,14 +1821,20 @@ class TODO:
|
||||||
print(f"\n── {t('SSH hosts written')} ──")
|
print(f"\n── {t('SSH hosts written')} ──")
|
||||||
for item in entries:
|
for item in entries:
|
||||||
via = f" ({t('via')} {item['parent']})" if item["parent"] else ""
|
via = f" ({t('via')} {item['parent']})" if item["parent"] else ""
|
||||||
print(f" ssh {' '.join(item['names']):<44} {item['ip']}{via}")
|
print(
|
||||||
|
f" ssh {' '.join(item['names']):<40}"
|
||||||
|
f" {item['user']}@{item['ip']}{via}"
|
||||||
|
)
|
||||||
|
|
||||||
# Les machines qui hébergent QEMU sont celles qui valent d'être
|
# Les machines qui hébergent QEMU 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
|
# On y présente le nom CHAÎNÉ, pour que l'imbrication se lise aussi
|
||||||
# dans l'interface graphique et pas seulement dans ~/.ssh/config.
|
# dans l'interface graphique et pas seulement dans ~/.ssh/config.
|
||||||
self._virt_manager_offer(
|
self._virt_manager_offer(
|
||||||
[chain_of.get(alias, alias) for alias in hosts_libvirt]
|
[
|
||||||
|
(chain_of.get(alias, alias), user_of.get(alias, ""))
|
||||||
|
for alias in hosts_libvirt
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def _qemu_stats(self):
|
def _qemu_stats(self):
|
||||||
|
|
@ -5409,6 +5462,39 @@ class TODO:
|
||||||
pass
|
pass
|
||||||
return names
|
return names
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _ssh_config_user(host):
|
||||||
|
"""`User` déclaré pour cet hôte dans ~/.ssh/config, ou "".
|
||||||
|
|
||||||
|
On suit la règle d'OpenSSH : le PREMIER `User` rencontré parmi les
|
||||||
|
blocs qui correspondent l'emporte, motifs (`Host *`) compris. Sans
|
||||||
|
déclaration on renvoie "" — il n'y a alors rien à copier, et
|
||||||
|
l'appelant garde son défaut plutôt que d'inventer le nom de session
|
||||||
|
locale."""
|
||||||
|
import fnmatch
|
||||||
|
|
||||||
|
path = os.path.expanduser("~/.ssh/config")
|
||||||
|
matching = False
|
||||||
|
try:
|
||||||
|
with open(path, encoding="utf-8") as fh:
|
||||||
|
for line in fh:
|
||||||
|
stripped = line.strip()
|
||||||
|
if re.match(r"^Host[ \t]+", stripped):
|
||||||
|
matching = any(
|
||||||
|
fnmatch.fnmatch(host, pattern)
|
||||||
|
for pattern in stripped.split()[1:]
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if matching:
|
||||||
|
found = re.match(
|
||||||
|
r"^User[ \t]+(\S+)", stripped, re.IGNORECASE
|
||||||
|
)
|
||||||
|
if found:
|
||||||
|
return found.group(1)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return ""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _port_is_free(port):
|
def _port_is_free(port):
|
||||||
"""Vrai si rien n'écoute sur ce port en local."""
|
"""Vrai si rien n'écoute sur ce port en local."""
|
||||||
|
|
|
||||||
|
|
@ -1541,6 +1541,10 @@ TRANSLATIONS = {
|
||||||
"fr": "Hôte ou IP :",
|
"fr": "Hôte ou IP :",
|
||||||
"en": "Host or IP:",
|
"en": "Host or IP:",
|
||||||
},
|
},
|
||||||
|
"User": {
|
||||||
|
"fr": "Utilisateur",
|
||||||
|
"en": "User",
|
||||||
|
},
|
||||||
"Name for ~/.ssh/config": {
|
"Name for ~/.ssh/config": {
|
||||||
"fr": "Nom pour ~/.ssh/config",
|
"fr": "Nom pour ~/.ssh/config",
|
||||||
"en": "Name for ~/.ssh/config",
|
"en": "Name for ~/.ssh/config",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue