Integrate claude, improve selenium and support qemu #1

Open
mathben wants to merge 224 commits from develop into master
2 changed files with 256 additions and 2 deletions
Showing only changes of commit b8bfb0f8e0 - Show all commits

View file

@ -1193,6 +1193,11 @@ class TODO:
)
},
{"prompt_description": t("Statistics (installs, durations, VMs)")},
{
"prompt_description": t(
"SSH configuration (~/.ssh/config, ProxyJump)"
)
},
{"section": t("Catalog")},
{"prompt_description": t("List available images and specs")},
]
@ -1231,6 +1236,8 @@ class TODO:
elif status == "12":
self._qemu_stats()
elif status == "13":
self._qemu_ssh_config_menu()
elif status == "14":
self._qemu_list_images()
else:
cmd_no_found = True
@ -1247,6 +1254,186 @@ class TODO:
if cmd_no_found:
print(t("Command not found !"))
# Profondeur d'exploration par défaut : hôte -> VM -> VM imbriquée. Le
# profil « ERPLibre Déploiement (+ QEMU + dev) » installe QEMU DANS la VM,
# donc un parc à deux niveaux est le cas courant.
_QEMU_SSH_DEPTH = 2
# Sonde exécutée SUR une machine : un couple « nom<TAB>ip » par VM
# libvirt. Une seule connexion SSH par niveau plutôt qu'une par VM. Le
# bail dnsmasq peut manquer, d'où le repli sur l'agent invité.
_QEMU_SSH_PROBE = (
"for n in $(sudo virsh list --all --name 2>/dev/null); do "
'ip=$(sudo virsh domifaddr "$n" --source lease 2>/dev/null '
"| grep -oE '([0-9]{1,3}\\.){3}[0-9]{1,3}' | head -1); "
'if [ -z "$ip" ]; then '
'ip=$(sudo virsh domifaddr "$n" --source agent 2>/dev/null '
"| grep -oE '([0-9]{1,3}\\.){3}[0-9]{1,3}' "
"| grep -v '^127\\.' | head -1); fi; "
'printf "%s\\t%s\\n" "$n" "$ip"; done'
)
def _qemu_ssh_config_menu(self):
"""Écrit les entrées ~/.ssh/config du parc QEMU."""
print(f"🔑 {t('SSH configuration for QEMU VMs')}")
choices = [
{"prompt_description": t("Update ~/.ssh/config for local VMs")},
{
"prompt_description": t(
"Add nested VMs through ProxyJump (recursive)"
)
},
]
help_info = self.fill_help_info(choices)
while True:
status = click.prompt(help_info)
print()
if status == "0":
return False
elif status == "1":
self._qemu_ssh_config_local()
elif status == "2":
self._qemu_ssh_config_nested()
else:
print(t("Command not found !"))
def _qemu_pick_domains(self):
"""Fait choisir des VM parmi celles définies. Vide = toutes."""
names = self._qemu_list_domains()
if not names:
print(t("No VM found."))
return []
for i, name in enumerate(names, 1):
print(f" [{i}] {name}")
raw = input(
t("Which VMs? (numbers, comma-separated; blank = all): ")
).strip()
if not raw:
return names
chosen = self._parse_index_selection(raw, 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
ip_map = self._qemu_resolve_ips(names, timeout=60)
written = 0
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)
written += 1
print(
f"\n✅ {written} {self._plural(t('entry'), written)}"
f" {t('written')} ({len(names) - written} {t('skipped')})"
)
def _qemu_ssh_probe_remote(self, alias):
"""VM libvirt vues DEPUIS `alias` : [(nom, ip)].
Passe par « ssh <alias> », donc par le bloc ~/.ssh/config qu'on vient
d'écrire : le ProxyJump du parent s'applique tout seul et la même
sonde marche à n'importe quelle profondeur."""
cmd = [
"ssh",
"-o",
"BatchMode=yes",
"-o",
"ConnectTimeout=10",
alias,
self._QEMU_SSH_PROBE,
]
try:
res = subprocess.run(
cmd, capture_output=True, text=True, timeout=90
)
except (OSError, subprocess.SubprocessError):
return None
if res.returncode != 0:
return None
found = []
for line in res.stdout.splitlines():
parts = line.strip().split("\t")
if len(parts) == 2 and parts[0]:
found.append((parts[0], parts[1].strip()))
return found
def _qemu_ssh_config_nested(self):
"""Descend le parc en profondeur et écrit un ProxyJump par niveau.
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.
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.
"""
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
# Niveau 0 : les VM locales, jointes directement.
ip_map = self._qemu_resolve_ips(roots, timeout=60)
aliases = {} # alias -> (nom, ip, parent|None)
frontier = []
for name in roots:
ip = ip_map.get(name)
if not ip:
print(f" ⏭ {name}: {t('no IP')}")
continue
self._write_ssh_config_entry(name, "erplibre", ip)
aliases[name] = (name, ip, None)
frontier.append(name)
for depth in range(1, max_depth + 1):
if not frontier:
break
print(
f"\n🔎 {t('Level')} {depth} — "
f"{len(frontier)} {t('machines to probe')}"
)
next_frontier = []
for parent in frontier:
found = self._qemu_ssh_probe_remote(parent)
if found is None:
print(f" ⏭ {parent}: {t('unreachable or no libvirt')}")
continue
if not found:
print(f" · {parent}: {t('no nested VM')}")
continue
for child, ip in found:
if not ip:
print(f" ⏭ {parent} › {child}: {t('no IP')}")
continue
# Le nom court est plus agréable à taper ; on ne le
# préfixe du parent qu'en cas de collision, pour ne
# jamais écraser l'entrée d'une autre machine.
alias = child
if alias in aliases:
alias = f"{parent}+{child}"
if alias in aliases:
continue # déjà vu (cycle)
self._write_ssh_config_entry(
alias, "erplibre", ip, proxy_jump=parent
)
aliases[alias] = (child, ip, parent)
next_frontier.append(alias)
frontier = next_frontier
print(f"\n── {t('SSH hosts written')} ──")
for alias, (name, ip, parent) in aliases.items():
via = f" ({t('via')} {parent})" if parent else ""
print(f" ssh {alias:<34} {ip}{via}")
def _qemu_stats(self):
"""Statistiques d'utilisation de QEMU, et remise à zéro.
@ -2534,8 +2721,12 @@ class TODO:
print(f"{t('Will execute:')} {cmd}")
self.execute.exec_command_live(cmd, source_erplibre=False)
def _write_ssh_config_entry(self, host, user, ip):
"""Écrit/remplace un bloc « Host <host> » dans ~/.ssh/config."""
def _write_ssh_config_entry(self, host, user, ip, proxy_jump=None):
"""Écrit/remplace un bloc « Host <host> » dans ~/.ssh/config.
`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
seul dès que le parent a lui-même le sien."""
cfg = os.path.expanduser("~/.ssh/config")
os.makedirs(os.path.dirname(cfg), exist_ok=True)
existing = ""
@ -2559,6 +2750,8 @@ class TODO:
f" StrictHostKeyChecking no\n"
f" UserKnownHostsFile /dev/null\n"
)
if proxy_jump:
block += f" ProxyJump {proxy_jump}\n"
content = (existing + "\n\n" + block) if existing else block
with open(cfg, "w", encoding="utf-8") as fh:
fh.write(content)

View file

@ -1399,6 +1399,67 @@ TRANSLATIONS = {
"fr": "Choix (numéro, vide = garder) :",
"en": "Choice (number, blank = keep):",
},
"SSH configuration (~/.ssh/config, ProxyJump)": {
"fr": "🔑 Configuration SSH (~/.ssh/config, ProxyJump)",
"en": "🔑 SSH configuration (~/.ssh/config, ProxyJump)",
},
"SSH configuration for QEMU VMs": {
"fr": "Configuration SSH des VM QEMU",
"en": "SSH configuration for QEMU VMs",
},
"Update ~/.ssh/config for local VMs": {
"fr": "📝 Mettre à jour ~/.ssh/config pour les VM locales",
"en": "📝 Update ~/.ssh/config for local VMs",
},
"Add nested VMs through ProxyJump (recursive)": {
"fr": "🪜 Ajouter les VM imbriquées via ProxyJump (récursif)",
"en": "🪜 Add nested VMs through ProxyJump (recursive)",
},
"Which VMs? (numbers, comma-separated; blank = all): ": {
"fr": "Quelles VM ? (numéros séparés par des virgules ; "
"vide = toutes) : ",
"en": "Which VMs? (numbers, comma-separated; blank = all): ",
},
"Depth (default:": {
"fr": "Profondeur (défaut :",
"en": "Depth (default:",
},
"entry": {
"fr": "entrée",
"en": "entry",
},
"written": {
"fr": "écrite(s)",
"en": "written",
},
"skipped": {
"fr": "ignorée(s)",
"en": "skipped",
},
"Level": {
"fr": "Niveau",
"en": "Level",
},
"machines to probe": {
"fr": "machines à sonder",
"en": "machines to probe",
},
"unreachable or no libvirt": {
"fr": "injoignable ou sans libvirt",
"en": "unreachable or no libvirt",
},
"no nested VM": {
"fr": "aucune VM imbriquée",
"en": "no nested VM",
},
"SSH hosts written": {
"fr": "Hôtes SSH écrits",
"en": "SSH hosts written",
},
"via": {
"fr": "via",
"en": "via",
},
"Odoo migration interface": {
"fr": "🚚 Interface de la migration Odoo",
"en": "🚚 Odoo migration interface",