[REF] qemu ssh: one flow, and pick where the machines come from
The two entries — « local VMs » and « nested, recursive » — wrote the same
thing and differed only by how far they walked. That is a question, not a
menu, so there is now a single flow and the depth is asked:
Profondeur (1 = ces machines seulement, défaut : 2)
Depth counts LEVELS of machines including the roots, so 1 writes the roots and
probes nothing. The old code always probed, which is why « local VMs only »
needed to be a separate entry at all.
The first question is where to look, because the machine to configure is not
always a VM of this host:
[1] Local QEMU VMs (virsh) the previous behaviour
[2] Hosts from ~/.ssh/config pick among what is already known; each is
probed over SSH and only those actually
running QEMU get their guests configured
[3] Type a host or an IP a raw IP gets an alias, without which
neither the children's ProxyJump nor
virt-manager would have a name to use
A root taken from ~/.ssh/config carries no IP: its address is already in the
file, so nothing is rewritten — the walk simply starts from it. That is the
single change that let the two flows merge.
Verified on a config holding a wildcard, a bastion and a hypervisor: local VMs
reaching their nested guest, ssh-config hosts skipping the bastion (no QEMU)
while configuring the hypervisor's guest and offering only the hypervisor to
virt-manager, a raw IP producing qemu-10-9-9-9 and its child, depth 1 probing
nothing, and [0] asking nothing further.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dcb1af4880
commit
d6ec01d289
2 changed files with 138 additions and 93 deletions
|
|
@ -1316,29 +1316,92 @@ class TODO:
|
||||||
'printf "%s\\t%s\\n" "$n" "$ip"; done'
|
'printf "%s\\t%s\\n" "$n" "$ip"; done'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _qemu_ssh_pick_roots(self):
|
||||||
|
"""D'où partir pour configurer ~/.ssh/config. Renvoie une liste de
|
||||||
|
racines [{alias, ip|None}], ou [] pour renoncer.
|
||||||
|
|
||||||
|
Trois provenances, parce que « la machine à configurer » n'est pas
|
||||||
|
toujours une VM d'ici : elle peut être un hôte déjà connu de
|
||||||
|
~/.ssh/config, ou une adresse qu'on vient d'obtenir."""
|
||||||
|
print(f"\n{t('Where should the machines come from?')}")
|
||||||
|
print(f" [1] {t('Local QEMU VMs (virsh)')} *")
|
||||||
|
print(f" [2] {t('Hosts from ~/.ssh/config')}")
|
||||||
|
print(f" [3] {t('Type a host or an IP')}")
|
||||||
|
print(f" [0] {t('Back')}")
|
||||||
|
answer = input(t("Choice (0-3, default 1): ")).strip()
|
||||||
|
if answer == "0":
|
||||||
|
return []
|
||||||
|
|
||||||
|
if answer == "2":
|
||||||
|
hosts = self._ssh_config_hosts()
|
||||||
|
if not hosts:
|
||||||
|
print(f" {t('~/.ssh/config holds no host.')}")
|
||||||
|
return []
|
||||||
|
for i, name in enumerate(hosts, 1):
|
||||||
|
print(f" [{i}] {name}")
|
||||||
|
raw = input(
|
||||||
|
t("Which hosts? (numbers, comma-separated; blank = all): ")
|
||||||
|
).strip()
|
||||||
|
chosen = (
|
||||||
|
hosts if not raw else self._parse_index_selection(raw, hosts)
|
||||||
|
)
|
||||||
|
# Déjà dans ~/.ssh/config : leur adresse y est, rien à réécrire.
|
||||||
|
return [{"alias": name, "ip": None} for name in chosen or hosts]
|
||||||
|
|
||||||
|
if answer == "3":
|
||||||
|
target = input(f"{t('Host or IP:')} ").strip()
|
||||||
|
if not target:
|
||||||
|
return []
|
||||||
|
if target in self._ssh_config_hosts():
|
||||||
|
return [{"alias": target, "ip": None}]
|
||||||
|
# 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.
|
||||||
|
default_alias = "qemu-" + target.replace(".", "-").replace(
|
||||||
|
"@", "-"
|
||||||
|
)
|
||||||
|
alias = (
|
||||||
|
input(
|
||||||
|
f"{t('Name for ~/.ssh/config')} ({default_alias}): "
|
||||||
|
).strip()
|
||||||
|
or default_alias
|
||||||
|
)
|
||||||
|
return [{"alias": alias, "ip": target}]
|
||||||
|
|
||||||
|
names = self._qemu_pick_domains()
|
||||||
|
if not names:
|
||||||
|
return []
|
||||||
|
ip_map = self._qemu_resolve_ips(names, timeout=60)
|
||||||
|
roots = []
|
||||||
|
for name in names:
|
||||||
|
ip = ip_map.get(name)
|
||||||
|
if not ip:
|
||||||
|
print(f" ⏭ {name}: {t('no IP')}")
|
||||||
|
continue
|
||||||
|
roots.append({"alias": name, "ip": ip})
|
||||||
|
return roots
|
||||||
|
|
||||||
def _qemu_ssh_config_menu(self):
|
def _qemu_ssh_config_menu(self):
|
||||||
"""Écrit les entrées ~/.ssh/config du parc QEMU."""
|
"""Écrit les entrées ~/.ssh/config du parc QEMU.
|
||||||
|
|
||||||
|
Un seul flux : les deux anciennes entrées (« VM locales » et
|
||||||
|
« imbriquées, récursif ») écrivaient la même chose et ne différaient
|
||||||
|
que par la profondeur — c'est donc une question, pas un menu."""
|
||||||
print(f"🔑 {t('SSH configuration for QEMU VMs')}")
|
print(f"🔑 {t('SSH configuration for QEMU VMs')}")
|
||||||
choices = [
|
roots = self._qemu_ssh_pick_roots()
|
||||||
{"prompt_description": t("Update ~/.ssh/config for local VMs")},
|
if not roots:
|
||||||
{
|
return
|
||||||
"prompt_description": t(
|
raw = input(
|
||||||
"Add nested VMs through ProxyJump (recursive)"
|
f"{t('Depth (1 = these machines only, default:')}"
|
||||||
)
|
f" {self._QEMU_SSH_DEPTH}): "
|
||||||
},
|
).strip()
|
||||||
]
|
try:
|
||||||
help_info = self.fill_help_info(choices)
|
max_depth = max(1, int(raw)) if raw else self._QEMU_SSH_DEPTH
|
||||||
while True:
|
except ValueError:
|
||||||
status = click.prompt(help_info)
|
max_depth = self._QEMU_SSH_DEPTH
|
||||||
print()
|
deploy_key = self._is_yes_default_yes(
|
||||||
if status == "0":
|
input(t("Create the SSH key if missing and deploy it? (Y/n): "))
|
||||||
return False
|
)
|
||||||
elif status == "1":
|
self._qemu_ssh_walk(roots, max_depth, deploy_key)
|
||||||
self._qemu_ssh_config_local()
|
|
||||||
elif status == "2":
|
|
||||||
self._qemu_ssh_config_nested()
|
|
||||||
else:
|
|
||||||
print(t("Command not found !"))
|
|
||||||
|
|
||||||
def _qemu_pick_domains(self):
|
def _qemu_pick_domains(self):
|
||||||
"""Fait choisir des VM parmi celles définies. Vide = toutes."""
|
"""Fait choisir des VM parmi celles définies. Vide = toutes."""
|
||||||
|
|
@ -1356,41 +1419,6 @@ class TODO:
|
||||||
chosen = self._parse_index_selection(raw, names)
|
chosen = self._parse_index_selection(raw, names)
|
||||||
return chosen or names
|
return chosen or names
|
||||||
|
|
||||||
def _qemu_ssh_config_local(self):
|
|
||||||
"""Une entrée ~/.ssh/config par VM locale, comme le fait déjà le
|
|
||||||
déploiement — mais sur un parc déjà en place."""
|
|
||||||
names = self._qemu_pick_domains()
|
|
||||||
if not names:
|
|
||||||
return
|
|
||||||
deploy_key = self._is_yes_default_yes(
|
|
||||||
input(t("Create the SSH key if missing and deploy it? (Y/n): "))
|
|
||||||
)
|
|
||||||
# La clé est fixée AVANT d'écrire : c'est elle qui va dans
|
|
||||||
# IdentityFile, et c'est la même qu'on déploiera ensuite.
|
|
||||||
pub = (
|
|
||||||
self._ssh_ensure_key()
|
|
||||||
if deploy_key
|
|
||||||
else self._qemu_default_ssh_key()
|
|
||||||
)
|
|
||||||
identity = self._ssh_private_key(pub)
|
|
||||||
ip_map = self._qemu_resolve_ips(names, timeout=60)
|
|
||||||
written = []
|
|
||||||
for name in names:
|
|
||||||
ip = ip_map.get(name)
|
|
||||||
if not ip:
|
|
||||||
print(f" ⏭ {name}: {t('no IP')}")
|
|
||||||
continue
|
|
||||||
self._write_ssh_config_entry(
|
|
||||||
name, "erplibre", ip, identity_file=identity
|
|
||||||
)
|
|
||||||
written.append(name)
|
|
||||||
print(
|
|
||||||
f"\n✅ {len(written)} {self._plural(t('entry'), len(written))}"
|
|
||||||
f" {t('written')} ({len(names) - len(written)} {t('skipped')})"
|
|
||||||
)
|
|
||||||
if deploy_key:
|
|
||||||
self._ssh_deploy_keys(written)
|
|
||||||
|
|
||||||
def _ssh_ensure_key(self):
|
def _ssh_ensure_key(self):
|
||||||
"""Chemin de la clé PUBLIQUE, générée si aucune n'existe.
|
"""Chemin de la clé PUBLIQUE, générée si aucune n'existe.
|
||||||
|
|
||||||
|
|
@ -1652,27 +1680,17 @@ class TODO:
|
||||||
found.append((parts[0], parts[1].strip()))
|
found.append((parts[0], parts[1].strip()))
|
||||||
return has_libvirt, found
|
return has_libvirt, found
|
||||||
|
|
||||||
def _qemu_ssh_config_nested(self):
|
def _qemu_ssh_walk(self, roots, max_depth, deploy_key):
|
||||||
"""Descend le parc en profondeur et écrit un ProxyJump par niveau.
|
"""Descend le parc depuis `roots` et écrit un ProxyJump par niveau.
|
||||||
|
|
||||||
Une VM du profil « Déploiement » héberge elle-même des VM : celles-ci
|
Une VM du profil « Déploiement » héberge elle-même des VM : celles-ci
|
||||||
n'ont pas d'IP joignable depuis l'hôte, seulement depuis leur parent.
|
n'ont pas d'IP joignable depuis l'hôte, seulement depuis leur parent.
|
||||||
ProxyJump enchaîne les sauts, et la chaîne se construit d'elle-même
|
ProxyJump enchaîne les sauts, et la chaîne se construit d'elle-même
|
||||||
puisque le parent est déjà dans ~/.ssh/config quand on écrit l'enfant.
|
puisque le parent est déjà dans ~/.ssh/config quand on écrit l'enfant.
|
||||||
|
|
||||||
|
Une racine sans IP est un hôte DÉJÀ décrit dans ~/.ssh/config : son
|
||||||
|
adresse y est, on ne la réécrit pas, on part simplement de lui.
|
||||||
"""
|
"""
|
||||||
roots = self._qemu_pick_domains()
|
|
||||||
if not roots:
|
|
||||||
return
|
|
||||||
raw = input(
|
|
||||||
f"{t('Depth (default:')} {self._QEMU_SSH_DEPTH}): "
|
|
||||||
).strip()
|
|
||||||
try:
|
|
||||||
max_depth = max(1, int(raw)) if raw else self._QEMU_SSH_DEPTH
|
|
||||||
except ValueError:
|
|
||||||
max_depth = self._QEMU_SSH_DEPTH
|
|
||||||
deploy_key = self._is_yes_default_yes(
|
|
||||||
input(t("Create the SSH key if missing and deploy it? (Y/n): "))
|
|
||||||
)
|
|
||||||
# La clé est fixée AVANT d'écrire quoi que ce soit : c'est elle qui
|
# La clé est fixée AVANT d'écrire quoi que ce soit : c'est elle qui
|
||||||
# va dans IdentityFile à chaque niveau, et celle qu'on déploiera.
|
# va dans IdentityFile à chaque niveau, et celle qu'on déploiera.
|
||||||
pub = (
|
pub = (
|
||||||
|
|
@ -1682,27 +1700,25 @@ class TODO:
|
||||||
)
|
)
|
||||||
identity = self._ssh_private_key(pub)
|
identity = self._ssh_private_key(pub)
|
||||||
|
|
||||||
# Niveau 0 : les VM locales, jointes directement.
|
|
||||||
ip_map = self._qemu_resolve_ips(roots, timeout=60)
|
|
||||||
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 »
|
||||||
hosts_libvirt = [] # machines qui font tourner QEMU
|
hosts_libvirt = [] # machines qui font tourner QEMU
|
||||||
frontier = []
|
frontier = []
|
||||||
for name in roots:
|
for root in roots:
|
||||||
ip = ip_map.get(name)
|
alias, ip = root["alias"], root.get("ip")
|
||||||
if not ip:
|
if ip:
|
||||||
print(f" ⏭ {name}: {t('no IP')}")
|
self._write_ssh_config_entry(
|
||||||
continue
|
alias, "erplibre", ip, identity_file=identity
|
||||||
self._write_ssh_config_entry(
|
)
|
||||||
name, "erplibre", ip, identity_file=identity
|
entries.append({"names": [alias], "ip": ip, "parent": None})
|
||||||
)
|
taken.add(alias)
|
||||||
entries.append({"names": [name], "ip": ip, "parent": None})
|
chain_of[alias] = alias
|
||||||
taken.add(name)
|
frontier.append(alias)
|
||||||
chain_of[name] = name
|
|
||||||
frontier.append(name)
|
|
||||||
|
|
||||||
for depth in range(1, max_depth + 1):
|
# `max_depth` compte les NIVEAUX de machines, racines comprises : une
|
||||||
|
# profondeur de 1 s'arrête donc ici, sans rien sonder.
|
||||||
|
for depth in range(1, max_depth):
|
||||||
if not frontier:
|
if not frontier:
|
||||||
break
|
break
|
||||||
# La clé est déployée AVANT de sonder : la sonde utilise
|
# La clé est déployée AVANT de sonder : la sonde utilise
|
||||||
|
|
@ -1711,7 +1727,7 @@ class TODO:
|
||||||
if deploy_key:
|
if deploy_key:
|
||||||
self._ssh_deploy_keys(frontier)
|
self._ssh_deploy_keys(frontier)
|
||||||
print(
|
print(
|
||||||
f"\n🔎 {t('Level')} {depth} — "
|
f"\n🔎 {t('Level')} {depth + 1} — "
|
||||||
f"{len(frontier)} {t('machines to probe')}"
|
f"{len(frontier)} {t('machines to probe')}"
|
||||||
)
|
)
|
||||||
next_frontier = []
|
next_frontier = []
|
||||||
|
|
|
||||||
|
|
@ -1512,13 +1512,42 @@ TRANSLATIONS = {
|
||||||
"fr": "Configuration SSH des VM QEMU",
|
"fr": "Configuration SSH des VM QEMU",
|
||||||
"en": "SSH configuration for QEMU VMs",
|
"en": "SSH configuration for QEMU VMs",
|
||||||
},
|
},
|
||||||
"Update ~/.ssh/config for local VMs": {
|
"Where should the machines come from?": {
|
||||||
"fr": "📝 Mettre à jour ~/.ssh/config pour les VM locales",
|
"fr": "D'où viennent les machines à configurer ?",
|
||||||
"en": "📝 Update ~/.ssh/config for local VMs",
|
"en": "Where should the machines come from?",
|
||||||
},
|
},
|
||||||
"Add nested VMs through ProxyJump (recursive)": {
|
"Local QEMU VMs (virsh)": {
|
||||||
"fr": "🪜 Ajouter les VM imbriquées via ProxyJump (récursif)",
|
"fr": "VM QEMU locales (virsh)",
|
||||||
"en": "🪜 Add nested VMs through ProxyJump (recursive)",
|
"en": "Local QEMU VMs (virsh)",
|
||||||
|
},
|
||||||
|
"Hosts from ~/.ssh/config": {
|
||||||
|
"fr": "Hôtes de ~/.ssh/config",
|
||||||
|
"en": "Hosts from ~/.ssh/config",
|
||||||
|
},
|
||||||
|
"Type a host or an IP": {
|
||||||
|
"fr": "Saisir un hôte ou une IP",
|
||||||
|
"en": "Type a host or an IP",
|
||||||
|
},
|
||||||
|
"~/.ssh/config holds no host.": {
|
||||||
|
"fr": "~/.ssh/config ne contient aucun hôte.",
|
||||||
|
"en": "~/.ssh/config holds no host.",
|
||||||
|
},
|
||||||
|
"Which hosts? (numbers, comma-separated; blank = all): ": {
|
||||||
|
"fr": "Quels hôtes ? (numéros séparés par des virgules ; "
|
||||||
|
"vide = tous) : ",
|
||||||
|
"en": "Which hosts? (numbers, comma-separated; blank = all): ",
|
||||||
|
},
|
||||||
|
"Host or IP:": {
|
||||||
|
"fr": "Hôte ou IP :",
|
||||||
|
"en": "Host or IP:",
|
||||||
|
},
|
||||||
|
"Name for ~/.ssh/config": {
|
||||||
|
"fr": "Nom pour ~/.ssh/config",
|
||||||
|
"en": "Name for ~/.ssh/config",
|
||||||
|
},
|
||||||
|
"Depth (1 = these machines only, default:": {
|
||||||
|
"fr": "Profondeur (1 = ces machines seulement, défaut :",
|
||||||
|
"en": "Depth (1 = these machines only, default:",
|
||||||
},
|
},
|
||||||
"Which VMs? (numbers, comma-separated; blank = all): ": {
|
"Which VMs? (numbers, comma-separated; blank = all): ": {
|
||||||
"fr": "Quelles VM ? (numéros séparés par des virgules ; "
|
"fr": "Quelles VM ? (numéros séparés par des virgules ; "
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue