Merge "Assert extensions match in config file and server"
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/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)