From 41afb64407d2c61161af4a5b5492aab09c2dfb9d Mon Sep 17 00:00:00 2001 From: Artur Korzeniewski Date: Thu, 18 Aug 2016 15:14:24 +0200 Subject: [PATCH] objects: add support for per parent type foreign keys Objects may need to be linked to different parent objects. For example, we want to expose segment objects both on networks as well as on ports (through port binding level objects). This change allows to specify a foreign key per parent object type. Note: this patch does NOT add support for multiple foreign keys for the same parent. Change-Id: Ibad4db8da08d9114ac7765e56fe5483434a567d9 Partially-Implements: blueprint adopt-oslo-versioned-objects-for-db --- neutron/objects/base.py | 13 +++- neutron/objects/network/network_segment.py | 2 +- neutron/objects/securitygroup.py | 2 +- neutron/objects/subnet.py | 8 +- neutron/objects/trunk.py | 2 +- neutron/tests/unit/objects/test_base.py | 87 ++++++++++++++++++++-- 6 files changed, 100 insertions(+), 14 deletions(-) diff --git a/neutron/objects/base.py b/neutron/objects/base.py index fac944e7f39..485243edc5b 100644 --- a/neutron/objects/base.py +++ b/neutron/objects/base.py @@ -68,6 +68,10 @@ class NeutronSyntheticFieldMultipleForeignKeys(exceptions.NeutronException): "foreign key") +class NeutronSyntheticFieldsForeignKeysNotFound(exceptions.NeutronException): + message = _("%(child)s does not define a foreign key for %(parent)s") + + def get_updatable_fields(cls, fields): fields = fields.copy() for field in cls.fields_no_update: @@ -445,6 +449,7 @@ class NeutronDbObject(NeutronObject): This method doesn't take care of loading synthetic fields that aren't stored in the DB, e.g. 'shared' in RBAC policy. """ + clsname = self.__class__.__name__ # TODO(rossella_s) Find a way to handle ObjectFields with # subclasses=True @@ -462,7 +467,11 @@ class NeutronDbObject(NeutronObject): # QosRule continue objclass = objclasses[0] - if len(objclass.foreign_keys.keys()) > 1: + foreign_keys = objclass.foreign_keys.get(clsname) + if not foreign_keys: + raise NeutronSyntheticFieldsForeignKeysNotFound( + parent=clsname, child=objclass.__name__) + if len(foreign_keys.keys()) > 1: raise NeutronSyntheticFieldMultipleForeignKeys(field=field) synthetic_field_db_name = ( @@ -482,7 +491,7 @@ class NeutronDbObject(NeutronObject): synth_objs = objclass.get_objects( self.obj_context, **{ k: getattr(self, v) - for k, v in objclass.foreign_keys.items()}) + for k, v in foreign_keys.items()}) if isinstance(self.fields[field], obj_fields.ObjectField): setattr(self, field, synth_objs[0] if synth_objs else None) else: diff --git a/neutron/objects/network/network_segment.py b/neutron/objects/network/network_segment.py index 9f8c0c6d4f4..9283a4b315d 100644 --- a/neutron/objects/network/network_segment.py +++ b/neutron/objects/network/network_segment.py @@ -34,4 +34,4 @@ class NetworkSegment(base.NeutronDbObject): 'segment_index': obj_fields.IntegerField(default=0) } - foreign_keys = {'network_id': 'id'} + foreign_keys = {'Network': {'network_id': 'id'}} diff --git a/neutron/objects/securitygroup.py b/neutron/objects/securitygroup.py index fcd17e37dff..129b36c56a7 100644 --- a/neutron/objects/securitygroup.py +++ b/neutron/objects/securitygroup.py @@ -106,7 +106,7 @@ class SecurityGroupRule(base.NeutronDbObject): 'remote_ip_prefix': obj_fields.IPNetworkField(nullable=True), } - foreign_keys = {'security_group_id': 'id'} + foreign_keys = {'SecurityGroup': {'security_group_id': 'id'}} fields_no_update = ['project_id', 'security_group_id'] diff --git a/neutron/objects/subnet.py b/neutron/objects/subnet.py index 6d17039243c..b57a2eb343f 100644 --- a/neutron/objects/subnet.py +++ b/neutron/objects/subnet.py @@ -32,7 +32,7 @@ class DNSNameServer(base.NeutronDbObject): primary_keys = ['address', 'subnet_id'] - foreign_keys = {'subnet_id': 'id'} + foreign_keys = {'Subnet': {'subnet_id': 'id'}} fields = { 'address': obj_fields.StringField(), @@ -62,7 +62,7 @@ class Route(base.NeutronDbObject): primary_keys = ['destination', 'nexthop', 'subnet_id'] - foreign_keys = {'subnet_id': 'id'} + foreign_keys = {'Subnet': {'subnet_id': 'id'}} fields = { 'subnet_id': obj_fields.UUIDField(), @@ -99,7 +99,7 @@ class IPAllocationPool(base.NeutronDbObject): db_model = models_v2.IPAllocationPool - foreign_keys = {'subnet_id': 'id'} + foreign_keys = {'Subnet': {'subnet_id': 'id'}} fields_need_translation = { 'start': 'first_ip', @@ -172,7 +172,7 @@ class Subnet(base.NeutronDbObject): synthetic_fields = ['allocation_pools', 'dns_nameservers', 'host_routes', 'shared'] - foreign_keys = {'network_id': 'id'} + foreign_keys = {'Network': {'network_id': 'id'}} fields_no_update = ['project_id'] diff --git a/neutron/objects/trunk.py b/neutron/objects/trunk.py index 96476bc6d69..e74015c8999 100644 --- a/neutron/objects/trunk.py +++ b/neutron/objects/trunk.py @@ -32,7 +32,7 @@ class SubPort(base.NeutronDbObject): db_model = models.SubPort primary_keys = ['port_id'] - foreign_keys = {'trunk_id': 'id'} + foreign_keys = {'Trunk': {'trunk_id': 'id'}} fields = { 'port_id': obj_fields.UUIDField(), diff --git a/neutron/tests/unit/objects/test_base.py b/neutron/tests/unit/objects/test_base.py index deb1f26fbca..2629ef01463 100644 --- a/neutron/tests/unit/objects/test_base.py +++ b/neutron/tests/unit/objects/test_base.py @@ -67,7 +67,32 @@ class FakeSmallNeutronObject(base.NeutronDbObject): primary_keys = ['field1'] - foreign_keys = {'field1': 'id'} + foreign_keys = { + 'FakeNeutronObjectCompositePrimaryKeyWithId': {'field1': 'id'}, + 'FakeNeutronDbObject': {'field2': 'id'}, + 'FakeNeutronObjectUniqueKey': {'field3': 'id'}, + } + + fields = { + 'field1': obj_fields.UUIDField(), + 'field2': obj_fields.UUIDField(), + 'field3': obj_fields.UUIDField(), + } + + +@obj_base.VersionedObjectRegistry.register_if(False) +class FakeSmallNeutronObjectWithMultipleParents(base.NeutronDbObject): + # Version 1.0: Initial version + VERSION = '1.0' + + db_model = ObjectFieldsModel + + primary_keys = ['field1', 'field2'] + + foreign_keys = { + 'FakeParent': {'field1': 'id'}, + 'FakeParent2': {'field2': 'id'}, + } fields = { 'field1': obj_fields.UUIDField(), @@ -75,6 +100,25 @@ class FakeSmallNeutronObject(base.NeutronDbObject): } +@obj_base.VersionedObjectRegistry.register_if(False) +class FakeParent(base.NeutronDbObject): + # Version 1.0: Initial version + VERSION = '1.0' + + db_model = ObjectFieldsModel + + primary_keys = ['field1', 'field2'] + + fields = { + 'id': obj_fields.UUIDField(), + 'children': obj_fields.ListOfObjectsField( + 'FakeSmallNeutronObjectWithMultipleParents', + nullable=True) + } + + synthetic_fields = ['children'] + + @obj_base.VersionedObjectRegistry.register_if(False) class FakeWeirdKeySmallNeutronObject(base.NeutronDbObject): # Version 1.0: Initial version @@ -84,7 +128,10 @@ class FakeWeirdKeySmallNeutronObject(base.NeutronDbObject): primary_keys = ['field1'] - foreign_keys = {'field1': 'weird_key'} + foreign_keys = { + 'FakeNeutronObjectNonStandardPrimaryKey': {'field1': 'weird_key'}, + 'FakeNeutronObjectCompositePrimaryKey': {'field2': 'weird_key'}, + } fields = { 'field1': obj_fields.UUIDField(), @@ -225,7 +272,9 @@ class FakeNeutronObjectMultipleForeignKeys(base.NeutronDbObject): db_model = ObjectFieldsModel - foreign_keys = {'field1': 'id', 'field2': 'id'} + foreign_keys = { + 'FakeNeutronObjectSyntheticField': {'field1': 'id', 'field2': 'id'}, + } fields = { 'field1': obj_fields.UUIDField(), @@ -494,12 +543,14 @@ class BaseObjectIfaceTestCase(_BaseObjectTestCase, test_base.BaseTestCase): if self._test_class.is_object_field(field): obj_class = self._get_ovo_object_class(self._test_class, field) + foreign_keys = obj_class.foreign_keys.get( + self._test_class.__name__) mock_calls.append( mock.call( self.context, obj_class.db_model, _pager=self.pager_map[obj_class.obj_name()], **{k: db_obj[v] - for k, v in obj_class.foreign_keys.items()})) + for k, v in foreign_keys.items()})) return mock_calls def test_get_objects(self): @@ -904,6 +955,31 @@ class BaseDbObjectMultipleForeignKeysTestCase(_BaseObjectTestCase, obj.load_synthetic_db_fields) +class BaseDbObjectMultipleParentsForForeignKeysTestCase( + _BaseObjectTestCase, + test_base.BaseTestCase): + + _test_class = FakeParent + + def test_load_synthetic_db_fields_with_multiple_parents(self): + child_cls = FakeSmallNeutronObjectWithMultipleParents + self.obj_registry.register(child_cls) + self.obj_registry.register(FakeParent) + obj = self._test_class(self.context, **self.obj_fields[0]) + fake_children = [ + child_cls( + self.context, **child_cls.modify_fields_from_db( + self.get_random_fields(obj_cls=child_cls)) + ) + for _ in range(5) + ] + with mock.patch.object(child_cls, 'get_objects', + return_value=fake_children) as get_objects: + obj.load_synthetic_db_fields() + get_objects.assert_called_once_with(self.context, field1=obj.id) + self.assertEqual(fake_children, obj.children) + + class BaseDbObjectTestCase(_BaseObjectTestCase, test_db_base_plugin_v2.DbOperationBoundMixin): def setUp(self): @@ -1130,7 +1206,8 @@ class BaseDbObjectTestCase(_BaseObjectTestCase, db_obj[field][0]) # make sure children point to the base object - for local_field, foreign_key in objclass.foreign_keys.items(): + foreign_keys = objclass.foreign_keys.get(obj.__class__.__name__) + for local_field, foreign_key in foreign_keys.items(): objclass_fields[local_field] = obj.get(foreign_key) synth_field_obj = objclass(self.context, **objclass_fields)