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:
Victor Morales 2016-03-04 08:31:48 -06:00 committed by Ihar Hrachyshka
parent bb99b8c648
commit 408173360f
8 changed files with 385 additions and 5 deletions

View File

@ -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.

View File

@ -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__(

View File

@ -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

View File

@ -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."""

View File

@ -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)]

View File

@ -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()

View File

@ -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',
}

View File

@ -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