Merge "Allow to connect to SSH server using an intermediate SSH server"
diff --git a/neutron_tempest_plugin/common/ssh.py b/neutron_tempest_plugin/common/ssh.py
index b919b65..99f731c 100644
--- a/neutron_tempest_plugin/common/ssh.py
+++ b/neutron_tempest_plugin/common/ssh.py
@@ -12,13 +12,103 @@
# License for the specific language governing permissions and limitations
# under the License.
+import os
+
+from oslo_log import log
from tempest.lib.common import ssh
from neutron_tempest_plugin import config
+CONF = config.CONF
+LOG = log.getLogger(__name__)
+
+
class Client(ssh.Client):
- def __init__(self, *args, **kwargs):
- if 'timeout' not in kwargs:
- kwargs['timeout'] = config.CONF.validation.ssh_timeout
- super(Client, self).__init__(*args, **kwargs)
+
+ timeout = CONF.validation.ssh_timeout
+
+ proxy_jump_host = CONF.neutron_plugin_options.ssh_proxy_jump_host
+ proxy_jump_username = CONF.neutron_plugin_options.ssh_proxy_jump_username
+ proxy_jump_password = CONF.neutron_plugin_options.ssh_proxy_jump_password
+ proxy_jump_keyfile = CONF.neutron_plugin_options.ssh_proxy_jump_keyfile
+ proxy_jump_port = CONF.neutron_plugin_options.ssh_proxy_jump_port
+
+ def __init__(self, host, username, password=None, timeout=None, pkey=None,
+ channel_timeout=10, look_for_keys=False, key_filename=None,
+ port=22, proxy_client=None):
+
+ timeout = timeout or self.timeout
+
+ if self.proxy_jump_host:
+ # Perform all SSH connections passing through configured SSH server
+ proxy_client = proxy_client or self.create_proxy_client(
+ timeout=timeout, channel_timeout=channel_timeout)
+
+ super(Client, self).__init__(
+ host=host, username=username, password=password, timeout=timeout,
+ pkey=pkey, channel_timeout=channel_timeout,
+ look_for_keys=look_for_keys, key_filename=key_filename, port=port,
+ proxy_client=proxy_client)
+
+ @classmethod
+ def create_proxy_client(cls, look_for_keys=True, **kwargs):
+ host = cls.proxy_jump_host
+ if not host:
+ # proxy_jump_host string cannot be empty or None
+ raise ValueError(
+ "'proxy_jump_host' configuration option is empty.")
+
+ # Let accept an empty string as a synonymous of default value on below
+ # options
+ password = cls.proxy_jump_password or None
+ key_file = cls.proxy_jump_keyfile or None
+ username = cls.proxy_jump_username
+
+ # Port must be a positive integer
+ port = cls.proxy_jump_port
+ if port <= 0 or port > 65535:
+ raise ValueError(
+ "Invalid value for 'proxy_jump_port' configuration option: "
+ "{!r}".format(port))
+
+ login = "{username}@{host}:{port}".format(username=username, host=host,
+ port=port)
+
+ if key_file:
+ # expand ~ character with user HOME directory
+ key_file = os.path.expanduser(key_file)
+ if os.path.isfile(key_file):
+ LOG.debug("Going to create SSH connection to %r using key "
+ "file: %s", login, key_file)
+
+ else:
+ # This message could help the user to identify a
+ # mis-configuration in tempest.conf
+ raise ValueError(
+ "Cannot find file specified as 'proxy_jump_keyfile' "
+ "option: {!r}".format(key_file))
+
+ elif password:
+ LOG.debug("Going to create SSH connection to %r using password.",
+ login)
+
+ elif look_for_keys:
+ # This message could help the user to identify a mis-configuration
+ # in tempest.conf
+ LOG.info("Both 'proxy_jump_password' and 'proxy_jump_keyfile' "
+ "options are empty. Going to create SSH connection to %r "
+ "looking for key file location into %r directory.",
+ login, os.path.expanduser('~/.ssh'))
+ else:
+ # An user that forces look_for_keys=False should really know what
+ # he really wants
+ LOG.warning("No authentication method provided to create an SSH "
+ "connection to %r. If it fails, then please "
+ "set 'proxy_jump_keyfile' to provide a valid SSH key "
+ "file.", login)
+
+ return ssh.Client(
+ host=host, username=username, password=password,
+ look_for_keys=look_for_keys, key_filename=key_file,
+ port=port, proxy_client=None, **kwargs)
diff --git a/neutron_tempest_plugin/config.py b/neutron_tempest_plugin/config.py
index fc07e81..e15748d 100644
--- a/neutron_tempest_plugin/config.py
+++ b/neutron_tempest_plugin/config.py
@@ -56,7 +56,25 @@
'"provider:network_type":<TYPE> - string '
'"mtu":<MTU> - integer '
'"cidr"<SUBNET/MASK> - string '
- '"provider:segmentation_id":<VLAN_ID> - integer')
+ '"provider:segmentation_id":<VLAN_ID> - integer'),
+
+ # Option for feature to connect via SSH to VMs using an intermediate SSH
+ # server
+ cfg.StrOpt('ssh_proxy_jump_host',
+ default=None,
+ help='Proxy jump host used to connect via SSH to VMs..'),
+ cfg.StrOpt('ssh_proxy_jump_username',
+ default='root',
+ help='User name used to connect to "ssh_proxy_jump_host".'),
+ cfg.StrOpt('ssh_proxy_jump_password',
+ default=None,
+ help='Password used to connect to "ssh_proxy_jump_host".'),
+ cfg.StrOpt('ssh_proxy_jump_keyfile',
+ default=None,
+ help='Keyfile used to connect to "ssh_proxy_jump_host".'),
+ cfg.IntOpt('ssh_proxy_jump_port',
+ default=22,
+ help='Port used to connect to "ssh_proxy_jump_host".'),
]
# TODO(amuller): Redo configuration options registration as part of the planned