Merge "Enable CRUD for trunk ports"
This commit is contained in:
commit
d4df22ae94
7
devstack/lib/trunk
Normal file
7
devstack/lib/trunk
Normal file
@ -0,0 +1,7 @@
|
||||
function configure_trunk_service_plugin {
|
||||
_neutron_service_plugin_class_add "trunk"
|
||||
}
|
||||
|
||||
function configure_trunk_extension {
|
||||
configure_trunk_service_plugin
|
||||
}
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
|
152
neutron/extensions/trunk.py
Executable file
152
neutron/extensions/trunk.py
Executable file
@ -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 {}
|
56
neutron/extensions/trunk_details.py
Normal file
56
neutron/extensions/trunk_details.py
Normal file
@ -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 {}
|
@ -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
|
||||
|
@ -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
|
20
neutron/services/trunk/constants.py
Normal file
20
neutron/services/trunk/constants.py
Normal file
@ -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'
|
@ -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)
|
@ -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.")
|
||||
|
178
neutron/services/trunk/plugin.py
Normal file
178
neutron/services/trunk/plugin.py
Normal file
@ -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']}
|
116
neutron/services/trunk/rules.py
Normal file
116
neutron/services/trunk/rules.py
Normal file
@ -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
|
18
neutron/services/trunk/validators/__init__.py
Normal file
18
neutron/services/trunk/validators/__init__.py
Normal file
@ -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()
|
39
neutron/services/trunk/validators/vlan.py
Normal file
39
neutron/services/trunk/validators/vlan.py
Normal file
@ -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
|
@ -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
|
||||
|
@ -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 ' ')"
|
||||
|
1
neutron/tests/contrib/hooks/trunk
Normal file
1
neutron/tests/contrib/hooks/trunk
Normal file
@ -0,0 +1 @@
|
||||
enable_service q-trunk
|
@ -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"
|
||||
}
|
||||
|
133
neutron/tests/tempest/api/test_trunk.py
Normal file
133
neutron/tests/tempest/api/test_trunk.py
Normal file
@ -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))
|
190
neutron/tests/tempest/api/test_trunk_negative.py
Normal file
190
neutron/tests/tempest/api/test_trunk_negative.py
Normal file
@ -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])
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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')
|
40
neutron/tests/unit/services/trunk/test_plugin.py
Normal file
40
neutron/tests/unit/services/trunk/test_plugin.py
Normal file
@ -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'])
|
150
neutron/tests/unit/services/trunk/test_rules.py
Normal file
150
neutron/tests/unit/services/trunk/test_rules.py
Normal file
@ -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)
|
@ -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 =
|
||||
|
Loading…
x
Reference in New Issue
Block a user