Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ Breaking Changes
* Remove undocumented support for `tcp://` and `socket://` DSNs.


Features
---------
* Add `--ssh-jump` CLI argument, restoring SSH jump functionality.


Documentation
---------
* Fix a typo in myclirc commentary.
Expand Down
77 changes: 40 additions & 37 deletions mycli/cli_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,45 +311,48 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
use_keyring = str_to_bool(cli_args.use_keyring)
reset_keyring = False

mycli.connect(
database=database,
user=cli_args.user,
passwd=cli_args.password,
host=cli_args.host,
port=cli_args.port,
socket=cli_args.socket,
local_infile=cli_args.local_infile,
ssl=ssl,
init_command=combined_init_cmd,
unbuffered=cli_args.unbuffered,
character_set=cli_args.character_set,
use_keyring=use_keyring,
reset_keyring=reset_keyring,
keepalive_ticks=keepalive_ticks,
)

if combined_init_cmd:
click.echo(f"Executing init-command: {combined_init_cmd}", err=True)

mycli.logger.debug(
"Launch Params: \n\tdatabase: %r\tuser: %r\thost: %r\tport: %r",
database,
cli_args.user,
cli_args.host,
cli_args.port,
)
try:
mycli.connect(
database=database,
user=cli_args.user,
passwd=cli_args.password,
host=cli_args.host,
port=cli_args.port,
socket=cli_args.socket,
local_infile=cli_args.local_infile,
ssl=ssl,
init_command=combined_init_cmd,
unbuffered=cli_args.unbuffered,
character_set=cli_args.character_set,
use_keyring=use_keyring,
reset_keyring=reset_keyring,
keepalive_ticks=keepalive_ticks,
ssh_jump=cli_args.ssh_jump,
)

if combined_init_cmd:
click.echo(f"Executing init-command: {combined_init_cmd}", err=True)

mycli.logger.debug(
"Launch Params: \n\tdatabase: %r\tuser: %r\thost: %r\tport: %r",
database,
cli_args.user,
cli_args.host,
cli_args.port,
)

if cli_args.execute is not None:
sys.exit(main_execute_from_cli(mycli, cli_args))
if cli_args.execute is not None:
sys.exit(main_execute_from_cli(mycli, cli_args))

if cli_args.batch is not None and cli_args.batch != '-' and cli_args.progress and sys.stderr.isatty():
sys.exit(main_batch_with_progress_bar(mycli, cli_args))
if cli_args.batch is not None and cli_args.batch != '-' and cli_args.progress and sys.stderr.isatty():
sys.exit(main_batch_with_progress_bar(mycli, cli_args))

if cli_args.batch is not None:
sys.exit(main_batch_without_progress_bar(mycli, cli_args))
if cli_args.batch is not None:
sys.exit(main_batch_without_progress_bar(mycli, cli_args))

if not sys.stdin.isatty():
sys.exit(main_batch_from_stdin(mycli, cli_args))
if not sys.stdin.isatty():
sys.exit(main_batch_from_stdin(mycli, cli_args))

mycli.run_cli()
mycli.close()
mycli.run_cli()
finally:
mycli.close()
16 changes: 14 additions & 2 deletions mycli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from mycli.schema_prefetcher import SchemaPrefetcher
from mycli.sqlcompleter import SQLCompleter
from mycli.sqlexecute import SQLExecute
from mycli.ssh_tunnel import SshTunnel
from mycli.types import Query

sqlparse.engine.grouping.MAX_GROUPING_DEPTH = None # type: ignore[assignment]
Expand Down Expand Up @@ -75,6 +76,7 @@ def __init__(
cli_verbosity: int = 0,
) -> None:
self.sqlexecute = sqlexecute
self.ssh_tunnel: SshTunnel | None = None
self.logfile = logfile
self.login_path = login_path
self.toolbar_error_message: str | None = None
Expand Down Expand Up @@ -198,10 +200,20 @@ def __init__(
special.set_destructive_keywords(self.destructive_keywords)

def close(self) -> None:
if hasattr(self, 'schema_prefetcher'):
try:
self.schema_prefetcher.stop()
except Exception:
pass
if self.sqlexecute is not None:
self.sqlexecute.close()
try:
self.sqlexecute.close()
except Exception:
pass
if self.ssh_tunnel is not None:
try:
self.ssh_tunnel.close()
except Exception:
pass

def run_cli(self) -> None:
repl_package.main_repl(self)
65 changes: 52 additions & 13 deletions mycli/client_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import sys
import traceback
from typing import TYPE_CHECKING, Any
from urllib.parse import quote as urlquote

import click
import keyring
Expand All @@ -22,6 +23,7 @@
)
from mycli.packages.filepaths import guess_socket_location
from mycli.sqlexecute import SQLExecute
from mycli.ssh_tunnel import SshTunnel, SshTunnelError

