Merge "Add scenario test for fip port_details"
diff --git a/.zuul.yaml b/.zuul.yaml
index 25714a2..cc1b61f 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -117,10 +117,11 @@
       devstack_localrc:
           PHYSICAL_NETWORK: default
           DOWNLOAD_DEFAULT_IMAGES: false
-          IMAGE_URLS: "http://cloud-images.ubuntu.com/releases/16.04/release-20170113/ubuntu-16.04-server-cloudimg-amd64-disk1.img,"
+          IMAGE_URLS: "http://cloud-images.ubuntu.com/releases/16.04/release-20180622/ubuntu-16.04-server-cloudimg-amd64-disk1.img,"
           DEFAULT_INSTANCE_TYPE: ds512M
           DEFAULT_INSTANCE_USER: ubuntu
           BUILD_TIMEOUT: 784
+          LIBVIRT_TYPE: kvm
       devstack_services:
         cinder: true
 
diff --git a/neutron_tempest_plugin/api/test_networks.py b/neutron_tempest_plugin/api/test_networks.py
index 7e9943d..c4b3596 100644
--- a/neutron_tempest_plugin/api/test_networks.py
+++ b/neutron_tempest_plugin/api/test_networks.py
@@ -209,6 +209,7 @@
     def test_list_no_pagination_limit_0(self):
         self._test_list_no_pagination_limit_0()
 
+    @decorators.skip_because(bug="1749820")
     @decorators.idempotent_id('3574ec9b-a8b8-43e3-9c11-98f5875df6a9')
     def test_list_validation_filters(self):
         self._test_list_validation_filters()
diff --git a/neutron_tempest_plugin/api/test_subnetpools.py b/neutron_tempest_plugin/api/test_subnetpools.py
index ec3753a..8adbc4c 100644
--- a/neutron_tempest_plugin/api/test_subnetpools.py
+++ b/neutron_tempest_plugin/api/test_subnetpools.py
@@ -414,6 +414,7 @@
     def test_list_no_pagination_limit_0(self):
         self._test_list_no_pagination_limit_0()
 
+    @decorators.skip_because(bug="1749820")
     @decorators.idempotent_id('27feb3f8-40f4-4e50-8cd2-7d0096a98682')
     def test_list_validation_filters(self):
         self._test_list_validation_filters()
diff --git a/neutron_tempest_plugin/api/test_subnets.py b/neutron_tempest_plugin/api/test_subnets.py
index fb2f4d6..b7a1b21 100644
--- a/neutron_tempest_plugin/api/test_subnets.py
+++ b/neutron_tempest_plugin/api/test_subnets.py
@@ -64,6 +64,7 @@
     def test_list_no_pagination_limit_0(self):
         self._test_list_no_pagination_limit_0()
 
+    @decorators.skip_because(bug="1749820")
     @decorators.idempotent_id('c0f9280b-9d81-4728-a967-6be22659d4c8')
     def test_list_validation_filters(self):
         self._test_list_validation_filters()
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
diff --git a/neutron_tempest_plugin/scenario/test_floatingip.py b/neutron_tempest_plugin/scenario/test_floatingip.py
index ae9ac11..504af12 100644
--- a/neutron_tempest_plugin/scenario/test_floatingip.py
+++ b/neutron_tempest_plugin/scenario/test_floatingip.py
@@ -289,7 +289,8 @@
 
 
 class FloatingIPQosTest(FloatingIpTestCasesMixin,
-                        test_qos.QoSTest):
+                        test_qos.QoSTestMixin,
+                        base.BaseTempestTestCase):
 
     same_network = True
 
diff --git a/neutron_tempest_plugin/scenario/test_migration.py b/neutron_tempest_plugin/scenario/test_migration.py
index 5e081f1..f4b918c 100644
--- a/neutron_tempest_plugin/scenario/test_migration.py
+++ b/neutron_tempest_plugin/scenario/test_migration.py
@@ -67,6 +67,19 @@
                 device_owner),
             timeout=300, sleep=5)
 
+    def _wait_until_router_ports_down(self, router_id):
+
+        def _is_port_down(port_id):
+            port = self.os_admin.network_client.show_port(port_id).get('port')
+            return port['status'] == const.DOWN
+
+        ports = self.os_admin.network_client.list_ports(
+            device_id=router_id).get('ports')
+        for port in ports:
+            common_utils.wait_until_true(
+                functools.partial(_is_port_down, port['id']),
+                timeout=300, sleep=5)
+
     def _is_port_active(self, router_id, device_owner):
         ports = self.os_admin.network_client.list_ports(
             device_id=router_id,
@@ -120,6 +133,8 @@
 
         self.os_admin.network_client.update_router(
             router_id=router['id'], admin_state_up=False)
+        self._wait_until_router_ports_down(router['id'])
+
         self.os_admin.network_client.update_router(
             router_id=router['id'], distributed=after_dvr, ha=after_ha)
         self._check_update(router, after_dvr, after_ha)
diff --git a/neutron_tempest_plugin/scenario/test_mtu.py b/neutron_tempest_plugin/scenario/test_mtu.py
index 0e3afe9..dbfde9b 100644
--- a/neutron_tempest_plugin/scenario/test_mtu.py
+++ b/neutron_tempest_plugin/scenario/test_mtu.py
@@ -19,6 +19,7 @@
 from tempest.common import waiters
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
+import testtools
 
 from neutron_tempest_plugin.common import ssh
 from neutron_tempest_plugin import config
@@ -118,6 +119,9 @@
                                     self.keypair['private_key'])
         return server_ssh_client1, fip1, server_ssh_client2, fip2
 
