Basic Extension and CRUD for Segments

This patch enables CRUD on Segments by defining a new entity called
'Segment' with an extension definition and some DB code to read the
existing segments DB.  A basic framework for create, update, and
delete are provided.

For now, this is just the basic boiler-plate but I've got to start
somewhere.  It is implemented as a service plugin that is disabled by
default because it has not been fully tested with any plugin.
Follow-on patches will implement support for this new extension in
ML2 and OVN at least.

Change-Id: Ifc370fdd38f9a5b296334635fa85bd93d270b910
Partially-Implements: blueprint routed-networks
This commit is contained in:
Carl Baldwin 2016-02-09 16:39:01 -07:00
parent f18db6f754
commit a34c3543d0
9 changed files with 547 additions and 0 deletions

View File

@ -61,6 +61,11 @@
"update_network:router:external": "rule:admin_only",
"delete_network": "rule:admin_or_owner",
"create_segment": "rule:admin_only",
"get_segment": "rule:admin_only",
"update_segment": "rule:admin_only",
"delete_segment": "rule:admin_only",
"network_device": "field:port:device_owner=~^network:",
"create_port": "",
"create_port:device_owner": "not rule:network_device or rule:context_is_advsvc or rule:admin_or_network_owner",

View File

@ -0,0 +1,202 @@
# Copyright (c) 2016 Hewlett Packard Enterprise Development Company, L.P.
#
# 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
import six
from neutron.api import extensions
from neutron.api.v2 import attributes
from neutron.api.v2 import base
from neutron.extensions import providernet
from neutron.services.segments import plugin
SEGMENT = 'segment'
SEGMENTS = '%ss' % SEGMENT
SEGMENT_ID = 'segment_id'
NETWORK_TYPE = 'network_type'
PHYSICAL_NETWORK = 'physical_network'
SEGMENTATION_ID = 'segmentation_id'
# Attribute Map
RESOURCE_ATTRIBUTE_MAP = {
SEGMENTS: {
'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,
'validate': {'type:string':
attributes.TENANT_ID_MAX_LEN},
'is_visible': False},
'network_id': {'allow_post': True,
'allow_put': False,
'validate': {'type:uuid': None},
'is_visible': True},
PHYSICAL_NETWORK: {'allow_post': True,
'allow_put': False,
'default': attributes.ATTR_NOT_SPECIFIED,
'validate': {'type:string':
providernet.PHYSICAL_NETWORK_MAX_LEN},
'is_visible': True},
NETWORK_TYPE: {'allow_post': True,
'allow_put': False,
'validate': {'type:string':
providernet.PHYSICAL_NETWORK_MAX_LEN},
'is_visible': True},
SEGMENTATION_ID: {'allow_post': True,
'allow_put': False,
'default': attributes.ATTR_NOT_SPECIFIED,
'convert_to': attributes.convert_to_int,
'is_visible': True},
},
}
class Segment(extensions.ExtensionDescriptor):
"""Extension class supporting Segments."""
@classmethod
def get_name(cls):
return "Segment"
@classmethod
def get_alias(cls):
return "segment"
@classmethod
def get_description(cls):
return "Segments extension."
@classmethod
def get_updated(cls):
return "2016-02-24T17:00:00-00:00"
@classmethod
def get_resources(cls):
"""Returns Extended Resource for service type management."""
attributes.PLURALS[SEGMENTS] = SEGMENT
resource_attributes = RESOURCE_ATTRIBUTE_MAP[SEGMENTS]
controller = base.create_resource(
SEGMENTS,
SEGMENT,
plugin.Plugin.get_instance(),
resource_attributes)
return [extensions.ResourceExtension(SEGMENTS,
controller,
attr_map=resource_attributes)]
def get_extended_resources(self, version):
if version == "2.0":
return RESOURCE_ATTRIBUTE_MAP
else:
return {}
@six.add_metaclass(abc.ABCMeta)
class SegmentPluginBase(object):
@abc.abstractmethod
def create_segment(self, context, segment):
"""Create a segment.
Create a segment, which represents an L2 segment of a network.
:param context: neutron api request context
:param segment: dictionary describing the segment, with keys
as listed in the :obj:`RESOURCE_ATTRIBUTE_MAP` object
in :file:`neutron/extensions/segment.py`. All keys
will be populated.
"""
@abc.abstractmethod
def update_segment(self, context, uuid, segment):
"""Update values of a segment.
:param context: neutron api request context
:param uuid: UUID representing the segment to update.
:param segment: 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/extensions/segment.py`.
"""
@abc.abstractmethod
def get_segment(self, context, uuid, fields=None):
"""Retrieve a segment.
:param context: neutron api request context
:param uuid: UUID representing the segment to fetch.
:param fields: a list of strings that are valid keys in a
segment dictionary as listed in the
:obj:`RESOURCE_ATTRIBUTE_MAP` object in
:file:`neutron/extensions/segment.py`. Only these fields
will be returned.
"""
@abc.abstractmethod
def get_segments(self, context, filters=None, fields=None,
sorts=None, limit=None, marker=None,
page_reverse=False):
"""Retrieve a list of segments.
The contents of the list depends on the identity of the user making the
request (as indicated by the context) as well as any filters.
:param context: neutron api request context
:param filters: a dictionary with keys that are valid keys for
a segment as listed in the
:obj:`RESOURCE_ATTRIBUTE_MAP` object in
:file:`neutron/extensions/segment.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
segment dictionary as listed in the
:obj:`RESOURCE_ATTRIBUTE_MAP` object in
:file:`neutron/extensions/segment.py`. Only these fields
will be returned.
"""
@abc.abstractmethod
def delete_segment(self, context, uuid):
"""Delete a segment.
:param context: neutron api request context
:param uuid: UUID representing the segment to delete.
"""
@abc.abstractmethod
def get_segments_count(self, context, filters=None):
"""Return the number of segments.
The result depends on the identity
of the user making the request (as indicated by the context) as well
as any filters.
:param context: neutron api request context
:param filters: a dictionary with keys that are valid keys for
a segment as listed in the
:obj:`RESOURCE_ATTRIBUTE_MAP` object
in :file:`neutron/extensions/segment.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.
"""

