Merge "Stop running designate scenario job on docs-only changes"
diff --git a/neutron_tempest_plugin/api/base.py b/neutron_tempest_plugin/api/base.py
index ae01d56..966b30d 100644
--- a/neutron_tempest_plugin/api/base.py
+++ b/neutron_tempest_plugin/api/base.py
@@ -128,10 +128,15 @@
         cls.log_objects = []
         cls.reserved_subnet_cidrs = set()
         cls.keypairs = []
+        cls.trunks = []
 
     @classmethod
     def resource_cleanup(cls):
         if CONF.service_available.neutron:
+            # Clean up trunks
+            for trunk in cls.trunks:
+                cls._try_delete_resource(cls.delete_trunk, trunk)
+
             # Clean up floating IPs
             for floating_ip in cls.floating_ips:
                 cls._try_delete_resource(cls.client.delete_floatingip,
@@ -680,6 +685,64 @@
                   cls.os_primary.keypairs_client)
         client.delete_keypair(keypair_name=keypair['name'])
 
+    @classmethod
+    def create_trunk(cls, port=None, subports=None, client=None, **kwargs):
+        """Create network trunk
+
+        :param port: dictionary containing parent port ID (port['id'])
+        :param client: client to be used for connecting to networking service
+        :param **kwargs: extra parameters to be forwarded to network service
+
+        :returns: dictionary containing created trunk details
+        """
+        client = client or cls.client
+
+        if port:
+            kwargs['port_id'] = port['id']
+
+        trunk = client.create_trunk(subports=subports, **kwargs)['trunk']
+        # Save client reference for later deletion
+        trunk['client'] = client
+        cls.trunks.append(trunk)
+        return trunk
+
+    @classmethod
+    def delete_trunk(cls, trunk, client=None):
+        """Delete network trunk
+
+        :param trunk: dictionary containing trunk ID (trunk['id'])
+
+        :param client: client to be used for connecting to networking service
+        """
+        client = client or trunk.get('client') or cls.client
+        trunk.update(client.show_trunk(trunk['id'])['trunk'])
+
+        if not trunk['admin_state_up']:
+            # Cannot touch trunk before admin_state_up is True
+            client.update_trunk(trunk['id'], admin_state_up=True)
+        if trunk['sub_ports']:
+            # Removes trunk ports before deleting it
+            cls._try_delete_resource(client.remove_subports, trunk['id'],
+                                     trunk['sub_ports'])
+
+        # we have to detach the interface from the server before
+        # the trunk can be deleted.
+        parent_port = {'id': trunk['port_id']}
+
+        def is_parent_port_detached():
+            parent_port.update(client.show_port(parent_port['id'])['port'])
+            return not parent_port['device_id']
+
+        if not is_parent_port_detached():
+            # this could probably happen when trunk is deleted and parent port
+            # has been assigned to a VM that is still running. Here we are
+            # assuming that device_id points to such VM.
+            cls.os_primary.compute.InterfacesClient().delete_interface(
+                parent_port['device_id'], parent_port['id'])
+            utils.wait_until_true(is_parent_port_detached)
+
+        client.delete_trunk(trunk['id'])
+
 
 class BaseAdminNetworkTest(BaseNetworkTest):
 
diff --git a/neutron_tempest_plugin/api/test_extensions.py b/neutron_tempest_plugin/api/test_extensions.py
index 1462ae1..5b7fe67 100644
--- a/neutron_tempest_plugin/api/test_extensions.py
+++ b/neutron_tempest_plugin/api/test_extensions.py
@@ -11,31 +11,46 @@
 #    under the License.
 
 from tempest.common import utils
+from tempest import config
 from tempest.lib import decorators
 
 from neutron_tempest_plugin.api import base
 
 
+CONF = config.CONF
+
+
 class ExtensionsTest(base.BaseNetworkTest):
 
-    def _test_list_extensions_includes(self, ext):
+    def _test_list_extensions_includes(self, exts):
         body = self.client.list_extensions()
         extensions = {ext_['alias'] for ext_ in body['extensions']}
         self.assertNotEmpty(extensions, "Extension list returned is empty")
-        ext_enabled = utils.is_extension_enabled(ext, "network")
-        if ext_enabled:
-            self.assertIn(ext, extensions)
-        else:
-            self.assertNotIn(ext, extensions)
+        for ext in exts:
+            ext_enabled = utils.is_extension_enabled(ext, "network")
+            if ext_enabled:
+                self.assertIn(ext, extensions)
+            else:
+                self.assertNotIn(ext, extensions)
 
     @decorators.idempotent_id('262420b7-a4bb-4a3e-b4b5-e73bad18df8c')
     def test_list_extensions_sorting(self):
-        self._test_list_extensions_includes('sorting')
+        self._test_list_extensions_includes(['sorting'])
 
     @decorators.idempotent_id('19db409e-a23f-445d-8bc8-ca3d64c84706')
     def test_list_extensions_pagination(self):
-        self._test_list_extensions_includes('pagination')
+        self._test_list_extensions_includes(['pagination'])
 
     @decorators.idempotent_id('155b7bc2-e358-4dd8-bf3e-1774c084567f')
     def test_list_extensions_project_id(self):
