From a732bbf19e31f6bab8d1ffd2540f6e367caab4c8 Mon Sep 17 00:00:00 2001 From: Hongbin Lu Date: Fri, 16 Feb 2018 23:34:54 +0000 Subject: [PATCH] Support filter attribute with empty string This will enable users to filter list of results with attributes with empty value. For example, the request below will list all unbound ports (unbound ports have blank device_id). GET "/ports?device_id=" APIImpact Change-Id: I9001214de126eb888c2425b6a6275f59ec8478e7 Closes-Bug: #1749304 --- neutron/api/api_common.py | 9 +++++- .../extensions/_empty_string_filtering_lib.py | 30 +++++++++++++++++++ neutron/extensions/empty_string_filtering.py | 19 ++++++++++++ neutron/plugins/ml2/plugin.py | 3 +- .../tests/contrib/hooks/api_all_extensions | 1 + neutron/tests/unit/api/v2/test_base.py | 19 +++++++++--- ...pty-string-filtering-4a39096b62b9abf2.yaml | 5 ++++ 7 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 neutron/extensions/_empty_string_filtering_lib.py create mode 100644 neutron/extensions/empty_string_filtering.py create mode 100644 releasenotes/notes/support-empty-string-filtering-4a39096b62b9abf2.yaml diff --git a/neutron/api/api_common.py b/neutron/api/api_common.py index aac92bf368c..7825bb4363c 100644 --- a/neutron/api/api_common.py +++ b/neutron/api/api_common.py @@ -75,12 +75,14 @@ def get_filters_from_dict(data, attr_info, skips=None): becomes: {'check': [u'a', u'b'], 'name': [u'Bob']} """ + is_empty_string_supported = is_empty_string_filtering_supported() skips = skips or [] res = {} for key, values in data.items(): if key in skips or hasattr(model_base.BASEV2, key): continue - values = [v for v in values if v] + values = [v for v in values + if v or (v == "" and is_empty_string_supported)] key_attr_info = attr_info.get(key, {}) if 'convert_list_to' in key_attr_info: values = key_attr_info['convert_list_to'](values) @@ -92,6 +94,11 @@ def get_filters_from_dict(data, attr_info, skips=None): return res +def is_empty_string_filtering_supported(): + return 'empty-string-filtering' in (extensions.PluginAwareExtensionManager. + get_instance().extensions) + + def get_previous_link(request, items, id_key): params = request.GET.copy() params.pop('marker', None) diff --git a/neutron/extensions/_empty_string_filtering_lib.py b/neutron/extensions/_empty_string_filtering_lib.py new file mode 100644 index 00000000000..c00ecc860a5 --- /dev/null +++ b/neutron/extensions/_empty_string_filtering_lib.py @@ -0,0 +1,30 @@ +# 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. + +""" +TODO(hongbin): This module should be deleted once neutron-lib containing +https://review.openstack.org/#/c/565342/ change is released. +""" + + +ALIAS = 'empty-string-filtering' +IS_SHIM_EXTENSION = True +IS_STANDARD_ATTR_EXTENSION = False +NAME = 'Empty String Filtering Extension' +DESCRIPTION = 'Allow filtering by attributes with empty string value' +UPDATED_TIMESTAMP = '2018-04-09T10:00:00-00:00' +RESOURCE_ATTRIBUTE_MAP = {} +SUB_RESOURCE_ATTRIBUTE_MAP = {} +ACTION_MAP = {} +REQUIRED_EXTENSIONS = [] +OPTIONAL_EXTENSIONS = [] +ACTION_STATUS = {} diff --git a/neutron/extensions/empty_string_filtering.py b/neutron/extensions/empty_string_filtering.py new file mode 100644 index 00000000000..bd69ffde7b7 --- /dev/null +++ b/neutron/extensions/empty_string_filtering.py @@ -0,0 +1,19 @@ +# 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 neutron_lib.api import extensions + +from neutron.extensions import _empty_string_filtering_lib as apidef + + +class Empty_string_filtering(extensions.APIExtensionDescriptor): + api_definition = apidef diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index bd1953f8b19..b15430d727a 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -158,7 +158,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, "default-subnetpools", "subnet-service-types", "ip-substring-filtering", - "port-security-groups-filtering"] + "port-security-groups-filtering", + "empty-string-filtering"] @property def supported_extension_aliases(self): diff --git a/neutron/tests/contrib/hooks/api_all_extensions b/neutron/tests/contrib/hooks/api_all_extensions index 473027e91ff..0045964345d 100644 --- a/neutron/tests/contrib/hooks/api_all_extensions +++ b/neutron/tests/contrib/hooks/api_all_extensions @@ -10,6 +10,7 @@ NETWORK_API_EXTENSIONS+=",default-subnetpools" NETWORK_API_EXTENSIONS+=",dhcp_agent_scheduler" NETWORK_API_EXTENSIONS+=",dns-integration" NETWORK_API_EXTENSIONS+=",dvr" +NETWORK_API_EXTENSIONS+=",empty-string-filtering" NETWORK_API_EXTENSIONS+=",ext-gw-mode" NETWORK_API_EXTENSIONS+=",external-net" NETWORK_API_EXTENSIONS+=",extra_dhcp_opt" diff --git a/neutron/tests/unit/api/v2/test_base.py b/neutron/tests/unit/api/v2/test_base.py index 42e35b74713..616093242e5 100644 --- a/neutron/tests/unit/api/v2/test_base.py +++ b/neutron/tests/unit/api/v2/test_base.py @@ -86,6 +86,7 @@ class APIv2TestBase(base.BaseTestCase): self._plugin_patcher = mock.patch(plugin, autospec=True) self.plugin = self._plugin_patcher.start() instance = self.plugin.return_value + instance.supported_extension_aliases = ['empty-string-filtering'] instance._NeutronPluginBaseV2__native_pagination_support = True instance._NeutronPluginBaseV2__native_sorting_support = True tools.make_mock_plugin_json_encodable(instance) @@ -202,7 +203,7 @@ class APIv2TestCase(APIv2TestBase): instance.get_networks.return_value = [] self.api.get(_get_path('networks'), {'name': ''}) - filters = {} + filters = {'name': ['']} kwargs = self._get_collection_kwargs(filters=filters) instance.get_networks.assert_called_once_with(mock.ANY, **kwargs) @@ -211,7 +212,7 @@ class APIv2TestCase(APIv2TestBase): instance.get_networks.return_value = [] self.api.get(_get_path('networks'), {'name': ['', '']}) - filters = {} + filters = {'name': ['', '']} kwargs = self._get_collection_kwargs(filters=filters) instance.get_networks.assert_called_once_with(mock.ANY, **kwargs) @@ -220,7 +221,7 @@ class APIv2TestCase(APIv2TestBase): instance.get_networks.return_value = [] self.api.get(_get_path('networks'), {'name': ['bar', '']}) - filters = {'name': ['bar']} + filters = {'name': ['bar', '']} kwargs = self._get_collection_kwargs(filters=filters) instance.get_networks.assert_called_once_with(mock.ANY, **kwargs) @@ -1552,11 +1553,21 @@ class FiltersTestCase(base.BaseTestCase): self.assertEqual({}, api_common.get_filters(request, None, ["fields"])) - def test_blank_values(self): + @mock.patch('neutron.api.api_common.is_empty_string_filtering_supported', + return_value=False) + def test_blank_values(self, mock_is_supported): path = '/?foo=&bar=&baz=&qux=' request = webob.Request.blank(path) self.assertEqual({}, api_common.get_filters(request, {})) + @mock.patch('neutron.api.api_common.is_empty_string_filtering_supported', + return_value=True) + def test_blank_values_with_filtering_supported(self, mock_is_supported): + path = '/?foo=&bar=&baz=&qux=' + request = webob.Request.blank(path) + self.assertEqual({'foo': [''], 'bar': [''], 'baz': [''], 'qux': ['']}, + api_common.get_filters(request, {})) + def test_no_attr_info(self): path = '/?foo=4&bar=3&baz=2&qux=1' request = webob.Request.blank(path) diff --git a/releasenotes/notes/support-empty-string-filtering-4a39096b62b9abf2.yaml b/releasenotes/notes/support-empty-string-filtering-4a39096b62b9abf2.yaml new file mode 100644 index 00000000000..5109b13159f --- /dev/null +++ b/releasenotes/notes/support-empty-string-filtering-4a39096b62b9abf2.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for filtering attributes with value as empty string. A shim + extension is added to indicate if this feature is supported.