objects: SubnetPool, SubnetPoolPrefix
This patch adds SubnetPool and SubnetPoolPrefix objects. Note: objects are not integrated into database plugin code. This is left for a follow-up patch. Other changes: - made modify_fields_to_db object instance method since some objects may need to access all fields set on the object; - provided a new RangeConstrainedInteger type that validates that value is in the range; - modified several base test cases to work with objects that modify other tables that are different from the object db model. Co-Authored-By: Ihar Hrachyshka <ihrachys@redhat.com> Related-Bug: #1541928 Change-Id: I7cbc694ab00c05d0a020fffe4f73141c6ceff7e9
This commit is contained in:
parent
bb99b8c648
commit
408173360f
|
@ -228,6 +228,11 @@ EGRESS_DIRECTION = 'egress'
|
|||
VALID_DIRECTIONS = (INGRESS_DIRECTION, EGRESS_DIRECTION)
|
||||
VALID_ETHERTYPES = (lib_constants.IPv4, lib_constants.IPv6)
|
||||
|
||||
IP_ALLOWED_VERSIONS = [lib_constants.IP_VERSION_4, lib_constants.IP_VERSION_6]
|
||||
|
||||
IPV4_MAX_PREFIXLEN = 32
|
||||
IPV6_MAX_PREFIXLEN = 128
|
||||
|
||||
# Neutron-lib migration shim. This will wrap any constants that are moved
|
||||
# to that library in a deprecation warning, until they can be updated to
|
||||
# import directly from their new location.
|
||||
|
|
|
@ -16,6 +16,12 @@ import six
|
|||
|
||||
from neutron._i18n import _
|
||||
from neutron.common import constants
|
||||
from neutron.common import exceptions
|
||||
|
||||
|
||||
class NeutronRangeConstrainedIntegerInvalidLimit(exceptions.NeutronException):
|
||||
message = _("Incorrect range limits specified: "
|
||||
"start = %(start)s, end = %(end)s")
|
||||
|
||||
|
||||
class IPV6ModeEnum(obj_fields.Enum):
|
||||
|
@ -31,6 +37,49 @@ class IPV6ModeEnumField(obj_fields.BaseEnumField):
|
|||
super(IPV6ModeEnumField, self).__init__(**kwargs)
|
||||
|
||||
|
||||
class RangeConstrainedInteger(obj_fields.Integer):
|
||||
def __init__(self, start, end, **kwargs):
|
||||
try:
|
||||
self._start = int(start)
|
||||
self._end = int(end)
|
||||
except (TypeError, ValueError):
|
||||
raise NeutronRangeConstrainedIntegerInvalidLimit(
|
||||
start=start, end=end)
|
||||
super(RangeConstrainedInteger, self).__init__(**kwargs)
|
||||
|
||||
def _validate_value(self, value):
|
||||
if not isinstance(value, six.integer_types):
|
||||
msg = _("Field value %s is not an integer") % value
|
||||
raise ValueError(msg)
|
||||
if not self._start <= value <= self._end:
|
||||
msg = _("Field value %s is invalid") % value
|
||||
raise ValueError(msg)
|
||||
|
||||
def coerce(self, obj, attr, value):
|
||||
self._validate_value(value)
|
||||
return super(RangeConstrainedInteger, self).coerce(obj, attr, value)
|
||||
|
||||
def stringify(self, value):
|
||||
self._validate_value(value)
|
||||
return super(RangeConstrainedInteger, self).stringify(value)
|
||||
|
||||
|
||||
class IPNetworkPrefixLen(RangeConstrainedInteger):
|
||||
"""IP network (CIDR) prefix length custom Enum"""
|
||||
def __init__(self, **kwargs):
|
||||
super(IPNetworkPrefixLen, self).__init__(
|
||||
start=0, end=constants.IPV6_MAX_PREFIXLEN,
|
||||
**kwargs)
|
||||
|
||||
|
||||
class IPNetworkPrefixLenField(obj_fields.AutoTypedField):
|
||||
AUTO_TYPE = IPNetworkPrefixLen()
|
||||
|
||||
|
||||
class ListOfIPNetworksField(obj_fields.AutoTypedField):
|
||||
AUTO_TYPE = obj_fields.List(obj_fields.IPNetwork())
|
||||
|
||||
|
||||
class IntegerEnum(obj_fields.Integer):
|
||||
def __init__(self, valid_values=None, **kwargs):
|
||||
if not valid_values:
|
||||
|
@ -64,6 +113,17 @@ class IntegerEnum(obj_fields.Integer):
|
|||
return super(IntegerEnum, self).stringify(value)
|
||||
|
||||
|
||||
class IPVersionEnum(IntegerEnum):
|
||||
"""IP version integer Enum"""
|
||||
def __init__(self, **kwargs):
|
||||
super(IPVersionEnum, self).__init__(
|
||||
valid_values=constants.IP_ALLOWED_VERSIONS, **kwargs)
|
||||
|
||||
|
||||
class IPVersionEnumField(obj_fields.AutoTypedField):
|
||||
AUTO_TYPE = IPVersionEnum()
|
||||
|
||||
|
||||
class DscpMark(IntegerEnum):
|
||||
def __init__(self, valid_values=None, **kwargs):
|
||||
super(DscpMark, self).__init__(
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
# Copyright (c) 2015 OpenStack Foundation.
|
||||
# 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 netaddr
|
||||
from oslo_log import log
|
||||
from oslo_versionedobjects import base as obj_base
|
||||
from oslo_versionedobjects import fields as obj_fields
|
||||
|
||||
from neutron.db import api as db_api
|
||||
from neutron.db import models_v2 as models
|
||||
from neutron.objects import base
|
||||
from neutron.objects import common_types
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
@obj_base.VersionedObjectRegistry.register
|
||||
class SubnetPool(base.NeutronDbObject):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
db_model = models.SubnetPool
|
||||
|
||||
fields = {
|
||||
'id': obj_fields.UUIDField(),
|
||||
'tenant_id': obj_fields.UUIDField(nullable=True),
|
||||
'name': obj_fields.StringField(nullable=True),
|
||||
'ip_version': common_types.IPVersionEnumField(),
|
||||
'default_prefixlen': common_types.IPNetworkPrefixLenField(),
|
||||
'min_prefixlen': common_types.IPNetworkPrefixLenField(),
|
||||
'max_prefixlen': common_types.IPNetworkPrefixLenField(),
|
||||
'shared': obj_fields.BooleanField(),
|
||||
'is_default': obj_fields.BooleanField(),
|
||||
'default_quota': obj_fields.IntegerField(nullable=True),
|
||||
'hash': obj_fields.StringField(nullable=True),
|
||||
'address_scope_id': obj_fields.UUIDField(nullable=True),
|
||||
'prefixes': common_types.ListOfIPNetworksField(nullable=True)
|
||||
}
|
||||
|
||||
fields_no_update = ['id', 'tenant_id']
|
||||
|
||||
synthetic_fields = ['prefixes']
|
||||
|
||||
@classmethod
|
||||
def modify_fields_from_db(cls, db_obj):
|
||||
fields = super(SubnetPool, cls).modify_fields_from_db(db_obj)
|
||||
if 'prefixes' in fields:
|
||||
fields['prefixes'] = [
|
||||
netaddr.IPNetwork(prefix.cidr)
|
||||
for prefix in fields['prefixes']
|
||||
]
|
||||
return fields
|
||||
|
||||
def modify_fields_to_db(self, fields):
|
||||
result = super(SubnetPool, self).modify_fields_to_db(fields)
|
||||
if 'prefixes' in result:
|
||||
result['prefixes'] = [
|
||||
models.SubnetPoolPrefix(cidr=str(prefix),
|
||||
subnetpool_id=self.id)
|
||||
for prefix in result['prefixes']
|
||||
]
|
||||
return result
|
||||
|
||||
def reload_prefixes(self):
|
||||
prefixes = [
|
||||
obj.cidr
|
||||
for obj in SubnetPoolPrefix.get_objects(
|
||||
self._context,
|
||||
subnetpool_id=self.id)
|
||||
]
|
||||
setattr(self, 'prefixes', prefixes)
|
||||
self.obj_reset_changes(['prefixes'])
|
||||
|
||||
@classmethod
|
||||
def get_object(cls, context, **kwargs):
|
||||
with db_api.autonested_transaction(context.session):
|
||||
pool_obj = super(SubnetPool, cls).get_object(context, **kwargs)
|
||||
if pool_obj is not None:
|
||||
pool_obj.reload_prefixes()
|
||||
return pool_obj
|
||||
|
||||
@classmethod
|
||||
def get_objects(cls, context, **kwargs):
|
||||
with db_api.autonested_transaction(context.session):
|
||||
objs = super(SubnetPool, cls).get_objects(context, **kwargs)
|
||||
for obj in objs:
|
||||
obj.reload_prefixes()
|
||||
return objs
|
||||
|
||||
def _get_changed_synthetic_fields(self):
|
||||
fields = self.obj_get_changes()
|
||||
for field in self._get_changed_persistent_fields():
|
||||
if field in fields:
|
||||
del fields[field]
|
||||
return fields
|
||||
|
||||
# TODO(ihrachys): Consider extending base to trigger registered methods
|
||||
def create(self):
|
||||
synthetic_changes = self._get_changed_synthetic_fields()
|
||||
with db_api.autonested_transaction(self._context.session):
|
||||
super(SubnetPool, self).create()
|
||||
if 'prefixes' in synthetic_changes:
|
||||
for prefix in self.prefixes:
|
||||
prefix = SubnetPoolPrefix(
|
||||
self._context, subnetpool_id=self.id, cidr=prefix)
|
||||
prefix.create()
|
||||
self.reload_prefixes()
|
||||
|
||||
# TODO(ihrachys): Consider extending base to trigger registered methods
|
||||
def update(self):
|
||||
with db_api.autonested_transaction(self._context.session):
|
||||
synthetic_changes = self._get_changed_synthetic_fields()
|
||||
super(SubnetPool, self).update()
|
||||
if synthetic_changes:
|
||||
if 'prefixes' in synthetic_changes:
|
||||
old = SubnetPoolPrefix.get_objects(
|
||||
self._context, subnetpool_id=self.id)
|
||||
for prefix in old:
|
||||
prefix.delete()
|
||||
for prefix in self.prefixes:
|
||||
prefix_obj = SubnetPoolPrefix(self._context,
|
||||
subnetpool_id=self.id,
|
||||
cidr=prefix)
|
||||
prefix_obj.create()
|
||||
|
||||
|
||||
@obj_base.VersionedObjectRegistry.register
|
||||
class SubnetPoolPrefix(base.NeutronDbObject):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
db_model = models.SubnetPoolPrefix
|
||||
|
||||
fields = {
|
||||
'subnetpool_id': obj_fields.UUIDField(),
|
||||
'cidr': obj_fields.IPNetworkField(),
|
||||
}
|
||||
|
||||
primary_keys = ['subnetpool_id', 'cidr']
|
||||
|
||||
# TODO(ihrachys): get rid of it once we switch the db model to using CIDR
|
||||
# custom type
|
||||
def modify_fields_to_db(self, fields):
|
||||
result = super(SubnetPoolPrefix, self).modify_fields_to_db(fields)
|
||||
if 'cidr' in result:
|
||||
result['cidr'] = str(result['cidr'])
|
||||
return result
|
||||
|
||||
# TODO(ihrachys): get rid of it once we switch the db model to using CIDR
|
||||
# custom type
|
||||
@classmethod
|
||||
def modify_fields_from_db(cls, db_obj):
|
||||
fields = super(SubnetPoolPrefix, cls).modify_fields_from_db(db_obj)
|
||||
if 'cidr' in fields:
|
||||
fields['cidr'] = netaddr.IPNetwork(fields['cidr'])
|
||||
return fields
|
|
@ -24,10 +24,12 @@ import warnings
|
|||
|
||||
import fixtures
|
||||
import mock
|
||||
import netaddr
|
||||
import six
|
||||
|
||||
import neutron
|
||||
from neutron.api.v2 import attributes
|
||||
from neutron.common import constants
|
||||
|
||||
|
||||
class AttributeMapMemento(fixtures.Fixture):
|
||||
|
@ -230,6 +232,17 @@ def get_random_integer(range_begin=0, range_end=1000):
|
|||
return random.randint(range_begin, range_end)
|
||||
|
||||
|
||||
def get_random_prefixlen(version=4):
|
||||
maxlen = constants.IPV4_MAX_PREFIXLEN
|
||||
if version == 6:
|
||||
maxlen = constants.IPV6_MAX_PREFIXLEN
|
||||
return random.randint(0, maxlen)
|
||||
|
||||
|
||||
def get_random_ip_version():
|
||||
return random.choice(constants.IP_ALLOWED_VERSIONS)
|
||||
|
||||
|
||||
def get_random_cidr(version=4):
|
||||
if version == 4:
|
||||
return '10.%d.%d.0/%d' % (random.randint(3, 254),
|
||||
|
@ -247,6 +260,10 @@ def get_random_mac():
|
|||
return ':'.join(map(lambda x: "%02x" % x, mac))
|
||||
|
||||
|
||||
def get_random_ip_network(version=4):
|
||||
return netaddr.IPNetwork(get_random_cidr(version=version))
|
||||
|
||||
|
||||
def is_bsd():
|
||||
"""Return True on BSD-based systems."""
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
import copy
|
||||
import random
|
||||
|
||||
|
@ -189,6 +190,10 @@ def get_random_dscp_mark():
|
|||
return random.choice(constants.VALID_DSCP_MARKS)
|
||||
|
||||
|
||||
def get_list_of_random_networks(num=10):
|
||||
return [tools.get_random_ip_network() for i in range(num)]
|
||||
|
||||
|
||||
FIELD_TYPE_VALUE_GENERATOR_MAP = {
|
||||
obj_fields.BooleanField: tools.get_random_boolean,
|
||||
obj_fields.IntegerField: tools.get_random_integer,
|
||||
|
@ -197,6 +202,10 @@ FIELD_TYPE_VALUE_GENERATOR_MAP = {
|
|||
obj_fields.ObjectField: lambda: None,
|
||||
obj_fields.ListOfObjectsField: lambda: [],
|
||||
common_types.DscpMarkField: get_random_dscp_mark,
|
||||
obj_fields.IPNetworkField: tools.get_random_ip_network,
|
||||
common_types.IPNetworkPrefixLenField: tools.get_random_prefixlen,
|
||||
common_types.ListOfIPNetworksField: get_list_of_random_networks,
|
||||
common_types.IPVersionEnumField: tools.get_random_ip_version,
|
||||
}
|
||||
|
||||
|
||||
|
@ -241,7 +250,8 @@ class _BaseObjectTestCase(object):
|
|||
if field not in obj_cls.synthetic_fields:
|
||||
generator = FIELD_TYPE_VALUE_GENERATOR_MAP[type(field_obj)]
|
||||
fields[field] = generator()
|
||||
return obj_cls.modify_fields_to_db(fields)
|
||||
obj = obj_cls(None, **fields)
|
||||
return obj.modify_fields_to_db(fields)
|
||||
|
||||
@classmethod
|
||||
def generate_object_keys(cls, obj_cls):
|
||||
|
@ -265,6 +275,11 @@ class _BaseObjectTestCase(object):
|
|||
|
||||
class BaseObjectIfaceTestCase(_BaseObjectTestCase, test_base.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(BaseObjectIfaceTestCase, self).setUp()
|
||||
self.model_map = collections.defaultdict(list)
|
||||
self.model_map[self._test_class.db_model] = self.db_objs
|
||||
|
||||
def test_get_object(self):
|
||||
with mock.patch.object(obj_db_api, 'get_object',
|
||||
return_value=self.db_obj) as get_object_mock:
|
||||
|
@ -303,8 +318,9 @@ class BaseObjectIfaceTestCase(_BaseObjectTestCase, test_base.BaseTestCase):
|
|||
return mock_calls
|
||||
|
||||
def test_get_objects(self):
|
||||
with mock.patch.object(obj_db_api, 'get_objects',
|
||||
return_value=self.db_objs) as get_objects_mock:
|
||||
with mock.patch.object(
|
||||
obj_db_api, 'get_objects',
|
||||
side_effect=self.fake_get_objects) as get_objects_mock:
|
||||
objs = self._test_class.get_objects(self.context)
|
||||
self._validate_objects(self.db_objs, objs)
|
||||
mock_calls = [mock.call(self.context, self._test_class.db_model)]
|
||||
|
@ -315,11 +331,11 @@ class BaseObjectIfaceTestCase(_BaseObjectTestCase, test_base.BaseTestCase):
|
|||
def test_get_objects_valid_fields(self):
|
||||
with mock.patch.object(
|
||||
obj_db_api, 'get_objects',
|
||||
return_value=[self.db_obj]) as get_objects_mock:
|
||||
side_effect=self.fake_get_objects) as get_objects_mock:
|
||||
|
||||
objs = self._test_class.get_objects(self.context,
|
||||
**self.valid_field_filter)
|
||||
self._validate_objects([self.db_obj], objs)
|
||||
self._validate_objects(self.db_objs, objs)
|
||||
|
||||
mock_calls = [mock.call(self.context, self._test_class.db_model,
|
||||
**self.valid_field_filter)]
|
||||
|
|
|
@ -81,6 +81,35 @@ class DscpMarkFieldTest(test_base.BaseTestCase, TestField):
|
|||
self.assertEqual("%s" % in_val, self.field.stringify(in_val))
|
||||
|
||||
|
||||
class IPNetworkPrefixLenFieldTest(test_base.BaseTestCase, TestField):
|
||||
def setUp(self):
|
||||
super(IPNetworkPrefixLenFieldTest, self).setUp()
|
||||
self.field = common_types.IPNetworkPrefixLenField()
|
||||
self.coerce_good_values = [(x, x) for x in (0, 32, 128, 42)]
|
||||
self.coerce_bad_values = ['len', '1', 129, -1]
|
||||
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))
|
||||
|
||||
|
||||
class IPVersionEnumFieldTest(test_base.BaseTestCase, TestField):
|
||||
def setUp(self):
|
||||
super(IPVersionEnumFieldTest, self).setUp()
|
||||
self.field = common_types.IPVersionEnumField()
|
||||
self.coerce_good_values = [(val, val)
|
||||
for val in constants.IP_ALLOWED_VERSIONS]
|
||||
self.coerce_bad_values = [5, 0, -1, 'str']
|
||||
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))
|
||||
|
||||
|
||||
class FlowDirectionEnumFieldTest(test_base.BaseTestCase, TestField):
|
||||
def setUp(self):
|
||||
super(FlowDirectionEnumFieldTest, self).setUp()
|
||||
|
|
|
@ -32,6 +32,8 @@ object_data = {
|
|||
'QosDscpMarkingRule': '1.1-0313c6554b34fd10c753cb63d638256c',
|
||||
'QosRuleType': '1.1-8a53fef4c6a43839d477a85b787d22ce',
|
||||
'QosPolicy': '1.1-721fa60ea8f0e8f15d456d6e917dfe59',
|
||||
'SubnetPool': '1.0-6e03cee0148ced4a60dd8342fed3d0be',
|
||||
'SubnetPoolPrefix': '1.0-13c15144135eb869faa4a76dc3ee3b6c',
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
# Copyright (c) 2015 OpenStack Foundation.
|
||||
# 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 itertools
|
||||
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from neutron.objects import subnetpool
|
||||
from neutron.tests.unit.objects import test_base as obj_test_base
|
||||
from neutron.tests.unit import testlib_api
|
||||
|
||||
|
||||
class SubnetPoolTestMixin(object):
|
||||
def _create_test_subnetpool(self):
|
||||
self._pool = subnetpool.SubnetPool(
|
||||
self.context,
|
||||
id=uuidutils.generate_uuid(),
|
||||
ip_version=4,
|
||||
default_prefixlen=24,
|
||||
min_prefixlen=0,
|
||||
max_prefixlen=32,
|
||||
shared=False)
|
||||
self._pool.create()
|
||||
|
||||
|
||||
class SubnetPoolIfaceObjectTestCase(obj_test_base.BaseObjectIfaceTestCase):
|
||||
|
||||
_test_class = subnetpool.SubnetPool
|
||||
|
||||
|
||||
class SubnetPoolDbObjectTestCase(obj_test_base.BaseDbObjectTestCase,
|
||||
testlib_api.SqlTestCase,
|
||||
SubnetPoolTestMixin):
|
||||
|
||||
_test_class = subnetpool.SubnetPool
|
||||
|
||||
def test_subnetpool_prefixes(self):
|
||||
self._create_test_subnetpool()
|
||||
prefixes = obj_test_base.get_list_of_random_networks()
|
||||
self._pool.prefixes = prefixes
|
||||
self._pool.update()
|
||||
|
||||
pool = self._test_class.get_object(self.context, id=self._pool.id)
|
||||
self.assertItemsEqual(prefixes, pool.prefixes)
|
||||
|
||||
prefixes.pop()
|
||||
self._pool.prefixes = prefixes
|
||||
self._pool.update()
|
||||
|
||||
pool = self._test_class.get_object(self.context, id=self._pool.id)
|
||||
self.assertItemsEqual(prefixes, pool.prefixes)
|
||||
|
||||
|
||||
class SubnetPoolPrefixIfaceObjectTestCase(
|
||||
obj_test_base.BaseObjectIfaceTestCase):
|
||||
|
||||
_test_class = subnetpool.SubnetPoolPrefix
|
||||
|
||||
|
||||
class SubnetPoolPrefixDbObjectTestCase(
|
||||
obj_test_base.BaseDbObjectTestCase,
|
||||
testlib_api.SqlTestCase,
|
||||
SubnetPoolTestMixin):
|
||||
|
||||
_test_class = subnetpool.SubnetPoolPrefix
|
||||
|
||||
def setUp(self):
|
||||
super(SubnetPoolPrefixDbObjectTestCase, self).setUp()
|
||||
self._create_test_subnetpool()
|
||||
for obj in itertools.chain(self.db_objs, self.obj_fields):
|
||||
obj['subnetpool_id'] = self._pool.id
|
Loading…
Reference in New Issue