Support Network Segment Range CRUD as extensions

This patch adds the support for network segment range CRUD. Subsequent
patches will be added to use this network segment range on segment
allocation if this extension is loaded.

Changes include:
- an API extension which exposes the segment range to be administered;
- standard attributes with tagging support for the new resource;
- a new service plugin "network_segment_range" for the feature
  enabling/disabling;
- a new network segment range DB table model along with operation
  logic;
- Oslo Versioned Objects for network segment range data model;
- policy-in-code support for network segment range.

Co-authored-by: Allain Legacy <Allain.legacy@windriver.com>

Partially-implements: blueprint network-segment-range-management
Change-Id: I75814e50b2c9402fe6776229d469745d7a72290b
This commit is contained in:
Kailun Qin 2019-03-05 18:44:28 +08:00 committed by Slawek Kaplonski
parent 513dd7f46b
commit 563a536d02
29 changed files with 1640 additions and 4 deletions

View File

@ -22,6 +22,7 @@ This includes:
* segments * segments
* policies * policies
* trunks * trunks
* network_segment_ranges
Use cases Use cases
~~~~~~~~~ ~~~~~~~~~

View File

@ -76,3 +76,4 @@ Current API resources extended by standard attr extensions:
- ports: neutron.db.models_v2.Port - ports: neutron.db.models_v2.Port
- security_groups: neutron.db.models.securitygroup.SecurityGroup - security_groups: neutron.db.models.securitygroup.SecurityGroup
- floatingips: neutron.db.l3_db.FloatingIP - floatingips: neutron.db.l3_db.FloatingIP
- network_segment_ranges: neutron.db.models.network_segment_range.NetworkSegmentRange

View File

@ -60,6 +60,7 @@ Current API resources extended by tag extensions:
- floatingips - floatingips
- networks - networks
- network_segment_ranges
- policies - policies
- ports - ports
- routers - routers

View File

@ -28,6 +28,7 @@ from neutron.conf.policies import logging
from neutron.conf.policies import metering from neutron.conf.policies import metering
from neutron.conf.policies import network from neutron.conf.policies import network
from neutron.conf.policies import network_ip_availability from neutron.conf.policies import network_ip_availability
from neutron.conf.policies import network_segment_range
from neutron.conf.policies import port from neutron.conf.policies import port
from neutron.conf.policies import qos from neutron.conf.policies import qos
from neutron.conf.policies import rbac from neutron.conf.policies import rbac
@ -55,6 +56,7 @@ def list_rules():
metering.list_rules(), metering.list_rules(),
network.list_rules(), network.list_rules(),
network_ip_availability.list_rules(), network_ip_availability.list_rules(),
network_segment_range.list_rules(),
port.list_rules(), port.list_rules(),
qos.list_rules(), qos.list_rules(),
rbac.list_rules(), rbac.list_rules(),

View File

@ -0,0 +1,78 @@
# Copyright (c) 2019 Intel Corporation.
#
# 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_policy import policy
from neutron.conf.policies import base
COLLECTION_PATH = '/network_segment_ranges'
RESOURCE_PATH = '/network_segment_ranges/{id}'
rules = [
policy.DocumentedRuleDefault(
'create_network_segment_range',
base.RULE_ADMIN_ONLY,
'Create a network segment range',
[
{
'method': 'POST',
'path': COLLECTION_PATH,
},
]
),
policy.DocumentedRuleDefault(
'get_network_segment_range',
base.RULE_ADMIN_ONLY,
'Get a network segment range',
[
{
'method': 'GET',
'path': COLLECTION_PATH,
},
{
'method': 'GET',
'path': RESOURCE_PATH,
},
]
),
policy.DocumentedRuleDefault(
'update_network_segment_range',
base.RULE_ADMIN_ONLY,
'Update a network segment range',
[
{
'method': 'PUT',
'path': RESOURCE_PATH,
},
]
),
policy.DocumentedRuleDefault(
'delete_network_segment_range',
base.RULE_ADMIN_ONLY,
'Delete a network segment range',
[
{
'method': 'DELETE',
'path': RESOURCE_PATH,
},
]
),
]
def list_rules():
return rules

View File

@ -1 +1 @@
fb0167bd9639 0ff9e3881597

View File

