Skip to content

Commit ee5126f

Browse files
authored
Merge pull request #844 from hxueh/feature/ssh_config
Feature/ssh config
2 parents 6b4174b + 520c37f commit ee5126f

File tree

6 files changed

+171
-2
lines changed

6 files changed

+171
-2
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
A command line client for MySQL that can do auto-completion and syntax highlighting.
88

9-
HomePage: [http://mycli.net](http://mycli.net)
9+
HomePage: [http://mycli.net](http://mycli.net)
1010
Documentation: [http://mycli.net/docs](http://mycli.net/docs)
1111

1212
![Completion](screenshots/tables.png)
@@ -63,6 +63,8 @@ $ sudo apt-get install mycli # Only on debian or ubuntu
6363
--ssh-password TEXT Password to connect to ssh server.
6464
--ssh-key-filename TEXT Private key filename (identify file) for the
6565
ssh connection.
66+
--ssh-config-path TEXT Path to ssh configuation.
67+
--ssh-config-host TEXT Host for ssh server in ssh configuations (requires paramiko).
6668
--ssl-ca PATH CA file in PEM format.
6769
--ssl-capath TEXT CA directory.
6870
--ssl-cert PATH X509 cert in PEM format.
@@ -78,6 +80,7 @@ $ sudo apt-get install mycli # Only on debian or ubuntu
7880
section of myclirc file.
7981
--list-dsn list of DSN configured into the [alias_dsn]
8082
section of myclirc file.
83+
--list-ssh-config list ssh configurations in the ssh config (requires paramiko).
8184
-R, --prompt TEXT Prompt format (Default: "\t \u@\h:\d> ").
8285
-l, --logfile FILENAME Log every query and its results to a file.
8386
--defaults-group-suffix TEXT Read MySQL config groups with the specified

changelog.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1+
TBD
2+
===
3+
4+
Features:
5+
---------
6+
* Add an option `--ssh-config-host` to read ssh configuration from OpenSSH configuration file.
7+
* Add an option `--list-ssh-config` to list ssh configurations.
8+
* Add an option `--ssh-config-path` to choose ssh configuration path.
9+
10+
111
1.21.1
212
======
313

14+
415
Bug Fixes:
516
----------
617

mycli/AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Contributors:
6868
* Mike Palandra
6969
* Georgy Frolov
7070
* Jonathan Lloyd
71+
* Nathan Huang
7172
* Jakub Boukal
7273
* Takeshi D. Itoh
7374
* laixintao

mycli/main.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -995,6 +995,9 @@ def get_last_query(self):
995995
@click.option('--ssh-port', default=22, help='Port to connect to ssh server.')
996996
@click.option('--ssh-password', help='Password to connect to ssh server.')
997997
@click.option('--ssh-key-filename', help='Private key filename (identify file) for the ssh connection.')
998+
@click.option('--ssh-config-path', help='Path to ssh configuration.',
999+
default=os.path.expanduser('~') + '/.ssh/config')
1000+
@click.option('--ssh-config-host', help='Host to connect to ssh server reading from ssh configuration.')
9981001
@click.option('--ssl-ca', help='CA file in PEM format.',
9991002
type=click.Path(exists=True))
10001003
@click.option('--ssl-capath', help='CA directory.')
@@ -1016,6 +1019,8 @@ def get_last_query(self):
10161019
help='Use DSN configured into the [alias_dsn] section of myclirc file.')
10171020
@click.option('--list-dsn', 'list_dsn', is_flag=True,
10181021
help='list of DSN configured into the [alias_dsn] section of myclirc file.')
1022+
@click.option('--list-ssh-config', 'list_ssh_config', is_flag=True,
1023+
help='list ssh configurations in the ssh config (requires paramiko).')
10191024
@click.option('-R', '--prompt', 'prompt',
10201025
help='Prompt format (Default: "{0}").'.format(
10211026
MyCli.default_prompt))
@@ -1048,7 +1053,7 @@ def cli(database, user, host, port, socket, password, dbname,
10481053
ssl_ca, ssl_capath, ssl_cert, ssl_key, ssl_cipher,
10491054
ssl_verify_server_cert, table, csv, warn, execute, myclirc, dsn,
10501055
list_dsn, ssh_user, ssh_host, ssh_port, ssh_password,
1051-
ssh_key_filename):
1056+
ssh_key_filename, list_ssh_config, ssh_config_path, ssh_config_host):
10521057
"""A MySQL terminal client with auto-completion and syntax highlighting.
10531058
10541059
\b
@@ -1085,6 +1090,31 @@ def cli(database, user, host, port, socket, password, dbname,
10851090
else:
10861091
click.secho(alias)
10871092
sys.exit(0)
1093+
if list_ssh_config:
1094+
if not paramiko:
1095+
click.secho(
1096+
"This features requires paramiko. Please install paramiko and try again.",
1097+
err=True, fg='red'
1098+
)
1099+
exit(1)
1100+
try:
1101+
ssh_config = paramiko.config.SSHConfig().from_path(ssh_config_path)
1102+
except paramiko.ssh_exception.ConfigParseError as err:
1103+
click.secho('Invalid SSH configuration file. '
1104+
'Please check the SSH configuration file.',
1105+
err=True, fg='red')
1106+
exit(1)
1107+
except FileNotFoundError as e:
1108+
click.secho(str(e), err=True, fg='red')
1109+
exit(1)
1110+
for host in ssh_config.get_hostnames():
1111+
if verbose:
1112+
host_config = ssh_config.lookup(host)
1113+
click.secho("{} : {}".format(
1114+
host, host_config.get('hostname')))
1115+
else:
1116+
click.secho(host)
1117+
sys.exit(0)
10881118
# Choose which ever one has a valid value.
10891119
database = dbname or database
10901120

@@ -1135,6 +1165,32 @@ def cli(database, user, host, port, socket, password, dbname,
11351165
if not port:
11361166
port = uri.port
11371167

1168+
if ssh_config_host:
1169+
if not paramiko:
1170+
click.secho(
1171+
"This features requires paramiko. Please install paramiko and try again.",
1172+
err=True, fg='red'
1173+
)
1174+
exit(1)
1175+
try:
1176+
ssh_config = paramiko.config.SSHConfig().from_path(ssh_config_path)
1177+
except paramiko.ssh_exception.ConfigParseError as err:
1178+
click.secho('Invalid SSH configuration file. '
1179+
'Please check the SSH configuration file.',
1180+
err=True, fg='red')
1181+
exit(1)
1182+
except FileNotFoundError as e:
1183+
click.secho(str(e), err=True, fg='red')
1184+
exit(1)
1185+
ssh_config = ssh_config.lookup(ssh_config_host)
1186+
ssh_host = ssh_host if ssh_host else ssh_config.get('hostname')
1187+
ssh_user = ssh_user if ssh_user else ssh_config.get('user')
1188+
if ssh_config.get('port') and ssh_port == 22:
1189+
# port has a default value, overwrite it if it's in the config
1190+
ssh_port = int(ssh_config.get('port'))
1191+
ssh_key_filename = ssh_key_filename if ssh_key_filename else ssh_config.get(
1192+
'identityfile', [''])[0]
1193+
11381194
if not paramiko and ssh_host:
11391195
click.secho(
11401196
"Cannot use SSH transport because paramiko isn't installed, "

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ autopep8==1.3.3
1111
colorama==0.4.1
1212
git+https://github.com/hayd/pep8radius.git # --error-status option not released
1313
click>=7.0
14+
paramiko==2.7.1

test/test_main.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,24 @@ def test_list_dsn():
283283
assert result.output == "test : mysql://test/test\n"
284284

285285

286+
def test_list_ssh_config():
287+
runner = CliRunner()
288+
with NamedTemporaryFile(mode="w") as ssh_config:
289+
ssh_config.write(dedent("""\
290+
Host test
291+
Hostname test.example.com
292+
User joe
293+
Port 22222
294+
IdentityFile ~/.ssh/gateway
295+
"""))
296+
ssh_config.flush()
297+
args = ['--list-ssh-config', '--ssh-config-path', ssh_config.name]
298+
result = runner.invoke(cli, args=args)
299+
assert "test\n" in result.output
300+
result = runner.invoke(cli, args=args + ['--verbose'])
301+
assert "test : test.example.com\n" in result.output
302+
303+
286304
def test_dsn(monkeypatch):
287305
# Setup classes to mock mycli.main.MyCli
288306
class Formatter:
@@ -395,3 +413,82 @@ def run_query(self, query, new_line=True):
395413
MockMyCli.connect_args["host"] == "dsn_host" and \
396414
MockMyCli.connect_args["port"] == 6 and \
397415
MockMyCli.connect_args["database"] == "dsn_database"
416+
417+
418+
def test_ssh_config(monkeypatch):
419+
# Setup classes to mock mycli.main.MyCli
420+
class Formatter:
421+
format_name = None
422+
423+
class Logger:
424+
def debug(self, *args, **args_dict):
425+
pass
426+
427+
def warning(self, *args, **args_dict):
428+
pass
429+
430+
class MockMyCli:
431+
config = {'alias_dsn': {}}
432+
433+
def __init__(self, **args):
434+
self.logger = Logger()
435+
self.destructive_warning = False
436+
self.formatter = Formatter()
437+
438+
def connect(self, **args):
439+
MockMyCli.connect_args = args
440+
441+
def run_query(self, query, new_line=True):
442+
pass
443+
444+
import mycli.main
445+
monkeypatch.setattr(mycli.main, 'MyCli', MockMyCli)
446+
runner = CliRunner()
447+
448+
# Setup temporary configuration
449+
with NamedTemporaryFile(mode="w") as ssh_config:
450+
ssh_config.write(dedent("""\
451+
Host test
452+
Hostname test.example.com
453+
User joe
454+
Port 22222
455+
IdentityFile ~/.ssh/gateway
456+
"""))
457+
ssh_config.flush()
458+
459+
# When a user supplies a ssh config.
460+
result = runner.invoke(mycli.main.cli, args=[
461+
"--ssh-config-path",
462+
ssh_config.name,
463+
"--ssh-config-host",
464+
"test"
465+
])
466+
assert result.exit_code == 0, result.output + \
467+
" " + str(result.exception)
468+
assert \
469+
MockMyCli.connect_args["ssh_user"] == "joe" and \
470+
MockMyCli.connect_args["ssh_host"] == "test.example.com" and \
471+
MockMyCli.connect_args["ssh_port"] == 22222 and \
472+
MockMyCli.connect_args["ssh_key_filename"] == os.getenv(
473+
"HOME") + "/.ssh/gateway"
474+
475+
# When a user supplies a ssh config host as argument to mycli,
476+
# and used command line arguments, use the command line
477+
# arguments.
478+
result = runner.invoke(mycli.main.cli, args=[
479+
"--ssh-config-path",
480+
ssh_config.name,
481+
"--ssh-config-host",
482+
"test",
483+
"--ssh-user", "arg_user",
484+
"--ssh-host", "arg_host",
485+
"--ssh-port", "3",
486+
"--ssh-key-filename", "/path/to/key"
487+
])
488+
assert result.exit_code == 0, result.output + \
489+
" " + str(result.exception)
490+
assert \
491+
MockMyCli.connect_args["ssh_user"] == "arg_user" and \
492+
MockMyCli.connect_args["ssh_host"] == "arg_host" and \
493+
MockMyCli.connect_args["ssh_port"] == 3 and \
494+
MockMyCli.connect_args["ssh_key_filename"] == "/path/to/key"

0 commit comments

Comments
 (0)