objects: support advanced criteria for get_objects
Those are needed to accommodate to API request needs without handling sorting or pagination in Python. Instead of adding four new arguments to get_objects interface, they are consolidated using a single Pager object that is passed through the _pager argument. The name uses underscore to avoid breaking objects that want to pass 'pager' filter into SQLAlchemy. Hopefully, no objects will ever have '_pager' attribute in their models. Related-Bug: #1541928 Change-Id: I7dafc4dbd80f0ac35dbc2c2f30e56e441f5b1fc0
This commit is contained in:
parent
067a5c2a47
commit
5decc850f1
@ -69,6 +69,29 @@ def get_updatable_fields(cls, fields):
|
|||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
|
||||||
|
class Pager(object):
|
||||||
|
'''
|
||||||
|
This class represents a pager object. It is consumed by get_objects to
|
||||||
|
specify sorting and pagination criteria.
|
||||||
|
'''
|
||||||
|
def __init__(self, sorts=None, limit=None, page_reverse=None, marker=None):
|
||||||
|
self.sorts = sorts
|
||||||
|
self.limit = limit
|
||||||
|
self.page_reverse = page_reverse
|
||||||
|
self.marker = marker
|
||||||
|
|
||||||
|
def to_kwargs(self, context, model):
|
||||||
|
res = {
|
||||||
|
attr: getattr(self, attr)
|
||||||
|
for attr in ('sorts', 'limit', 'page_reverse')
|
||||||
|
if getattr(self, attr) is not None
|
||||||
|
}
|
||||||
|
if self.marker and self.limit:
|
||||||
|
res['marker_obj'] = obj_db_api.get_object(
|
||||||
|
context, model, id=self.marker)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class NeutronObject(obj_base.VersionedObject,
|
class NeutronObject(obj_base.VersionedObject,
|
||||||
obj_base.VersionedObjectDictCompat,
|
obj_base.VersionedObjectDictCompat,
|
||||||
@ -126,7 +149,7 @@ class NeutronObject(obj_base.VersionedObject,
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def get_objects(cls, context, **kwargs):
|
def get_objects(cls, context, _pager=None, **kwargs):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def create(self):
|
def create(self):
|
||||||
@ -241,11 +264,10 @@ class NeutronDbObject(NeutronObject):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_object(cls, context, **kwargs):
|
def get_object(cls, context, **kwargs):
|
||||||
"""
|
"""
|
||||||
This method fetches object from DB and convert it to versioned
|
Fetch object from DB and convert it to a versioned object.
|
||||||
object.
|
|
||||||
|
|
||||||
:param context:
|
:param context:
|
||||||
:param kwargs: multiple primary keys defined key=value pairs
|
:param kwargs: multiple keys defined by key=value pairs
|
||||||
:return: single object of NeutronDbObject class
|
:return: single object of NeutronDbObject class
|
||||||
"""
|
"""
|
||||||
missing_keys = set(cls.primary_keys).difference(kwargs.keys())
|
missing_keys = set(cls.primary_keys).difference(kwargs.keys())
|
||||||
@ -259,10 +281,20 @@ class NeutronDbObject(NeutronObject):
|
|||||||
return cls._load_object(context, db_obj)
|
return cls._load_object(context, db_obj)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_objects(cls, context, **kwargs):
|
def get_objects(cls, context, _pager=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Fetch objects from DB and convert them to versioned objects.
|
||||||
|
|
||||||
|
:param context:
|
||||||
|
:param _pager: a Pager object representing advanced sorting/pagination
|
||||||
|
criteria
|
||||||
|
:param kwargs: multiple keys defined by key=value pairs
|
||||||
|
:return: list of objects of NeutronDbObject class
|
||||||
|
"""
|
||||||
cls.validate_filters(**kwargs)
|
cls.validate_filters(**kwargs)
|
||||||
with db_api.autonested_transaction(context.session):
|
with db_api.autonested_transaction(context.session):
|
||||||
db_objs = obj_db_api.get_objects(context, cls.db_model, **kwargs)
|
db_objs = obj_db_api.get_objects(
|
||||||
|
context, cls.db_model, _pager=_pager, **kwargs)
|
||||||
return [cls._load_object(context, db_obj) for db_obj in db_objs]
|
return [cls._load_object(context, db_obj) for db_obj in db_objs]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -14,6 +14,7 @@ from neutron_lib import exceptions as n_exc
|
|||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
from neutron.db import common_db_mixin
|
from neutron.db import common_db_mixin
|
||||||
|
from neutron import manager
|
||||||
|
|
||||||
|
|
||||||
# Common database operation implementations
|
# Common database operation implementations
|
||||||
@ -24,11 +25,22 @@ def get_object(context, model, **kwargs):
|
|||||||
.first())
|
.first())
|
||||||
|
|
||||||
|
|
||||||
def get_objects(context, model, **kwargs):
|
def _kwargs_to_filters(**kwargs):
|
||||||
|
return {k: [v]
|
||||||
|
for k, v in kwargs.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def get_objects(context, model, _pager=None, **kwargs):
|
||||||
with context.session.begin(subtransactions=True):
|
with context.session.begin(subtransactions=True):
|
||||||
return (common_db_mixin.model_query(context, model)
|
filters = _kwargs_to_filters(**kwargs)
|
||||||
.filter_by(**kwargs)
|
# TODO(ihrachys): decompose _get_collection from plugin instance
|
||||||
.all())
|
plugin = manager.NeutronManager.get_plugin()
|
||||||
|
return plugin._get_collection(
|
||||||
|
context, model,
|
||||||
|
# TODO(ihrachys): avoid this no-op call per model found
|
||||||
|
lambda obj, fields: obj,
|
||||||
|
filters=filters,
|
||||||
|
**(_pager.to_kwargs(context, model) if _pager else {}))
|
||||||
|
|
||||||
|
|
||||||
def create_object(context, model, values):
|
def create_object(context, model, values):
|
||||||
|
0
neutron/tests/unit/objects/db/__init__.py
Normal file
0
neutron/tests/unit/objects/db/__init__.py
Normal file
49
neutron/tests/unit/objects/db/test_api.py
Normal file
49
neutron/tests/unit/objects/db/test_api.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# 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 mock
|
||||||
|
|
||||||
|
from neutron import context
|
||||||
|
from neutron import manager
|
||||||
|
from neutron.objects import base
|
||||||
|
from neutron.objects.db import api
|
||||||
|
from neutron.tests import base as test_base
|
||||||
|
|
||||||
|
|
||||||
|
PLUGIN_NAME = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2'
|
||||||
|
|
||||||
|
|
||||||
|
class GetObjectsTestCase(test_base.BaseTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(GetObjectsTestCase, self).setUp()
|
||||||
|
# TODO(ihrachys): revisit plugin setup once we decouple
|
||||||
|
# objects.db.objects.api from core plugin instance
|
||||||
|
self.setup_coreplugin(PLUGIN_NAME)
|
||||||
|
|
||||||
|
def test_get_objects_pass_marker_obj_when_limit_and_marker_passed(self):
|
||||||
|
ctxt = context.get_admin_context()
|
||||||
|
model = mock.sentinel.model
|
||||||
|
marker = mock.sentinel.marker
|
||||||
|
limit = mock.sentinel.limit
|
||||||
|
pager = base.Pager(marker=marker, limit=limit)
|
||||||
|
|
||||||
|
plugin = manager.NeutronManager.get_plugin()
|
||||||
|
with mock.patch.object(plugin, '_get_collection') as get_collection:
|
||||||
|
with mock.patch.object(api, 'get_object') as get_object:
|
||||||
|
api.get_objects(ctxt, model, _pager=pager)
|
||||||
|
get_object.assert_called_with(ctxt, model, id=marker)
|
||||||
|
get_collection.assert_called_with(
|
||||||
|
ctxt, model, mock.ANY,
|
||||||
|
filters={},
|
||||||
|
limit=limit,
|
||||||
|
marker_obj=get_object.return_value)
|
@ -65,7 +65,7 @@ class QosPolicyObjectTestCase(test_base.BaseObjectIfaceTestCase):
|
|||||||
objs = self._test_class.get_objects(self.context)
|
objs = self._test_class.get_objects(self.context)
|
||||||
context_mock.assert_called_once_with()
|
context_mock.assert_called_once_with()
|
||||||
self.get_objects.assert_any_call(
|
self.get_objects.assert_any_call(
|
||||||
admin_context, self._test_class.db_model)
|
admin_context, self._test_class.db_model, _pager=None)
|
||||||
self._validate_objects(self.db_objs, objs)
|
self._validate_objects(self.db_objs, objs)
|
||||||
|
|
||||||
def test_get_objects_valid_fields(self):
|
def test_get_objects_valid_fields(self):
|
||||||
@ -85,7 +85,7 @@ class QosPolicyObjectTestCase(test_base.BaseObjectIfaceTestCase):
|
|||||||
**self.valid_field_filter)
|
**self.valid_field_filter)
|
||||||
context_mock.assert_called_once_with()
|
context_mock.assert_called_once_with()
|
||||||
get_objects_mock.assert_any_call(
|
get_objects_mock.assert_any_call(
|
||||||
admin_context, self._test_class.db_model,
|
admin_context, self._test_class.db_model, _pager=None,
|
||||||
**self.valid_field_filter)
|
**self.valid_field_filter)
|
||||||
self._validate_objects([self.db_obj], objs)
|
self._validate_objects([self.db_obj], objs)
|
||||||
|
|
||||||
|
@ -364,7 +364,7 @@ class BaseObjectIfaceTestCase(_BaseObjectTestCase, test_base.BaseTestCase):
|
|||||||
field].objname)[0]
|
field].objname)[0]
|
||||||
mock_calls.append(
|
mock_calls.append(
|
||||||
mock.call(
|
mock.call(
|
||||||
self.context, obj_class.db_model,
|
self.context, obj_class.db_model, _pager=None,
|
||||||
**{k: db_obj[v]
|
**{k: db_obj[v]
|
||||||
for k, v in obj_class.foreign_keys.items()}))
|
for k, v in obj_class.foreign_keys.items()}))
|
||||||
return mock_calls
|
return mock_calls
|
||||||
@ -375,7 +375,9 @@ class BaseObjectIfaceTestCase(_BaseObjectTestCase, test_base.BaseTestCase):
|
|||||||
side_effect=self.fake_get_objects) as get_objects_mock:
|
side_effect=self.fake_get_objects) as get_objects_mock:
|
||||||
objs = self._test_class.get_objects(self.context)
|
objs = self._test_class.get_objects(self.context)
|
||||||
self._validate_objects(self.db_objs, objs)
|
self._validate_objects(self.db_objs, objs)
|
||||||
mock_calls = [mock.call(self.context, self._test_class.db_model)]
|
mock_calls = [
|
||||||
|
mock.call(self.context, self._test_class.db_model, _pager=None)
|
||||||
|
]
|
||||||
mock_calls.extend(self._get_synthetic_fields_get_objects_calls(
|
mock_calls.extend(self._get_synthetic_fields_get_objects_calls(
|
||||||
self.db_objs))
|
self.db_objs))
|
||||||
get_objects_mock.assert_has_calls(mock_calls)
|
get_objects_mock.assert_has_calls(mock_calls)
|
||||||
@ -389,8 +391,10 @@ class BaseObjectIfaceTestCase(_BaseObjectTestCase, test_base.BaseTestCase):
|
|||||||
**self.valid_field_filter)
|
**self.valid_field_filter)
|
||||||
self._validate_objects(self.db_objs, objs)
|
self._validate_objects(self.db_objs, objs)
|
||||||
|
|
||||||
mock_calls = [mock.call(self.context, self._test_class.db_model,
|
mock_calls = [
|
||||||
**self.valid_field_filter)]
|
mock.call(self.context, self._test_class.db_model, _pager=None,
|
||||||
|
**self.valid_field_filter)
|
||||||
|
]
|
||||||
mock_calls.extend(self._get_synthetic_fields_get_objects_calls(
|
mock_calls.extend(self._get_synthetic_fields_get_objects_calls(
|
||||||
[self.db_obj]))
|
[self.db_obj]))
|
||||||
get_objects_mock.assert_has_calls(mock_calls)
|
get_objects_mock.assert_has_calls(mock_calls)
|
||||||
@ -601,6 +605,13 @@ class BaseObjectIfaceTestCase(_BaseObjectTestCase, test_base.BaseTestCase):
|
|||||||
dict_ = obj.to_dict()
|
dict_ = obj.to_dict()
|
||||||
self.assertEqual(child_dict, dict_[field])
|
self.assertEqual(child_dict, dict_[field])
|
||||||
|
|
||||||
|
def test_get_objects_pager_is_passed_through(self):
|
||||||
|
with mock.patch.object(obj_db_api, 'get_objects') as get_objects:
|
||||||
|
pager = base.Pager()
|
||||||
|
self._test_class.get_objects(self.context, _pager=pager)
|
||||||
|
get_objects.assert_called_once_with(
|
||||||
|
mock.ANY, self._test_class.db_model, _pager=pager)
|
||||||
|
|
||||||
|
|
||||||
class BaseDbObjectNonStandardPrimaryKeyTestCase(BaseObjectIfaceTestCase):
|
class BaseDbObjectNonStandardPrimaryKeyTestCase(BaseObjectIfaceTestCase):
|
||||||
|
|
||||||
@ -635,6 +646,14 @@ class BaseDbObjectMultipleForeignKeysTestCase(_BaseObjectTestCase,
|
|||||||
|
|
||||||
class BaseDbObjectTestCase(_BaseObjectTestCase):
|
class BaseDbObjectTestCase(_BaseObjectTestCase):
|
||||||
|
|
||||||
|
CORE_PLUGIN = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(BaseDbObjectTestCase, self).setUp()
|
||||||
|
# TODO(ihrachys): revisit plugin setup once we decouple
|
||||||
|
# neutron.objects.db.api from core plugin instance
|
||||||
|
self.setup_coreplugin(self.CORE_PLUGIN)
|
||||||
|
|
||||||
def _create_test_network(self):
|
def _create_test_network(self):
|
||||||
# TODO(ihrachys): replace with network.create() once we get an object
|
# TODO(ihrachys): replace with network.create() once we get an object
|
||||||
# implementation for networks
|
# implementation for networks
|
||||||
|
@ -2031,8 +2031,12 @@ class TestML2PluggableIPAM(test_ipam.UseIpamMixin, TestMl2SubnetsV2):
|
|||||||
|
|
||||||
|
|
||||||
class TestMl2PluginCreateUpdateDeletePort(base.BaseTestCase):
|
class TestMl2PluginCreateUpdateDeletePort(base.BaseTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestMl2PluginCreateUpdateDeletePort, self).setUp()
|
super(TestMl2PluginCreateUpdateDeletePort, self).setUp()
|
||||||
|
# TODO(ihrachys): revisit plugin setup once we decouple
|
||||||
|
# neutron.objects.db.api from core plugin instance
|
||||||
|
self.setup_coreplugin(PLUGIN_NAME)
|
||||||
self.context = mock.MagicMock()
|
self.context = mock.MagicMock()
|
||||||
self.notify_p = mock.patch('neutron.callbacks.registry.notify')
|
self.notify_p = mock.patch('neutron.callbacks.registry.notify')
|
||||||
self.notify = self.notify_p.start()
|
self.notify = self.notify_p.start()
|
||||||
|
Loading…
Reference in New Issue
Block a user