View File

View File

@ -0,0 +1,116 @@
# Copyright 2016 Hewlett Packard Enterprise Development, 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.
import functools
from oslo_log import helpers as log_helpers
from oslo_utils import uuidutils
from sqlalchemy.orm import exc
from neutron.api.v2 import attributes
from neutron.db import common_db_mixin
from neutron.db import segments_db as db
from neutron.services.segments import exceptions
class SegmentDbMixin(common_db_mixin.CommonDbMixin):
"""Mixin class to add segment."""
def _make_segment_dict(self, segment_db, fields=None):
res = {'id': segment_db['id'],
'network_id': segment_db['network_id'],
db.PHYSICAL_NETWORK: segment_db[db.PHYSICAL_NETWORK],
db.NETWORK_TYPE: segment_db[db.NETWORK_TYPE],
db.SEGMENTATION_ID: segment_db[db.SEGMENTATION_ID]}
return self._fields(res, fields)
def _get_segment(self, context, segment_id):
try:
return self._get_by_id(
context, db.NetworkSegment, segment_id)
except exc.NoResultFound:
raise exceptions.SegmentNotFound(segment_id=segment_id)
@log_helpers.log_method_call
def create_segment(self, context, segment):
"""Create a segment."""
segment = segment['segment']
segment_id = segment.get('id') or uuidutils.generate_uuid()
with context.session.begin(subtransactions=True):
network_id = segment['network_id']
# FIXME couldn't use constants because of a circular import problem
physical_network = segment['physical_network']
if physical_network == attributes.ATTR_NOT_SPECIFIED:
physical_network = None
network_type = segment['network_type']
segmentation_id = segment['segmentation_id']
if segmentation_id == attributes.ATTR_NOT_SPECIFIED:
segmentation_id = None
args = {'id': segment_id,
'network_id': network_id,
db.PHYSICAL_NETWORK: physical_network,
db.NETWORK_TYPE: network_type,
db.SEGMENTATION_ID: segmentation_id}
new_segment = db.NetworkSegment(**args)
context.session.add(new_segment)
return self._make_segment_dict(new_segment)
@log_helpers.log_method_call
def update_segment(self, context, uuid, segment):
"""Update an existing segment."""
segment = segment['segment']
with context.session.begin(subtransactions=True):
curr_segment = self._get_segment(context, uuid)
curr_segment.update(segment)
return self._make_segment_dict(curr_segment)
@log_helpers.log_method_call
def get_segment(self, context, uuid, fields=None):
segment_db = self._get_segment(context, uuid)
return self._make_segment_dict(segment_db, fields)
@log_helpers.log_method_call
def get_segments(self, context, filters=None, fields=None,
sorts=None, limit=None, marker=None,
page_reverse=False):
marker_obj = self._get_marker_obj(context, 'segment', limit, marker)
make_segment_dict = functools.partial(self._make_segment_dict)
return self._get_collection(context,
db.NetworkSegment,
make_segment_dict,
filters=filters,
fields=fields,
sorts=sorts,
limit=limit,
marker_obj=marker_obj,
page_reverse=page_reverse)
@log_helpers.log_method_call
def get_segments_count(self, context, filters=None):
return self._get_collection_count(context,
db.NetworkSegment,
filters=filters)
@log_helpers.log_method_call
def delete_segment(self, context, uuid):
"""Delete an existing segment."""
with context.session.begin(subtransactions=True):
query = self._model_query(context, db.NetworkSegment)
query = query.filter(db.NetworkSegment.id == uuid)
if 0 == query.delete():
raise exceptions.SegmentNotFound(segment_id=uuid)