@ -0,0 +1,56 @@
# Copyright 2019 OpenStack Foundation
#
# 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 alembic import op
import sqlalchemy as sa
"""description of revision
Revision ID: 0ff9e3881597
Revises: fb0167bd9639
Create Date: 2019-02-27 14:40:15.492884
"""
# revision identifiers, used by Alembic.
revision = '0ff9e3881597'
down_revision = 'fb0167bd9639'
network_segment_range_network_type = sa.Enum(
'vlan', 'vxlan', 'gre', 'geneve',
name='network_segment_range_network_type')
def upgrade():
op.create_table(
'network_segment_ranges',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('default', sa.Boolean(), nullable=False),
sa.Column('shared', sa.Boolean(), nullable=False),
sa.Column('project_id', sa.String(length=255), nullable=True),
sa.Column('network_type', network_segment_range_network_type,
nullable=False),
sa.Column('physical_network', sa.String(length=64), nullable=True),
sa.Column('minimum', sa.Integer(), nullable=True),
sa.Column('maximum', sa.Integer(), nullable=True),
sa.Column('standard_attr_id', sa.BigInteger(), nullable=False),
sa.ForeignKeyConstraint(['standard_attr_id'],
['standardattributes.id'],
ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('standard_attr_id')
)

View File

@ -0,0 +1,79 @@
# Copyright (c) 2019 Intel Corporation.
#
# 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.api.definitions import network_segment_range as range_apidef
from neutron_lib import constants
from neutron_lib.db import constants as db_const
from neutron_lib.db import model_base
import sqlalchemy as sa
from neutron.db import standard_attr
class NetworkSegmentRange(standard_attr.HasStandardAttributes,
model_base.BASEV2, model_base.HasId,
model_base.HasProject):
"""Represents network segment range data."""
__tablename__ = 'network_segment_ranges'
# user-defined network segment range name
name = sa.Column(sa.String(db_const.NAME_FIELD_SIZE), nullable=True)
# defines whether the network segment range is loaded from host config
# files and used as the default range when there is no other available
default = sa.Column(sa.Boolean, default=False, nullable=False)
# defines whether multiple tenants can use this network segment range
shared = sa.Column(sa.Boolean, default=True, nullable=False)
# the project_id is the subject that the policy will affect. this may
# also be a wildcard '*' to indicate all tenants or it may be a role if
# neutron gets better integration with keystone
project_id = sa.Column(sa.String(db_const.PROJECT_ID_FIELD_SIZE),
nullable=True)
# network segment range network type
network_type = sa.Column(sa.Enum(
constants.TYPE_VLAN,
constants.TYPE_VXLAN,
constants.TYPE_GRE,
constants.TYPE_GENEVE,
name='network_segment_range_network_type'),
nullable=False)
# network segment range physical network, only applicable for VLAN.
physical_network = sa.Column(sa.String(64))
# minimum segmentation id value
minimum = sa.Column(sa.Integer)
# maximum segmentation id value
maximum = sa.Column(sa.Integer)
api_collections = [range_apidef.COLLECTION_NAME]
collection_resource_map = {
range_apidef.COLLECTION_NAME: range_apidef.RESOURCE_NAME}
tag_support = True
def __init__(self, *args, **kwargs):
super(NetworkSegmentRange, self).__init__(*args, **kwargs)
self.project_id = None if self.shared else kwargs['project_id']
is_vlan = self.network_type == constants.TYPE_VLAN
self.physical_network = kwargs['physical_network'] if is_vlan else None
def __repr__(self):
return "<NetworkSegmentRange(%s,%s,%s,%s,%s,%s - %s,%s)>" % (
self.id, self.name, str(self.shared), self.project_id,
self.network_type, self.physical_network, self.minimum,
self.maximum)

View File

@ -25,6 +25,10 @@ class GeneveAllocation(model_base.BASEV2):
allocated = sa.Column(sa.Boolean, nullable=False, default=False, allocated = sa.Column(sa.Boolean, nullable=False, default=False,
server_default=sql.false(), index=True) server_default=sql.false(), index=True)
@classmethod
def get_segmentation_id(cls):
return cls.geneve_vni
class GeneveEndpoints(model_base.BASEV2): class GeneveEndpoints(model_base.BASEV2):
"""Represents tunnel endpoint in RPC mode.""" """Represents tunnel endpoint in RPC mode."""

View File

@ -27,6 +27,10 @@ class GreAllocation(model_base.BASEV2):
allocated = sa.Column(sa.Boolean, nullable=False, default=False, allocated = sa.Column(sa.Boolean, nullable=False, default=False,
server_default=sql.false(), index=True) server_default=sql.false(), index=True)
@classmethod
def get_segmentation_id(cls):
return cls.gre_id
class GreEndpoints(model_base.BASEV2): class GreEndpoints(model_base.BASEV2):
"""Represents tunnel endpoint in RPC mode.""" """Represents tunnel endpoint in RPC mode."""

View File

@ -39,3 +39,7 @@ class VlanAllocation(model_base.BASEV2):
vlan_id = sa.Column(sa.Integer, nullable=False, primary_key=True, vlan_id = sa.Column(sa.Integer, nullable=False, primary_key=True,
autoincrement=False) autoincrement=False)
allocated = sa.Column(sa.Boolean, nullable=False) allocated = sa.Column(sa.Boolean, nullable=False)
@classmethod
def get_segmentation_id(cls):
return cls.vlan_id

View File

@ -27,6 +27,10 @@ class VxlanAllocation(model_base.BASEV2):
allocated = sa.Column(sa.Boolean, nullable=False, default=False, allocated = sa.Column(sa.Boolean, nullable=False, default=False,
server_default=sql.false(), index=True) server_default=sql.false(), index=True)
@classmethod
def get_segmentation_id(cls):
return cls.vxlan_vni
class VxlanEndpoints(model_base.BASEV2): class VxlanEndpoints(model_base.BASEV2):
"""Represents tunnel endpoint in RPC mode.""" """Represents tunnel endpoint in RPC mode."""

View File

@ -122,3 +122,57 @@ def delete_network_segment(context, segment_id):
"""Release a dynamic segment for the params provided if one exists.""" """Release a dynamic segment for the params provided if one exists."""
with db_api.CONTEXT_WRITER.using(context): with db_api.CONTEXT_WRITER.using(context):
network_obj.NetworkSegment.delete_objects(context, id=segment_id) network_obj.NetworkSegment.delete_objects(context, id=segment_id)
def network_segments_exist_in_range(context, network_type, physical_network,
segment_range=None):
"""Check whether one or more network segments exist in a range."""
with db_api.CONTEXT_READER.using(context):
filters = {
'network_type': network_type,
'physical_network': physical_network,
}
segment_objs = network_obj.NetworkSegment.get_objects(
context, **filters)
if segment_range:
minimum_id = segment_range['minimum']
maximum_id = segment_range['maximum']
segment_objs = [
segment for segment in segment_objs if
minimum_id <= segment.segmentation_id <= maximum_id]
return len(segment_objs) > 0
def min_max_actual_segments_in_range(context, network_type, physical_network,
segment_range=None):
"""Return the minimum and maximum segmentation IDs used in a network
segment range
"""
with db_api.CONTEXT_READER.using(context):
filters = {
'network_type': network_type,
'physical_network': physical_network,
}
pager = base_obj.Pager()
# (NOTE) True means ASC, False is DESC
pager.sorts = [('segmentation_id', True)]
segment_objs = network_obj.NetworkSegment.get_objects(
context, _pager=pager, **filters)
if segment_range:
minimum_id = segment_range['minimum']
maximum_id = segment_range['maximum']
segment_objs = [
segment for segment in segment_objs if
minimum_id <= segment.segmentation_id <= maximum_id]
if segment_objs:
return (segment_objs[0].segmentation_id,
segment_objs[-1].segmentation_id)
else:
LOG.debug("No existing segment found for "
"Network type:%(network_type)s, "
"Physical network:%(physical_network)s",
{'network_type': network_type,
'physical_network': physical_network})
return None, None

View File

@ -0,0 +1,165 @@
# Copyright (c) 2019 Intel Corporation.
#
# 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 abc
from neutron_lib.api.definitions import network_segment_range as apidef
from neutron_lib.api import extensions as api_extensions
from neutron_lib.plugins import constants as plugin_constants
from neutron_lib.plugins import directory
from neutron_lib.services import base as service_base
from oslo_log import log as logging
import six
from neutron.api import extensions
from neutron.api.v2 import base
LOG = logging.getLogger(__name__)
class Network_segment_range(api_extensions.APIExtensionDescriptor):
"""Extension class supporting Network segment ranges.
This class is used by neutron's extension framework to make metadata
about the network segment range extension available to clients.
With admin rights, one will be able to create, update, read and delete the
values.
"""
api_definition = apidef
@classmethod
def get_resources(cls):
"""Returns extension resources"""
plugin = directory.get_plugin(plugin_constants.NETWORK_SEGMENT_RANGE)
collection_name = apidef.COLLECTION_NAME.replace('_', '-')
params = apidef.RESOURCE_ATTRIBUTE_MAP.get(apidef.COLLECTION_NAME,
dict())
controller = base.create_resource(collection_name,
apidef.RESOURCE_NAME,
plugin, params, allow_bulk=True,
allow_pagination=True,
allow_sorting=True)
ex = extensions.ResourceExtension(collection_name, controller,
attr_map=params)
return [ex]
@classmethod
def get_plugin_interface(cls):
return NetworkSegmentRangePluginBase
@six.add_metaclass(abc.ABCMeta)
class NetworkSegmentRangePluginBase(service_base.ServicePluginBase):
"""REST API to manage network segment ranges.
All methods must be in an admin context.
"""
@classmethod
def get_plugin_type(cls):
return plugin_constants.NETWORK_SEGMENT_RANGE
def get_plugin_description(self):
return "Adds network segment ranges to Neutron resources"
@abc.abstractmethod
def create_network_segment_range(self, context, network_segment_range):
"""Create a network segment range.
Create a network segment range, which represents the range of L2
segments for tenant network allocation.
:param context: neutron api request context
:param network_segment_range: dictionary describing the network segment
range, with keys as listed in the :obj:`RESOURCE_ATTRIBUTE_MAP`
object in
:file:`neutron_lib/api/definitions/network_segment_range.py`.
"""
pass
@abc.abstractmethod
def delete_network_segment_range(self, context, id):
"""Delete a network segment range.
:param context: neutron api request context
:param id: UUID representing the network segment range to delete.
"""
pass
@abc.abstractmethod
def update_network_segment_range(self, context, id, network_segment_range):
"""Update values of a network segment range.
:param context: neutron api request context
:param id: UUID representing the network segment range to update.
:param network_segment_range: dictionary with keys indicating fields to
update. valid keys are those that have a value of True for
'allow_put' as listed in the :obj:`RESOURCE_ATTRIBUTE_MAP`
object in
:file:`neutron_lib/api/definitions/network_segment_range.py`.
"""
pass
@abc.abstractmethod
def get_network_segment_ranges(self, context, filters=None, fields=None,
sorts=None, limit=None, marker=None,
page_reverse=False):
"""Retrieve a list of network segment ranges.
The contents of the list depends on the filters.
:param context: neutron api request context
:param filters: a dictionary with keys that are valid keys for
a network segment range as listed in the
:obj:`RESOURCE_ATTRIBUTE_MAP` object in
:file:`neutron_lib/api/definitions/
network_segment_range.py`.
Values in this dictionary are an iterable containing
values that will be used for an exact match
comparison for that value. Each result returned by
this function will have matched one of the values
for each key in filters.
:param fields: a list of strings that are valid keys in a
network segment range dictionary as listed in the
:obj:`RESOURCE_ATTRIBUTE_MAP` object in
:file:`neutron_lib/api/definitions/
network_segment_range.py`.
Only these fields will be returned.
:param sorts: A list of (key, direction) tuples.
direction: True == ASC, False == DESC
:param limit: maximum number of items to return
:param marker: the last item of the previous page; when used, returns
next results after the marker resource.
:param page_reverse: True if sort direction is reversed.
"""
pass
@abc.abstractmethod
def get_network_segment_range(self, context, id, fields=None):
"""Retrieve a network segment range.
:param context: neutron api request context
:param id: UUID representing the network segment range to fetch.
:param fields: a list of strings that are valid keys in a
network segment range dictionary as listed in the
:obj:`RESOURCE_ATTRIBUTE_MAP` object in
:file:`neutron_lib/api/definitions/
network_segment_range.py`.
Only these fields will be returned.
"""
pass

View File

@ -313,3 +313,8 @@ class FloatingIPStatusEnumField(obj_fields.AutoTypedField):
class RouterStatusEnumField(obj_fields.AutoTypedField): class RouterStatusEnumField(obj_fields.AutoTypedField):
AUTO_TYPE = obj_fields.Enum(valid_values=constants.VALID_ROUTER_STATUS) AUTO_TYPE = obj_fields.Enum(valid_values=constants.VALID_ROUTER_STATUS)
class NetworkSegmentRangeNetworkTypeEnumField(obj_fields.AutoTypedField):
AUTO_TYPE = obj_fields.Enum(
valid_values=lib_constants.NETWORK_SEGMENT_RANGE_TYPES)

View File

@ -0,0 +1,127 @@
# Copyright (c) 2019 Intel Corporation.
#
# 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_lib.db import utils as db_utils
from neutron_lib import exceptions as n_exc
from oslo_versionedobjects import fields as obj_fields
from sqlalchemy import and_
from sqlalchemy import not_
from neutron._i18n import _
from neutron.db.models import network_segment_range as range_model
from neutron.db.models.plugins.ml2 import geneveallocation as \
geneve_alloc_model
from neutron.db.models.plugins.ml2 import gre_allocation_endpoints as \
gre_alloc_model
from neutron.db.models.plugins.ml2 import vlanallocation as vlan_alloc_model
from neutron.db.models.plugins.ml2 import vxlanallocation as vxlan_alloc_model
from neutron.db.models import segment as segments_model
from neutron.db import models_v2
from neutron.objects import base
from neutron.objects import common_types
models_map = {
constants.TYPE_VLAN: vlan_alloc_model.VlanAllocation,
constants.TYPE_VXLAN: vxlan_alloc_model.VxlanAllocation,
constants.TYPE_GRE: gre_alloc_model.GreAllocation,
constants.TYPE_GENEVE: geneve_alloc_model.GeneveAllocation
}
@base.NeutronObjectRegistry.register
class NetworkSegmentRange(base.NeutronDbObject):
# Version 1.0: Initial version
VERSION = '1.0'
db_model = range_model.NetworkSegmentRange
primary_keys = ['id']
fields = {
'id': common_types.UUIDField(),
'name': obj_fields.StringField(nullable=True),
'default': obj_fields.BooleanField(nullable=False),
'shared': obj_fields.BooleanField(nullable=False),
'project_id': obj_fields.StringField(nullable=True),
'network_type': common_types.NetworkSegmentRangeNetworkTypeEnumField(
nullable=False),
'physical_network': obj_fields.StringField(nullable=True),
'minimum': obj_fields.IntegerField(nullable=True),
'maximum': obj_fields.IntegerField(nullable=True)
}
def to_dict(self, fields=None):
_dict = super(NetworkSegmentRange, self).to_dict()
# extend the network segment range dict with `available` and `used`
# fields
_dict.update({'available': self._get_available_allocation()})
_dict.update({'used': self._get_used_allocation_mapping()})
# TODO(kailun): For tag mechanism. This will be removed in bug/1704137
try:
_dict['tags'] = [t.tag for t in self.db_obj.standard_attr.tags]
except AttributeError:
# AttrtibuteError can be raised when accessing self.db_obj
# or self.db_obj.standard_attr
pass
return db_utils.resource_fields(_dict, fields)
def _get_allocation_model_details(self):
model = models_map.get(self.network_type)
if model is not None:
alloc_segmentation_id = model.get_segmentation_id()
else:
msg = (_("network_type '%s' unknown for getting allocation "
"information") % self.network_type)
raise n_exc.InvalidInput(error_message=msg)
allocated = model.allocated
return model, alloc_segmentation_id, allocated
def _get_available_allocation(self):
with self.db_context_reader(self.obj_context):
model, alloc_segmentation_id, allocated = (
self._get_allocation_model_details())
query = self.obj_context.session.query(alloc_segmentation_id)
query = query.filter(and_(
alloc_segmentation_id >= self.minimum,
alloc_segmentation_id <= self.maximum),
not_(allocated))
if self.network_type == constants.TYPE_VLAN:
alloc_available = query.filter(
model.physical_network == self.physical_network).all()
else:
alloc_available = query.all()
return [segmentation_id for (segmentation_id,) in alloc_available]
def _get_used_allocation_mapping(self):
with self.db_context_reader(self.obj_context):
query = self.obj_context.session.query(
segments_model.NetworkSegment.segmentation_id,
models_v2.Network.project_id)
alloc_used = (query.filter(and_(
segments_model.NetworkSegment.network_type ==
self.network_type,
segments_model.NetworkSegment.physical_network ==
self.physical_network,
segments_model.NetworkSegment.segmentation_id >= self.minimum,
segments_model.NetworkSegment.segmentation_id <= self.maximum))
.filter(
segments_model.NetworkSegment.network_id ==
models_v2.Network.id)).all()
return {segmentation_id: project_id
for segmentation_id, project_id in alloc_used}

View File

@ -0,0 +1,266 @@
# Copyright (c) 2019 Intel Corporation.
#
# 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.api.definitions import network_segment_range as range_def
from neutron_lib import constants as const
from neutron_lib.db import api as db_api
from neutron_lib import exceptions as lib_exc
from neutron_lib.exceptions import network_segment_range as range_exc
from neutron_lib.plugins import directory
from neutron_lib.plugins import utils as plugin_utils
from oslo_log import helpers as log_helpers
from oslo_log import log
import six
from neutron._i18n import _
from neutron.db import segments_db
from neutron.extensions import network_segment_range as ext_range
from neutron.objects import base as base_obj
from neutron.objects import network_segment_range as obj_network_segment_range
LOG = log.getLogger(__name__)
class NetworkSegmentRangePlugin(ext_range.NetworkSegmentRangePluginBase):
"""Implements Neutron Network Segment Range Service plugin."""
supported_extension_aliases = [range_def.ALIAS]
__native_pagination_support = True
__native_sorting_support = True
__filter_validation_support = True
def __init__(self):
super(NetworkSegmentRangePlugin, self).__init__()
self.type_manager = directory.get_plugin().type_manager
self.type_manager.initialize_network_segment_range_support()
def _get_network_segment_range(self, context, id):
obj = obj_network_segment_range.NetworkSegmentRange.get_object(
context, id=id)
if obj is None:
raise range_exc.NetworkSegmentRangeNotFound(range_id=id)
return obj
def _validate_network_segment_range_eligible(self, network_segment_range):
range_data = (network_segment_range.get('minimum'),
network_segment_range.get('maximum'))
# Currently, network segment range only supports VLAN, VxLAN,
# GRE and Geneve.
if network_segment_range.get('network_type') == const.TYPE_VLAN:
plugin_utils.verify_vlan_range(range_data)
else:
plugin_utils.verify_tunnel_range(
range_data, network_segment_range.get('network_type'))
def _validate_network_segment_range_overlap(self, context,
network_segment_range):
filters = {
'default': False,
'network_type': network_segment_range['network_type'],
'physical_network': (network_segment_range['physical_network']
if network_segment_range['network_type'] ==
const.TYPE_VLAN else None),
}
range_objs = obj_network_segment_range.NetworkSegmentRange.get_objects(
context, **filters)
overlapped_range_id = [
obj.id for obj in range_objs if
(network_segment_range['minimum'] <= obj.maximum and
network_segment_range['maximum'] >= obj.minimum)]
if overlapped_range_id:
raise range_exc.NetworkSegmentRangeOverlaps(
range_id=', '.join(overlapped_range_id))
def _add_unchanged_range_attributes(self, updates, existing):
"""Adds data for unspecified fields on incoming update requests."""
for key, value in six.iteritems(existing):
updates.setdefault(key, value)
return updates
def _is_network_segment_range_referenced(self, context,
network_segment_range):
return segments_db.network_segments_exist_in_range(
context, network_segment_range['network_type'],
network_segment_range.get('physical_network'),
network_segment_range)
def _is_network_segment_range_type_supported(self, network_type):
if not (self.type_manager.network_type_supported(network_type) and
network_type in const.NETWORK_SEGMENT_RANGE_TYPES):
# TODO(kailun): To use
# range_exc.NetworkSegmentRangeNetTypeNotSupported when the
# neutron-lib patch https://review.openstack.org/640777 is merged
# and released.
message = _("Network type %s does not support "
"network segment ranges.") % network_type
raise lib_exc.BadRequest(resource=range_def.RESOURCE_NAME,
msg=message)
return True
def _are_allocated_segments_in_range_impacted(self, context,
existing_range,
updated_range):
updated_range_min = updated_range.get('minimum',
existing_range['minimum'])
updated_range_max = updated_range.get('maximum',
existing_range['maximum'])
existing_range_min, existing_range_max = (
segments_db.min_max_actual_segments_in_range(
context, existing_range['network_type'],
existing_range.get('physical_network'), existing_range))
if existing_range_min and existing_range_max:
return bool(updated_range_min >= existing_range_min or
updated_range_max <= existing_range_max)
return False
@log_helpers.log_method_call
def create_network_segment_range(self, context, network_segment_range):
"""Check network types supported on network segment range creation."""
range_data = network_segment_range['network_segment_range']
if self._is_network_segment_range_type_supported(
range_data['network_type']):
with db_api.CONTEXT_WRITER.using(context):
self._validate_network_segment_range_eligible(range_data)
self._validate_network_segment_range_overlap(context,
range_data)
network_segment_range = (
obj_network_segment_range.NetworkSegmentRange(
context, name=range_data['name'],
description=range_data.get('description'),
default=False,
shared=range_data['shared'],
project_id=(range_data['project_id']
if not range_data['shared'] else None),
network_type=range_data['network_type'],
physical_network=(range_data['physical_network']
if range_data['network_type'] ==
const.TYPE_VLAN else None),
minimum=range_data['minimum'],
maximum=range_data['maximum'])
)
network_segment_range.create()
self.type_manager.update_network_segment_range_allocations(
network_segment_range['network_type'])
return network_segment_range.to_dict()
@log_helpers.log_method_call
def get_network_segment_range(self, context, id, fields=None):
network_segment_range = self._get_network_segment_range(
context, id)
return network_segment_range.to_dict(fields=fields)
@log_helpers.log_method_call
def get_network_segment_ranges(self, context, filters=None, fields=None,
sorts=None, limit=None, marker=None,
page_reverse=False):
# TODO(kailun): Based on the current spec:
# https://review.openstack.org/599980, this method call may
# possibly return a large amount of data since ``available``
# segment list and ``used`` segment/project mapping will be also
# returned and they can be large sometimes. Considering that this
# API is admin-only and list operations won't be called often based
# on the use cases, we'll keep this open for now and evaluate the
# potential impacts. An alternative is to return the ``available``
# and ``used`` segment number or percentage.
pager = base_obj.Pager(sorts, limit, page_reverse, marker)
filters = filters or {}
network_segment_ranges = (
obj_network_segment_range.NetworkSegmentRange.get_objects(
context, _pager=pager, **filters))
return [
network_segment_range.to_dict(fields=fields)
for network_segment_range in network_segment_ranges
]
@log_helpers.log_method_call
def update_network_segment_range(self, context, id, network_segment_range):
"""Check existing network segment range impact on range updates."""
updated_range_data = network_segment_range['network_segment_range']
with db_api.CONTEXT_WRITER.using(context):
network_segment_range = self._get_network_segment_range(context,
id)
existing_range_data = network_segment_range.to_dict()
if existing_range_data['default']:
# TODO(kailun): To use
# range_exc.NetworkSegmentRangeDefaultReadOnly when the
# neutron-lib patch https://review.openstack.org/640777 is
# merged and released.
message = _("Network Segment Range %s is a "
"default segment range which could not be "
"updated or deleted.") % id
raise lib_exc.BadRequest(resource=range_def.RESOURCE_NAME,
msg=message)
if self._are_allocated_segments_in_range_impacted(
context,
existing_range=existing_range_data,
updated_range=updated_range_data):
# TODO(kailun): To use
# range_exc.NetworkSegmentRangeReferencedByProject when the
# neutron-lib patch https://review.openstack.org/640777 is
# merged and released.
message = _("Network Segment Range %s is referenced by "
"one or more tenant networks.") % id
raise lib_exc.InUse(resource=range_def.RESOURCE_NAME,
msg=message)
new_range_data = self._add_unchanged_range_attributes(
updated_range_data, existing_range_data)
self._validate_network_segment_range_eligible(new_range_data)
network_segment_range.update_fields(new_range_data)
network_segment_range.update()
self.type_manager.update_network_segment_range_allocations(
network_segment_range['network_type'])
return network_segment_range.to_dict()
@log_helpers.log_method_call
def delete_network_segment_range(self, context, id):
"""Check segment reference on network segment range deletion."""
with db_api.CONTEXT_WRITER.using(context):
network_segment_range = self._get_network_segment_range(context,
id)
range_data = network_segment_range.to_dict()
if range_data['default']:
# TODO(kailun): To use
# range_exc.NetworkSegmentRangeDefaultReadOnly when the
# neutron-lib patch https://review.openstack.org/640777 is
# merged and released.
message = _("Network Segment Range %s is a "
"default segment range which could not be "
"updated or deleted.") % id
raise lib_exc.BadRequest(resource=range_def.RESOURCE_NAME,
msg=message)
if self._is_network_segment_range_referenced(
context, range_data):
# TODO(kailun): To use
# range_exc.NetworkSegmentRangeReferencedByProject when the
# neutron-lib patch https://review.openstack.org/640777 is
# merged and released.
message = _("Network Segment Range %s is referenced by "
"one or more tenant networks.") % id
raise lib_exc.InUse(resource=range_def.RESOURCE_NAME,
msg=message)
network_segment_range.delete()
self.type_manager.update_network_segment_range_allocations(
network_segment_range['network_type'])

View File

@ -327,6 +327,13 @@ def get_random_port_binding_statuses():
return random.choice(n_const.PORT_BINDING_STATUSES) return random.choice(n_const.PORT_BINDING_STATUSES)
def get_random_network_segment_range_network_type():
return random.choice([constants.TYPE_VLAN,
constants.TYPE_VXLAN,
constants.TYPE_GRE,
constants.TYPE_GENEVE])
def is_bsd(): def is_bsd():
"""Return True on BSD-based systems.""" """Return True on BSD-based systems."""

View File

@ -120,7 +120,7 @@ class StandardAttrAPIImapctTestCase(testlib_api.SqlTestCase):
expected = ['subnets', 'trunks', 'routers', 'segments', expected = ['subnets', 'trunks', 'routers', 'segments',
'security_group_rules', 'networks', 'policies', 'security_group_rules', 'networks', 'policies',
'subnetpools', 'ports', 'security_groups', 'floatingips', 'subnetpools', 'ports', 'security_groups', 'floatingips',
'logs'] 'logs', 'network_segment_ranges']
self.assertEqual( self.assertEqual(
set(expected), set(expected),
set(standard_attr.get_standard_attr_resource_model_map().keys()) set(standard_attr.get_standard_attr_resource_model_map().keys())
@ -132,7 +132,8 @@ class StandardAttrAPIImapctTestCase(testlib_api.SqlTestCase):
# should be exposed in release note for API users. And also it should # should be exposed in release note for API users. And also it should
# be list as other tag support resources in doc/source/devref/tag.rst # be list as other tag support resources in doc/source/devref/tag.rst
expected = ['subnets', 'trunks', 'routers', 'networks', 'policies', expected = ['subnets', 'trunks', 'routers', 'networks', 'policies',
'subnetpools', 'ports', 'security_groups', 'floatingips'] 'subnetpools', 'ports', 'security_groups', 'floatingips',
'network_segment_ranges']
self.assertEqual( self.assertEqual(
set(expected), set(expected),
set(standard_attr.get_tag_resource_parent_map().keys()) set(standard_attr.get_tag_resource_parent_map().keys())

View File

@ -0,0 +1,350 @@
# Copyright (c) 2019 Intel Corporation.
#
# 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
from neutron_lib import context
from oslo_config import cfg
import webob.exc
from neutron.db import db_base_plugin_v2
from neutron.db import segments_db
from neutron.extensions import network_segment_range as ext_range
from neutron.services.network_segment_range import plugin as plugin_range
from neutron.tests.unit.db import test_db_base_plugin_v2
SERVICE_PLUGIN_KLASS = ('neutron.services.network_segment_range.plugin.'
'NetworkSegmentRangePlugin')
TEST_PLUGIN_KLASS = (
'neutron.tests.unit.extensions.test_network_segment_range.'
'NetworkSegmentRangeTestPlugin')
TARGET_PLUGIN = 'neutron.plugins.ml2.plugin.Ml2Plugin'
class NetworkSegmentRangeExtensionManager(object):
def get_resources(self):
return ext_range.Network_segment_range.get_resources()
def get_actions(self):
return []
def get_request_extensions(self):
return []
class NetworkSegmentRangeTestBase(test_db_base_plugin_v2.
NeutronDbPluginV2TestCase):
def _create_network_segment_range(self, fmt, expected_res_status=None,
**kwargs):
network_segment_range = {'network_segment_range': {}}
for k, v in kwargs.items():
network_segment_range['network_segment_range'][k] = str(v)
network_segment_range_req = self.new_create_request(
'network-segment-ranges', network_segment_range, fmt)
network_segment_range_res = network_segment_range_req.get_response(
self.ext_api)
if expected_res_status:
self.assertEqual(expected_res_status,
network_segment_range_res.status_int)
return network_segment_range_res
def network_segment_range(self, **kwargs):
res = self._create_network_segment_range(self.fmt, **kwargs)
if res.status_int >= webob.exc.HTTPClientError.code:
raise webob.exc.HTTPClientError(code=res.status_int)
return self.deserialize(self.fmt, res)
def _test_create_network_segment_range(self, expected=None, **kwargs):
network_segment_range = self.network_segment_range(**kwargs)
self._validate_resource(network_segment_range, kwargs,
'network_segment_range')
if expected:
self._compare_resource(network_segment_range, expected,
'network_segment_range')
return network_segment_range
def _test_update_network_segment_range(self, range_id,
data, expected=None):
update_req = self.new_update_request(
'network-segment-ranges', data, range_id)
update_res = update_req.get_response(self.ext_api)
if expected:
network_segment_range = self.deserialize(self.fmt, update_res)
self._compare_resource(network_segment_range, expected,
'network_segment_range')
return network_segment_range
return update_res
class NetworkSegmentRangeTestPlugin(db_base_plugin_v2.NeutronDbPluginV2,
plugin_range.NetworkSegmentRangePlugin):
"""Test plugin to mixin the network segment range extension."""
__native_pagination_support = True
__native_sorting_support = True
__filter_validation_support = True
supported_extension_aliases = ["provider", "network-segment-range"]
def __init__(self):
super(NetworkSegmentRangeTestPlugin, self).__init__()
self.type_manager = mock.Mock()
class TestNetworkSegmentRange(NetworkSegmentRangeTestBase):
def setUp(self, plugin=None):
if not plugin:
plugin = TEST_PLUGIN_KLASS
service_plugins = {'network_segment_range_plugin_name':
SERVICE_PLUGIN_KLASS}
cfg.CONF.set_override('service_plugins', [SERVICE_PLUGIN_KLASS])
ext_mgr = NetworkSegmentRangeExtensionManager()
super(TestNetworkSegmentRange, self).setUp(
plugin=plugin, ext_mgr=ext_mgr, service_plugins=service_plugins)
def _test_create_network_segment_range(self, expected=None, **kwargs):
for d in (kwargs, expected):
if d is None:
continue
d.setdefault('name', '')
d.setdefault('shared', True)
d.setdefault('project_id', None)
d.setdefault('network_type', constants.TYPE_VLAN)
d.setdefault('physical_network', 'phys_net')
d.setdefault('minimum', 200)
d.setdefault('maximum', 300)
return (super(TestNetworkSegmentRange, self).
_test_create_network_segment_range(expected, **kwargs))
def test_create_network_segment_range_empty_name(self):
expected_range = {'name': '',
'shared': True,
'project_id': None,
'network_type': constants.TYPE_VLAN,
'physical_network': 'phys_net',
'minimum': 200,
'maximum': 300}
self._test_create_network_segment_range(expected=expected_range)
def test_create_network_segment_range_with_name(self):
expected_range = {'name': 'foo-range-name',
'shared': True,
'project_id': None,
'network_type': constants.TYPE_VLAN,
'physical_network': 'phys_net',
'minimum': 200,
'maximum': 300}
self._test_create_network_segment_range(
name='foo-range-name',
expected=expected_range)
def test_create_network_segment_range_unsupported_network_type(self):
exc = self.assertRaises(webob.exc.HTTPClientError,
self._test_create_network_segment_range,
network_type='foo-network-type')
self.assertEqual(webob.exc.HTTPClientError.code, exc.code)
self.assertIn('The server could not comply with the request',
exc.explanation)
def test_create_network_segment_range_no_physical_network(self):
expected_range = {'shared': True,
'project_id': None,
'network_type': constants.TYPE_VXLAN,
'physical_network': None}
self._test_create_network_segment_range(
network_type=constants.TYPE_VXLAN,
physical_network=None,
expected=expected_range)
def test_create_network_segment_range_tenant_specific(self):
expected_range = {'shared': False,
'project_id': test_db_base_plugin_v2.TEST_TENANT_ID,
'network_type': constants.TYPE_VLAN,
'physical_network': 'phys_net',
'minimum': 200,
'maximum': 300}
self._test_create_network_segment_range(
shared=False,
project_id=test_db_base_plugin_v2.TEST_TENANT_ID,
network_type=constants.TYPE_VLAN,
physical_network='phys_net',
expected=expected_range)
def test_create_network_segment_ranges_in_certain_order(self):
ctx = context.get_admin_context()
range1 = self._test_create_network_segment_range(
name='foo-range1', physical_network='phys_net1')
range2 = self._test_create_network_segment_range(
name='foo-range2', physical_network='phys_net2')
range3 = self._test_create_network_segment_range(
name='foo-range3', physical_network='phys_net3')
network_segment_ranges = (
NetworkSegmentRangeTestPlugin.get_network_segment_ranges(
NetworkSegmentRangeTestPlugin(), ctx))
self.assertEqual(range1['network_segment_range']['id'],
network_segment_ranges[0]['id'])
self.assertEqual(range2['network_segment_range']['id'],
network_segment_ranges[1]['id'])
self.assertEqual(range3['network_segment_range']['id'],
network_segment_ranges[2]['id'])
def test_create_network_segment_range_failed_with_vlan_minimum_id(self):
exc = self.assertRaises(webob.exc.HTTPClientError,
self._test_create_network_segment_range,
minimum=0)
self.assertEqual(webob.exc.HTTPClientError.code, exc.code)
self.assertIn('The server could not comply with the request',
exc.explanation)
def test_create_network_segment_range_failed_with_vlan_maximum_id(self):
exc = self.assertRaises(webob.exc.HTTPClientError,
self._test_create_network_segment_range,
minimum=4095)
self.assertEqual(webob.exc.HTTPServerError.code, exc.code)
self.assertIn('The server could not comply with the request',
exc.explanation)
def test_create_network_segment_range_failed_with_tunnel_minimum_id(self):
tunnel_type = [constants.TYPE_VXLAN,
constants.TYPE_GRE,
constants.TYPE_GENEVE]
for network_type in tunnel_type:
exc = self.assertRaises(webob.exc.HTTPClientError,
self._test_create_network_segment_range,
network_type=network_type,
physical_network=None,
minimum=0)
self.assertEqual(webob.exc.HTTPClientError.code, exc.code)
self.assertIn('The server could not comply with the request',
exc.explanation)
def test_create_network_segment_range_failed_with_tunnel_maximum_id(self):
expected_res = [(constants.TYPE_VXLAN, 2 ** 24),
(constants.TYPE_GRE, 2 ** 32),
(constants.TYPE_GENEVE, 2 ** 24)]
for network_type, max_id in expected_res:
exc = self.assertRaises(webob.exc.HTTPClientError,
self._test_create_network_segment_range,
network_type=network_type,
physical_network=None,
maximum=max_id)
if network_type == constants.TYPE_GRE:
self.assertEqual(webob.exc.HTTPClientError.code, exc.code)
else:
self.assertEqual(webob.exc.HTTPServerError.code, exc.code)
self.assertIn('The server could not comply with the request',
exc.explanation)
def test_update_network_segment_range_set_name(self):
network_segment_range = self._test_create_network_segment_range()
with mock.patch.object(segments_db, 'min_max_actual_segments_in_range',
return_value=(None, None)):
result = self._update(
'network-segment-ranges',
network_segment_range['network_segment_range']['id'],
{'network_segment_range': {'name': 'foo-name'}},
expected_code=webob.exc.HTTPOk.code)
self.assertEqual('foo-name',
result['network_segment_range']['name'])
def test_update_network_segment_range_set_name_to_empty(self):
network_segment_range = self._test_create_network_segment_range(
name='foo-range-name')
with mock.patch.object(segments_db, 'min_max_actual_segments_in_range',
return_value=(None, None)):
result = self._update(
'network-segment-ranges',
network_segment_range['network_segment_range']['id'],
{'network_segment_range': {'name': ''}},
expected_code=webob.exc.HTTPOk.code)
self.assertEqual('', result['network_segment_range']['name'])
def test_update_network_segment_range_min_max(self):
network_segment_range = self._test_create_network_segment_range()
with mock.patch.object(segments_db, 'min_max_actual_segments_in_range',
return_value=(None, None)):
result = self._update(
'network-segment-ranges',
network_segment_range['network_segment_range']['id'],
{'network_segment_range': {'minimum': 1200, 'maximum': 1300}},
expected_code=webob.exc.HTTPOk.code)
self.assertEqual(1200, result['network_segment_range']['minimum'])
self.assertEqual(1300, result['network_segment_range']['maximum'])
def test_get_network_segment_range(self):
network_segment_range = self._test_create_network_segment_range()
req = self.new_show_request(
'network-segment-ranges',
network_segment_range['network_segment_range']['id'])
res = self.deserialize(self.fmt, req.get_response(self.ext_api))
self.assertEqual(
network_segment_range['network_segment_range']['id'],
res['network_segment_range']['id'])
def test_list_network_segment_ranges(self):
self._test_create_network_segment_range(name='foo-range1')
self._test_create_network_segment_range(
name='foo-range2', minimum=400, maximum=500)
res = self._list('network-segment-ranges')
self.assertEqual(2, len(res['network_segment_ranges']))
def test_list_network_segment_ranges_with_sort(self):
range1 = self._test_create_network_segment_range(
name='foo-range1', physical_network='phys_net1')
range2 = self._test_create_network_segment_range(
name='foo-range2', physical_network='phys_net2')
self._test_list_with_sort('network-segment-range',
(range2, range1),
[('physical_network', 'desc')])
def test_list_network_segment_ranges_with_pagination(self):
range1 = self._test_create_network_segment_range(
name='foo-range1', physical_network='phys_net1')
range2 = self._test_create_network_segment_range(
name='foo-range2', physical_network='phys_net2')
range3 = self._test_create_network_segment_range(
name='foo-range3', physical_network='phys_net3')
self._test_list_with_pagination(
'network-segment-range',
(range1, range2, range3),
('physical_network', 'asc'), 2, 2)
def test_list_network_segment_ranges_with_pagination_reverse(self):
range1 = self._test_create_network_segment_range(
name='foo-range1', physical_network='phys_net1')
range2 = self._test_create_network_segment_range(
name='foo-range2', physical_network='phys_net2')
range3 = self._test_create_network_segment_range(
name='foo-range3', physical_network='phys_net3')
self._test_list_with_pagination_reverse(
'network-segment-range',
(range1, range2, range3),
('physical_network', 'asc'), 2, 2)
def test_delete_network_segment_range(self):
network_segment_range = self._test_create_network_segment_range()
with mock.patch.object(segments_db, 'network_segments_exist_in_range',
return_value=False):
self._delete('network-segment-ranges',
network_segment_range['network_segment_range']['id'])
self._show('network-segment-ranges',
network_segment_range['network_segment_range']['id'],
expected_code=webob.exc.HTTPNotFound.code)

View File

@ -517,6 +517,8 @@ FIELD_TYPE_VALUE_GENERATOR_MAP = {
common_types.IpProtocolEnumField: tools.get_random_ip_protocol, common_types.IpProtocolEnumField: tools.get_random_ip_protocol,
common_types.ListOfIPNetworksField: get_list_of_random_networks, common_types.ListOfIPNetworksField: get_list_of_random_networks,
common_types.MACAddressField: tools.get_random_EUI, common_types.MACAddressField: tools.get_random_EUI,
common_types.NetworkSegmentRangeNetworkTypeEnumField:
tools.get_random_network_segment_range_network_type,
common_types.PortBindingStatusEnumField: common_types.PortBindingStatusEnumField:
tools.get_random_port_binding_statuses, tools.get_random_port_binding_statuses,
common_types.PortRangeField: tools.get_random_port, common_types.PortRangeField: tools.get_random_port,
@ -595,7 +597,7 @@ class _BaseObjectTestCase(object):
self.valid_field = [f for f in self._test_class.fields self.valid_field = [f for f in self._test_class.fields
if f not in invalid_fields][0] if f not in invalid_fields][0]
self.valid_field_filter = {self.valid_field: self.valid_field_filter = {self.valid_field:
self.obj_fields[-1][self.valid_field]} self.obj_fields[-1].get(self.valid_field)}
self.obj_registry = self.useFixture( self.obj_registry = self.useFixture(
NeutronObjectRegistryFixture()) NeutronObjectRegistryFixture())
self.obj_registry.register(FakeSmallNeutronObject) self.obj_registry.register(FakeSmallNeutronObject)

View File

@ -283,3 +283,22 @@ class DictOfMiscValuesFieldTest(test_base.BaseTestCase, TestField):
for in_val, out_val in self.coerce_good_values: for in_val, out_val in self.coerce_good_values:
self.assertEqual(jsonutils.dumps(in_val), self.assertEqual(jsonutils.dumps(in_val),
self.field.stringify(in_val)) self.field.stringify(in_val))
class NetworkSegmentRangeNetworkTypeEnumFieldTest(test_base.BaseTestCase,
TestField):
def setUp(self):
super(NetworkSegmentRangeNetworkTypeEnumFieldTest, self).setUp()
self.field = common_types.NetworkSegmentRangeNetworkTypeEnumField()
self.coerce_good_values = [(val, val)
for val in [const.TYPE_VLAN,
const.TYPE_VXLAN,
const.TYPE_GRE,
const.TYPE_GENEVE]]
self.coerce_bad_values = [const.TYPE_FLAT, 'foo-network-type']
self.to_primitive_values = self.coerce_good_values
self.from_primitive_values = self.coerce_good_values
def test_stringify(self):
for in_val, out_val in self.coerce_good_values:
self.assertEqual("'%s'" % in_val, self.field.stringify(in_val))

View File

@ -0,0 +1,138 @@
# Copyright (c) 2019 Intel Corporation.
#
# 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 random
import mock
from neutron_lib import constants
from neutron_lib.utils import helpers
from oslo_utils import uuidutils
from neutron.objects import network as net_obj
from neutron.objects import network_segment_range
from neutron.objects.plugins.ml2 import vlanallocation as vlan_alloc_obj
from neutron.tests.unit.objects import test_base as obj_test_base
from neutron.tests.unit import testlib_api
TEST_TENANT_ID = '46f70361-ba71-4bd0-9769-3573fd227c4b'
TEST_PHYSICAL_NETWORK = 'phys_net'
class NetworkSegmentRangeIfaceObjectTestCase(
obj_test_base.BaseObjectIfaceTestCase):
_test_class = network_segment_range.NetworkSegmentRange
def setUp(self):
self._mock_get_available_allocation = mock.patch.object(
network_segment_range.NetworkSegmentRange,
'_get_available_allocation',
return_value=[])
self.mock_get_available_allocation = (
self._mock_get_available_allocation.start())
self._mock_get_used_allocation_mapping = mock.patch.object(
network_segment_range.NetworkSegmentRange,
'_get_used_allocation_mapping',
return_value={})
self.mock_get_used_allocation_mapping = (
self._mock_get_used_allocation_mapping.start())
super(NetworkSegmentRangeIfaceObjectTestCase, self).setUp()
# `project_id` and `physical_network` attributes in
# network_segment_range are nullable, depending on the value of
# `shared` and `network_type` respectively.
# Hack to always populate test project_id and physical_network
# fields in network segment range Iface object testing so that related
# tests like `test_extra_fields`, `test_create_updates_from_db_object`,
# `test_update_updates_from_db_object` can have those fields.
# Alternatives can be skipping those tests when executing
# NetworkSegmentRangeIfaceObjectTestCase, or making base test case
# adjustments.
self.update_obj_fields({'project_id': TEST_TENANT_ID,
'physical_network': TEST_PHYSICAL_NETWORK})
class NetworkSegmentRangeDbObjectTestCase(obj_test_base.BaseDbObjectTestCase,
testlib_api.SqlTestCase):
_test_class = network_segment_range.NetworkSegmentRange
def _create_test_vlan_allocation(self, vlan_id=None, allocated=False):
attr = self.get_random_object_fields(vlan_alloc_obj.VlanAllocation)
attr.update({
'vlan_id': vlan_id or random.randint(
constants.MIN_VLAN_TAG, constants.MAX_VLAN_TAG),
'physical_network': 'foo',
'allocated': allocated})
_vlan_allocation = vlan_alloc_obj.VlanAllocation(self.context, **attr)
_vlan_allocation.create()
return _vlan_allocation
def _create_test_network(self, name=None, network_id=None):
name = "test-network-%s" % helpers.get_random_string(4)
network_id = (uuidutils.generate_uuid() if network_id is None
else network_id)
_network = net_obj.Network(self.context, name=name, id=network_id,
project_id=uuidutils.generate_uuid())
_network.create()
return _network
def _create_test_vlan_segment(self, segmentation_id=None, network_id=None):
attr = self.get_random_object_fields(net_obj.NetworkSegment)
attr.update({
'network_id': network_id or self._create_test_network_id(),
'network_type': constants.TYPE_VLAN,
'physical_network': 'foo',
'segmentation_id': segmentation_id or random.randint(
constants.MIN_VLAN_TAG, constants.MAX_VLAN_TAG)})
_segment = net_obj.NetworkSegment(self.context, **attr)
_segment.create()
return _segment
def _create_test_vlan_network_segment_range_obj(self, minimum, maximum):
kwargs = self.get_random_db_fields()
kwargs.update({'network_type': constants.TYPE_VLAN,
'physical_network': 'foo',
'minimum': minimum,
'maximum': maximum})
db_obj = self._test_class.db_model(**kwargs)
obj_fields = self._test_class.modify_fields_from_db(db_obj)
obj = self._test_class(self.context, **obj_fields)
return obj
def test__get_available_allocation(self):
range_minimum = 100
range_maximum = 120
to_alloc = range(range_minimum, range_maximum - 5)
not_to_alloc = range(range_maximum - 5, range_maximum + 1)
for vlan_id in to_alloc:
self._create_test_vlan_allocation(vlan_id=vlan_id, allocated=True)
for vlan_id in not_to_alloc:
self._create_test_vlan_allocation(vlan_id=vlan_id, allocated=False)
obj = self._create_test_vlan_network_segment_range_obj(range_minimum,
range_maximum)
available_alloc = self._test_class._get_available_allocation(obj)
self.assertItemsEqual(not_to_alloc, available_alloc)
def test__get_used_allocation_mapping(self):
alloc_mapping = {}
for _ in range(5):
network = self._create_test_network()
segment = self._create_test_vlan_segment(network_id=network.id)
alloc_mapping.update({segment.segmentation_id: network.project_id})
obj = self._create_test_vlan_network_segment_range_obj(
minimum=min(list(alloc_mapping.keys())),
maximum=max(list(alloc_mapping.keys())))
ret_alloc_mapping = self._test_class._get_used_allocation_mapping(obj)
self.assertDictEqual(alloc_mapping, ret_alloc_mapping)

View File

@ -62,6 +62,7 @@ object_data = {
'NetworkPortSecurity': '1.0-b30802391a87945ee9c07582b4ff95e3', 'NetworkPortSecurity': '1.0-b30802391a87945ee9c07582b4ff95e3',
'NetworkRBAC': '1.2-192845c5ed0718e1c54fac36936fcd7d', 'NetworkRBAC': '1.2-192845c5ed0718e1c54fac36936fcd7d',
'NetworkSegment': '1.0-57b7f2960971e3b95ded20cbc59244a8', 'NetworkSegment': '1.0-57b7f2960971e3b95ded20cbc59244a8',
'NetworkSegmentRange': '1.0-bdec1fffc9058ea676089b1f2f2b3cf3',
'Port': '1.4-1b6183bccfc2cd210919a1a72faefce1', 'Port': '1.4-1b6183bccfc2cd210919a1a72faefce1',
'PortBinding': '1.0-3306deeaa6deb01e33af06777d48d578', 'PortBinding': '1.0-3306deeaa6deb01e33af06777d48d578',
'PortBindingLevel': '1.1-50d47f63218f87581b6cd9a62db574e5', 'PortBindingLevel': '1.1-50d47f63218f87581b6cd9a62db574e5',

View File

@ -0,0 +1,240 @@
# Copyright (c) 2019 Intel Corporation.
#
# 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
from neutron_lib import context
from neutron_lib import exceptions as exc
from neutron_lib.utils import helpers
from oslo_config import cfg
from neutron.db import segments_db
from neutron.services.network_segment_range import plugin as range_plugin
from neutron.tests.unit.db import test_db_base_plugin_v2 as test_plugin
from neutron.tests.unit import testlib_api
SERVICE_PLUGIN_KLASS = ('neutron.services.network_segment_range.plugin.'
'NetworkSegmentRangePlugin')
class TestNetworkSegmentRange(testlib_api.SqlTestCase):
_foo_range = {'name': 'foo-range',
'default': False,
'shared': False,
'project_id': test_plugin.TEST_TENANT_ID,
'network_type': 'foo_network_type',
'physical_network': 'foo_phys_net',
'minimum': 200,
'maximum': 300}
_flat_range = {'name': 'foo-flat-range',
'default': False,
'shared': False,
'project_id': test_plugin.TEST_TENANT_ID,
'network_type': constants.TYPE_FLAT,
'physical_network': None,
'minimum': 0,
'maximum': 0}
_vlan_range = {'name': 'foo-vlan-range',
'default': False,
'shared': False,
'project_id': test_plugin.TEST_TENANT_ID,
'network_type': constants.TYPE_VLAN,
'physical_network': 'phys_net',
'minimum': 200,
'maximum': 300}
_vxlan_range = {'name': 'foo-vxlan-range',
'default': False,
'shared': False,
'project_id': test_plugin.TEST_TENANT_ID,
'network_type': constants.TYPE_VXLAN,
'physical_network': None,
'minimum': 400,
'maximum': 500}
_gre_range = {'name': 'foo-vlan-range',
'default': False,
'shared': False,
'project_id': test_plugin.TEST_TENANT_ID,
'network_type': constants.TYPE_GRE,
'physical_network': None,
'minimum': 600,
'maximum': 700}
_geneve_range = {'name': 'foo-geneve-range',
'default': False,
'shared': False,
'project_id': test_plugin.TEST_TENANT_ID,
'network_type': constants.TYPE_GENEVE,
'physical_network': None,
'minimum': 800,
'maximum': 900}
def setUp(self):
super(TestNetworkSegmentRange, self).setUp()
with mock.patch("neutron_lib.plugins.directory.get_plugin"):
self.plugin = range_plugin.NetworkSegmentRangePlugin()
self.context = context.get_admin_context()
cfg.CONF.set_override('service_plugins', [SERVICE_PLUGIN_KLASS])
def _validate_resource(self, resource, keys, res_name):
for k in keys:
self.assertIn(k, resource[res_name])
if isinstance(keys[k], list):
self.assertEqual(
sorted(keys[k], key=helpers.safe_sort_key),
sorted(resource[res_name][k], key=helpers.safe_sort_key))
else:
self.assertEqual(keys[k], resource[res_name][k])
def test__is_network_segment_range_referenced(self):
with mock.patch.object(segments_db,
'network_segments_exist_in_range',
return_value=True):
self.assertTrue(self.plugin._is_network_segment_range_referenced(
self.context, self._vlan_range))
def test__is_network_segment_range_unreferenced(self):
with mock.patch.object(segments_db,
'network_segments_exist_in_range',
return_value=False):
self.assertFalse(self.plugin._is_network_segment_range_referenced(
self.context, self._vlan_range))
def test__is_network_segment_range_type_supported(self):
for foo_range in [self._vlan_range, self._vxlan_range,
self._gre_range, self._geneve_range]:
self.assertTrue(
self.plugin.
_is_network_segment_range_type_supported(
foo_range['network_type']))
def test__is_network_segment_range_type_unsupported(self):
self.assertRaises(
exc.NeutronException,
self.plugin._is_network_segment_range_type_supported,
self._foo_range['network_type'])
self.assertRaises(
exc.NeutronException,
self.plugin._is_network_segment_range_type_supported,
self._flat_range['network_type'])
def test__are_allocated_segments_in_range_impacted(self):
existing_range = self._foo_range
updated_range = self._vlan_range
impacted_existing_ranges = [(150, 250), (250, 320),
(200, 300), (180, 330)]
for ret in impacted_existing_ranges:
with mock.patch.object(segments_db,
'min_max_actual_segments_in_range',
return_value=ret):
self.assertTrue(
self.plugin._are_allocated_segments_in_range_impacted(
self.context, existing_range, updated_range))
def test__are_allocated_segments_in_range_unimpacted(self):
existing_range = self._foo_range
updated_range = self._vlan_range
with mock.patch.object(segments_db,
'min_max_actual_segments_in_range',
return_value=(220, 270)):
self.assertFalse(
self.plugin._are_allocated_segments_in_range_impacted(
self.context, existing_range, updated_range))
def test_create_network_segment_range(self):
test_range = self._vlan_range
network_segment_range = {'network_segment_range': test_range}
ret = self.plugin.create_network_segment_range(self.context,
network_segment_range)
res = {'network_segment_range': ret}
self._validate_resource(res, test_range, 'network_segment_range')
def test_create_network_segment_range_failed_with_unsupported_network_type(
self):
test_range = self._flat_range
network_segment_range = {'network_segment_range': test_range}
self.assertRaises(
exc.NeutronException,
self.plugin.create_network_segment_range,
self.context,
network_segment_range)
def test_update_network_segment_range(self):
test_range = self._vlan_range
network_segment_range = {'network_segment_range': test_range}
ret = self.plugin.create_network_segment_range(self.context,
network_segment_range)
updated_network_segment_range = {
'network_segment_range': {'minimum': 700, 'maximum': 800}}
with mock.patch.object(self.plugin,
'_are_allocated_segments_in_range_impacted',
return_value=False):
updated_ret = self.plugin.update_network_segment_range(
self.context, ret['id'], updated_network_segment_range)
res = {'network_segment_range': updated_ret}
test_range['minimum'] = 700
test_range['maximum'] = 800
self._validate_resource(res, test_range, 'network_segment_range')
def test_update_network_segment_range_failed_with_impacted_existing_range(
self):
test_range = self._vlan_range
network_segment_range = {'network_segment_range': test_range}
ret = self.plugin.create_network_segment_range(self.context,
network_segment_range)
updated_network_segment_range = {
'network_segment_range': {'minimum': 150, 'maximum': 250}}
with mock.patch.object(self.plugin,
'_are_allocated_segments_in_range_impacted',
return_value=True):
self.assertRaises(
exc.NeutronException,
self.plugin.update_network_segment_range,
self.context,
ret['id'],
updated_network_segment_range)
def test_delete_network_segment_range(self):
test_range = self._vlan_range
network_segment_range = {'network_segment_range': test_range}
ret = self.plugin.create_network_segment_range(self.context,
network_segment_range)
with mock.patch.object(self.plugin,
'_is_network_segment_range_referenced',
return_value=False):
try:
self.plugin.delete_network_segment_range(
self.context, ret['id'])
except exc.NeutronException:
self.fail("delete_network_segment_range raised "
"NeutronException unexpectedly!")
def test_delete_network_segment_range_failed_with_segment_referenced(self):
test_range = self._vlan_range
network_segment_range = {'network_segment_range': test_range}
ret = self.plugin.create_network_segment_range(self.context,
network_segment_range)
with mock.patch.object(self.plugin,
'_is_network_segment_range_referenced',
return_value=True):
self.assertRaises(
exc.NeutronException,
self.plugin.delete_network_segment_range,
self.context,
ret['id'])

View File

@ -0,0 +1,25 @@
---
prelude: >
Added support for network segment range management. This introduces the
ability for administrators to control the segment ranges globally or on
a per-tenant basis via the Neutron API.
features:
- |
Before Stein, network segment ranges were configured as an entry in ML2
config file ``/etc/neutron/plugins/ml2/ml2_conf.ini`` that was statically
defined for tenant network allocation and therefore had to be managed as
part of the host deployment and management.
The new ``network-segment-range`` API extension has been introduced, which
exposes the network segment ranges to be administered via API. This
allows users with admin privileges to be able to dynamically manage
the shared and/or tenant specific network segment ranges.
Standard attributes with tagging support are introduced to the new
resource.
The feature is controlled by the newly-added service plugin
``network_segment_range``.
A set of ``default`` network segment ranges will be created out of
the ranges that are defined in the host ML2 config file
``/etc/neutron/plugins/ml2/ml2_conf.ini``, such as
``network_vlan_ranges``, ``vni_ranges`` for ml2_type_vxlan,
``tunnel_id_ranges`` for ml2_type_gre and ``vni_ranges`` for
ml2_type_geneve.

View File

@ -68,6 +68,7 @@ neutron.service_plugins =
auto_allocate = neutron.services.auto_allocate.plugin:Plugin auto_allocate = neutron.services.auto_allocate.plugin:Plugin
segments = neutron.services.segments.plugin:Plugin segments = neutron.services.segments.plugin:Plugin
network_ip_availability = neutron.services.network_ip_availability.plugin:NetworkIPAvailabilityPlugin network_ip_availability = neutron.services.network_ip_availability.plugin:NetworkIPAvailabilityPlugin
network_segment_range = neutron.services.network_segment_range.plugin:NetworkSegmentRangePlugin
revisions = neutron.services.revisions.revision_plugin:RevisionPlugin revisions = neutron.services.revisions.revision_plugin:RevisionPlugin
timestamp = neutron.services.timestamp.timestamp_plugin:TimeStampPlugin timestamp = neutron.services.timestamp.timestamp_plugin:TimeStampPlugin
trunk = neutron.services.trunk.plugin:TrunkPlugin trunk = neutron.services.trunk.plugin:TrunkPlugin
@ -196,6 +197,7 @@ neutron.objects =
NetworkPortSecurity = neutron.objects.network:NetworkPortSecurity NetworkPortSecurity = neutron.objects.network:NetworkPortSecurity
NetworkRBAC = neutron.objects.network:NetworkRBAC NetworkRBAC = neutron.objects.network:NetworkRBAC
NetworkSegment = neutron.objects.network:NetworkSegment NetworkSegment = neutron.objects.network:NetworkSegment
NetworkSegmentRange = neutron.objects.network:NetworkSegmentRange
Port = neutron.objects.ports:Port Port = neutron.objects.ports:Port
PortBinding = neutron.objects.ports:PortBinding PortBinding = neutron.objects.ports:PortBinding
PortBindingLevel = neutron.objects.ports:PortBindingLevel PortBindingLevel = neutron.objects.ports:PortBindingLevel