-        self._test_list_extensions_includes('project-id')
+        self._test_list_extensions_includes(['project-id'])
+
+    @decorators.idempotent_id('c7597fac-2404-45b1-beb4-523c8b1d4604')
+    def test_list_extensions_includes_all(self):
+        extensions = CONF.network_feature_enabled.api_extensions
+        if not extensions:
+            raise self.skipException("Extension list is empty")
+        if extensions[0] == 'all':
+            raise self.skipException("No lists of enabled extensions provided")
+
+        self._test_list_extensions_includes(extensions)
diff --git a/neutron_tempest_plugin/common/socat.py b/neutron_tempest_plugin/common/socat.py
new file mode 100644
index 0000000..6bd1fdc
--- /dev/null
+++ b/neutron_tempest_plugin/common/socat.py
@@ -0,0 +1,105 @@
+# Copyright 2018 Red Hat, Inc.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+
+COMMAND = 'socat'
+
+
+class SocatAddress(object):
+
+    def __init__(self, address, args=None, options=None):
+        self.address = address
+        self.args = args
+        self.options = options
+
+    @classmethod
+    def udp_datagram(cls, host, port, options=None, ip_version=None):
+        address = 'UDP{}-DATAGRAM'.format(ip_version or '')
+        return cls(address, (host, int(port)), options)
+
+    @classmethod
+    def udp_recvfrom(cls, port, options=None, ip_version=None):
+        address = 'UDP{}-RECVFROM'.format(ip_version or '')
+        return cls(address, (int(port),), options)
+
+    @classmethod
+    def stdio(cls):
+        return cls('STDIO')
+
+    def __str__(self):
+        address = self.address
+        if self.args:
+            address += ':' + ':'.join(str(a) for a in self.args)
+        if self.options:
+            address += ',' + ','.join(str(o) for o in self.options)
+        return address
+
+    def format(self, *args, **kwargs):
+        return str(self).format(*args, **kwargs)
+
+
+STDIO = SocatAddress.stdio()
+
+
+class SocatOption(object):
+
+    def __init__(self, name, *args):
+        self.name = name
+        self.args = args
+
+    @classmethod
+    def bind(cls, host):
+        return cls('bind', host)
+
+    @classmethod
+    def fork(cls):
+        return cls('fork')
+
+    @classmethod
+    def ip_multicast_ttl(cls, ttl):
+        return cls('ip-multicast-ttl', int(ttl))
+
+    @classmethod
+    def ip_multicast_if(cls, interface_address):
+        return cls('ip-multicast-if', interface_address)
+
+    @classmethod
+    def ip_add_membership(cls, multicast_address, interface_address):
+        return cls('ip-add-membership', multicast_address, interface_address)
+
+    def __str__(self):
+        result = self.name
+        args = self.args
+        if args:
+            result += '=' + ':'.join(str(a) for a in args)
+        return result
+
+
+class SocatCommand(object):
+
+    def __init__(self, source=STDIO, destination=STDIO, command=COMMAND):
+        self.source = source
+        self.destination = destination
+        self.command = command
+
+    def __str__(self):
+        words = [self.command, self.source, self.destination]
+        return ' '.join(str(obj) for obj in words)
+
+
+def socat_command(source=STDIO, destination=STDIO, command=COMMAND):
+    command = SocatCommand(source=source, destination=destination,
+                           command=command)
+    return str(command)
diff --git a/neutron_tempest_plugin/services/network/json/network_client.py b/neutron_tempest_plugin/services/network/json/network_client.py
index 930cbfd..b316ce4 100644
--- a/neutron_tempest_plugin/services/network/json/network_client.py
+++ b/neutron_tempest_plugin/services/network/json/network_client.py
@@ -745,26 +745,23 @@
         body = jsonutils.loads(body)
         return service_client.ResponseBody(resp, body)
 
-    def create_trunk(self, parent_port_id, subports,
+    def create_trunk(self, parent_port_id=None, subports=None,
                      tenant_id=None, name=None, admin_state_up=None,
-                     description=None):
+                     description=None, **kwargs):
         uri = '%s/trunks' % self.uri_prefix
-        post_data = {
-            'trunk': {
-                'port_id': parent_port_id,
-            }
-        }
+        if parent_port_id:
+            kwargs['port_id'] = parent_port_id
         if subports is not None:
-            post_data['trunk']['sub_ports'] = subports
+            kwargs['sub_ports'] = subports
         if tenant_id is not None:
-            post_data['trunk']['tenant_id'] = tenant_id
+            kwargs['tenant_id'] = tenant_id
         if name is not None:
-            post_data['trunk']['name'] = name
+            kwargs['name'] = name
         if description is not None:
-            post_data['trunk']['description'] = description
+            kwargs['description'] = description
         if admin_state_up is not None:
-            post_data['trunk']['admin_state_up'] = admin_state_up
-        resp, body = self.post(uri, self.serialize(post_data))
+            kwargs['admin_state_up'] = admin_state_up
+        resp, body = self.post(uri, self.serialize({'trunk': kwargs}))
         body = self.deserialize_single(body)
         self.expected_success(201, resp.status)
         return service_client.ResponseBody(resp, body)