Merge pull request #347 from mesosphere/DCOS-2993-master-proxy
[DCOS-2993] Added `--master-proxy` option to `dcos node ssh`. This enables `dcos
This commit is contained in:
29
cli/dcoscli/data/help/node.txt
Normal file
29
cli/dcoscli/data/help/node.txt
Normal file
@@ -0,0 +1,29 @@
|
||||
Manage DCOS nodes
|
||||
|
||||
Usage:
|
||||
dcos node --info
|
||||
dcos node [--json]
|
||||
dcos node log [--follow --lines=N --master --slave=<slave-id>]
|
||||
dcos node ssh [--option SSHOPT=VAL ...]
|
||||
[--config-file=<path>]
|
||||
[--user=<user>]
|
||||
[--master-proxy]
|
||||
(--master | --slave=<slave-id>)
|
||||
|
||||
Options:
|
||||
-h, --help Show this screen
|
||||
--info Show a short description of this subcommand
|
||||
--json Print json-formatted nodes
|
||||
--follow Print data as the file grows
|
||||
--lines=N Print the last N lines [default: 10]
|
||||
--master Access the leading master
|
||||
--master-proxy Proxy the SSH connection through a master node. This can be useful when
|
||||
accessing DCOS from a separate network. For example, in the default AWS
|
||||
configuration, the private slaves are unreachable from the public
|
||||
internet. You can access them using this option, which will first hop
|
||||
from the publicly available master.
|
||||
--slave=<slave-id> Access the slave with the provided ID
|
||||
--option SSHOPT=VAL SSH option (see `man ssh_config`)
|
||||
--config-file=<path> Path to SSH config file
|
||||
--user=<user> SSH user [default: core]
|
||||
--version Show version
|
||||
@@ -1,32 +1,9 @@
|
||||
"""Manage DCOS nodes
|
||||
|
||||
Usage:
|
||||
dcos node --info
|
||||
dcos node [--json]
|
||||
dcos node log [--follow --lines=N --master --slave=<slave-id>]
|
||||
dcos node ssh [--option SSHOPT=VAL ...]
|
||||
[--config-file=<path>]
|
||||
[--user=<user>]
|
||||
(--master | --slave=<slave-id>)
|
||||
|
||||
Options:
|
||||
-h, --help Show this screen
|
||||
--info Show a short description of this subcommand
|
||||
--json Print json-formatted nodes
|
||||
--follow Print data as the file grows
|
||||
--lines=N Print the last N lines [default: 10]
|
||||
--master Access the leading master
|
||||
--slave=<slave-id> Access the slave with the provided ID
|
||||
--option SSHOPT=VAL SSH option (see `man ssh_config`)
|
||||
--config-file=<path> Path to SSH config file
|
||||
--user=<user> SSH user [default: core]
|
||||
--version Show version
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import dcoscli
|
||||
import docopt
|
||||
import pkg_resources
|
||||
from dcos import cmds, emitting, errors, mesos, util
|
||||
from dcos.errors import DCOSException, DefaultError
|
||||
from dcoscli import log, tables
|
||||
@@ -49,12 +26,21 @@ def _main():
|
||||
util.configure_process_from_environ()
|
||||
|
||||
args = docopt.docopt(
|
||||
__doc__,
|
||||
_doc(),
|
||||
version="dcos-node version {}".format(dcoscli.version))
|
||||
|
||||
return cmds.execute(_cmds(), args)
|
||||
|
||||
|
||||
def _doc():
|
||||
"""
|
||||
:rtype: str
|
||||
"""
|
||||
return pkg_resources.resource_string(
|
||||
'dcoscli',
|
||||
'data/help/node.txt').decode('utf-8')
|
||||
|
||||
|
||||
def _cmds():
|
||||
"""
|
||||
:returns: All of the supported commands
|
||||
@@ -75,7 +61,7 @@ def _cmds():
|
||||
cmds.Command(
|
||||
hierarchy=['node', 'ssh'],
|
||||
arg_keys=['--master', '--slave', '--option', '--config-file',
|
||||
'--user'],
|
||||
'--user', '--master-proxy'],
|
||||
function=_ssh),
|
||||
|
||||
cmds.Command(
|
||||
@@ -92,7 +78,7 @@ def _info():
|
||||
:rtype: int
|
||||
"""
|
||||
|
||||
emitter.publish(__doc__.split('\n')[0])
|
||||
emitter.publish(_doc().split('\n')[0])
|
||||
return 0
|
||||
|
||||
|
||||
@@ -167,7 +153,7 @@ def _mesos_files(master, slave_id):
|
||||
return files
|
||||
|
||||
|
||||
def _ssh(master, slave, option, config_file, user):
|
||||
def _ssh(master, slave, option, config_file, user, master_proxy):
|
||||
"""SSH into a DCOS node using the IP addresses found in master's
|
||||
state.json
|
||||
|
||||
@@ -182,16 +168,19 @@ def _ssh(master, slave, option, config_file, user):
|
||||
:type config_file: str | None
|
||||
:param user: SSH user
|
||||
:type user: str | None
|
||||
:param master_proxy: If True, SSH-hop from a master
|
||||
:type master_proxy: bool | None
|
||||
:rtype: int
|
||||
:returns: process return code
|
||||
"""
|
||||
|
||||
ssh_options = util.get_ssh_options(config_file, option)
|
||||
dcos_client = mesos.DCOSClient()
|
||||
|
||||
if master:
|
||||
host = mesos.MesosDNSClient().hosts('leader.mesos.')[0]['ip']
|
||||
else:
|
||||
summary = mesos.DCOSClient().get_state_summary()
|
||||
summary = dcos_client.get_state_summary()
|
||||
slave_obj = next((slave_ for slave_ in summary['slaves']
|
||||
if slave_['id'] == slave),
|
||||
None)
|
||||
@@ -200,11 +189,35 @@ def _ssh(master, slave, option, config_file, user):
|
||||
else:
|
||||
raise DCOSException('No slave found with ID [{}]'.format(slave))
|
||||
|
||||
cmd = "ssh -t {0}{1}@{2}".format(
|
||||
ssh_options,
|
||||
user,
|
||||
host)
|
||||
master_public_ip = dcos_client.metadata().get('PUBLIC_IPV4')
|
||||
if master_proxy:
|
||||
if not os.environ.get('SSH_AUTH_SOCK'):
|
||||
raise DCOSException(
|
||||
"There is no SSH_AUTH_SOCK env variable, which likely means "
|
||||
"you aren't running `ssh-agent`. `dcos node ssh "
|
||||
"--master-proxy` depends on `ssh-agent` to safely use your "
|
||||
"private key to hop between nodes in your cluster. Please "
|
||||
"run `ssh-agent`, then add your private key with `ssh-add`.")
|
||||
if not master_public_ip:
|
||||
raise DCOSException(("Cannot use --master-proxy. Failed to find "
|
||||
"'PUBLIC_IPV4' at {}").format(
|
||||
dcos_client.get_dcos_url('metadata')))
|
||||
|
||||
cmd = "ssh -A -t {0}{1}@{2} ssh -A -t {1}@{3}".format(
|
||||
ssh_options,
|
||||
user,
|
||||
master_public_ip,
|
||||
host)
|
||||
else:
|
||||
cmd = "ssh -t {0}{1}@{2}".format(
|
||||
ssh_options,
|
||||
user,
|
||||
host)
|
||||
|
||||
emitter.publish(DefaultError("Running `{}`".format(cmd)))
|
||||
if (not master_proxy) and master_public_ip:
|
||||
emitter.publish(
|
||||
DefaultError("If you are running this command from a separate "
|
||||
"network than DCOS, consider using `--master-proxy`"))
|
||||
|
||||
return subprocess.call(cmd, shell=True)
|
||||
|
||||
@@ -7,6 +7,7 @@ Usage:
|
||||
dcos node ssh [--option SSHOPT=VAL ...]
|
||||
[--config-file=<path>]
|
||||
[--user=<user>]
|
||||
[--master-proxy]
|
||||
(--master | --slave=<slave-id>)
|
||||
|
||||
Options:
|
||||
@@ -16,6 +17,11 @@ Options:
|
||||
--follow Print data as the file grows
|
||||
--lines=N Print the last N lines [default: 10]
|
||||
--master Access the leading master
|
||||
--master-proxy Proxy the SSH connection through a master node. This can be useful when
|
||||
accessing DCOS from a separate network. For example, in the default AWS
|
||||
configuration, the private slaves are unreachable from the public
|
||||
internet. You can access them using this option, which will first hop
|
||||
from the publicly available master.
|
||||
--slave=<slave-id> Access the slave with the provided ID
|
||||
--option SSHOPT=VAL SSH option (see `man ssh_config`)
|
||||
--config-file=<path> Path to SSH config file
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
import dcos.util as util
|
||||
import six
|
||||
from dcos import mesos
|
||||
from dcos.util import create_schema
|
||||
|
||||
@@ -10,30 +12,8 @@ from .common import assert_command, assert_lines, exec_command, ssh_output
|
||||
|
||||
|
||||
def test_help():
|
||||
stdout = b"""Manage DCOS nodes
|
||||
|
||||
Usage:
|
||||
dcos node --info
|
||||
dcos node [--json]
|
||||
dcos node log [--follow --lines=N --master --slave=<slave-id>]
|
||||
dcos node ssh [--option SSHOPT=VAL ...]
|
||||
[--config-file=<path>]
|
||||
[--user=<user>]
|
||||
(--master | --slave=<slave-id>)
|
||||
|
||||
Options:
|
||||
-h, --help Show this screen
|
||||
--info Show a short description of this subcommand
|
||||
--json Print json-formatted nodes
|
||||
--follow Print data as the file grows
|
||||
--lines=N Print the last N lines [default: 10]
|
||||
--master Access the leading master
|
||||
--slave=<slave-id> Access the slave with the provided ID
|
||||
--option SSHOPT=VAL SSH option (see `man ssh_config`)
|
||||
--config-file=<path> Path to SSH config file
|
||||
--user=<user> SSH user [default: core]
|
||||
--version Show version
|
||||
"""
|
||||
with open('tests/data/help/node.txt') as content:
|
||||
stdout = six.b(content.read())
|
||||
assert_command(['dcos', 'node', '--help'], stdout=stdout)
|
||||
|
||||
|
||||
@@ -141,11 +121,31 @@ def test_node_ssh_user():
|
||||
assert b'Permission denied' in stderr
|
||||
|
||||
|
||||
def test_node_ssh_master_proxy_no_agent():
|
||||
env = os.environ.copy()
|
||||
env.pop('SSH_AUTH_SOCK', None)
|
||||
stderr = (b"There is no SSH_AUTH_SOCK env variable, which likely means "
|
||||
b"you aren't running `ssh-agent`. `dcos node ssh "
|
||||
b"--master-proxy` depends on `ssh-agent` to safely use your "
|
||||
b"private key to hop between nodes in your cluster. Please "
|
||||
b"run `ssh-agent`, then add your private key with `ssh-add`.\n")
|
||||
|
||||
assert_command(['dcos', 'node', 'ssh', '--master-proxy', '--master'],
|
||||
stderr=stderr,
|
||||
returncode=1,
|
||||
env=env)
|
||||
|
||||
|
||||
def test_node_ssh_master_proxy():
|
||||
_node_ssh(['--master', '--master-proxy'])
|
||||
|
||||
|
||||
def _node_ssh_output(args):
|
||||
cmd = ('dcos node ssh --option ' +
|
||||
'IdentityFile=/host-home/.vagrant.d/insecure_private_key ' +
|
||||
'--option StrictHostKeyChecking=no ' +
|
||||
'{}').format(' '.join(args))
|
||||
cli_test_ssh_key_path = os.environ['CLI_TEST_SSH_KEY_PATH']
|
||||
cmd = ('ssh-agent /bin/bash -c "ssh-add {} 2> /dev/null && ' +
|
||||
'dcos node ssh --option StrictHostKeyChecking=no {}"').format(
|
||||
cli_test_ssh_key_path,
|
||||
' '.join(args))
|
||||
|
||||
return ssh_output(cmd)
|
||||
|
||||
@@ -156,8 +156,10 @@ def _node_ssh(args):
|
||||
assert stdout
|
||||
assert b"Running `" in stderr
|
||||
num_lines = len(stderr.decode().split('\n'))
|
||||
assert (num_lines == 2 or
|
||||
(num_lines == 3 and b'Warning: Permanently added' in stderr))
|
||||
expected_num_lines = 2 if '--master-proxy' in args else 3
|
||||
assert (num_lines == expected_num_lines or
|
||||
(num_lines == (expected_num_lines + 1) and
|
||||
b'Warning: Permanently added' in stderr))
|
||||
|
||||
|
||||
def _get_schema(slave):
|
||||
|
||||
Reference in New Issue
Block a user