From 9cda319687731bc839700c4085dc73e194b07e95 Mon Sep 17 00:00:00 2001 From: Ryan Tidwell Date: Thu, 18 Feb 2016 17:34:43 +0800 Subject: [PATCH] Enable CRUD for trunk ports This patch enables basic CRUD operations on trunk ports and defines related API extensions. Trunk ports and sub-ports can be persisted in the Neutron model and are made visible through the API, but the L2 agent is not notified and no trunk ports or subports are actually instantiated on compute hosts. This one of the main patches in the series that implement the end to end functionality. Partially-implements: blueprint vlan-aware-vms Co-Authored-By: Armando Migliaccio Change-Id: I26453eb9a1b25e116193417271400994ac57e4c1 --- devstack/lib/trunk | 7 + devstack/plugin.sh | 4 + etc/policy.json | 9 +- neutron/extensions/trunk.py | 152 ++++++++++++++ neutron/extensions/trunk_details.py | 56 ++++++ neutron/objects/trunk.py | 11 + neutron/services/trunk/__init__.py | 15 ++ neutron/services/trunk/constants.py | 20 ++ neutron/services/trunk/db.py | 35 ---- neutron/services/trunk/exceptions.py | 19 ++ neutron/services/trunk/plugin.py | 178 ++++++++++++++++ neutron/services/trunk/rules.py | 116 +++++++++++ neutron/services/trunk/validators/__init__.py | 18 ++ neutron/services/trunk/validators/vlan.py | 39 ++++ neutron/tests/contrib/gate_hook.sh | 1 + neutron/tests/contrib/hooks/api_extensions | 4 +- neutron/tests/contrib/hooks/trunk | 1 + neutron/tests/etc/policy.json | 9 +- neutron/tests/tempest/api/test_trunk.py | 133 ++++++++++++ .../tests/tempest/api/test_trunk_negative.py | 190 ++++++++++++++++++ .../services/network/json/network_client.py | 58 ++++++ neutron/tests/unit/objects/test_trunk.py | 10 + neutron/tests/unit/services/trunk/test_db.py | 50 ----- .../tests/unit/services/trunk/test_plugin.py | 40 ++++ .../tests/unit/services/trunk/test_rules.py | 150 ++++++++++++++ setup.cfg | 1 + 26 files changed, 1238 insertions(+), 88 deletions(-) create mode 100644 devstack/lib/trunk create mode 100755 neutron/extensions/trunk.py create mode 100644 neutron/extensions/trunk_details.py create mode 100644 neutron/services/trunk/constants.py delete mode 100644 neutron/services/trunk/db.py create mode 100644 neutron/services/trunk/plugin.py create mode 100644 neutron/services/trunk/rules.py create mode 100644 neutron/services/trunk/validators/__init__.py create mode 100644 neutron/services/trunk/validators/vlan.py create mode 100644 neutron/tests/contrib/hooks/trunk create mode 100644 neutron/tests/tempest/api/test_trunk.py create mode 100644 neutron/tests/tempest/api/test_trunk_negative.py delete mode 100644 neutron/tests/unit/services/trunk/test_db.py create mode 100644 neutron/tests/unit/services/trunk/test_plugin.py create mode 100644 neutron/tests/unit/services/trunk/test_rules.py diff --git a/devstack/lib/trunk b/devstack/lib/trunk new file mode 100644 index 00000000000..c1471a1cf1d --- /dev/null +++ b/devstack/lib/trunk @@ -0,0 +1,7 @@ +function configure_trunk_service_plugin { + _neutron_service_plugin_class_add "trunk" +} + +function configure_trunk_extension { + configure_trunk_service_plugin +} diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 0411c636f21..1943d9f0ff5 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -6,6 +6,7 @@ source $LIBDIR/l2_agent_sriovnicswitch source $LIBDIR/ml2 source $LIBDIR/qos source $LIBDIR/ovs +source $LIBDIR/trunk Q_BUILD_OVS_FROM_GIT=$(trueorfalse False Q_BUILD_OVS_FROM_GIT) @@ -22,6 +23,9 @@ if [[ "$1" == "stack" ]]; then if is_service_enabled q-qos; then configure_qos fi + if is_service_enabled q-trunk; then + configure_trunk_extension + fi if [[ "$Q_AGENT" == "openvswitch" ]] && \ [[ "$Q_BUILD_OVS_FROM_GIT" == "True" ]]; then remove_ovs_packages diff --git a/etc/policy.json b/etc/policy.json index 36b16225048..930b1a6be42 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -218,5 +218,12 @@ "create_flavor_service_profile": "rule:admin_only", "delete_flavor_service_profile": "rule:admin_only", "get_flavor_service_profile": "rule:regular_user", - "get_auto_allocated_topology": "rule:admin_or_owner" + "get_auto_allocated_topology": "rule:admin_or_owner", + + "create_trunk": "rule:regular_user", + "get_trunk": "rule:admin_or_owner", + "delete_trunk": "rule:admin_or_owner", + "get_subports": "", + "add_subports": "rule:admin_or_owner", + "remove_subports": "rule:admin_or_owner" } diff --git a/neutron/extensions/trunk.py b/neutron/extensions/trunk.py new file mode 100755 index 00000000000..3d8f7ac48c4 --- /dev/null +++ b/neutron/extensions/trunk.py @@ -0,0 +1,152 @@ +# Copyright (c) 2016 ZTE 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. + +from oslo_log import log as logging + +from neutron_lib.api import converters +from neutron_lib.api import validators + +from neutron._i18n import _ +from neutron.api import extensions +from neutron.api.v2 import attributes as attr +from neutron.api.v2 import resource_helper + +LOG = logging.getLogger(__name__) + + +# TODO(armax): this validator was introduced in neutron-lib in +# https://review.openstack.org/#/c/319386/; remove it as soon +# as there is a new release. +def validate_subports(data, valid_values=None): + if not isinstance(data, list): + msg = _("Invalid data format for subports: '%s'") % data + LOG.debug(msg) + return msg + + subport_ids = set() + segmentation_ids = set() + for subport in data: + if not isinstance(subport, dict): + msg = _("Invalid data format for subport: '%s'") % subport + LOG.debug(msg) + return msg + + # Expect a non duplicated and valid port_id for the subport + if 'port_id' not in subport: + msg = _("A valid port UUID must be specified") + LOG.debug(msg) + return msg + elif validators.validate_uuid(subport["port_id"]): + msg = _("Invalid UUID for subport: '%s'") % subport["port_id"] + return msg + elif subport["port_id"] in subport_ids: + msg = _("Non unique UUID for subport: '%s'") % subport["port_id"] + return msg + subport_ids.add(subport["port_id"]) + + # Validate that both segmentation id and segmentation type are + # specified, and that the client does not duplicate segmentation + # ids + segmentation_id = subport.get("segmentation_id") + segmentation_type = subport.get("segmentation_type") + if (not segmentation_id or not segmentation_type) and len(subport) > 1: + msg = _("Invalid subport details '%s': missing segmentation " + "information. Must specify both segmentation_id and " + "segmentation_type") % subport + LOG.debug(msg) + return msg + if segmentation_id in segmentation_ids: + msg = _("Segmentation ID '%(seg_id)s' for '%(subport)s' is not " + "unique") % {"seg_id": segmentation_id, + "subport": subport["port_id"]} + LOG.debug(msg) + return msg + if segmentation_id: + segmentation_ids.add(segmentation_id) + + +validators.validators['type:subports'] = validate_subports + + +RESOURCE_ATTRIBUTE_MAP = { + 'trunks': { + 'id': {'allow_post': False, 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, 'primary_key': True}, + 'tenant_id': {'allow_post': True, 'allow_put': False, + 'required_by_policy': True, + 'validate': + {'type:string': attr.TENANT_ID_MAX_LEN}, + 'is_visible': True}, + 'port_id': {'allow_post': True, 'allow_put': False, + 'required_by_policy': True, + 'validate': {'type:uuid': None}, + 'is_visible': True}, + 'sub_ports': {'allow_post': True, 'allow_put': False, + 'default': [], + 'convert_list_to': converters.convert_kvp_list_to_dict, + 'validate': {'type:subports': None}, + 'enforce_policy': True, + 'is_visible': True} + }, +} + + +class Trunk(extensions.ExtensionDescriptor): + """Trunk API extension.""" + + @classmethod + def get_name(cls): + return "Trunk Extension" + + @classmethod + def get_alias(cls): + return "trunk" + + @classmethod + def get_description(cls): + return "Provides support for trunk ports" + + @classmethod + def get_updated(cls): + return "2016-01-01T10:00:00-00:00" + + @classmethod + def get_resources(cls): + """Returns Ext Resources.""" + plural_mappings = resource_helper.build_plural_mappings( + {}, RESOURCE_ATTRIBUTE_MAP) + attr.PLURALS.update(plural_mappings) + action_map = {'trunk': {'add_subports': 'PUT', + 'remove_subports': 'PUT', + 'get_subports': 'GET'}} + return resource_helper.build_resource_info(plural_mappings, + RESOURCE_ATTRIBUTE_MAP, + 'trunk', + action_map=action_map, + register_quota=True) + + def update_attributes_map(self, attributes, extension_attrs_map=None): + super(Trunk, self).update_attributes_map( + attributes, extension_attrs_map=RESOURCE_ATTRIBUTE_MAP) + + def get_required_extensions(self): + return ["binding"] + + def get_extended_resources(self, version): + if version == "2.0": + return RESOURCE_ATTRIBUTE_MAP + else: + return {} diff --git a/neutron/extensions/trunk_details.py b/neutron/extensions/trunk_details.py new file mode 100644 index 00000000000..a9982b048fc --- /dev/null +++ b/neutron/extensions/trunk_details.py @@ -0,0 +1,56 @@ +# 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. + +from neutron_lib import constants + +from neutron.api import extensions + + +# NOTE(armax): because of the API machinery, this extension must be on +# its own. This aims at providing subport information for ports that +# are parent in a trunk so that consumers of the Neutron API, like Nova +# can efficiently access trunk information for things like metadata or +# config-drive configuration. +EXTENDED_ATTRIBUTES_2_0 = { + 'ports': {'trunk_details': {'allow_post': False, 'allow_put': False, + 'default': constants.ATTR_NOT_SPECIFIED, + 'is_visible': True, + 'enforce_policy': True, + 'required_by_policy': True}}, +} + + +class Trunk_details(extensions.ExtensionDescriptor): + + @classmethod + def get_name(cls): + return "Trunk port details" + + @classmethod + def get_alias(cls): + return "trunk-details" + + @classmethod + def get_description(cls): + return "Expose trunk port details" + + @classmethod + def get_updated(cls): + return "2016-01-01T10:00:00-00:00" + + def get_extended_resources(self, version): + if version == "2.0": + return EXTENDED_ATTRIBUTES_2_0 + else: + return {} diff --git a/neutron/objects/trunk.py b/neutron/objects/trunk.py index 61488698c1d..32a14256d35 100644 --- a/neutron/objects/trunk.py +++ b/neutron/objects/trunk.py @@ -43,6 +43,12 @@ class SubPort(base.NeutronDbObject): fields_no_update = ['segmentation_type', 'segmentation_id'] + def to_dict(self): + _dict = super(SubPort, self).to_dict() + # trunk_id is redundant in the subport dict. + _dict.pop('trunk_id') + return _dict + def create(self): with db_api.autonested_transaction(self.obj_context.session): try: @@ -66,6 +72,11 @@ class SubPort(base.NeutronDbObject): raise t_exc.TrunkNotFound(trunk_id=self.trunk_id) raise n_exc.PortNotFound(port_id=self.port_id) + except base.NeutronDbObjectDuplicateEntry: + raise t_exc.DuplicateSubPort( + segmentation_type=self.segmentation_type, + segmentation_id=self.segmentation_id, + trunk_id=self.trunk_id) @obj_base.VersionedObjectRegistry.register diff --git a/neutron/services/trunk/__init__.py b/neutron/services/trunk/__init__.py index e69de29bb2d..616ade0b3bc 100644 --- a/neutron/services/trunk/__init__.py +++ b/neutron/services/trunk/__init__.py @@ -0,0 +1,15 @@ +# 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. + +import neutron.services.trunk.validators # noqa diff --git a/neutron/services/trunk/constants.py b/neutron/services/trunk/constants.py new file mode 100644 index 00000000000..4b388e7561d --- /dev/null +++ b/neutron/services/trunk/constants.py @@ -0,0 +1,20 @@ +# 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. + +PARENT_PORT = 'parent_port' +SUBPORT = 'subport' +TRUNK = 'trunk' +TRUNK_PLUGIN = 'trunk_plugin' + +VLAN = 'vlan' diff --git a/neutron/services/trunk/db.py b/neutron/services/trunk/db.py deleted file mode 100644 index 2da581083fd..00000000000 --- a/neutron/services/trunk/db.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2016 Hewlett Packard Enterprise Development Company, LP -# -# 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. - -from oslo_db import exception as db_exc -from oslo_utils import uuidutils - -from neutron.services.trunk import exceptions -from neutron.services.trunk import models - - -def create_trunk(context, port_id, description=None): - """Create a trunk (with description) given the parent port uuid.""" - try: - with context.session.begin(subtransactions=True): - context.session.add( - models.Trunk( - id=uuidutils.generate_uuid(), - tenant_id=context.tenant_id, - port_id=port_id, - description=description)) - except db_exc.DBDuplicateEntry: - raise exceptions.TrunkPortInUse(port_id=port_id) diff --git a/neutron/services/trunk/exceptions.py b/neutron/services/trunk/exceptions.py index 3f8cfe1eb1b..fe6f5087820 100644 --- a/neutron/services/trunk/exceptions.py +++ b/neutron/services/trunk/exceptions.py @@ -24,3 +24,22 @@ class TrunkPortInUse(n_exc.InUse): class TrunkNotFound(n_exc.NotFound): message = _("Trunk %(trunk_id)s could not be found.") + + +class SubPortNotFound(n_exc.NotFound): + message = _("SubPort on trunk %(trunk_id)s with parent port %(port_id)s " + "could not be found.") + + +class DuplicateSubPort(n_exc.InUse): + message = _("segmentation_type %(segmentation_type)s and segmentation_id " + "%(segmentation_id)s already in use on trunk %(trunk_id)s.") + + +class ParentPortInUse(n_exc.InUse): + message = _("Port %(port_id)s is currently in use and is not " + "eligible for use as a parent port.") + + +class TrunkInUse(n_exc.InUse): + message = _("Trunk %(trunk_id)s is currently in use.") diff --git a/neutron/services/trunk/plugin.py b/neutron/services/trunk/plugin.py new file mode 100644 index 00000000000..f7cfb96eae5 --- /dev/null +++ b/neutron/services/trunk/plugin.py @@ -0,0 +1,178 @@ +# 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. + +from oslo_log import log as logging +from oslo_utils import uuidutils + +from neutron.callbacks import events +from neutron.callbacks import registry +from neutron.db import api as db_api +from neutron.db import common_db_mixin +from neutron.db import db_base_plugin_common +from neutron.objects import base as objects_base +from neutron.objects import trunk as trunk_objects +from neutron.services import service_base +from neutron.services.trunk import constants +from neutron.services.trunk import exceptions as trunk_exc +from neutron.services.trunk import rules + +LOG = logging.getLogger(__name__) + + +class TrunkPlugin(service_base.ServicePluginBase, + common_db_mixin.CommonDbMixin): + + supported_extension_aliases = ["trunk", "trunk-details"] + + def __init__(self): + self._segmentation_types = {} + #TODO(tidwellr) notify using events.AFTER_INIT once available + registry.notify(constants.TRUNK_PLUGIN, events.AFTER_CREATE, self) + LOG.debug('Trunk plugin loaded') + + def add_segmentation_type(self, segmentation_type, id_validator): + self._segmentation_types[segmentation_type] = id_validator + LOG.debug('Added support for segmentation type %s', segmentation_type) + + def validate(self, context, trunk): + """Return a valid trunk or raises an error if unable to do so.""" + trunk_details = trunk + + trunk_validator = rules.TrunkPortValidator(trunk['port_id']) + trunk_details['port_id'] = trunk_validator.validate(context) + + subports_validator = rules.SubPortsValidator( + self._segmentation_types, trunk['sub_ports'], trunk['port_id']) + trunk_details['sub_ports'] = subports_validator.validate(context) + return trunk_details + + def get_plugin_description(self): + return "Trunk port service plugin" + + @classmethod + def get_plugin_type(cls): + return "trunk" + + @db_base_plugin_common.filter_fields + @db_base_plugin_common.convert_result_to_dict + def get_trunk(self, context, trunk_id, fields=None): + """Return information for the specified trunk.""" + obj = trunk_objects.Trunk.get_object(context, id=trunk_id) + if obj is None: + raise trunk_exc.TrunkNotFound(trunk_id=trunk_id) + + return obj + + @db_base_plugin_common.filter_fields + @db_base_plugin_common.convert_result_to_dict + def get_trunks(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, page_reverse=False): + """Return information for available trunks.""" + filters = filters or {} + pager = objects_base.Pager(sorts=sorts, limit=limit, + page_reverse=page_reverse, marker=marker) + return trunk_objects.Trunk.get_objects(context, _pager=pager, + **filters) + + @db_base_plugin_common.convert_result_to_dict + def create_trunk(self, context, trunk): + """Create a trunk.""" + trunk = self.validate(context, trunk['trunk']) + sub_ports = [trunk_objects.SubPort( + context=context, + port_id=p['port_id'], + segmentation_id=p['segmentation_id'], + segmentation_type=p['segmentation_type']) + for p in trunk['sub_ports']] + trunk_obj = trunk_objects.Trunk(context=context, + id=uuidutils.generate_uuid(), + tenant_id=trunk['tenant_id'], + port_id=trunk['port_id'], + sub_ports=sub_ports) + trunk_obj.create() + return trunk_obj + + def delete_trunk(self, context, trunk_id): + """Delete the specified trunk.""" + trunk = trunk_objects.Trunk.get_object(context, id=trunk_id) + if trunk: + trunk_validator = rules.TrunkPortValidator(trunk.port_id) + if not trunk_validator.is_bound(context): + trunk.delete() + return + raise trunk_exc.TrunkInUse(trunk_id=trunk_id) + + raise trunk_exc.TrunkNotFound(trunk_id=trunk_id) + + @db_base_plugin_common.convert_result_to_dict + def add_subports(self, context, trunk_id, subports): + """Add one or more subports to trunk.""" + trunk = trunk_objects.Trunk.get_object(context, id=trunk_id) + if trunk is None: + raise trunk_exc.TrunkNotFound(trunk_id=trunk_id) + + # Check for basic validation since the request body here is not + # automatically validated by the API layer. + subports_validator = rules.SubPortsValidator( + self._segmentation_types, subports) + subports = subports_validator.validate(context, basic_validation=True) + + with db_api.autonested_transaction(context.session): + for subport in subports: + obj = trunk_objects.SubPort( + context=context, + trunk_id=trunk_id, + port_id=subport['port_id'], + segmentation_type=subport['segmentation_type'], + segmentation_id=subport['segmentation_id']) + obj.create() + trunk['sub_ports'].append(obj) + + return trunk + + @db_base_plugin_common.convert_result_to_dict + def remove_subports(self, context, trunk_id, subports): + """Remove one or more subports from trunk.""" + with db_api.autonested_transaction(context.session): + trunk = trunk_objects.Trunk.get_object(context, id=trunk_id) + if trunk is None: + raise trunk_exc.TrunkNotFound(trunk_id=trunk_id) + + subports_validator = rules.SubPortsValidator( + self._segmentation_types, subports) + # the subports are being removed, therefore we do not need to + # enforce any specific trunk rules, other than basic validation + # of the request body. + subports = subports_validator.validate( + context, basic_validation=True, + trunk_validation=False) + + current_subports = {p.port_id: p for p in trunk.sub_ports} + + for subport in subports: + subport_obj = current_subports.pop(subport['port_id'], None) + + if not subport_obj: + raise trunk_exc.SubPortNotFound(trunk_id=trunk_id, + port_id=subport['port_id']) + subport_obj.delete() + + trunk.sub_ports = list(current_subports.values()) + return trunk + + @db_base_plugin_common.filter_fields + def get_subports(self, context, trunk_id, fields=None): + """Return subports for the specified trunk.""" + trunk = self.get_trunk(context, trunk_id) + return {'sub_ports': trunk['sub_ports']} diff --git a/neutron/services/trunk/rules.py b/neutron/services/trunk/rules.py new file mode 100644 index 00000000000..98add6dcaf4 --- /dev/null +++ b/neutron/services/trunk/rules.py @@ -0,0 +1,116 @@ +# 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. + +from neutron_lib import constants as n_const +from neutron_lib import exceptions as n_exc + +from neutron._i18n import _ +from neutron.extensions import portbindings +from neutron.extensions import trunk +from neutron import manager +from neutron.objects import trunk as trunk_objects +from neutron.services.trunk import exceptions as trunk_exc + + +# This layer is introduced for keeping busines logic and +# data persistence decoupled. + +class TrunkPortValidator(object): + + def __init__(self, port_id): + self.port_id = port_id + + def validate(self, context): + """Validate that the port can be used in a trunk.""" + # TODO(tidwellr): there is a chance of a race between the + # time these checks are performed and the time the trunk + # creation is executed. To be revisited, if it bites. + + # Validate that the given port_id is not used by a subport. + subports = trunk_objects.SubPort.get_objects( + context, port_id=self.port_id) + if subports: + raise trunk_exc.TrunkPortInUse(port_id=self.port_id) + + # Validate that the given port_id is not used by a trunk. + trunks = trunk_objects.Trunk.get_objects(context, port_id=self.port_id) + if trunks: + raise trunk_exc.ParentPortInUse(port_id=self.port_id) + + if self.is_bound(context): + raise trunk_exc.ParentPortInUse(port_id=self.port_id) + + return self.port_id + + def is_bound(self, context): + """Return true if the port is bound, false otherwise.""" + # Validate that the given port_id does not have a port binding. + core_plugin = manager.NeutronManager.get_plugin() + port = core_plugin.get_port(context, self.port_id) + device_owner = port.get('device_owner', '') + return port.get(portbindings.HOST_ID) or \ + device_owner.startswith(n_const.DEVICE_OWNER_COMPUTE_PREFIX) + + +class SubPortsValidator(object): + + def __init__(self, segmentation_types, subports, trunk_port_id=None): + self._segmentation_types = segmentation_types + self.subports = subports + self.trunk_port_id = trunk_port_id + + def validate(self, context, + basic_validation=False, trunk_validation=True): + """Validate that subports can be used in a trunk.""" + # Perform basic validation on subports, in case subports + # are not automatically screened by the API layer. + if basic_validation: + msg = trunk.validate_subports(self.subports) + if msg: + raise n_exc.InvalidInput(error_message=msg) + if trunk_validation: + return [self._validate(context, s) for s in self.subports] + else: + return self.subports + + def _validate(self, context, subport): + # Check that the subport doesn't reference the same port_id as a + # trunk we may be in the middle of trying to create, in other words + # make the validation idiot proof. + if subport['port_id'] == self.trunk_port_id: + raise trunk_exc.ParentPortInUse(port_id=subport['port_id']) + + # If the segmentation details are missing, we will need to + # figure out defaults when the time comes to support Ironic. + # We can reasonably expect segmentation details to be provided + # in all other cases for now. + segmentation_id = subport.get("segmentation_id") + segmentation_type = subport.get("segmentation_type") + if not segmentation_id or not segmentation_type: + msg = _("Invalid subport details '%s': missing segmentation " + "information. Must specify both segmentation_id and " + "segmentation_type") % subport + raise n_exc.InvalidInput(error_message=msg) + + if segmentation_type not in self._segmentation_types: + msg = _("Invalid segmentation_type '%s'") % segmentation_type + raise n_exc.InvalidInput(error_message=msg) + + if not self._segmentation_types[segmentation_type](segmentation_id): + msg = _("Invalid segmentation id '%s'") % segmentation_id + raise n_exc.InvalidInput(error_message=msg) + + trunk_validator = TrunkPortValidator(subport['port_id']) + trunk_validator.validate(context) + return subport diff --git a/neutron/services/trunk/validators/__init__.py b/neutron/services/trunk/validators/__init__.py new file mode 100644 index 00000000000..5ef4e49c021 --- /dev/null +++ b/neutron/services/trunk/validators/__init__.py @@ -0,0 +1,18 @@ +# 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. + +from neutron.services.trunk.validators import vlan + +# Register segmentation_type validation drivers +vlan.register() diff --git a/neutron/services/trunk/validators/vlan.py b/neutron/services/trunk/validators/vlan.py new file mode 100644 index 00000000000..69133d5a217 --- /dev/null +++ b/neutron/services/trunk/validators/vlan.py @@ -0,0 +1,39 @@ +# 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. + +from oslo_log import log as logging + +from neutron.callbacks import events +from neutron.callbacks import registry +from neutron.plugins.common import constants as common_consts +from neutron.services.trunk import constants as trunk_consts + +LOG = logging.getLogger(__name__) + + +def register(): + #TODO(tidwellr) register for AFTER_INIT once available + registry.subscribe(handler, trunk_consts.TRUNK_PLUGIN, + events.AFTER_CREATE) + LOG.debug('Registering for trunk support') + + +def handler(resource, event, trigger, **kwargs): + trigger.add_segmentation_type(trunk_consts.VLAN, vlan_range) + LOG.debug('Registration complete') + + +def vlan_range(segmentation_id): + min_vid, max_vid = common_consts.MIN_VLAN_TAG, common_consts.MAX_VLAN_TAG + return min_vid <= segmentation_id <= max_vid diff --git a/neutron/tests/contrib/gate_hook.sh b/neutron/tests/contrib/gate_hook.sh index 51fc58e15f6..abcfcf75bc2 100644 --- a/neutron/tests/contrib/gate_hook.sh +++ b/neutron/tests/contrib/gate_hook.sh @@ -66,6 +66,7 @@ case $VENV in load_conf_hook sorting load_conf_hook pagination load_rc_hook qos + load_rc_hook trunk load_conf_hook osprofiler if [[ "$VENV" =~ "pecan" ]]; then load_conf_hook pecan diff --git a/neutron/tests/contrib/hooks/api_extensions b/neutron/tests/contrib/hooks/api_extensions index 32a4123708e..1d7e34a6169 100644 --- a/neutron/tests/contrib/hooks/api_extensions +++ b/neutron/tests/contrib/hooks/api_extensions @@ -33,5 +33,7 @@ NETWORK_API_EXTENSIONS=" standard-attr-description, \ subnet_allocation, \ tag, \ - timestamp_core" + timestamp_core, \ + trunk, \ + trunk-details" NETWORK_API_EXTENSIONS="$(echo $NETWORK_API_EXTENSIONS | tr -d ' ')" diff --git a/neutron/tests/contrib/hooks/trunk b/neutron/tests/contrib/hooks/trunk new file mode 100644 index 00000000000..a2af3601ab2 --- /dev/null +++ b/neutron/tests/contrib/hooks/trunk @@ -0,0 +1 @@ +enable_service q-trunk diff --git a/neutron/tests/etc/policy.json b/neutron/tests/etc/policy.json index 36b16225048..930b1a6be42 100644 --- a/neutron/tests/etc/policy.json +++ b/neutron/tests/etc/policy.json @@ -218,5 +218,12 @@ "create_flavor_service_profile": "rule:admin_only", "delete_flavor_service_profile": "rule:admin_only", "get_flavor_service_profile": "rule:regular_user", - "get_auto_allocated_topology": "rule:admin_or_owner" + "get_auto_allocated_topology": "rule:admin_or_owner", + + "create_trunk": "rule:regular_user", + "get_trunk": "rule:admin_or_owner", + "delete_trunk": "rule:admin_or_owner", + "get_subports": "", + "add_subports": "rule:admin_or_owner", + "remove_subports": "rule:admin_or_owner" } diff --git a/neutron/tests/tempest/api/test_trunk.py b/neutron/tests/tempest/api/test_trunk.py new file mode 100644 index 00000000000..589acf9e147 --- /dev/null +++ b/neutron/tests/tempest/api/test_trunk.py @@ -0,0 +1,133 @@ +# Copyright 2016 Hewlett Packard Enterprise Development Company LP +# +# 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. + +from tempest.lib import exceptions as lib_exc +from tempest import test + +from neutron.tests.tempest.api import base + + +class TrunkTestJSONBase(base.BaseAdminNetworkTest): + + def _create_trunk_with_network_and_parent(self, subports): + network = self.create_network() + parent_port = self.create_port(network) + return self.client.create_trunk(parent_port['id'], subports) + + +class TrunkTestJSON(TrunkTestJSONBase): + + @classmethod + @test.requires_ext(extension="trunk", service="network") + def resource_setup(cls): + super(TrunkTestJSON, cls).resource_setup() + + def tearDown(self): + # NOTE(tidwellr) These tests create networks and ports, clean them up + # after each test to avoid hitting quota limits + self.resource_cleanup() + super(TrunkTestJSON, self).tearDown() + + @test.idempotent_id('e1a6355c-4768-41f3-9bf8-0f1d192bd501') + def test_create_trunk_empty_subports_list(self): + trunk = self._create_trunk_with_network_and_parent([]) + observed_trunk = self.client.show_trunk(trunk['trunk']['id']) + self.assertEqual(trunk, observed_trunk) + + @test.idempotent_id('382dfa39-ca03-4bd3-9a1c-91e36d2e3796') + def test_create_trunk_subports_not_specified(self): + trunk = self._create_trunk_with_network_and_parent(None) + observed_trunk = self.client.show_trunk(trunk['trunk']['id']) + self.assertEqual(trunk, observed_trunk) + + @test.idempotent_id('7de46c22-e2b6-4959-ac5a-0e624632ab32') + def test_create_show_delete_trunk(self): + trunk = self._create_trunk_with_network_and_parent(None) + trunk_id = trunk['trunk']['id'] + parent_port_id = trunk['trunk']['port_id'] + res = self.client.show_trunk(trunk_id) + self.assertEqual(trunk_id, res['trunk']['id']) + self.assertEqual(parent_port_id, res['trunk']['port_id']) + self.client.delete_trunk(trunk_id) + self.assertRaises(lib_exc.NotFound, self.client.show_trunk, trunk_id) + + @test.idempotent_id('73365f73-bed6-42cd-960b-ec04e0c99d85') + def test_list_trunks(self): + trunk1 = self._create_trunk_with_network_and_parent(None) + trunk2 = self._create_trunk_with_network_and_parent(None) + expected_trunks = {trunk1['trunk']['id']: trunk1['trunk'], + trunk2['trunk']['id']: trunk2['trunk']} + trunk_list = self.client.list_trunks()['trunks'] + matched_trunks = [x for x in trunk_list if x['id'] in expected_trunks] + self.assertEqual(2, len(matched_trunks)) + for trunk in matched_trunks: + self.assertEqual(expected_trunks[trunk['id']], trunk) + + @test.idempotent_id('bb5fcead-09b5-484a-bbe6-46d1e06d6cc0') + def test_add_subport(self): + trunk = self._create_trunk_with_network_and_parent([]) + network = self.create_network() + port = self.create_port(network) + subports = [{'port_id': port['id'], + 'segmentation_type': 'vlan', + 'segmentation_id': 2}] + self.client.add_subports(trunk['trunk']['id'], subports) + trunk = self.client.show_trunk(trunk['trunk']['id']) + observed_subports = trunk['trunk']['sub_ports'] + self.assertEqual(1, len(observed_subports)) + created_subport = observed_subports[0] + self.assertEqual(subports[0], created_subport) + + @test.idempotent_id('96eea398-a03c-4c3e-a99e-864392c2ca53') + def test_remove_subport(self): + subport_parent1 = self.create_port(self.create_network()) + subport_parent2 = self.create_port(self.create_network()) + subports = [{'port_id': subport_parent1['id'], + 'segmentation_type': 'vlan', + 'segmentation_id': 2}, + {'port_id': subport_parent2['id'], + 'segmentation_type': 'vlan', + 'segmentation_id': 4}] + trunk = self._create_trunk_with_network_and_parent(subports) + removed_subport = trunk['trunk']['sub_ports'][0] + expected_subport = None + + for subport in subports: + if subport['port_id'] != removed_subport['port_id']: + expected_subport = subport + break + + # Remove the subport and validate PUT response + res = self.client.remove_subports(trunk['trunk']['id'], + [removed_subport]) + self.assertEqual(1, len(res['sub_ports'])) + self.assertEqual(expected_subport, res['sub_ports'][0]) + + # Validate the results of a subport list + trunk = self.client.show_trunk(trunk['trunk']['id']) + observed_subports = trunk['trunk']['sub_ports'] + self.assertEqual(1, len(observed_subports)) + self.assertEqual(expected_subport, observed_subports[0]) + + @test.idempotent_id('bb5fcaad-09b5-484a-dde6-4cd1ea6d6ff0') + def test_get_subports(self): + network = self.create_network() + port = self.create_port(network) + subports = [{'port_id': port['id'], + 'segmentation_type': 'vlan', + 'segmentation_id': 2}] + trunk = self._create_trunk_with_network_and_parent(subports) + trunk = self.client.get_subports(trunk['trunk']['id']) + observed_subports = trunk['sub_ports'] + self.assertEqual(1, len(observed_subports)) diff --git a/neutron/tests/tempest/api/test_trunk_negative.py b/neutron/tests/tempest/api/test_trunk_negative.py new file mode 100644 index 00000000000..1cf37b7a9c1 --- /dev/null +++ b/neutron/tests/tempest/api/test_trunk_negative.py @@ -0,0 +1,190 @@ +# Copyright 2016 Hewlett Packard Enterprise Development Company LP +# +# 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. + +from oslo_utils import uuidutils +from tempest.lib import exceptions as lib_exc +from tempest import test + +from neutron.tests.tempest.api import test_trunk + + +class TrunkTestJSON(test_trunk.TrunkTestJSONBase): + + def tearDown(self): + # NOTE(tidwellr) These tests create networks and ports, clean them up + # after each test to avoid hitting quota limits + self.resource_cleanup() + super(TrunkTestJSON, self).tearDown() + + @classmethod + @test.requires_ext(extension="trunk", service="network") + def resource_setup(cls): + super(test_trunk.TrunkTestJSONBase, cls).resource_setup() + + @test.attr(type='negative') + @test.idempotent_id('1b5cf87a-1d3a-4a94-ba64-647153d54f32') + def test_create_trunk_nonexistent_port_id(self): + self.assertRaises(lib_exc.NotFound, self.client.create_trunk, + uuidutils.generate_uuid(), []) + + @test.attr(type='negative') + @test.idempotent_id('980bca3b-b0be-45ac-8067-b401e445b796') + def test_create_trunk_nonexistent_subport_port_id(self): + network = self.create_network() + parent_port = self.create_port(network) + self.assertRaises(lib_exc.NotFound, self.client.create_trunk, + parent_port['id'], + [{'port_id': uuidutils.generate_uuid(), + 'segmentation_type': 'vlan', + 'segmentation_id': 2}]) + + @test.attr(type='negative') + @test.idempotent_id('a5c5200a-72a0-43c5-a11a-52f808490344') + def test_create_subport_nonexistent_port_id(self): + trunk = self._create_trunk_with_network_and_parent([]) + self.assertRaises(lib_exc.NotFound, self.client.add_subports, + trunk['trunk']['id'], + [{'port_id': uuidutils.generate_uuid(), + 'segmentation_type': 'vlan', + 'segmentation_id': 2}]) + + @test.attr(type='negative') + @test.idempotent_id('80deb6a9-da2a-48db-b7fd-bcef5b14edc1') + def test_create_subport_nonexistent_trunk(self): + network = self.create_network() + parent_port = self.create_port(network) + self.assertRaises(lib_exc.NotFound, self.client.add_subports, + uuidutils.generate_uuid(), + [{'port_id': parent_port['id'], + 'segmentation_type': 'vlan', + 'segmentation_id': 2}]) + + @test.attr(type='negative') + @test.idempotent_id('7e0f99ab-fe37-408b-a889-9e44ef300084') + def test_create_subport_missing_segmentation_id(self): + trunk = self._create_trunk_with_network_and_parent([]) + subport_network = self.create_network() + parent_port = self.create_port(subport_network) + self.assertRaises(lib_exc.BadRequest, self.client.add_subports, + trunk['trunk']['id'], + [{'port_id': parent_port['id'], + 'segmentation_type': 'vlan'}]) + + @test.attr(type='negative') + @test.idempotent_id('a315d78b-2f43-4efa-89ae-166044c568aa') + def test_create_trunk_with_subport_missing_segmentation_id(self): + subport_network = self.create_network() + parent_port = self.create_port(subport_network) + self.assertRaises(lib_exc.BadRequest, self.client.create_trunk, + parent_port['id'], + [{'port_id': uuidutils.generate_uuid(), + 'segmentation_type': 'vlan'}]) + + @test.attr(type='negative') + @test.idempotent_id('33498618-f75a-4796-8ae6-93d4fd203fa4') + def test_create_trunk_with_subport_missing_segmentation_type(self): + subport_network = self.create_network() + parent_port = self.create_port(subport_network) + self.assertRaises(lib_exc.BadRequest, self.client.create_trunk, + parent_port['id'], + [{'port_id': uuidutils.generate_uuid(), + 'segmentation_id': 3}]) + + @test.attr(type='negative') + @test.idempotent_id('a717691c-4e07-4d81-a98d-6f1c18c5d183') + def test_create_trunk_with_subport_missing_port_id(self): + subport_network = self.create_network() + parent_port = self.create_port(subport_network) + self.assertRaises(lib_exc.BadRequest, self.client.create_trunk, + parent_port['id'], + [{'segmentation_type': 'vlan', + 'segmentation_id': 3}]) + + @test.attr(type='negative') + @test.idempotent_id('40aed9be-e976-47d0-a555-bde2c7e74e57') + def test_create_trunk_duplicate_subport_segmentation_ids(self): + trunk = self._create_trunk_with_network_and_parent([]) + subport_network1 = self.create_network() + subport_network2 = self.create_network() + parent_port1 = self.create_port(subport_network1) + parent_port2 = self.create_port(subport_network2) + self.assertRaises(lib_exc.BadRequest, self.client.create_trunk, + trunk['trunk']['id'], + [{'port_id': parent_port1['id'], + 'segmentation_id': 2, + 'segmentation_type': 'vlan'}, + {'port_id': parent_port2['id'], + 'segmentation_id': 2, + 'segmentation_type': 'vlan'}]) + + @test.attr(type='negative') + @test.idempotent_id('6f132ccc-1380-42d8-9c44-50411612bd01') + def test_add_subport_port_id_uses_trunk_port_id(self): + trunk = self._create_trunk_with_network_and_parent(None) + self.assertRaises(lib_exc.Conflict, self.client.add_subports, + trunk['trunk']['id'], + [{'port_id': trunk['trunk']['port_id'], + 'segmentation_type': 'vlan', + 'segmentation_id': 2}]) + + @test.attr(type='negative') + @test.idempotent_id('00cb40bb-1593-44c8-808c-72b47e64252f') + def test_add_subport_duplicate_segmentation_details(self): + trunk = self._create_trunk_with_network_and_parent(None) + network = self.create_network() + parent_port1 = self.create_port(network) + parent_port2 = self.create_port(network) + self.client.add_subports(trunk['trunk']['id'], + [{'port_id': parent_port1['id'], + 'segmentation_type': 'vlan', + 'segmentation_id': 2}]) + self.assertRaises(lib_exc.Conflict, self.client.add_subports, + trunk['trunk']['id'], + [{'port_id': parent_port2['id'], + 'segmentation_type': 'vlan', + 'segmentation_id': 2}]) + + @test.attr(type='negative') + @test.idempotent_id('4eac8c25-83ee-4051-9620-34774f565730') + def test_add_subport_passing_dict(self): + trunk = self._create_trunk_with_network_and_parent(None) + self.assertRaises(lib_exc.BadRequest, self.client.add_subports, + trunk['trunk']['id'], + {'port_id': trunk['trunk']['port_id'], + 'segmentation_type': 'vlan', + 'segmentation_id': 2}) + + @test.attr(type='negative') + @test.idempotent_id('17ca7dd7-96a8-445a-941e-53c0c86c2fe2') + def test_remove_subport_passing_dict(self): + network = self.create_network() + parent_port = self.create_port(network) + subport_data = {'port_id': parent_port['id'], + 'segmentation_type': 'vlan', + 'segmentation_id': 2} + trunk = self._create_trunk_with_network_and_parent([subport_data]) + self.assertRaises(lib_exc.BadRequest, self.client.remove_subports, + trunk['trunk']['id'], subport_data) + + @test.attr(type='negative') + @test.idempotent_id('aaca7dd7-96b8-445a-931e-63f0d86d2fe2') + def test_remove_subport_not_found(self): + network = self.create_network() + parent_port = self.create_port(network) + subport_data = {'port_id': parent_port['id'], + 'segmentation_type': 'vlan', + 'segmentation_id': 2} + trunk = self._create_trunk_with_network_and_parent([]) + self.assertRaises(lib_exc.NotFound, self.client.remove_subports, + trunk['trunk']['id'], [subport_data]) diff --git a/neutron/tests/tempest/services/network/json/network_client.py b/neutron/tests/tempest/services/network/json/network_client.py index d9e333c4a18..32206047b38 100644 --- a/neutron/tests/tempest/services/network/json/network_client.py +++ b/neutron/tests/tempest/services/network/json/network_client.py @@ -659,6 +659,64 @@ class NetworkClientJSON(service_client.RestClient): body = jsonutils.loads(body) return service_client.ResponseBody(resp, body) + def create_trunk(self, parent_port_id, subports, tenant_id=None): + uri = '%s/trunks' % self.uri_prefix + post_data = { + 'trunk': { + 'port_id': parent_port_id, + } + } + if subports is not None: + post_data['trunk']['sub_ports'] = subports + if tenant_id is not None: + post_data['trunk']['tenant_id'] = tenant_id + resp, body = self.post(uri, self.serialize(post_data)) + body = self.deserialize_single(body) + self.expected_success(201, resp.status) + return service_client.ResponseBody(resp, body) + + def show_trunk(self, trunk_id): + uri = '%s/trunks/%s' % (self.uri_prefix, trunk_id) + resp, body = self.get(uri) + body = self.deserialize_single(body) + self.expected_success(200, resp.status) + return service_client.ResponseBody(resp, body) + + def list_trunks(self, **kwargs): + uri = '%s/trunks' % self.uri_prefix + if kwargs: + uri += '?' + urlparse.urlencode(kwargs, doseq=1) + resp, body = self.get(uri) + self.expected_success(200, resp.status) + body = self.deserialize_single(body) + return service_client.ResponseBody(resp, body) + + def delete_trunk(self, trunk_id): + uri = '%s/trunks/%s' % (self.uri_prefix, trunk_id) + resp, body = self.delete(uri) + self.expected_success(204, resp.status) + return service_client.ResponseBody(resp, body) + + def _subports_action(self, action, trunk_id, subports): + uri = '%s/trunks/%s/%s' % (self.uri_prefix, trunk_id, action) + resp, body = self.put(uri, jsonutils.dumps(subports)) + body = self.deserialize_single(body) + self.expected_success(200, resp.status) + return service_client.ResponseBody(resp, body) + + def add_subports(self, trunk_id, subports): + return self._subports_action('add_subports', trunk_id, subports) + + def remove_subports(self, trunk_id, subports): + return self._subports_action('remove_subports', trunk_id, subports) + + def get_subports(self, trunk_id): + uri = '%s/trunks/%s/%s' % (self.uri_prefix, trunk_id, 'get_subports') + resp, body = self.get(uri) + self.expected_success(200, resp.status) + body = jsonutils.loads(body) + return service_client.ResponseBody(resp, body) + def get_auto_allocated_topology(self, tenant_id=None): uri = '%s/auto-allocated-topology/%s' % (self.uri_prefix, tenant_id) resp, body = self.get(uri) diff --git a/neutron/tests/unit/objects/test_trunk.py b/neutron/tests/unit/objects/test_trunk.py index d9943621284..ec4b43fd8ff 100644 --- a/neutron/tests/unit/objects/test_trunk.py +++ b/neutron/tests/unit/objects/test_trunk.py @@ -13,9 +13,13 @@ # License for the specific language governing permissions and limitations # under the License. +import mock + from neutron_lib import exceptions as n_exc +from oslo_db import exception as obj_exc from oslo_utils import uuidutils +from neutron.objects.db import api as obj_db_api from neutron.objects import trunk as t_obj from neutron.services.trunk import exceptions as t_exc from neutron.tests.unit.objects import test_base @@ -26,6 +30,12 @@ class SubPortObjectTestCase(test_base.BaseObjectIfaceTestCase): _test_class = t_obj.SubPort + def test_create_duplicates(self): + with mock.patch.object(obj_db_api, 'create_object', + side_effect=obj_exc.DBDuplicateEntry): + obj = self._test_class(self.context, **self.obj_fields[0]) + self.assertRaises(t_exc.DuplicateSubPort, obj.create) + class SubPortDbObjectTestCase(test_base.BaseDbObjectTestCase, testlib_api.SqlTestCase): diff --git a/neutron/tests/unit/services/trunk/test_db.py b/neutron/tests/unit/services/trunk/test_db.py deleted file mode 100644 index c9aac638b80..00000000000 --- a/neutron/tests/unit/services/trunk/test_db.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2016 Hewlett Packard Enterprise Development Company, LP -# -# 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. - -from neutron import context -from neutron.db import models_v2 -from neutron.services.trunk import db -from neutron.services.trunk import exceptions -from neutron.tests.unit import testlib_api - - -class TrunkDBTestCase(testlib_api.SqlTestCase): - - def setUp(self): - super(TrunkDBTestCase, self).setUp() - self.ctx = context.get_admin_context() - - def _add_network(self, net_id): - with self.ctx.session.begin(subtransactions=True): - self.ctx.session.add(models_v2.Network(id=net_id)) - - def _add_port(self, net_id, port_id): - with self.ctx.session.begin(subtransactions=True): - port = models_v2.Port(id=port_id, - network_id=net_id, - mac_address='foo_mac_%s' % port_id, - admin_state_up=True, - status='DOWN', - device_id='', - device_owner='') - self.ctx.session.add(port) - - def test_create_trunk_raise_port_in_use(self): - self._add_network('foo_net') - self._add_port('foo_net', 'foo_port') - db.create_trunk(self.ctx, 'foo_port') - self.assertRaises(exceptions.TrunkPortInUse, - db.create_trunk, - self.ctx, 'foo_port') diff --git a/neutron/tests/unit/services/trunk/test_plugin.py b/neutron/tests/unit/services/trunk/test_plugin.py new file mode 100644 index 00000000000..55447b68846 --- /dev/null +++ b/neutron/tests/unit/services/trunk/test_plugin.py @@ -0,0 +1,40 @@ +# Copyright 2016 Hewlett Packard Enterprise Development Company, LP +# +# 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. + +from neutron import manager +from neutron.services.trunk import exceptions as trunk_exc +from neutron.services.trunk import plugin as trunk_plugin +from neutron.tests.unit.plugins.ml2 import test_plugin + + +class TrunkPluginTestCase(test_plugin.Ml2PluginV2TestCase): + + def setUp(self): + super(TrunkPluginTestCase, self).setUp() + self.trunk_plugin = trunk_plugin.TrunkPlugin() + + def test_delete_trunk_raise_in_use(self): + with self.port() as port: + trunk = {'port_id': port['port']['id'], + 'tenant_id': 'test_tenant', + 'sub_ports': []} + response = ( + self.trunk_plugin.create_trunk(self.context, {'trunk': trunk})) + core_plugin = manager.NeutronManager.get_plugin() + port['port']['binding:host_id'] = 'host' + core_plugin.update_port(self.context, port['port']['id'], port) + self.assertRaises(trunk_exc.TrunkInUse, + self.trunk_plugin.delete_trunk, + self.context, response['id']) diff --git a/neutron/tests/unit/services/trunk/test_rules.py b/neutron/tests/unit/services/trunk/test_rules.py new file mode 100644 index 00000000000..ba205c691ee --- /dev/null +++ b/neutron/tests/unit/services/trunk/test_rules.py @@ -0,0 +1,150 @@ +# Copyright 2016 Hewlett Packard Enterprise Development Company, LP +# +# 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. + +import mock + +from neutron_lib import constants as n_const +from neutron_lib import exceptions as n_exc +from oslo_utils import uuidutils + +from neutron import manager +from neutron.services.trunk import constants +from neutron.services.trunk import exceptions as trunk_exc +from neutron.services.trunk import plugin as trunk_plugin +from neutron.services.trunk import rules +from neutron.services.trunk.validators import vlan as vlan_driver +from neutron.tests import base +from neutron.tests.unit.plugins.ml2 import test_plugin + + +class SubPortsValidatorTestCase(base.BaseTestCase): + + def setUp(self): + super(SubPortsValidatorTestCase, self).setUp() + self.segmentation_types = {constants.VLAN: vlan_driver.vlan_range} + self.context = mock.ANY + + def test_validate_subport_subport_and_trunk_shared_port_id(self): + shared_id = uuidutils.generate_uuid() + validator = rules.SubPortsValidator( + self.segmentation_types, + [{'port_id': shared_id, + 'segmentation_type': 'vlan', + 'segmentation_id': 2}], + shared_id) + self.assertRaises(trunk_exc.ParentPortInUse, + validator.validate, self.context) + + def test_validate_subport_invalid_vlan_id(self): + validator = rules.SubPortsValidator( + self.segmentation_types, + [{'port_id': uuidutils.generate_uuid(), + 'segmentation_type': 'vlan', + 'segmentation_id': 5000}]) + self.assertRaises(n_exc.InvalidInput, + validator.validate, + self.context) + + def test_validate_subport_subport_invalid_segmenation_type(self): + validator = rules.SubPortsValidator( + self.segmentation_types, + [{'port_id': uuidutils.generate_uuid(), + 'segmentation_type': 'fake', + 'segmentation_id': 100}]) + self.assertRaises(n_exc.InvalidInput, + validator.validate, + self.context) + + def test_validate_subport_missing_segmenation_type(self): + validator = rules.SubPortsValidator( + self.segmentation_types, + [{'port_id': uuidutils.generate_uuid(), + 'segmentation_id': 100}]) + self.assertRaises(n_exc.InvalidInput, + validator.validate, + self.context) + + def test_validate_subport_missing_segmenation_id(self): + validator = rules.SubPortsValidator( + self.segmentation_types, + [{'port_id': uuidutils.generate_uuid(), + 'segmentation_type': 'fake'}]) + self.assertRaises(n_exc.InvalidInput, + validator.validate, + self.context) + + def test_validate_subport_missing_port_id(self): + validator = rules.SubPortsValidator( + self.segmentation_types, + [{'segmentation_type': 'fake', + 'segmentation_id': 100}]) + self.assertRaises(n_exc.InvalidInput, + validator.validate, + self.context, basic_validation=True) + + +class TrunkPortValidatorTestCase(test_plugin.Ml2PluginV2TestCase): + + def setUp(self): + super(TrunkPortValidatorTestCase, self).setUp() + self.trunk_plugin = trunk_plugin.TrunkPlugin() + self.trunk_plugin.add_segmentation_type(constants.VLAN, + vlan_driver.vlan_range) + + def test_validate_port_parent_in_use_by_trunk(self): + with self.port() as trunk_parent: + trunk = {'port_id': trunk_parent['port']['id'], + 'tenant_id': 'test_tenant', + 'sub_ports': []} + self.trunk_plugin.create_trunk(self.context, {'trunk': trunk}) + validator = rules.TrunkPortValidator(trunk_parent['port']['id']) + self.assertRaises(trunk_exc.ParentPortInUse, + validator.validate, + self.context) + + def test_validate_port_id_in_use_by_unrelated_trunk(self): + with self.port() as trunk_parent,\ + self.port() as subport: + trunk = {'port_id': trunk_parent['port']['id'], + 'tenant_id': 'test_tenant', + 'sub_ports': [{'port_id': subport['port']['id'], + 'segmentation_type': 'vlan', + 'segmentation_id': 2}]} + self.trunk_plugin.create_trunk(self.context, {'trunk': trunk}) + validator = rules.TrunkPortValidator(subport['port']['id']) + self.assertRaises(trunk_exc.TrunkPortInUse, + validator.validate, + self.context) + + def test_validate_port_has_binding_host(self): + with self.port() as port: + core_plugin = manager.NeutronManager.get_plugin() + port['port']['binding:host_id'] = 'host' + core_plugin.update_port(self.context, port['port']['id'], port) + validator = rules.TrunkPortValidator(port['port']['id']) + self.assertRaises(trunk_exc.ParentPortInUse, + validator.validate, + self.context) + + def test_validate_port_has_device_owner_compute(self): + with self.port() as port: + core_plugin = manager.NeutronManager.get_plugin() + device_owner = n_const.DEVICE_OWNER_COMPUTE_PREFIX + 'test' + port['port']['device_owner'] = device_owner + core_plugin.update_port(self.context, port['port']['id'], port) + validator = rules.TrunkPortValidator(port['port']['id']) + self.assertRaises(trunk_exc.ParentPortInUse, + validator.validate, + self.context) diff --git a/setup.cfg b/setup.cfg index 21676c1f92c..d9e3a7ab492 100644 --- a/setup.cfg +++ b/setup.cfg @@ -82,6 +82,7 @@ neutron.service_plugins = segments = neutron.services.segments.plugin:Plugin network_ip_availability = neutron.services.network_ip_availability.plugin:NetworkIPAvailabilityPlugin timestamp_core = neutron.services.timestamp.timestamp_plugin:TimeStampPlugin + trunk = neutron.services.trunk.plugin:TrunkPlugin neutron.qos.notification_drivers = message_queue = neutron.services.qos.notification_drivers.message_queue:RpcQosServiceNotificationDriver neutron.ml2.type_drivers =