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.