try:
from pwd import getpwuid
Expand Down Expand Up @@ -58,6 +60,7 @@ def connect(
use_keyring: bool | None = None,
reset_keyring: bool | None = None,
keepalive_ticks: int | None = None,
ssh_jump: str | None = None,
) -> None:
mylogin_cnf: dict[str, Any] = self.read_mylogin_cnf(self.mylogin_cnf)
# Fall back to .mylogin.cnf values only if user did not specify a value.
Expand All @@ -67,13 +70,39 @@ def connect(
ssl_config: dict[str, Any] = ssl or {}
user_connection_config = self.config_without_package_defaults.get('connection', {})
self.keepalive_ticks = keepalive_ticks
self.ssh_tunnel = None

int_port = port and int(port)
if not int_port:
int_port = DEFAULT_PORT
if not host or host == DEFAULT_HOST:
socket = socket or user_connection_config.get("default_socket") or mylogin_cnf["socket"] or guess_socket_location()

if ssh_jump:
remote_host = host or DEFAULT_HOST
remote_port = int_port or DEFAULT_PORT
remote_socket = socket or None
ssh_executable = self.config.get('ssh', {}).get('ssh_executable', 'ssh') or 'ssh'
ssh_options = self.config.get('ssh', {}).get('ssh_options')
try:
self.ssh_tunnel = SshTunnel.from_target(
ssh_jump,
remote_host=remote_host,
remote_port=int(remote_port),
remote_socket=remote_socket,
ssh_executable=ssh_executable,
ssh_options=ssh_options,
)
self.ssh_tunnel.start()
except (OSError, ValueError, SshTunnelError) as exc:
click.secho(f'Error: Unable to start SSH tunnel: {exc}', err=True, fg='red')
try:
if self.ssh_tunnel:
self.ssh_tunnel.close()
except Exception:
pass
sys.exit(1)

passwd = passwd if isinstance(passwd, (str, int)) else mylogin_cnf["password"]

if not character_set:
Expand Down Expand Up @@ -135,7 +164,10 @@ def connect(
# 5. .mylogin.cnf
# 6. keyring

keyring_identifier = f'{user}@{host}:{"" if socket else int_port}:{socket or ""}'
ssh_tunnel_field = urlquote(ssh_jump or '')
if ssh_tunnel_field:
ssh_tunnel_field = ':' + ssh_tunnel_field
keyring_identifier = f'{user}@{host}:{"" if socket else int_port}:{socket or ""}{ssh_tunnel_field}'
keyring_domain = 'mycli.net'
keyring_retrieved_cleanly = False

Expand All @@ -152,19 +184,24 @@ def connect(
# should not fail, but will help the typechecker
assert not isinstance(passwd, int)

connection_info: dict[Any, Any] = {
"database": database,
"user": user,
"password": passwd,
"host": host,
"port": int_port,
"socket": socket,
"character_set": character_set,
"local_infile": use_local_infile,
"ssl": ssl_config,
"init_command": init_command,
"unbuffered": unbuffered,
connection_info: dict[str, Any] = {
'database': database,
'user': user,
'password': passwd,
'character_set': character_set,
'local_infile': use_local_infile,
'ssl': ssl_config,
'init_command': init_command,
'unbuffered': unbuffered,
}
if self.ssh_tunnel:
connection_info['host'] = self.ssh_tunnel.local_host
connection_info['port'] = self.ssh_tunnel.local_port
connection_info['socket'] = None
else:
connection_info['host'] = host
connection_info['port'] = int_port
connection_info['socket'] = socket

def _update_keyring(password: str | None, keyring_retrieved_cleanly: bool):
if not password:
Expand Down Expand Up @@ -239,6 +276,8 @@ def _connect(
socket_owner = getpwuid(os.stat(socket).st_uid).pw_name
except KeyError:
socket_owner = '<unknown>'
except FileNotFoundError:
socket_owner = '<unknown>'
self.echo(f"Connecting to socket {socket}, owned by user {socket_owner}", err=True)
try:
_connect(keyring_retrieved_cleanly=keyring_retrieved_cleanly)
Expand Down
22 changes: 13 additions & 9 deletions mycli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ class CliArgs:
type=int,
help='Send regular keepalive pings to the connection, roughly every <int> seconds.',
)
ssh_jump: str | None = clickdc.option(
type=str,
help='Open an SSH tunnel via [user@]host[:port] and connect to MySQL through it.',
)
checkup: bool = clickdc.option(
is_flag=True,
help='Run a checkup on your configuration.',
Expand All @@ -287,47 +291,47 @@ class CliArgs:
ssh_user: str | None = clickdc.option(
type=str,
hidden=True,
deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
deprecated='No effect. See --ssh-jump.',
)
ssh_host: str | None = clickdc.option(
type=str,
hidden=True,
deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
deprecated='No effect. See --ssh-jump.',
)
ssh_port: int = clickdc.option(
type=int,
hidden=True,
deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
deprecated='No effect. See --ssh-jump.',
)
ssh_password: str | None = clickdc.option(
type=str,
hidden=True,
deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
deprecated='No effect. See --ssh-jump.',
)
ssh_key_filename: str | None = clickdc.option(
type=str,
hidden=True,
deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
deprecated='No effect. See --ssh-jump.',
)
ssh_config_path: str = clickdc.option(
type=str,
hidden=True,
deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
deprecated='No effect. See --ssh-jump.',
)
ssh_config_host: str | None = clickdc.option(
type=str,
hidden=True,
deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
deprecated='No effect. See --ssh-jump.',
)
list_ssh_config: bool = clickdc.option(
is_flag=True,
hidden=True,
deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
deprecated='No effect. See --ssh-jump.',
)
ssh_warning_off: bool = clickdc.option(
is_flag=True,
hidden=True,
deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
deprecated='No effect. See --ssh-jump.',
)


Expand Down
7 changes: 7 additions & 0 deletions mycli/myclirc
Original file line number Diff line number Diff line change
Expand Up @@ -403,3 +403,10 @@ matching-bracket.other = '#000000 bg:#aacccc'
[alias_dsn.init-commands]
# Define one or more SQL statements per alias (semicolon-separated).
# example_dsn = "SET sql_select_limit=1000; SET time_zone='+00:00'"

[ssh]
# Path to the ssh executable used by --ssh-jump.
ssh_executable = ssh

# options to pass to the ssh executable when making a tunnel
ssh_options = -a -o ServerAliveInterval=60 -o ExitOnForwardFailure=yes -o IPQoS=af13 -o LogLevel=FATAL
Loading
Loading