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 =