View File

@ -0,0 +1,23 @@
# Copyright 2016 Hewlett Packard Enterprise Development, 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 neutron._i18n import _
from neutron_lib import exceptions
class SegmentNotFound(exceptions.NotFound):
message = _("Segment %(segment_id)s could not be found.")

View File

@ -0,0 +1,37 @@
# Copyright 2016 Hewlett Packard Enterprise Development, 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 neutron.services.segments import db
class Plugin(db.SegmentDbMixin):
_instance = None
supported_extension_aliases = ["segment"]
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
def get_plugin_description(self):
return "Network Segments"
def get_plugin_type(self):
return "segments"

View File

@ -61,6 +61,11 @@
"update_network:router:external": "rule:admin_only",
"delete_network": "rule:admin_or_owner",
"create_segment": "rule:admin_only",
"get_segment": "rule:admin_only",
"update_segment": "rule:admin_only",
"delete_segment": "rule:admin_only",
"network_device": "field:port:device_owner=~^network:",
"create_port": "",
"create_port:device_owner": "not rule:network_device or rule:context_is_advsvc or rule:admin_or_network_owner",

View File

@ -0,0 +1,158 @@
# Copyright (c) 2016 Hewlett Packard Enterprise Development Company, L.P.
#
# 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 webob.exc
from neutron.api.v2 import attributes
from neutron.db import db_base_plugin_v2
from neutron.extensions import segment as ext_segment
from neutron.services.segments import db
from neutron.tests.unit.db import test_db_base_plugin_v2
DB_PLUGIN_KLASS = ('neutron.tests.unit.extensions.test_segment.'
'SegmentTestPlugin')
class SegmentTestExtensionManager(object):
def get_resources(self):
# Add the resources to the global attribute map
# This is done here as the setup process won't
# initialize the main API router which extends
# the global attribute map
attributes.RESOURCE_ATTRIBUTE_MAP.update(
ext_segment.RESOURCE_ATTRIBUTE_MAP)
return ext_segment.Segment.get_resources()
def get_actions(self):
return []
def get_request_extensions(self):
return []
class SegmentTestCase(test_db_base_plugin_v2.NeutronDbPluginV2TestCase):
def setUp(self):
plugin = DB_PLUGIN_KLASS
ext_mgr = SegmentTestExtensionManager()
super(SegmentTestCase, self).setUp(plugin=plugin, ext_mgr=ext_mgr)
def _create_segment(self, fmt, expected_res_status=None, **kwargs):
segment = {'segment': {}}
for k, v in kwargs.items():
segment['segment'][k] = str(v)
segment_req = self.new_create_request(
'segments', segment, fmt)
segment_res = segment_req.get_response(self.ext_api)
if expected_res_status:
self.assertEqual(segment_res.status_int, expected_res_status)
return segment_res
def _make_segment(self, fmt, **kwargs):
res = self._create_segment(fmt, **kwargs)
if res.status_int >= webob.exc.HTTPClientError.code:
raise webob.exc.HTTPClientError(
code=res.status_int, explanation=str(res))
return self.deserialize(fmt, res)
def segment(self, **kwargs):
kwargs.setdefault('network_type', 'net_type')
return self._make_segment(
self.fmt, tenant_id=self._tenant_id, **kwargs)
def _test_create_segment(self, expected=None, **kwargs):
keys = kwargs.copy()
segment = self.segment(**keys)
self._validate_resource(segment, keys, 'segment')
if expected:
self._compare_resource(segment, expected, 'segment')
return segment
class SegmentTestPlugin(db_base_plugin_v2.NeutronDbPluginV2,
db.SegmentDbMixin):
__native_pagination_support = True
__native_sorting_support = True
supported_extension_aliases = ["segment"]
class TestSegment(SegmentTestCase):
def test_create_segment(self):
with self.network() as network:
network = network['network']
expected_segment = {'network_id': network['id'],
'physical_network': 'phys_net',
'network_type': 'net_type',
'segmentation_id': 200}
self._test_create_segment(network_id=network['id'],
physical_network='phys_net',
segmentation_id=200,
expected=expected_segment)
def test_create_segment_no_phys_net(self):
with self.network() as network:
network = network['network']
expected_segment = {'network_id': network['id'],
'physical_network': None,
'network_type': 'net_type',
'segmentation_id': 200}
self._test_create_segment(network_id=network['id'],
segmentation_id=200,
expected=expected_segment)
def test_create_segment_no_segmentation_id(self):
with self.network() as network:
network = network['network']
expected_segment = {'network_id': network['id'],
'physical_network': 'phys_net',
'network_type': 'net_type',
'segmentation_id': None}
self._test_create_segment(network_id=network['id'],
physical_network='phys_net',
expected=expected_segment)
def test_delete_segment(self):
with self.network() as network:
network = network['network']
segment = self.segment(network_id=network['id'])
self._delete('segments', segment['segment']['id'])
self._show('segments', segment['segment']['id'],
expected_code=webob.exc.HTTPNotFound.code)
def test_get_segment(self):
with self.network() as network:
network = network['network']
segment = self._test_create_segment(network_id=network['id'],
physical_network='phys_net',
segmentation_id=200)
req = self.new_show_request('segments', segment['segment']['id'])
res = self.deserialize(self.fmt, req.get_response(self.ext_api))
self.assertEqual(segment['segment']['id'], res['segment']['id'])
def test_list_segments(self):
with self.network() as network:
network = network['network']
self._test_create_segment(network_id=network['id'],
physical_network='phys_net1',
segmentation_id=200)
self._test_create_segment(network_id=network['id'],
physical_network='phys_net2',
segmentation_id=200)
res = self._list('segments')
self.assertEqual(2, len(res['segments']))

View File

@ -79,6 +79,7 @@ neutron.service_plugins =
tag = neutron.services.tag.tag_plugin:TagPlugin
flavors = neutron.services.flavors.flavors_plugin:FlavorsPlugin
auto_allocate = neutron.services.auto_allocate.plugin:Plugin
segments = neutron.services.segments.plugin:Plugin
network_ip_availability = neutron.services.network_ip_availability.plugin:NetworkIPAvailabilityPlugin
timestamp_core = neutron.services.timestamp.timestamp_plugin:TimeStampPlugin
neutron.qos.notification_drivers =