compute: Pass through args to ssh

Why limit a user to preset ssh arguments? Capture them all and send
them along to ssh to deal with. This allows users to use the full range of
ssh arguments, including specifying a command to run on the
instance. For example:

  openstack server ssh -4 upg -- -l cirros -i ~/id_rsa_upg "date; uptime"

SSH arguments that openstackclient currently mirrors are deprecated
except for -4 and -6, as they are useful for retrieving the correct
instance IP.

Change-Id: Ia50786d5eee52688e180550fe16aeb8af610154b
Co-authored-by: Stephen Finucane <stephen@that.guru>
This commit is contained in:
Hugh Saunders 2016-11-25 11:44:13 +00:00 committed by Stephen Finucane
parent ba69870d86
commit 3a929611c0
3 changed files with 115 additions and 25 deletions

View File

@ -4461,26 +4461,30 @@ class SshServer(command.Command):
metavar='<server>', metavar='<server>',
help=_('Server (name or ID)'), help=_('Server (name or ID)'),
) )
# Deprecated during the Yoga cycle
parser.add_argument( parser.add_argument(
'--login', '-l', '--login', '-l',
metavar='<login-name>', metavar='<login-name>',
help=_('Login name (ssh -l option)'), help=argparse.SUPPRESS,
) )
# Deprecated during the Yoga cycle
parser.add_argument( parser.add_argument(
'--port', '-p', '--port', '-p',
metavar='<port>', metavar='<port>',
type=int, type=int,
help=_('Destination port (ssh -p option)'), help=argparse.SUPPRESS,
) )
# Deprecated during the Yoga cycle
parser.add_argument( parser.add_argument(
'--identity', '-i', '--identity', '-i',
metavar='<keyfile>', metavar='<keyfile>',
help=_('Private key file (ssh -i option)'), help=argparse.SUPPRESS,
) )
# Deprecated during the Yoga cycle
parser.add_argument( parser.add_argument(
'--option', '-o', '--option', '-o',
metavar='<config-options>', metavar='<config-options>',
help=_('Options in ssh_config(5) format (ssh -o option)'), help=argparse.SUPPRESS,
) )
ip_group = parser.add_mutually_exclusive_group() ip_group = parser.add_mutually_exclusive_group()
ip_group.add_argument( ip_group.add_argument(
@ -4521,6 +4525,7 @@ class SshServer(command.Command):
default='public', default='public',
help=_('Use other IP address (public, private, etc)'), help=_('Use other IP address (public, private, etc)'),
) )
# Deprecated during the Yoga cycle
parser.add_argument( parser.add_argument(
'-v', '-v',
dest='verbose', dest='verbose',
@ -4528,46 +4533,77 @@ class SshServer(command.Command):
default=False, default=False,
help=argparse.SUPPRESS, help=argparse.SUPPRESS,
) )
parser.add_argument(
'ssh_args',
nargs='*',
metavar='-- <standard ssh args>',
help=(
'Any argument or option that ssh allows. '
'Use -- once between openstackclient args and SSH args.'
),
)
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute compute_client = self.app.client_manager.compute
server = utils.find_resource( server = utils.find_resource(
compute_client.servers, compute_client.servers,
parsed_args.server, parsed_args.server,
) )
# Build the command # first, handle the deprecated options
cmd = "ssh" if any((
parsed_args.port,
parsed_args.identity,
parsed_args.option,
parsed_args.login,
parsed_args.verbose,
)):
msg = _(
'The ssh options have been deprecated. The ssh equivalent '
'options can be used instead as arguments after "--" on '
'the command line.'
)
self.log.warning(msg)
ip_address_family = [4, 6] ip_address_family = [4, 6]
if parsed_args.ipv4: if parsed_args.ipv4:
ip_address_family = [4] ip_address_family = [4]
cmd += " -4"
if parsed_args.ipv6: if parsed_args.ipv6:
ip_address_family = [6] ip_address_family = [6]
cmd += " -6"
args = parsed_args.ssh_args[:]
if parsed_args.port: if parsed_args.port:
cmd += " -p %d" % parsed_args.port args.extend(['-p', str(parsed_args.port)])
if parsed_args.identity: if parsed_args.identity:
cmd += " -i %s" % parsed_args.identity args.extend(['-i', parsed_args.identity])
if parsed_args.option: if parsed_args.option:
cmd += " -o %s" % parsed_args.option args.extend(['-o', parsed_args.option])
if parsed_args.login: if parsed_args.login:
login = parsed_args.login login = parsed_args.login
else: args.extend(['-l', login])
elif '-l' not in args:
login = self.app.client_manager.auth_ref.username login = self.app.client_manager.auth_ref.username
if parsed_args.verbose: args.extend(['-l', login])
cmd += " -v"
cmd += " %s@%s" if parsed_args.verbose:
ip_address = _get_ip_address(server.addresses, args.append('-v')
parsed_args.address_type,
ip_address_family) ip_address = _get_ip_address(
LOG.debug("ssh command: %s", (cmd % (login, ip_address))) server.addresses,
os.system(cmd % (login, ip_address)) parsed_args.address_type,
ip_address_family,
)
cmd = ' '.join(['ssh', ip_address] + args)
LOG.debug("ssh command: {cmd}".format(cmd=cmd))
os.system(cmd)
class StartServer(command.Command): class StartServer(command.Command):

