[ADD] deploy: SSH port forwarding, to reach Odoo from the local browser
Reaching a VM's Odoo from the workstation browser meant remembering the
-L syntax and which side « localhost » refers to. Entry [3] of the Deploy
menu asks for a host, a remote port (8069 by default) and a local one, then
holds the tunnel open.
Nothing has to be said about jumps: the ProxyJump already in ~/.ssh/config
applies on its own, which is what makes a NESTED VM reachable — its address
means nothing from here, only from its parent.
Two guards, both from getting it wrong by hand:
· a local port already in use is reported before ssh fails on it;
· a local port that differs from the remote one gets a warning, because
Odoo redirects using web.base.url and would send the browser to an
address that does not exist locally. With matching ports and the usual
web.base.url = http://localhost:8069, there is nothing to adjust.
The host list is read from ~/.ssh/config by a small shared helper: it expands
a Host line carrying several names and drops the wildcard patterns, which are
rules rather than machines.
Verified against a config holding « Host * », a plain host and a two-name
line: the three real names listed in order, selection by number and by name,
8069/8069 by default, 9072:localhost:8072 warning about web.base.url,
matching ports staying silent, an empty host cancelling without running
anything, and the busy-port probe answering correctly on a socket bound then
released.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
00eb0a3c7a
commit
188dad9960
2 changed files with 138 additions and 2 deletions
|
|
@ -876,6 +876,11 @@ class TODO:
|
|||
{"section": t("Local")},
|
||||
{"prompt_description": t("Clone ERPLibre locally (git clone)")},
|
||||
{"prompt_description": t("Configure sshfs")},
|
||||
{
|
||||
"prompt_description": t(
|
||||
"SSH port forwarding (open Odoo in the browser)"
|
||||
)
|
||||
},
|
||||
{"section": t("Remote & services")},
|
||||
{"prompt_description": t("SSH (remote host)...")},
|
||||
{
|
||||
|
|
@ -901,10 +906,12 @@ class TODO:
|
|||
elif status == "2":
|
||||
self._configure_sshfs()
|
||||
elif status == "3":
|
||||
self.prompt_execute_deploy_ssh()
|
||||
self._deploy_port_forward()
|
||||
elif status == "4":
|
||||
self.prompt_execute_qemu()
|
||||
self.prompt_execute_deploy_ssh()
|
||||
elif status == "5":
|
||||
self.prompt_execute_qemu()
|
||||
elif status == "6":
|
||||
self._deploy_ntfy_server()
|
||||
else:
|
||||
print(t("Command not found !"))
|
||||
|
|
@ -5330,6 +5337,87 @@ class TODO:
|
|||
except Exception as e:
|
||||
print(f"{t('Error installing NTFY server: ')}{e}")
|
||||
|
||||
@staticmethod
|
||||
def _ssh_config_hosts():
|
||||
"""Noms d'hôtes déclarés dans ~/.ssh/config, dans l'ordre du fichier.
|
||||
|
||||
Une ligne « Host » peut porter plusieurs noms : on les rend tous. Les
|
||||
motifs (`*`, `?`) sont écartés — ce sont des règles, pas des machines
|
||||
auxquelles se connecter."""
|
||||
path = os.path.expanduser("~/.ssh/config")
|
||||
names = []
|
||||
try:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
if not re.match(r"^[ \t]*Host[ \t]+", line):
|
||||
continue
|
||||
for name in line.split()[1:]:
|
||||
if "*" in name or "?" in name or name in names:
|
||||
continue
|
||||
names.append(name)
|
||||
except OSError:
|
||||
pass
|
||||
return names
|
||||
|
||||
@staticmethod
|
||||
def _port_is_free(port):
|
||||
"""Vrai si rien n'écoute sur ce port en local."""
|
||||
import socket
|
||||
|
||||
with socket.socket() as sock:
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
sock.bind(("127.0.0.1", int(port)))
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def _deploy_port_forward(self):
|
||||
"""Ouvre un tunnel SSH pour joindre un service distant depuis le
|
||||
navigateur local.
|
||||
|
||||
Le port distant est vu DEPUIS la machine cible : « -L
|
||||
local:localhost:distant ». Un rebond éventuel n'a pas à être indiqué —
|
||||
le ProxyJump du bloc ~/.ssh/config s'applique tout seul, ce qui rend
|
||||
joignable une VM imbriquée sans route directe."""
|
||||
print(f"\n🔌 {t('SSH port forwarding')}")
|
||||
hosts = self._ssh_config_hosts()
|
||||
if hosts:
|
||||
for i, name in enumerate(hosts, 1):
|
||||
print(f" [{i}] {name}")
|
||||
host = input(f"{t('Host (number or name):')} ").strip()
|
||||
if not host:
|
||||
print(t("Cancelled."))
|
||||
return
|
||||
if host.isdigit() and 1 <= int(host) <= len(hosts):
|
||||
host = hosts[int(host) - 1]
|
||||
|
||||
raw = input(f"{t('Remote port (default:')} 8069): ").strip()
|
||||
remote = raw if raw.isdigit() else "8069"
|
||||
raw = input(f"{t('Local port (default:')} {remote}): ").strip()
|
||||
local = raw if raw.isdigit() else remote
|
||||
|
||||
if not self._port_is_free(local):
|
||||
print(f" ⚠ {t('Local port already in use:')} {local}")
|
||||
if not self._is_yes(input(t("Try anyway? (y/N): "))):
|
||||
return
|
||||
if local != remote:
|
||||
# Odoo redirige d'après web.base.url : un port local différent
|
||||
# renvoie le navigateur vers une adresse qui n'existe pas chez lui.
|
||||
print(f" ⚠ {t('Local port differs from the remote one.')}")
|
||||
print(f" {t('Odoo redirects using web.base.url; check it')}")
|
||||
print(f" {t('matches http://localhost:')}{local}")
|
||||
|
||||
cmd = f"ssh -N -L {local}:localhost:{remote} {shlex.quote(host)}"
|
||||
print(f"\n 🌐 http://localhost:{local}")
|
||||
print(f" {t('Will execute:')} {cmd}")
|
||||
print(f" {t('Ctrl+C closes the tunnel.')}\n")
|
||||
try:
|
||||
self.execute.exec_command_live(cmd, source_erplibre=False)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
print(f"\n {t('Tunnel closed.')}")
|
||||
|
||||
def _configure_sshfs(self):
|
||||
import getpass
|
||||
import re
|
||||
|
|
|
|||
|
|
@ -1399,6 +1399,54 @@ TRANSLATIONS = {
|
|||
"fr": "Choix (numéro, vide = garder) :",
|
||||
"en": "Choice (number, blank = keep):",
|
||||
},
|
||||
"SSH port forwarding (open Odoo in the browser)": {
|
||||
"fr": "🔌 Redirection de port SSH (ouvrir Odoo dans le navigateur)",
|
||||
"en": "🔌 SSH port forwarding (open Odoo in the browser)",
|
||||
},
|
||||
"SSH port forwarding": {
|
||||
"fr": "Redirection de port SSH",
|
||||
"en": "SSH port forwarding",
|
||||
},
|
||||
"Host (number or name):": {
|
||||
"fr": "Hôte (numéro ou nom) :",
|
||||
"en": "Host (number or name):",
|
||||
},
|
||||
"Remote port (default:": {
|
||||
"fr": "Port distant (défaut :",
|
||||
"en": "Remote port (default:",
|
||||
},
|
||||
"Local port (default:": {
|
||||
"fr": "Port local (défaut :",
|
||||
"en": "Local port (default:",
|
||||
},
|
||||
"Local port already in use:": {
|
||||
"fr": "Port local déjà occupé :",
|
||||
"en": "Local port already in use:",
|
||||
},
|
||||
"Try anyway? (y/N): ": {
|
||||
"fr": "Essayer quand même ? (o/N, défaut : non) : ",
|
||||
"en": "Try anyway? (y/N, default: no): ",
|
||||
},
|
||||
"Local port differs from the remote one.": {
|
||||
"fr": "Le port local diffère du port distant.",
|
||||
"en": "Local port differs from the remote one.",
|
||||
},
|
||||
"Odoo redirects using web.base.url; check it": {
|
||||
"fr": "Odoo redirige d'après web.base.url ; vérifier qu'il",
|
||||
"en": "Odoo redirects using web.base.url; check it",
|
||||
},
|
||||
"matches http://localhost:": {
|
||||
"fr": "vaut bien http://localhost:",
|
||||
"en": "matches http://localhost:",
|
||||
},
|
||||
"Ctrl+C closes the tunnel.": {
|
||||
"fr": "Ctrl+C referme le tunnel.",
|
||||
"en": "Ctrl+C closes the tunnel.",
|
||||
},
|
||||
"Tunnel closed.": {
|
||||
"fr": "Tunnel refermé.",
|
||||
"en": "Tunnel closed.",
|
||||
},
|
||||
"SSH configuration (~/.ssh/config, ProxyJump)": {
|
||||
"fr": "🔑 Configuration SSH (~/.ssh/config, ProxyJump)",
|
||||
"en": "🔑 SSH configuration (~/.ssh/config, ProxyJump)",
|
||||
|
|
|
|||
Loading…
Reference in a new issue