Merge "Introduce ovo objects for security groups"
This commit is contained in:
commit
ca0d752d81
|
@ -132,6 +132,9 @@ VALID_ETHERTYPES = (lib_constants.IPv4, lib_constants.IPv6)
|
|||
|
||||
IP_ALLOWED_VERSIONS = [lib_constants.IP_VERSION_4, lib_constants.IP_VERSION_6]
|
||||
|
||||
PORT_RANGE_MIN = 1
|
||||
PORT_RANGE_MAX = 65535
|
||||
|
||||
# Some components communicate using private address ranges, define
|
||||
# them all here. These address ranges should not cause any issues
|
||||
# even if they overlap since they are used in disjoint namespaces,
|
||||
|
|
|
@ -248,7 +248,7 @@ class DeclarativeObject(abc.ABCMeta):
|
|||
cls.has_standard_attributes()):
|
||||
standardattributes.add_standard_attributes(cls)
|
||||
# Instantiate extra filters per class
|
||||
cls.extra_filter_names = set()
|
||||
cls.extra_filter_names = set(cls.extra_filter_names)
|
||||
|
||||
|
||||
@six.add_metaclass(DeclarativeObject)
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import itertools
|
||||
|
||||
import netaddr
|
||||
from neutron_lib import constants as n_const
|
||||
from oslo_versionedobjects import fields as obj_fields
|
||||
|
@ -62,6 +64,16 @@ class IPNetworkPrefixLenField(obj_fields.AutoTypedField):
|
|||
AUTO_TYPE = IPNetworkPrefixLen()
|
||||
|
||||
|
||||
class PortRange(RangeConstrainedInteger):
|
||||
def __init__(self, **kwargs):
|
||||
super(PortRange, self).__init__(start=constants.PORT_RANGE_MIN,
|
||||
end=constants.PORT_RANGE_MAX, **kwargs)
|
||||
|
||||
|
||||
class PortRangeField(obj_fields.AutoTypedField):
|
||||
AUTO_TYPE = PortRange()
|
||||
|
||||
|
||||
class ListOfIPNetworksField(obj_fields.AutoTypedField):
|
||||
AUTO_TYPE = obj_fields.List(obj_fields.IPNetwork())
|
||||
|
||||
|
@ -121,9 +133,21 @@ class EtherTypeEnumField(obj_fields.AutoTypedField):
|
|||
AUTO_TYPE = obj_fields.Enum(valid_values=constants.VALID_ETHERTYPES)
|
||||
|
||||
|
||||
class IpProtocolEnum(obj_fields.Enum):
|
||||
"""IP protocol number Enum"""
|
||||
def __init__(self, **kwargs):
|
||||
super(IpProtocolEnum, self).__init__(
|
||||
valid_values=list(
|
||||
itertools.chain(
|
||||
n_const.IP_PROTOCOL_MAP.keys(),
|
||||
[str(v) for v in n_const.IP_PROTOCOL_MAP.values()]
|
||||
)
|
||||
),
|
||||
**kwargs)
|
||||
|
||||
|
||||
class IpProtocolEnumField(obj_fields.AutoTypedField):
|
||||
AUTO_TYPE = obj_fields.Enum(
|
||||
valid_values=list(n_const.IP_PROTOCOL_MAP.keys()))
|
||||
AUTO_TYPE = IpProtocolEnum()
|
||||
|
||||
|
||||
class MACAddress(obj_fields.FieldType):
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
# 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_versionedobjects import base as obj_base
|
||||
from oslo_versionedobjects import fields as obj_fields
|
||||
|
||||
from neutron.common import utils
|
||||
from neutron.db import api as db_api
|
||||
from neutron.db.models import securitygroup as sg_models
|
||||
from neutron.objects import base
|
||||
from neutron.objects import common_types
|
||||
|
||||
|
||||
@obj_base.VersionedObjectRegistry.register
|
||||
class SecurityGroup(base.NeutronDbObject):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
db_model = sg_models.SecurityGroup
|
||||
|
||||
fields = {
|
||||
'id': obj_fields.UUIDField(),
|
||||
'name': obj_fields.StringField(nullable=True),
|
||||
'project_id': obj_fields.StringField(nullable=True),
|
||||
'is_default': obj_fields.BooleanField(default=False),
|
||||
'rules': obj_fields.ListOfObjectsField(
|
||||
'SecurityGroupRule', nullable=True
|
||||
),
|
||||
# NOTE(ihrachys): we don't include source_rules that is present in the
|
||||
# model until we realize it's actually needed
|
||||
}
|
||||
|
||||
fields_no_update = ['project_id', 'is_default']
|
||||
|
||||
synthetic_fields = ['is_default', 'rules']
|
||||
|
||||
extra_filter_names = {'is_default'}
|
||||
|
||||
def create(self):
|
||||
# save is_default before super() resets it to False
|
||||
is_default = self.is_default
|
||||
with db_api.autonested_transaction(self.obj_context.session):
|
||||
super(SecurityGroup, self).create()
|
||||
if is_default:
|
||||
default_group = _DefaultSecurityGroup(
|
||||
self.obj_context,
|
||||
project_id=self.project_id,
|
||||
security_group_id=self.id)
|
||||
default_group.create()
|
||||
self.is_default = True
|
||||
self.obj_reset_changes(['is_default'])
|
||||
|
||||
def from_db_object(self, *objs):
|
||||
super(SecurityGroup, self).from_db_object(*objs)
|
||||
for obj in objs:
|
||||
self._load_is_default(obj)
|
||||
|
||||
def _load_is_default(self, db_obj):
|
||||
setattr(self, 'is_default', bool(db_obj.get('default_security_group')))
|
||||
self.obj_reset_changes(['is_default'])
|
||||
|
||||
|
||||
@obj_base.VersionedObjectRegistry.register
|
||||
class _DefaultSecurityGroup(base.NeutronDbObject):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
db_model = sg_models.DefaultSecurityGroup
|
||||
|
||||
fields = {
|
||||
'project_id': obj_fields.StringField(),
|
||||
'security_group_id': obj_fields.UUIDField(),
|
||||
}
|
||||
|
||||
fields_no_update = ['security_group_id']
|
||||
|
||||
primary_keys = ['project_id']
|
||||
|
||||
|
||||
@obj_base.VersionedObjectRegistry.register
|
||||
class SecurityGroupRule(base.NeutronDbObject):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
db_model = sg_models.SecurityGroupRule
|
||||
|
||||
fields = {
|
||||
'id': obj_fields.UUIDField(),
|
||||
'project_id': obj_fields.StringField(nullable=True),
|
||||
'security_group_id': obj_fields.UUIDField(),
|
||||
'remote_group_id': obj_fields.UUIDField(nullable=True),
|
||||
'direction': common_types.FlowDirectionEnumField(nullable=True),
|
||||
'ethertype': common_types.EtherTypeEnumField(nullable=True),
|
||||
'protocol': common_types.IpProtocolEnumField(nullable=True),
|
||||
'port_range_min': common_types.PortRangeField(nullable=True),
|
||||
'port_range_max': common_types.PortRangeField(nullable=True),
|
||||
'remote_ip_prefix': obj_fields.IPNetworkField(nullable=True),
|
||||
}
|
||||
|
||||
foreign_keys = {'security_group_id': 'id'}
|
||||
|
||||
fields_no_update = ['project_id', 'security_group_id']
|
||||
|
||||
# TODO(sayalilunkad): get rid of it once we switch the db model to using
|
||||
# custom types.
|
||||
@classmethod
|
||||
def modify_fields_to_db(cls, fields):
|
||||
result = super(SecurityGroupRule, cls).modify_fields_to_db(fields)
|
||||
remote_ip_prefix = result.get('remote_ip_prefix')
|
||||
if remote_ip_prefix:
|
||||
result['remote_ip_prefix'] = cls.filter_to_str(remote_ip_prefix)
|
||||
return result
|
||||
|
||||
# TODO(sayalilunkad): get rid of it once we switch the db model to using
|
||||
# custom types.
|
||||
@classmethod
|
||||
def modify_fields_from_db(cls, db_obj):
|
||||
fields = super(SecurityGroupRule, cls).modify_fields_from_db(db_obj)
|
||||
if 'remote_ip_prefix' in fields:
|
||||
fields['remote_ip_prefix'] = (
|
||||
utils.AuthenticIPNetwork(fields['remote_ip_prefix']))
|
||||
return fields
|
|
@ -253,6 +253,10 @@ def get_random_prefixlen(version=4):
|
|||
return random.randint(0, maxlen)
|
||||
|
||||
|
||||
def get_random_port():
|
||||
return random.randint(n_const.PORT_RANGE_MIN, n_const.PORT_RANGE_MAX)
|
||||
|
||||
|
||||
def get_random_ip_version():
|
||||
return random.choice(n_const.IP_ALLOWED_VERSIONS)
|
||||
|
||||
|
@ -294,6 +298,18 @@ def get_random_ip_address(version=4):
|
|||
return ip
|
||||
|
||||
|
||||
def get_random_flow_direction():
|
||||
return random.choice(n_const.VALID_DIRECTIONS)
|
||||
|
||||
|
||||
def get_random_ether_type():
|
||||
return random.choice(n_const.VALID_ETHERTYPES)
|
||||
|
||||
|
||||
def get_random_ip_protocol():
|
||||
return random.choice(list(constants.IP_PROTOCOL_MAP.keys()))
|
||||
|
||||
|
||||
def is_bsd():
|
||||
"""Return True on BSD-based systems."""
|
||||
|
||||
|
|
|
@ -320,6 +320,10 @@ FIELD_TYPE_VALUE_GENERATOR_MAP = {
|
|||
obj_fields.IPAddressField: tools.get_random_ip_address,
|
||||
common_types.MACAddressField: tools.get_random_EUI,
|
||||
common_types.IPV6ModeEnumField: tools.get_random_ipv6_mode,
|
||||
common_types.FlowDirectionEnumField: tools.get_random_flow_direction,
|
||||
common_types.EtherTypeEnumField: tools.get_random_ether_type,
|
||||
common_types.IpProtocolEnumField: tools.get_random_ip_protocol,
|
||||
common_types.PortRangeField: tools.get_random_port,
|
||||
}
|
||||
|
||||
|
||||
|
@ -532,13 +536,17 @@ class BaseObjectIfaceTestCase(_BaseObjectTestCase, test_base.BaseTestCase):
|
|||
get_objects_mock.assert_has_calls(mock_calls)
|
||||
|
||||
def test_get_objects_mixed_fields(self):
|
||||
synthetic_fields = self._test_class.synthetic_fields
|
||||
synthetic_fields = (
|
||||
set(self._test_class.synthetic_fields) -
|
||||
self._test_class.extra_filter_names
|
||||
)
|
||||
if not synthetic_fields:
|
||||
self.skipTest('No synthetic fields found in test class %r' %
|
||||
self.skipTest('No synthetic fields that are not extra filters '
|
||||
'found in test class %r' %
|
||||
self._test_class)
|
||||
|
||||
filters = copy.copy(self.valid_field_filter)
|
||||
filters[synthetic_fields[0]] = 'xxx'
|
||||
filters[synthetic_fields.pop()] = 'xxx'
|
||||
|
||||
with mock.patch.object(obj_db_api, 'get_objects',
|
||||
return_value=self.db_objs):
|
||||
|
@ -546,17 +554,21 @@ class BaseObjectIfaceTestCase(_BaseObjectTestCase, test_base.BaseTestCase):
|
|||
self._test_class.get_objects, self.context,
|
||||
**filters)
|
||||
|
||||
def test_get_objects_synthetic_fields(self):
|
||||
synthetic_fields = self._test_class.synthetic_fields
|
||||
def test_get_objects_synthetic_fields_not_extra_filters(self):
|
||||
synthetic_fields = (
|
||||
set(self._test_class.synthetic_fields) -
|
||||
self._test_class.extra_filter_names
|
||||
)
|
||||
if not synthetic_fields:
|
||||
self.skipTest('No synthetic fields found in test class %r' %
|
||||
self.skipTest('No synthetic fields that are not extra filters '
|
||||
'found in test class %r' %
|
||||
self._test_class)
|
||||
|
||||
with mock.patch.object(obj_db_api, 'get_objects',
|
||||
side_effect=self.fake_get_objects):
|
||||
self.assertRaises(base.exceptions.InvalidInput,
|
||||
self._test_class.get_objects, self.context,
|
||||
**{synthetic_fields[0]: 'xxx'})
|
||||
**{synthetic_fields.pop(): 'xxx'})
|
||||
|
||||
def test_get_objects_invalid_fields(self):
|
||||
with mock.patch.object(obj_db_api, 'get_objects',
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
# under the License.
|
||||
|
||||
import abc
|
||||
import itertools
|
||||
import random
|
||||
|
||||
from neutron_lib import constants as const
|
||||
|
||||
|
@ -187,10 +189,31 @@ class IpProtocolEnumFieldTest(test_base.BaseTestCase, TestField):
|
|||
def setUp(self):
|
||||
super(IpProtocolEnumFieldTest, self).setUp()
|
||||
self.field = common_types.IpProtocolEnumField()
|
||||
self.coerce_good_values = [(val, val)
|
||||
for val in
|
||||
list(const.IP_PROTOCOL_MAP.keys())]
|
||||
self.coerce_bad_values = ['test', '8', 10, 'Udp']
|
||||
self.coerce_good_values = [
|
||||
(val, val)
|
||||
for val in itertools.chain(
|
||||
const.IP_PROTOCOL_MAP.keys(),
|
||||
[str(v) for v in const.IP_PROTOCOL_MAP.values()]
|
||||
)
|
||||
]
|
||||
self.coerce_bad_values = ['test', 'Udp', 256]
|
||||
try:
|
||||
# pick a random protocol number that is not in the map of supported
|
||||
# protocols
|
||||
self.coerce_bad_values.append(
|
||||
str(
|
||||
random.choice(
|
||||
list(
|
||||
set(range(256)) -
|
||||
set(const.IP_PROTOCOL_MAP.values())
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
except IndexError:
|
||||
# stay paranoid and guard against the impossible future when all
|
||||
# protocols are in the map
|
||||
pass
|
||||
self.to_primitive_values = self.coerce_good_values
|
||||
self.from_primitive_values = self.coerce_good_values
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ from neutron.tests import tools
|
|||
# NOTE: The hashes in this list should only be changed if they come with a
|
||||
# corresponding version bump in the affected objects.
|
||||
object_data = {
|
||||
'_DefaultSecurityGroup': '1.0-971520cb2e0ec06d747885a0cf78347f',
|
||||
'AddressScope': '1.0-25560799db384acfe1549634959a82b4',
|
||||
'DNSNameServer': '1.0-bf87a85327e2d812d1666ede99d9918b',
|
||||
'ExtraDhcpOpt': '1.0-632f689cbeb36328995a7aed1d0a78d3',
|
||||
|
@ -38,6 +39,8 @@ object_data = {
|
|||
'QosRuleType': '1.1-8a53fef4c6a43839d477a85b787d22ce',
|
||||
'QosPolicy': '1.1-7c5659e1c1f64395223592d3d3293e22',
|
||||
'Route': '1.0-a9883a63b416126f9e345523ec09483b',
|
||||
'SecurityGroup': '1.0-e26b90c409b31fd2e3c6fcec402ac0b9',
|
||||
'SecurityGroupRule': '1.0-e9b8dace9d48b936c62ad40fe1f339d5',
|
||||
'Subnet': '1.0-b71e720f45fff2a39759940e010be7d1',
|
||||
'SubnetPool': '1.0-e8300bfbc4762cc88a7f6205b52da2f8',
|
||||
'SubnetPoolPrefix': '1.0-13c15144135eb869faa4a76dc3ee3b6c',
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
# 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 neutron.objects import securitygroup
|
||||
from neutron.tests.unit.objects import test_base
|
||||
from neutron.tests.unit import testlib_api
|
||||
|
||||
|
||||
class SecurityGroupIfaceObjTestCase(test_base.BaseObjectIfaceTestCase):
|
||||
|
||||
_test_class = securitygroup.SecurityGroup
|
||||
|
||||
|
||||
class SecurityGroupDbObjTestCase(test_base.BaseDbObjectTestCase,
|
||||
testlib_api.SqlTestCase):
|
||||
|
||||
_test_class = securitygroup.SecurityGroup
|
||||
|
||||
def setUp(self):
|
||||
super(SecurityGroupDbObjTestCase, self).setUp()
|
||||
# TODO(ihrachys): consider refactoring base test class to set None for
|
||||
# all nullable fields
|
||||
for db_obj in self.db_objs:
|
||||
for rule in db_obj['rules']:
|
||||
# we either make it null, or create remote groups for each rule
|
||||
# generated; we picked the former here
|
||||
rule['remote_group_id'] = None
|
||||
|
||||
def test_is_default_True(self):
|
||||
fields = self.obj_fields[0].copy()
|
||||
sg_obj = self._make_object(fields)
|
||||
sg_obj.is_default = True
|
||||
sg_obj.create()
|
||||
|
||||
default_sg_obj = securitygroup._DefaultSecurityGroup.get_object(
|
||||
self.context,
|
||||
project_id=sg_obj.project_id,
|
||||
security_group_id=sg_obj.id)
|
||||
self.assertIsNotNone(default_sg_obj)
|
||||
|
||||
sg_obj = securitygroup.SecurityGroup.get_object(
|
||||
self.context,
|
||||
id=sg_obj.id,
|
||||
project_id=sg_obj.project_id
|
||||
)
|
||||
self.assertTrue(sg_obj.is_default)
|
||||
|
||||
def test_is_default_False(self):
|
||||
fields = self.obj_fields[0].copy()
|
||||
sg_obj = self._make_object(fields)
|
||||
sg_obj.is_default = False
|
||||
sg_obj.create()
|
||||
|
||||
default_sg_obj = securitygroup._DefaultSecurityGroup.get_object(
|
||||
self.context,
|
||||
project_id=sg_obj.project_id,
|
||||
security_group_id=sg_obj.id)
|
||||
self.assertIsNone(default_sg_obj)
|
||||
|
||||
sg_obj = securitygroup.SecurityGroup.get_object(
|
||||
self.context,
|
||||
id=sg_obj.id,
|
||||
project_id=sg_obj.project_id
|
||||
)
|
||||
self.assertFalse(sg_obj.is_default)
|
||||
|
||||
def test_get_object_filter_by_is_default(self):
|
||||
fields = self.obj_fields[0].copy()
|
||||
sg_obj = self._make_object(fields)
|
||||
sg_obj.is_default = True
|
||||
sg_obj.create()
|
||||
|
||||
listed_obj = securitygroup.SecurityGroup.get_object(
|
||||
self.context,
|
||||
id=sg_obj.id,
|
||||
project_id=sg_obj.project_id,
|
||||
is_default=True
|
||||
)
|
||||
self.assertIsNotNone(listed_obj)
|
||||
self.assertEqual(sg_obj, listed_obj)
|
||||
|
||||
def test_get_objects_queries_constant(self):
|
||||
# TODO(electrocucaracha) SecurityGroup is using SecurityGroupRule
|
||||
# object to reload rules, which costs extra SQL query each time
|
||||
# _load_is_default are called in get_object(s). SecurityGroup has
|
||||
# defined relationship for SecurityGroupRules, so it should be possible
|
||||
# to reuse side loaded values fo this. To be reworked in follow-up
|
||||
# patch.
|
||||
pass
|
||||
|
||||
|
||||
class DefaultSecurityGroupIfaceObjTestCase(test_base.BaseObjectIfaceTestCase):
|
||||
|
||||
_test_class = securitygroup._DefaultSecurityGroup
|
||||
|
||||
|
||||
class DefaultSecurityGroupDbObjTestCase(test_base.BaseDbObjectTestCase,
|
||||
testlib_api.SqlTestCase):
|
||||
|
||||
_test_class = securitygroup._DefaultSecurityGroup
|
||||
|
||||
def setUp(self):
|
||||
super(DefaultSecurityGroupDbObjTestCase, self).setUp()
|
||||
sg_db_obj = self.get_random_fields(securitygroup.SecurityGroup)
|
||||
sg_fields = securitygroup.SecurityGroup.modify_fields_from_db(
|
||||
sg_db_obj)
|
||||
self.sg_obj = securitygroup.SecurityGroup(
|
||||
self.context, **test_base.remove_timestamps_from_fields(sg_fields))
|
||||
self.sg_obj.create()
|
||||
for obj in itertools.chain(self.db_objs, self.obj_fields):
|
||||
obj['security_group_id'] = self.sg_obj['id']
|
||||
|
||||
|
||||
class SecurityGroupRuleIfaceObjTestCase(test_base.BaseObjectIfaceTestCase):
|
||||
|
||||
_test_class = securitygroup.SecurityGroupRule
|
||||
|
||||
|
||||
class SecurityGroupRuleDbObjTestCase(test_base.BaseDbObjectTestCase,
|
||||
testlib_api.SqlTestCase):
|
||||
|
||||
_test_class = securitygroup.SecurityGroupRule
|
||||
|
||||
def setUp(self):
|
||||
super(SecurityGroupRuleDbObjTestCase, self).setUp()
|
||||
sg_db_obj = self.get_random_fields(securitygroup.SecurityGroup)
|
||||
sg_fields = securitygroup.SecurityGroup.modify_fields_from_db(
|
||||
sg_db_obj)
|
||||
self.sg_obj = securitygroup.SecurityGroup(
|
||||
self.context, **test_base.remove_timestamps_from_fields(sg_fields))
|
||||
self.sg_obj.create()
|
||||
for obj in itertools.chain(self.db_objs, self.obj_fields):
|
||||
obj['security_group_id'] = self.sg_obj['id']
|
||||
obj['remote_group_id'] = self.sg_obj['id']
|
Loading…
Reference in New Issue