+    @testtools.skipUnless(
+          CONF.neutron_plugin_options.image_is_advanced,
+          "Advanced image is required to run this test.")
     @decorators.idempotent_id('3d73ec1a-2ec6-45a9-b0f8-04a273d9d344')
     def test_connectivity_min_max_mtu(self):
         server_ssh_client, _, _, fip2 = self._create_setup()
@@ -207,6 +211,9 @@
                                     self.keypair['private_key'])
         return server_ssh_client1, fip1, server_ssh_client2, fip2
 
+    @testtools.skipUnless(
+          CONF.neutron_plugin_options.image_is_advanced,
+          "Advanced image is required to run this test.")
     @decorators.idempotent_id('bc470200-d8f4-4f07-b294-1b4cbaaa35b9')
     def test_connectivity_min_max_mtu(self):
         server_ssh_client, _, _, fip2 = self._create_setup()
diff --git a/neutron_tempest_plugin/scenario/test_qos.py b/neutron_tempest_plugin/scenario/test_qos.py
index 0611160..702bbaa 100644
--- a/neutron_tempest_plugin/scenario/test_qos.py
+++ b/neutron_tempest_plugin/scenario/test_qos.py
@@ -66,7 +66,7 @@
                                                                port=port)
 
 
-class QoSTest(base.BaseTempestTestCase):
+class QoSTestMixin(object):
     credentials = ['primary', 'admin']
     force_tenant_isolation = False
 
@@ -81,22 +81,16 @@
 
     NC_PORT = 1234
 
-    @classmethod
-    @tutils.requires_ext(extension="qos", service="network")
-    @base_api.require_qos_rule_type(qos_consts.RULE_TYPE_BANDWIDTH_LIMIT)
-    def resource_setup(cls):
-        super(QoSTest, cls).resource_setup()
-
     def _create_file_for_bw_tests(self, ssh_client):
         cmd = ("(dd if=/dev/zero bs=%(bs)d count=%(count)d of=%(file_path)s) "
-               % {'bs': QoSTest.BS, 'count': QoSTest.COUNT,
-               'file_path': QoSTest.FILE_PATH})
+               % {'bs': QoSTestMixin.BS, 'count': QoSTestMixin.COUNT,
+               'file_path': QoSTestMixin.FILE_PATH})
         ssh_client.exec_command(cmd)
-        cmd = "stat -c %%s %s" % QoSTest.FILE_PATH
+        cmd = "stat -c %%s %s" % QoSTestMixin.FILE_PATH
         filesize = ssh_client.exec_command(cmd)
-        if int(filesize.strip()) != QoSTest.FILE_SIZE:
+        if int(filesize.strip()) != QoSTestMixin.FILE_SIZE:
             raise sc_exceptions.FileCreationFailedException(
-                file=QoSTest.FILE_PATH)
+                file=QoSTestMixin.FILE_PATH)
 
     def _check_bw(self, ssh_client, host, port):
         cmd = "killall -q nc"
@@ -105,15 +99,15 @@
         except exceptions.SSHExecCommandFailed:
             pass
         cmd = ("(nc -ll -p %(port)d < %(file_path)s > /dev/null &)" % {
-                'port': port, 'file_path': QoSTest.FILE_PATH})
+                'port': port, 'file_path': QoSTestMixin.FILE_PATH})
         ssh_client.exec_command(cmd)
 
         start_time = time.time()
         client_socket = _connect_socket(host, port)
         total_bytes_read = 0
 
-        while total_bytes_read < QoSTest.FILE_SIZE:
-            data = client_socket.recv(QoSTest.BUFFER_SIZE)
+        while total_bytes_read < QoSTestMixin.FILE_SIZE:
+            data = client_socket.recv(QoSTestMixin.BUFFER_SIZE)
             total_bytes_read += len(data)
 
         time_elapsed = time.time() - start_time
@@ -126,7 +120,7 @@
                    'total_bytes_read': total_bytes_read,
                    'bytes_per_second': bytes_per_second})
 
-        return bytes_per_second <= QoSTest.LIMIT_BYTES_SEC
+        return bytes_per_second <= QoSTestMixin.LIMIT_BYTES_SEC
 
     def _create_ssh_client(self):
         return ssh.Client(self.fip['floating_ip_address'],
@@ -153,6 +147,14 @@
                                         shared=True)
         return policy['policy']['id']
 
+
+class QoSTest(QoSTestMixin, base.BaseTempestTestCase):
+    @classmethod
+    @tutils.requires_ext(extension="qos", service="network")
+    @base_api.require_qos_rule_type(qos_consts.RULE_TYPE_BANDWIDTH_LIMIT)
+    def resource_setup(cls):
+        super(QoSTest, cls).resource_setup()
+
     @decorators.idempotent_id('1f7ed39b-428f-410a-bd2b-db9f465680df')
     def test_qos(self):
         """This is a basic test that check that a QoS policy with
diff --git a/requirements.txt b/requirements.txt
index 2ecce4e..5660c68 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,7 +3,7 @@
 # process, which may cause wedges in the gate later.
 
 pbr!=2.1.0,>=2.0.0 # Apache-2.0
-neutron-lib>=1.13.0 # Apache-2.0
+neutron-lib>=1.18.0 # Apache-2.0
 oslo.config>=5.2.0 # Apache-2.0
 ipaddress>=1.0.17;python_version<'3.3' # PSF
 netaddr>=0.7.18 # BSD