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