From 8702eaa0e2f1f39b70184d98f6c14a2918b2ebf3 Mon Sep 17 00:00:00 2001 From: tonytan4ever Date: Wed, 11 Jan 2017 16:47:44 -0500 Subject: [PATCH] Support object string field filtering on "LIKE" statement This patch allows objects' get_objects method take in "LIKE" filters. e.g: objs = agent.Agent.get_objects( self.context, resource_versions=obj_utils.StringContains('obj2') ) Partially-Implements: blueprint adopt-oslo-versioned-objects-for-db Change-Id: I15a76ce20defbcb5b23a13171f93049e84383e0c Co-Authored-By: Manjeet Singh Bhatia --- doc/source/devref/objects_usage.rst | 5 +++ neutron/db/_model_query.py | 11 +++++ neutron/objects/agent.py | 9 +++- neutron/objects/db/api.py | 5 ++- neutron/objects/utils.py | 36 ++++++++++++++++ neutron/tests/unit/objects/test_base.py | 56 +++++++++++++++++++++++++ 6 files changed, 119 insertions(+), 3 deletions(-) diff --git a/doc/source/devref/objects_usage.rst b/doc/source/devref/objects_usage.rst index 4ccbd3cf869..d46bb77acf6 100644 --- a/doc/source/devref/objects_usage.rst +++ b/doc/source/devref/objects_usage.rst @@ -71,6 +71,11 @@ using example of DNSNameServer: dnses = DNSNameServer.get_objects(context) # will return list of all dns name servers from DB + # for fetching objects with substrings in a string field: + from neutron.objects import utils as obj_utils + dnses = DNSNameServer.get_objects(context, address=obj_utils.StringMatchingContains('10.0.0')) + # will return list of all dns name servers from DB that has '10.0.0' in their addresses + # to update fields: dns = DNSNameServer.get_object(context, address='asd', subnet_id='xxx') dns.order = 2 diff --git a/neutron/db/_model_query.py b/neutron/db/_model_query.py index 7c9341c0e65..719c5376cc5 100644 --- a/neutron/db/_model_query.py +++ b/neutron/db/_model_query.py @@ -24,6 +24,7 @@ from sqlalchemy.ext import associationproxy from neutron.api.v2 import attributes from neutron.common import utils from neutron.db import _utils as ndb_utils +from neutron.objects import utils as obj_utils # Classes implementing extensions will register hooks into this dictionary # for "augmenting" the "core way" of building a query for retrieving objects @@ -190,6 +191,16 @@ def apply_filters(query, model, filters, context=None): # do multiple equals matches query = query.filter( or_(*[column == v for v in value])) + elif isinstance(value, obj_utils.StringMatchingFilterObj): + if value.is_contains: + query = query.filter( + column.contains(value.contains)) + elif value.is_starts: + query = query.filter( + column.startswith(value.starts)) + elif value.is_ends: + query = query.filter( + column.endswith(value.ends)) else: query = query.filter(column.in_(value)) elif key == 'shared' and hasattr(model, 'rbac_entries'): diff --git a/neutron/objects/agent.py b/neutron/objects/agent.py index 79fe4810a15..dc66ce72fdc 100644 --- a/neutron/objects/agent.py +++ b/neutron/objects/agent.py @@ -21,6 +21,7 @@ from neutron.db.models import agent as agent_model from neutron.db.models import l3agent as rb_model from neutron.objects import base from neutron.objects import common_types +from neutron.objects import utils as obj_utils @obj_base.VersionedObjectRegistry.register @@ -50,11 +51,15 @@ class Agent(base.NeutronDbObject): @classmethod def modify_fields_to_db(cls, fields): result = super(Agent, cls).modify_fields_to_db(fields) - if 'configurations' in result: + if ('configurations' in result and + not isinstance(result['configurations'], + obj_utils.StringMatchingFilterObj)): # dump configuration into string, set '' if empty '{}' result['configurations'] = ( cls.filter_to_json_str(result['configurations'], default='')) - if 'resource_versions' in result: + if ('resource_versions' in result and + not isinstance(result['resource_versions'], + obj_utils.StringMatchingFilterObj)): # dump resource version into string, set None if empty '{}' or None result['resource_versions'] = ( cls.filter_to_json_str(result['resource_versions'])) diff --git a/neutron/objects/db/api.py b/neutron/objects/db/api.py index f37878fcd7a..f46be541c9a 100644 --- a/neutron/objects/db/api.py +++ b/neutron/objects/db/api.py @@ -17,6 +17,7 @@ from neutron_lib import exceptions as n_exc from oslo_utils import uuidutils from neutron.db import _model_query as model_query +from neutron.objects import utils as obj_utils # Common database operation implementations @@ -36,7 +37,9 @@ def count(context, model, **kwargs): def _kwargs_to_filters(**kwargs): - return {k: v if isinstance(v, list) else [v] + return {k: v if (isinstance(v, list) or + isinstance(v, obj_utils.StringMatchingFilterObj)) + else [v] for k, v in kwargs.items()} diff --git a/neutron/objects/utils.py b/neutron/objects/utils.py index 4ceb63deb2b..d3d50be891f 100644 --- a/neutron/objects/utils.py +++ b/neutron/objects/utils.py @@ -23,3 +23,39 @@ def convert_filters(**kwargs): result['project_id'] = result.pop('tenant_id') return result + + +class StringMatchingFilterObj(object): + + @property + def is_contains(self): + return bool(getattr(self, "contains")) + + @property + def is_starts(self): + return bool(getattr(self, "starts")) + + @property + def is_ends(self): + return bool(getattr(self, "ends")) + + +class StringContains(StringMatchingFilterObj): + + def __init__(self, matching_string): + super(StringContains, self).__init__() + self.contains = matching_string + + +class StringStarts(StringMatchingFilterObj): + + def __init__(self, matching_string): + super(StringStarts, self).__init__() + self.starts = matching_string + + +class StringEnds(StringMatchingFilterObj): + + def __init__(self, matching_string): + super(StringEnds, self).__init__() + self.ends = matching_string diff --git a/neutron/tests/unit/objects/test_base.py b/neutron/tests/unit/objects/test_base.py index d55b34ea3b2..7afea172742 100644 --- a/neutron/tests/unit/objects/test_base.py +++ b/neutron/tests/unit/objects/test_base.py @@ -46,6 +46,7 @@ from neutron.objects import ports from neutron.objects import rbac_db from neutron.objects import securitygroup from neutron.objects import subnet +from neutron.objects import utils as obj_utils from neutron.tests import base as test_base from neutron.tests import tools from neutron.tests.unit.db import test_db_base_plugin_v2 @@ -836,6 +837,61 @@ class BaseObjectIfaceTestCase(_BaseObjectTestCase, test_base.BaseTestCase): self.context, {'unknown_filter': 'new_value'}, validate_filters=False, unknown_filter='value') + def _prep_string_field(self): + self.filter_string_field = None + # find the first string field to use as string matching filter + for field in self.obj_fields[0]: + if isinstance(field, obj_fields.StringField): + self.filter_string_field = field + break + + if self.filter_string_field is None: + self.skipTest('There is no string field in this object') + + def test_get_objects_with_string_matching_filters_contains(self): + self._prep_string_field() + + filter_dict_contains = { + self.filter_string_field: obj_utils.StringContains( + "random_thing")} + + with mock.patch.object( + obj_db_api, 'get_objects', + side_effect=self.fake_get_objects): + res = self._test_class.get_objects(self.context, + **filter_dict_contains) + self.assertEqual([], res) + + def test_get_objects_with_string_matching_filters_starts(self): + self._prep_string_field() + + filter_dict_starts = { + self.filter_string_field: obj_utils.StringStarts( + "random_thing") + } + + with mock.patch.object( + obj_db_api, 'get_objects', + side_effect=self.fake_get_objects): + res = self._test_class.get_objects(self.context, + **filter_dict_starts) + self.assertEqual([], res) + + def test_get_objects_with_string_matching_filters_ends(self): + self._prep_string_field() + + filter_dict_ends = { + self.filter_string_field: obj_utils.StringEnds( + "random_thing") + } + + with mock.patch.object( + obj_db_api, 'get_objects', + side_effect=self.fake_get_objects): + res = self._test_class.get_objects(self.context, + **filter_dict_ends) + self.assertEqual([], res) + def test_delete_objects(self): '''Test that delete_objects calls to underlying db_api.''' with mock.patch.object(