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
This commit is contained in:
Hongbin Lu 2018-02-16 23:34:54 +00:00
parent b371afd463
commit a732bbf19e
7 changed files with 80 additions and 6 deletions

View File

@ -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)

View File

@ -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 = {}

View File

@ -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

View File

@ -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):

View File

@ -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"

View File

@ -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)

View File

@ -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.