View File

@ -8307,15 +8307,48 @@ class TestServerSsh(TestServer):
('ipv6', False), ('ipv6', False),
('address_type', 'public'), ('address_type', 'public'),
('verbose', False), ('verbose', False),
('ssh_args', []),
] ]
parsed_args = self.check_parser(self.cmd, arglist, verifylist) parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args) with mock.patch.object(self.cmd.log, 'warning') as mock_warning:
result = self.cmd.take_action(parsed_args)
self.assertIsNone(result) self.assertIsNone(result)
mock_exec.assert_called_once_with('ssh cloud@192.168.1.30') mock_exec.assert_called_once_with('ssh 192.168.1.30 -l cloud')
mock_warning.assert_not_called()
def test_server_ssh_opts(self, mock_exec): def test_server_ssh_passthrough_opts(self, mock_exec):
arglist = [
self.server.name,
'--',
'-l', 'username',
'-p', '2222',
]
verifylist = [
('server', self.server.name),
('login', None),
('port', None),
('identity', None),
('option', None),
('ipv4', False),
('ipv6', False),
('address_type', 'public'),
('verbose', False),
('ssh_args', ['-l', 'username', '-p', '2222']),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
with mock.patch.object(self.cmd.log, 'warning') as mock_warning:
result = self.cmd.take_action(parsed_args)
self.assertIsNone(result)
mock_exec.assert_called_once_with(
'ssh 192.168.1.30 -l username -p 2222'
)
mock_warning.assert_not_called()
def test_server_ssh_deprecated_opts(self, mock_exec):
arglist = [ arglist = [
self.server.name, self.server.name,
'-l', 'username', '-l', 'username',
@ -8331,14 +8364,21 @@ class TestServerSsh(TestServer):
('ipv6', False), ('ipv6', False),
('address_type', 'public'), ('address_type', 'public'),
('verbose', False), ('verbose', False),
('ssh_args', []),
] ]
parsed_args = self.check_parser(self.cmd, arglist, verifylist) parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args) with mock.patch.object(self.cmd.log, 'warning') as mock_warning:
result = self.cmd.take_action(parsed_args)
self.assertIsNone(result) self.assertIsNone(result)
mock_exec.assert_called_once_with( mock_exec.assert_called_once_with(
'ssh -p 2222 username@192.168.1.30' 'ssh 192.168.1.30 -p 2222 -l username'
)
mock_warning.assert_called_once()
self.assertIn(
'The ssh options have been deprecated.',
mock_warning.call_args[0][0],
) )

View File

@ -0,0 +1,14 @@
---
features:
- |
Added the ability to pass arguments through to the ``ssh`` command When
using ``openstack server ssh``. This allows the user to use any ``ssh``
option without needing to add that option to the openstack client.
Existing openstackclient options that mirror SSH options are now
deprecated.
deprecations:
- |
``openstack server ssh`` options that mirror ``ssh`` options are now
deprecated (``--login, -l, --port, --identity, --option, -o, -vz``).
The ``ssh`` equivalent of each deprecated option should be used instead.
For example ``openstack server ssh instance -- -l user -i key``