Merge "Implement filter validation"
This commit is contained in:
commit
ca13e651c9
|
@ -62,13 +62,21 @@ def check_request_for_revision_constraint(request):
|
||||||
return revision_number
|
return revision_number
|
||||||
|
|
||||||
|
|
||||||
def get_filters(request, attr_info, skips=None):
|
def is_filter_validation_enabled():
|
||||||
|
return 'filter-validation' in (extensions.PluginAwareExtensionManager.
|
||||||
|
get_instance().extensions)
|
||||||
|
|
||||||
|
|
||||||
|
def get_filters(request, attr_info, skips=None,
|
||||||
|
is_filter_validation_supported=False):
|
||||||
return get_filters_from_dict(request.GET.dict_of_lists(),
|
return get_filters_from_dict(request.GET.dict_of_lists(),
|
||||||
attr_info,
|
attr_info,
|
||||||
skips)
|
skips,
|
||||||
|
is_filter_validation_supported)
|
||||||
|
|
||||||
|
|
||||||
def get_filters_from_dict(data, attr_info, skips=None):
|
def get_filters_from_dict(data, attr_info, skips=None,
|
||||||
|
is_filter_validation_supported=False):
|
||||||
"""Extracts the filters from a dict of query parameters.
|
"""Extracts the filters from a dict of query parameters.
|
||||||
|
|
||||||
Returns a dict of lists for the filters:
|
Returns a dict of lists for the filters:
|
||||||
|
@ -80,12 +88,19 @@ def get_filters_from_dict(data, attr_info, skips=None):
|
||||||
is_empty_string_supported = is_empty_string_filtering_supported()
|
is_empty_string_supported = is_empty_string_filtering_supported()
|
||||||
skips = skips or []
|
skips = skips or []
|
||||||
res = {}
|
res = {}
|
||||||
|
invalid_keys = []
|
||||||
|
check_is_filter = False
|
||||||
|
if is_filter_validation_supported and is_filter_validation_enabled():
|
||||||
|
check_is_filter = True
|
||||||
for key, values in data.items():
|
for key, values in data.items():
|
||||||
if key in skips or hasattr(model_base.BASEV2, key):
|
if key in skips or hasattr(model_base.BASEV2, key):
|
||||||
continue
|
continue
|
||||||
values = [v for v in values
|
values = [v for v in values
|
||||||
if v or (v == "" and is_empty_string_supported)]
|
if v or (v == "" and is_empty_string_supported)]
|
||||||
key_attr_info = attr_info.get(key, {})
|
key_attr_info = attr_info.get(key, {})
|
||||||
|
if check_is_filter and not key_attr_info.get('is_filter'):
|
||||||
|
invalid_keys.append(key)
|
||||||
|
continue
|
||||||
if 'convert_list_to' in key_attr_info:
|
if 'convert_list_to' in key_attr_info:
|
||||||
values = key_attr_info['convert_list_to'](values)
|
values = key_attr_info['convert_list_to'](values)
|
||||||
elif 'convert_to' in key_attr_info:
|
elif 'convert_to' in key_attr_info:
|
||||||
|
@ -93,6 +108,9 @@ def get_filters_from_dict(data, attr_info, skips=None):
|
||||||
values = [convert_to(v) for v in values]
|
values = [convert_to(v) for v in values]
|
||||||
if values:
|
if values:
|
||||||
res[key] = values
|
res[key] = values
|
||||||
|
if invalid_keys:
|
||||||
|
msg = _("%s is invalid attribute for filtering") % invalid_keys
|
||||||
|
raise exc.HTTPBadRequest(explanation=msg)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@ -248,6 +266,12 @@ def is_native_sorting_supported(plugin):
|
||||||
return getattr(plugin, native_sorting_attr_name, False)
|
return getattr(plugin, native_sorting_attr_name, False)
|
||||||
|
|
||||||
|
|
||||||
|
def is_filter_validation_supported(plugin):
|
||||||
|
filter_validation_attr_name = ("_%s__filter_validation_support"
|
||||||
|
% plugin.__class__.__name__)
|
||||||
|
return getattr(plugin, filter_validation_attr_name, False)
|
||||||
|
|
||||||
|
|
||||||
class PaginationHelper(object):
|
class PaginationHelper(object):
|
||||||
|
|
||||||
def __init__(self, request, primary_key='id'):
|
def __init__(self, request, primary_key='id'):
|
||||||
|
|
|
@ -105,6 +105,7 @@ class Controller(object):
|
||||||
self._native_bulk = self._is_native_bulk_supported()
|
self._native_bulk = self._is_native_bulk_supported()
|
||||||
self._native_pagination = self._is_native_pagination_supported()
|
self._native_pagination = self._is_native_pagination_supported()
|
||||||
self._native_sorting = self._is_native_sorting_supported()
|
self._native_sorting = self._is_native_sorting_supported()
|
||||||
|
self._filter_validation = self._is_filter_validation_supported()
|
||||||
self._policy_attrs = self._init_policy_attrs()
|
self._policy_attrs = self._init_policy_attrs()
|
||||||
self._notifier = n_rpc.get_notifier('network')
|
self._notifier = n_rpc.get_notifier('network')
|
||||||
self._member_actions = member_actions
|
self._member_actions = member_actions
|
||||||
|
@ -151,6 +152,9 @@ class Controller(object):
|
||||||
def _is_native_sorting_supported(self):
|
def _is_native_sorting_supported(self):
|
||||||
return api_common.is_native_sorting_supported(self._plugin)
|
return api_common.is_native_sorting_supported(self._plugin)
|
||||||
|
|
||||||
|
def _is_filter_validation_supported(self):
|
||||||
|
return api_common.is_filter_validation_supported(self._plugin)
|
||||||
|
|
||||||
def _exclude_attributes_by_policy(self, context, data):
|
def _exclude_attributes_by_policy(self, context, data):
|
||||||
"""Identifies attributes to exclude according to authZ policies.
|
"""Identifies attributes to exclude according to authZ policies.
|
||||||
|
|
||||||
|
@ -282,9 +286,11 @@ class Controller(object):
|
||||||
# plugin before returning.
|
# plugin before returning.
|
||||||
original_fields, fields_to_add = self._do_field_list(
|
original_fields, fields_to_add = self._do_field_list(
|
||||||
api_common.list_args(request, 'fields'))
|
api_common.list_args(request, 'fields'))
|
||||||
filters = api_common.get_filters(request, self._attr_info,
|
filters = api_common.get_filters(
|
||||||
['fields', 'sort_key', 'sort_dir',
|
request, self._attr_info,
|
||||||
'limit', 'marker', 'page_reverse'])
|
['fields', 'sort_key', 'sort_dir',
|
||||||
|
'limit', 'marker', 'page_reverse'],
|
||||||
|
is_filter_validation_supported=self._filter_validation)
|
||||||
kwargs = {'filters': filters,
|
kwargs = {'filters': filters,
|
||||||
'fields': original_fields}
|
'fields': original_fields}
|
||||||
sorting_helper = self._get_sorting_helper(request)
|
sorting_helper = self._get_sorting_helper(request)
|
||||||
|
|
|
@ -44,6 +44,7 @@ import six
|
||||||
|
|
||||||
import neutron
|
import neutron
|
||||||
from neutron._i18n import _
|
from neutron._i18n import _
|
||||||
|
from neutron.api import api_common
|
||||||
from neutron.common import exceptions
|
from neutron.common import exceptions
|
||||||
from neutron.db import api as db_api
|
from neutron.db import api as db_api
|
||||||
|
|
||||||
|
@ -818,3 +819,11 @@ def get_port_binding_by_status_and_host(bindings, status, host='',
|
||||||
return binding
|
return binding
|
||||||
if raise_if_not_found:
|
if raise_if_not_found:
|
||||||
raise exceptions.PortBindingNotFound(port_id=port_id, host=host)
|
raise exceptions.PortBindingNotFound(port_id=port_id, host=host)
|
||||||
|
|
||||||
|
|
||||||
|
def disable_extension_by_service_plugin(core_plugin, service_plugin):
|
||||||
|
if ('filter-validation' in core_plugin.supported_extension_aliases and
|
||||||
|
not api_common.is_filter_validation_supported(service_plugin)):
|
||||||
|
core_plugin.supported_extension_aliases.remove('filter-validation')
|
||||||
|
LOG.info('Disable filter validation extension by service plugin '
|
||||||
|
'%s.', service_plugin.__class__.__name__)
|
||||||
|
|
|
@ -117,6 +117,11 @@ core_opts = [
|
||||||
cfg.BoolOpt('vlan_transparent', default=False,
|
cfg.BoolOpt('vlan_transparent', default=False,
|
||||||
help=_('If True, then allow plugins that support it to '
|
help=_('If True, then allow plugins that support it to '
|
||||||
'create VLAN transparent networks.')),
|
'create VLAN transparent networks.')),
|
||||||
|
cfg.BoolOpt('filter_validation', default=True,
|
||||||
|
help=_('If True, then allow plugins to decide '
|
||||||
|
'whether to perform validations on filter parameters. '
|
||||||
|
'Filter validation is enabled if this config'
|
||||||
|
'is turned on and it is supported by all plugins')),
|
||||||
cfg.IntOpt('global_physnet_mtu', default=constants.DEFAULT_NETWORK_MTU,
|
cfg.IntOpt('global_physnet_mtu', default=constants.DEFAULT_NETWORK_MTU,
|
||||||
deprecated_name='segment_mtu', deprecated_group='ml2',
|
deprecated_name='segment_mtu', deprecated_group='ml2',
|
||||||
help=_('MTU of the underlying physical network. Neutron uses '
|
help=_('MTU of the underlying physical network. Neutron uses '
|
||||||
|
|
|
@ -140,6 +140,10 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
||||||
__native_bulk_support = True
|
__native_bulk_support = True
|
||||||
__native_pagination_support = True
|
__native_pagination_support = True
|
||||||
__native_sorting_support = True
|
__native_sorting_support = True
|
||||||
|
# This attribute specifies whether the plugin supports or not
|
||||||
|
# filter validations. Name mangling is used in
|
||||||
|
# order to ensure it is qualified by class
|
||||||
|
__filter_validation_support = False
|
||||||
|
|
||||||
def has_native_datastore(self):
|
def has_native_datastore(self):
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module should be deleted once neutron-lib containing
|
||||||
|
https://review.openstack.org/#/c/580190/ change is released.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
ALIAS = 'filter-validation'
|
||||||
|
IS_SHIM_EXTENSION = True
|
||||||
|
IS_STANDARD_ATTR_EXTENSION = False
|
||||||
|
NAME = 'Filter parameters validation'
|
||||||
|
DESCRIPTION = 'Provides validation on filter parameters.'
|
||||||
|
UPDATED_TIMESTAMP = '2018-03-21T10:00:00-00:00'
|
||||||
|
RESOURCE_ATTRIBUTE_MAP = {}
|
||||||
|
SUB_RESOURCE_ATTRIBUTE_MAP = {}
|
||||||
|
ACTION_MAP = {}
|
||||||
|
REQUIRED_EXTENSIONS = []
|
||||||
|
OPTIONAL_EXTENSIONS = []
|
||||||
|
ACTION_STATUS = {}
|
|
@ -46,6 +46,8 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||||
'convert_to': convert_to_mac_if_none,
|
'convert_to': convert_to_mac_if_none,
|
||||||
'validate': {'type:mac_address': None},
|
'validate': {'type:mac_address': None},
|
||||||
'enforce_policy': True,
|
'enforce_policy': True,
|
||||||
|
'is_filter': True,
|
||||||
|
'is_sort_key': True,
|
||||||
'is_visible': True},
|
'is_visible': True},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Copyright (c) 2017 Huawei Technology, Inc. All rights reserved.
|
||||||
|
#
|
||||||
|
# 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 oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from neutron.extensions import _filter_validation_lib as apidef
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _disable_extension_by_config(aliases):
|
||||||
|
if not cfg.CONF.filter_validation:
|
||||||
|
if 'filter-validation' in aliases:
|
||||||
|
aliases.remove('filter-validation')
|
||||||
|
LOG.info('Disabled filter validation extension.')
|
||||||
|
|
||||||
|
|
||||||
|
class Filter_validation(extensions.APIExtensionDescriptor):
|
||||||
|
"""Extension class supporting filter validation."""
|
||||||
|
|
||||||
|
api_definition = apidef
|
|
@ -30,6 +30,9 @@ class Network_ip_availability(api_extensions.APIExtensionDescriptor):
|
||||||
"""Returns Extended Resource for service type management."""
|
"""Returns Extended Resource for service type management."""
|
||||||
resource_attributes = apidef.RESOURCE_ATTRIBUTE_MAP[
|
resource_attributes = apidef.RESOURCE_ATTRIBUTE_MAP[
|
||||||
apidef.RESOURCE_PLURAL]
|
apidef.RESOURCE_PLURAL]
|
||||||
|
# TODO(hongbin): Delete _populate_is_filter_keyword once neutron-lib
|
||||||
|
# containing https://review.openstack.org/#/c/583838/ is released.
|
||||||
|
cls._populate_is_filter_keyword(resource_attributes)
|
||||||
controller = base.create_resource(
|
controller = base.create_resource(
|
||||||
apidef.RESOURCE_PLURAL,
|
apidef.RESOURCE_PLURAL,
|
||||||
apidef.RESOURCE_NAME,
|
apidef.RESOURCE_NAME,
|
||||||
|
@ -38,3 +41,13 @@ class Network_ip_availability(api_extensions.APIExtensionDescriptor):
|
||||||
return [extensions.ResourceExtension(apidef.COLLECTION_NAME,
|
return [extensions.ResourceExtension(apidef.COLLECTION_NAME,
|
||||||
controller,
|
controller,
|
||||||
attr_map=resource_attributes)]
|
attr_map=resource_attributes)]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _populate_is_filter_keyword(cls, params):
|
||||||
|
filter_keys = ['network_id', 'network_name', 'tenant_id',
|
||||||
|
'project_id']
|
||||||
|
for name in params:
|
||||||
|
if name in filter_keys:
|
||||||
|
params[name]['is_filter'] = True
|
||||||
|
params['ip_version'] = {'allow_post': False, 'allow_put': False,
|
||||||
|
'is_visible': False, 'is_filter': True}
|
||||||
|
|
|
@ -52,6 +52,9 @@ class Portbindings_extended(api_extensions.ExtensionDescriptor):
|
||||||
|
|
||||||
params = pbe_ext.SUB_RESOURCE_ATTRIBUTE_MAP[
|
params = pbe_ext.SUB_RESOURCE_ATTRIBUTE_MAP[
|
||||||
pbe_ext.COLLECTION_NAME]['parameters']
|
pbe_ext.COLLECTION_NAME]['parameters']
|
||||||
|
# TODO(hongbin): Delete _populate_is_filter_keyword once neutron-lib
|
||||||
|
# containing https://review.openstack.org/#/c/583437/ is released.
|
||||||
|
cls._populate_is_filter_keyword(params)
|
||||||
parent = pbe_ext.SUB_RESOURCE_ATTRIBUTE_MAP[
|
parent = pbe_ext.SUB_RESOURCE_ATTRIBUTE_MAP[
|
||||||
pbe_ext.COLLECTION_NAME]['parent']
|
pbe_ext.COLLECTION_NAME]['parent']
|
||||||
controller = base.create_resource(
|
controller = base.create_resource(
|
||||||
|
@ -75,3 +78,11 @@ class Portbindings_extended(api_extensions.ExtensionDescriptor):
|
||||||
]
|
]
|
||||||
|
|
||||||
return exts
|
return exts
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _populate_is_filter_keyword(cls, params):
|
||||||
|
filter_keys = [pbe_ext.HOST, pbe_ext.VIF_TYPE, pbe_ext.VNIC_TYPE,
|
||||||
|
pbe_ext.STATUS]
|
||||||
|
for name in params:
|
||||||
|
if name in filter_keys:
|
||||||
|
params[name]['is_filter'] = True
|
||||||
|
|
|
@ -54,22 +54,27 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||||
RESOURCE_COLLECTION: {
|
RESOURCE_COLLECTION: {
|
||||||
'id': {'allow_post': False, 'allow_put': False,
|
'id': {'allow_post': False, 'allow_put': False,
|
||||||
'validate': {'type:uuid': None},
|
'validate': {'type:uuid': None},
|
||||||
'is_visible': True, 'primary_key': True},
|
'is_visible': True, 'primary_key': True,
|
||||||
|
'is_filter': True},
|
||||||
'object_type': {'allow_post': True, 'allow_put': False,
|
'object_type': {'allow_post': True, 'allow_put': False,
|
||||||
'convert_to': convert_valid_object_type,
|
'convert_to': convert_valid_object_type,
|
||||||
'is_visible': True, 'default': None,
|
'is_visible': True, 'default': None,
|
||||||
|
'is_filter': True,
|
||||||
'enforce_policy': True},
|
'enforce_policy': True},
|
||||||
'object_id': {'allow_post': True, 'allow_put': False,
|
'object_id': {'allow_post': True, 'allow_put': False,
|
||||||
'validate': {'type:uuid': None},
|
'validate': {'type:uuid': None},
|
||||||
'is_visible': True, 'enforce_policy': True},
|
'is_visible': True, 'enforce_policy': True,
|
||||||
|
'is_filter': True},
|
||||||
'target_tenant': {'allow_post': True, 'allow_put': True,
|
'target_tenant': {'allow_post': True, 'allow_put': True,
|
||||||
'validate': {
|
'validate': {
|
||||||
'type:string': db_const.PROJECT_ID_FIELD_SIZE},
|
'type:string': db_const.PROJECT_ID_FIELD_SIZE},
|
||||||
'is_visible': True, 'enforce_policy': True},
|
'is_visible': True, 'enforce_policy': True,
|
||||||
|
'is_filter': True},
|
||||||
'tenant_id': {'allow_post': True, 'allow_put': False,
|
'tenant_id': {'allow_post': True, 'allow_put': False,
|
||||||
'validate': {
|
'validate': {
|
||||||
'type:string': db_const.PROJECT_ID_FIELD_SIZE},
|
'type:string': db_const.PROJECT_ID_FIELD_SIZE},
|
||||||
'required_by_policy': True, 'is_visible': True},
|
'required_by_policy': True, 'is_visible': True,
|
||||||
|
'is_filter': True},
|
||||||
'action': {'allow_post': True, 'allow_put': False,
|
'action': {'allow_post': True, 'allow_put': False,
|
||||||
# action depends on type so validation has to occur in
|
# action depends on type so validation has to occur in
|
||||||
# the extension
|
# the extension
|
||||||
|
@ -77,7 +82,8 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||||
'type:string': db_const.DESCRIPTION_FIELD_SIZE},
|
'type:string': db_const.DESCRIPTION_FIELD_SIZE},
|
||||||
# we set enforce_policy so operators can define policies
|
# we set enforce_policy so operators can define policies
|
||||||
# that restrict actions
|
# that restrict actions
|
||||||
'is_visible': True, 'enforce_policy': True}
|
'is_visible': True, 'enforce_policy': True,
|
||||||
|
'is_filter': True}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -221,16 +221,17 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||||
'id': {'allow_post': False, 'allow_put': False,
|
'id': {'allow_post': False, 'allow_put': False,
|
||||||
'validate': {'type:uuid': None},
|
'validate': {'type:uuid': None},
|
||||||
'is_visible': True,
|
'is_visible': True,
|
||||||
|
'is_filter': True,
|
||||||
'primary_key': True},
|
'primary_key': True},
|
||||||
'name': {'allow_post': True, 'allow_put': True,
|
'name': {'allow_post': True, 'allow_put': True,
|
||||||
'is_visible': True, 'default': '',
|
'is_visible': True, 'default': '', 'is_filter': True,
|
||||||
'validate': {
|
'validate': {
|
||||||
'type:name_not_default': db_const.NAME_FIELD_SIZE}},
|
'type:name_not_default': db_const.NAME_FIELD_SIZE}},
|
||||||
'tenant_id': {'allow_post': True, 'allow_put': False,
|
'tenant_id': {'allow_post': True, 'allow_put': False,
|
||||||
'required_by_policy': True,
|
'required_by_policy': True,
|
||||||
'validate': {
|
'validate': {
|
||||||
'type:string': db_const.PROJECT_ID_FIELD_SIZE},
|
'type:string': db_const.PROJECT_ID_FIELD_SIZE},
|
||||||
'is_visible': True},
|
'is_visible': True, 'is_filter': True},
|
||||||
SECURITYGROUPRULES: {'allow_post': False, 'allow_put': False,
|
SECURITYGROUPRULES: {'allow_post': False, 'allow_put': False,
|
||||||
'is_visible': True},
|
'is_visible': True},
|
||||||
},
|
},
|
||||||
|
@ -238,35 +239,43 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||||
'id': {'allow_post': False, 'allow_put': False,
|
'id': {'allow_post': False, 'allow_put': False,
|
||||||
'validate': {'type:uuid': None},
|
'validate': {'type:uuid': None},
|
||||||
'is_visible': True,
|
'is_visible': True,
|
||||||
|
'is_filter': True,
|
||||||
'primary_key': True},
|
'primary_key': True},
|
||||||
'security_group_id': {'allow_post': True, 'allow_put': False,
|
'security_group_id': {'allow_post': True, 'allow_put': False,
|
||||||
'is_visible': True, 'required_by_policy': True},
|
'is_visible': True, 'required_by_policy': True,
|
||||||
|
'is_filter': True},
|
||||||
'remote_group_id': {'allow_post': True, 'allow_put': False,
|
'remote_group_id': {'allow_post': True, 'allow_put': False,
|
||||||
'default': None, 'is_visible': True},
|
'default': None, 'is_visible': True,
|
||||||
|
'is_filter': True},
|
||||||
'direction': {'allow_post': True, 'allow_put': False,
|
'direction': {'allow_post': True, 'allow_put': False,
|
||||||
'is_visible': True,
|
'is_visible': True, 'is_filter': True,
|
||||||
'validate': {'type:values': ['ingress', 'egress']}},
|
'validate': {'type:values': ['ingress', 'egress']}},
|
||||||
'protocol': {'allow_post': True, 'allow_put': False,
|
'protocol': {'allow_post': True, 'allow_put': False,
|
||||||
'is_visible': True, 'default': None,
|
'is_visible': True, 'default': None,
|
||||||
|
'is_filter': True,
|
||||||
'convert_to': convert_protocol},
|
'convert_to': convert_protocol},
|
||||||
'port_range_min': {'allow_post': True, 'allow_put': False,
|
'port_range_min': {'allow_post': True, 'allow_put': False,
|
||||||
'convert_to': convert_validate_port_value,
|
'convert_to': convert_validate_port_value,
|
||||||
'default': None, 'is_visible': True},
|
'default': None, 'is_visible': True,
|
||||||
|
'is_filter': True},
|
||||||
'port_range_max': {'allow_post': True, 'allow_put': False,
|
'port_range_max': {'allow_post': True, 'allow_put': False,
|
||||||
'convert_to': convert_validate_port_value,
|
'convert_to': convert_validate_port_value,
|
||||||
'default': None, 'is_visible': True},
|
'default': None, 'is_visible': True,
|
||||||
|
'is_filter': True},
|
||||||
'ethertype': {'allow_post': True, 'allow_put': False,
|
'ethertype': {'allow_post': True, 'allow_put': False,
|
||||||
'is_visible': True, 'default': 'IPv4',
|
'is_visible': True, 'default': 'IPv4',
|
||||||
|
'is_filter': True,
|
||||||
'convert_to': convert_ethertype_to_case_insensitive,
|
'convert_to': convert_ethertype_to_case_insensitive,
|
||||||
'validate': {'type:values': sg_supported_ethertypes}},
|
'validate': {'type:values': sg_supported_ethertypes}},
|
||||||
'remote_ip_prefix': {'allow_post': True, 'allow_put': False,
|
'remote_ip_prefix': {'allow_post': True, 'allow_put': False,
|
||||||
'default': None, 'is_visible': True,
|
'default': None, 'is_visible': True,
|
||||||
|
'is_filter': True,
|
||||||
'convert_to': convert_ip_prefix_to_cidr},
|
'convert_to': convert_ip_prefix_to_cidr},
|
||||||
'tenant_id': {'allow_post': True, 'allow_put': False,
|
'tenant_id': {'allow_post': True, 'allow_put': False,
|
||||||
'required_by_policy': True,
|
'required_by_policy': True,
|
||||||
'validate': {
|
'validate': {
|
||||||
'type:string': db_const.PROJECT_ID_FIELD_SIZE},
|
'type:string': db_const.PROJECT_ID_FIELD_SIZE},
|
||||||
'is_visible': True},
|
'is_visible': True, 'is_filter': True},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,6 +284,7 @@ EXTENDED_ATTRIBUTES_2_0 = {
|
||||||
'ports': {SECURITYGROUPS: {'allow_post': True,
|
'ports': {SECURITYGROUPS: {'allow_post': True,
|
||||||
'allow_put': True,
|
'allow_put': True,
|
||||||
'is_visible': True,
|
'is_visible': True,
|
||||||
|
'is_filter': True,
|
||||||
'convert_to':
|
'convert_to':
|
||||||
converters.convert_none_to_empty_list,
|
converters.convert_none_to_empty_list,
|
||||||
'validate': {'type:uuid_list': None},
|
'validate': {'type:uuid_list': None},
|
||||||
|
|
|
@ -44,6 +44,7 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||||
'id': {'allow_post': False,
|
'id': {'allow_post': False,
|
||||||
'allow_put': False,
|
'allow_put': False,
|
||||||
'validate': {'type:uuid': None},
|
'validate': {'type:uuid': None},
|
||||||
|
'is_filter': True,
|
||||||
'is_visible': True,
|
'is_visible': True,
|
||||||
'primary_key': True},
|
'primary_key': True},
|
||||||
'tenant_id': {'allow_post': True,
|
'tenant_id': {'allow_post': True,
|
||||||
|
@ -54,17 +55,20 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||||
'network_id': {'allow_post': True,
|
'network_id': {'allow_post': True,
|
||||||
'allow_put': False,
|
'allow_put': False,
|
||||||
'validate': {'type:uuid': None},
|
'validate': {'type:uuid': None},
|
||||||
|
'is_filter': True,
|
||||||
'is_visible': True},
|
'is_visible': True},
|
||||||
PHYSICAL_NETWORK: {'allow_post': True,
|
PHYSICAL_NETWORK: {'allow_post': True,
|
||||||
'allow_put': False,
|
'allow_put': False,
|
||||||
'default': constants.ATTR_NOT_SPECIFIED,
|
'default': constants.ATTR_NOT_SPECIFIED,
|
||||||
'validate': {'type:string':
|
'validate': {'type:string':
|
||||||
providernet.PHYSICAL_NETWORK_MAX_LEN},
|
providernet.PHYSICAL_NETWORK_MAX_LEN},
|
||||||
|
'is_filter': True,
|
||||||
'is_visible': True},
|
'is_visible': True},
|
||||||
NETWORK_TYPE: {'allow_post': True,
|
NETWORK_TYPE: {'allow_post': True,
|
||||||
'allow_put': False,
|
'allow_put': False,
|
||||||
'validate': {'type:string':
|
'validate': {'type:string':
|
||||||
providernet.NETWORK_TYPE_MAX_LEN},
|
providernet.NETWORK_TYPE_MAX_LEN},
|
||||||
|
'is_filter': True,
|
||||||
'is_visible': True},
|
'is_visible': True},
|
||||||
SEGMENTATION_ID: {'allow_post': True,
|
SEGMENTATION_ID: {'allow_post': True,
|
||||||
'allow_put': False,
|
'allow_put': False,
|
||||||
|
@ -75,6 +79,7 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||||
'allow_put': True,
|
'allow_put': True,
|
||||||
'default': constants.ATTR_NOT_SPECIFIED,
|
'default': constants.ATTR_NOT_SPECIFIED,
|
||||||
'validate': {'type:string_or_none': NAME_LEN},
|
'validate': {'type:string_or_none': NAME_LEN},
|
||||||
|
'is_filter': True,
|
||||||
'is_visible': True}
|
'is_visible': True}
|
||||||
},
|
},
|
||||||
subnet_def.COLLECTION_NAME: {
|
subnet_def.COLLECTION_NAME: {
|
||||||
|
@ -82,6 +87,7 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||||
'allow_put': False,
|
'allow_put': False,
|
||||||
'default': None,
|
'default': None,
|
||||||
'validate': {'type:uuid_or_none': None},
|
'validate': {'type:uuid_or_none': None},
|
||||||
|
'is_filter': True,
|
||||||
'is_visible': True, },
|
'is_visible': True, },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ from osprofiler import profiler
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from neutron._i18n import _
|
from neutron._i18n import _
|
||||||
|
from neutron.common import utils
|
||||||
from neutron.plugins.common import constants
|
from neutron.plugins.common import constants
|
||||||
|
|
||||||
|
|
||||||
|
@ -221,6 +222,9 @@ class NeutronManager(object):
|
||||||
hasattr(plugin_inst, 'agent_notifiers')):
|
hasattr(plugin_inst, 'agent_notifiers')):
|
||||||
plugin.agent_notifiers.update(plugin_inst.agent_notifiers)
|
plugin.agent_notifiers.update(plugin_inst.agent_notifiers)
|
||||||
|
|
||||||
|
# disable incompatible extensions in core plugin if any
|
||||||
|
utils.disable_extension_by_service_plugin(plugin, plugin_inst)
|
||||||
|
|
||||||
LOG.debug("Successfully loaded %(type)s plugin. "
|
LOG.debug("Successfully loaded %(type)s plugin. "
|
||||||
"Description: %(desc)s",
|
"Description: %(desc)s",
|
||||||
{"type": plugin_type,
|
{"type": plugin_type,
|
||||||
|
|
|
@ -167,6 +167,8 @@ class NeutronPecanController(object):
|
||||||
raise exceptions.Invalid(
|
raise exceptions.Invalid(
|
||||||
_("Native pagination depends on native sorting")
|
_("Native pagination depends on native sorting")
|
||||||
)
|
)
|
||||||
|
self.filter_validation = api_common.is_filter_validation_supported(
|
||||||
|
self.plugin)
|
||||||
self.primary_key = self._get_primary_key()
|
self.primary_key = self._get_primary_key()
|
||||||
|
|
||||||
self.parent = parent_resource
|
self.parent = parent_resource
|
||||||
|
|
|
@ -77,7 +77,8 @@ def _set_filters(state, controller):
|
||||||
{k: _listify(v) for k, v in params.items()},
|
{k: _listify(v) for k, v in params.items()},
|
||||||
controller.resource_info,
|
controller.resource_info,
|
||||||
skips=['fields', 'sort_key', 'sort_dir',
|
skips=['fields', 'sort_key', 'sort_dir',
|
||||||
'limit', 'marker', 'page_reverse'])
|
'limit', 'marker', 'page_reverse'],
|
||||||
|
is_filter_validation_supported=controller.filter_validation)
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -87,6 +87,7 @@ from neutron.db import securitygroups_rpc_base as sg_db_rpc
|
||||||
from neutron.db import segments_db
|
from neutron.db import segments_db
|
||||||
from neutron.db import subnet_service_type_mixin
|
from neutron.db import subnet_service_type_mixin
|
||||||
from neutron.db import vlantransparent_db
|
from neutron.db import vlantransparent_db
|
||||||
|
from neutron.extensions import filter_validation
|
||||||
from neutron.extensions import providernet as provider
|
from neutron.extensions import providernet as provider
|
||||||
from neutron.extensions import vlantransparent
|
from neutron.extensions import vlantransparent
|
||||||
from neutron.objects import base as base_obj
|
from neutron.objects import base as base_obj
|
||||||
|
@ -148,6 +149,10 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
|
||||||
__native_bulk_support = True
|
__native_bulk_support = True
|
||||||
__native_pagination_support = True
|
__native_pagination_support = True
|
||||||
__native_sorting_support = True
|
__native_sorting_support = True
|
||||||
|
# This attribute specifies whether the plugin supports or not
|
||||||
|
# filter validations. Name mangling is used in
|
||||||
|
# order to ensure it is qualified by class
|
||||||
|
__filter_validation_support = True
|
||||||
|
|
||||||
# List of supported extensions
|
# List of supported extensions
|
||||||
_supported_extension_aliases = ["provider", "external-net", "binding",
|
_supported_extension_aliases = ["provider", "external-net", "binding",
|
||||||
|
@ -166,6 +171,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
|
||||||
"ip-substring-filtering",
|
"ip-substring-filtering",
|
||||||
"port-security-groups-filtering",
|
"port-security-groups-filtering",
|
||||||
"empty-string-filtering",
|
"empty-string-filtering",
|
||||||
|
"filter-validation",
|
||||||
"port-mac-address-regenerate",
|
"port-mac-address-regenerate",
|
||||||
"binding-extended"]
|
"binding-extended"]
|
||||||
|
|
||||||
|
@ -176,6 +182,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
|
||||||
aliases += self.extension_manager.extension_aliases()
|
aliases += self.extension_manager.extension_aliases()
|
||||||
sg_rpc.disable_security_group_extension_by_config(aliases)
|
sg_rpc.disable_security_group_extension_by_config(aliases)
|
||||||
vlantransparent._disable_extension_by_config(aliases)
|
vlantransparent._disable_extension_by_config(aliases)
|
||||||
|
filter_validation._disable_extension_by_config(aliases)
|
||||||
self._aliases = aliases
|
self._aliases = aliases
|
||||||
return self._aliases
|
return self._aliases
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,8 @@ class Plugin(db.AutoAllocatedTopologyMixin):
|
||||||
|
|
||||||
supported_extension_aliases = ["auto-allocated-topology"]
|
supported_extension_aliases = ["auto-allocated-topology"]
|
||||||
|
|
||||||
|
__filter_validation_support = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_instance(cls):
|
def get_instance(cls):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
|
|
|
@ -25,6 +25,8 @@ class FlavorsPlugin(service_base.ServicePluginBase,
|
||||||
|
|
||||||
supported_extension_aliases = ['flavors', 'service-type']
|
supported_extension_aliases = ['flavors', 'service-type']
|
||||||
|
|
||||||
|
__filter_validation_support = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_plugin_type(cls):
|
def get_plugin_type(cls):
|
||||||
return constants.FLAVORS
|
return constants.FLAVORS
|
||||||
|
|
|
@ -88,6 +88,7 @@ class L3RouterPlugin(service_base.ServicePluginBase,
|
||||||
|
|
||||||
__native_pagination_support = True
|
__native_pagination_support = True
|
||||||
__native_sorting_support = True
|
__native_sorting_support = True
|
||||||
|
__filter_validation_support = True
|
||||||
|
|
||||||
@resource_registry.tracked_resources(router=l3_models.Router,
|
@resource_registry.tracked_resources(router=l3_models.Router,
|
||||||
floatingip=l3_models.FloatingIP)
|
floatingip=l3_models.FloatingIP)
|
||||||
|
|
|
@ -31,6 +31,7 @@ class LoggingPlugin(log_ext.LoggingPluginBase):
|
||||||
|
|
||||||
__native_pagination_support = True
|
__native_pagination_support = True
|
||||||
__native_sorting_support = True
|
__native_sorting_support = True
|
||||||
|
__filter_validation_support = True
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(LoggingPlugin, self).__init__()
|
super(LoggingPlugin, self).__init__()
|
||||||
|
|
|
@ -26,6 +26,7 @@ class MeteringPlugin(metering_db.MeteringDbMixin):
|
||||||
"""Implementation of the Neutron Metering Service Plugin."""
|
"""Implementation of the Neutron Metering Service Plugin."""
|
||||||
supported_extension_aliases = [metering_apidef.ALIAS]
|
supported_extension_aliases = [metering_apidef.ALIAS]
|
||||||
path_prefix = "/metering"
|
path_prefix = "/metering"
|
||||||
|
__filter_validation_support = True
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(MeteringPlugin, self).__init__()
|
super(MeteringPlugin, self).__init__()
|
||||||
|
|
|
@ -27,6 +27,8 @@ class NetworkIPAvailabilityPlugin(ip_availability_db.IpAvailabilityMixin,
|
||||||
|
|
||||||
supported_extension_aliases = ["network-ip-availability"]
|
supported_extension_aliases = ["network-ip-availability"]
|
||||||
|
|
||||||
|
__filter_validation_support = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_instance(cls):
|
def get_instance(cls):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
|
|
|
@ -47,6 +47,7 @@ class QoSPlugin(qos.QoSPluginBase):
|
||||||
|
|
||||||
__native_pagination_support = True
|
__native_pagination_support = True
|
||||||
__native_sorting_support = True
|
__native_sorting_support = True
|
||||||
|
__filter_validation_support = True
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(QoSPlugin, self).__init__()
|
super(QoSPlugin, self).__init__()
|
||||||
|
|
|
@ -33,6 +33,8 @@ class RevisionPlugin(service_base.ServicePluginBase):
|
||||||
supported_extension_aliases = ['standard-attr-revisions',
|
supported_extension_aliases = ['standard-attr-revisions',
|
||||||
'revision-if-match']
|
'revision-if-match']
|
||||||
|
|
||||||
|
__filter_validation_support = True
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(RevisionPlugin, self).__init__()
|
super(RevisionPlugin, self).__init__()
|
||||||
db_api.sqla_listen(se.Session, 'before_flush', self.bump_revisions)
|
db_api.sqla_listen(se.Session, 'before_flush', self.bump_revisions)
|
||||||
|
|
|
@ -68,6 +68,7 @@ class Plugin(db.SegmentDbMixin, segment.SegmentPluginBase):
|
||||||
|
|
||||||
__native_pagination_support = True
|
__native_pagination_support = True
|
||||||
__native_sorting_support = True
|
__native_sorting_support = True
|
||||||
|
__filter_validation_support = True
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.nova_updater = NovaSegmentNotifier()
|
self.nova_updater = NovaSegmentNotifier()
|
||||||
|
|
|
@ -39,6 +39,8 @@ class TagPlugin(common_db_mixin.CommonDbMixin, tagging.TagPluginBase):
|
||||||
|
|
||||||
supported_extension_aliases = ['standard-attr-tag']
|
supported_extension_aliases = ['standard-attr-tag']
|
||||||
|
|
||||||
|
__filter_validation_support = True
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
inst = super(TagPlugin, cls).__new__(cls, *args, **kwargs)
|
inst = super(TagPlugin, cls).__new__(cls, *args, **kwargs)
|
||||||
inst._filter_methods = [] # prevent GC of our partial functions
|
inst._filter_methods = [] # prevent GC of our partial functions
|
||||||
|
|
|
@ -25,6 +25,8 @@ class TimeStampPlugin(service_base.ServicePluginBase,
|
||||||
|
|
||||||
supported_extension_aliases = ['standard-attr-timestamp']
|
supported_extension_aliases = ['standard-attr-timestamp']
|
||||||
|
|
||||||
|
__filter_validation_support = True
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(TimeStampPlugin, self).__init__()
|
super(TimeStampPlugin, self).__init__()
|
||||||
self.register_db_events()
|
self.register_db_events()
|
||||||
|
|
|
@ -50,6 +50,7 @@ class TrunkPlugin(service_base.ServicePluginBase,
|
||||||
|
|
||||||
__native_pagination_support = True
|
__native_pagination_support = True
|
||||||
__native_sorting_support = True
|
__native_sorting_support = True
|
||||||
|
__filter_validation_support = True
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._rpc_backend = None
|
self._rpc_backend = None
|
||||||
|
|
|
@ -17,6 +17,7 @@ NETWORK_API_EXTENSIONS+=",ext-gw-mode"
|
||||||
NETWORK_API_EXTENSIONS+=",external-net"
|
NETWORK_API_EXTENSIONS+=",external-net"
|
||||||
NETWORK_API_EXTENSIONS+=",extra_dhcp_opt"
|
NETWORK_API_EXTENSIONS+=",extra_dhcp_opt"
|
||||||
NETWORK_API_EXTENSIONS+=",extraroute"
|
NETWORK_API_EXTENSIONS+=",extraroute"
|
||||||
|
NETWORK_API_EXTENSIONS+=",filter-validation"
|
||||||
NETWORK_API_EXTENSIONS+=",fip-port-details"
|
NETWORK_API_EXTENSIONS+=",fip-port-details"
|
||||||
NETWORK_API_EXTENSIONS+=",flavors"
|
NETWORK_API_EXTENSIONS+=",flavors"
|
||||||
NETWORK_API_EXTENSIONS+=",ip-substring-filtering"
|
NETWORK_API_EXTENSIONS+=",ip-substring-filtering"
|
||||||
|
|
|
@ -86,9 +86,11 @@ class APIv2TestBase(base.BaseTestCase):
|
||||||
self._plugin_patcher = mock.patch(plugin, autospec=True)
|
self._plugin_patcher = mock.patch(plugin, autospec=True)
|
||||||
self.plugin = self._plugin_patcher.start()
|
self.plugin = self._plugin_patcher.start()
|
||||||
instance = self.plugin.return_value
|
instance = self.plugin.return_value
|
||||||
instance.supported_extension_aliases = ['empty-string-filtering']
|
instance.supported_extension_aliases = ['empty-string-filtering',
|
||||||
|
'filter-validation']
|
||||||
instance._NeutronPluginBaseV2__native_pagination_support = True
|
instance._NeutronPluginBaseV2__native_pagination_support = True
|
||||||
instance._NeutronPluginBaseV2__native_sorting_support = True
|
instance._NeutronPluginBaseV2__native_sorting_support = True
|
||||||
|
instance._NeutronPluginBaseV2__filter_validation_support = True
|
||||||
tools.make_mock_plugin_json_encodable(instance)
|
tools.make_mock_plugin_json_encodable(instance)
|
||||||
|
|
||||||
api = router.APIRouter()
|
api = router.APIRouter()
|
||||||
|
@ -1612,6 +1614,52 @@ class FiltersTestCase(base.BaseTestCase):
|
||||||
'project_id': {'key': 'val'}}
|
'project_id': {'key': 'val'}}
|
||||||
self.assertEqual(expect_attr_info, attr_info)
|
self.assertEqual(expect_attr_info, attr_info)
|
||||||
|
|
||||||
|
@mock.patch('neutron.api.api_common.is_filter_validation_enabled',
|
||||||
|
return_value=True)
|
||||||
|
def test_attr_info_with_filter_validation(self, mock_validation_enabled):
|
||||||
|
attr_info = {}
|
||||||
|
self._test_attr_info(attr_info)
|
||||||
|
|
||||||
|
attr_info = {'foo': {}}
|
||||||
|
self._test_attr_info(attr_info)
|
||||||
|
|
||||||
|
attr_info = {'foo': {'is_filter': False}}
|
||||||
|
self._test_attr_info(attr_info)
|
||||||
|
|
||||||
|
attr_info = {'foo': {'is_filter': False}, 'bar': {'is_filter': True},
|
||||||
|
'baz': {'is_filter': True}, 'qux': {'is_filter': True}}
|
||||||
|
self._test_attr_info(attr_info)
|
||||||
|
|
||||||
|
attr_info = {'foo': {'is_filter': True}, 'bar': {'is_filter': True},
|
||||||
|
'baz': {'is_filter': True}, 'qux': {'is_filter': True}}
|
||||||
|
expect_val = {'foo': ['4'], 'bar': ['3'], 'baz': ['2'], 'qux': ['1']}
|
||||||
|
self._test_attr_info(attr_info, expect_val)
|
||||||
|
|
||||||
|
attr_info = {'foo': {'is_filter': True}, 'bar': {'is_filter': True},
|
||||||
|
'baz': {'is_filter': True}, 'qux': {'is_filter': True},
|
||||||
|
'quz': {}}
|
||||||
|
expect_val = {'foo': ['4'], 'bar': ['3'], 'baz': ['2'], 'qux': ['1']}
|
||||||
|
self._test_attr_info(attr_info, expect_val)
|
||||||
|
|
||||||
|
attr_info = {'foo': {'is_filter': True}, 'bar': {'is_filter': True},
|
||||||
|
'baz': {'is_filter': True}, 'qux': {'is_filter': True},
|
||||||
|
'quz': {'is_filter': False}}
|
||||||
|
expect_val = {'foo': ['4'], 'bar': ['3'], 'baz': ['2'], 'qux': ['1']}
|
||||||
|
self._test_attr_info(attr_info, expect_val)
|
||||||
|
|
||||||
|
def _test_attr_info(self, attr_info, expect_val=None):
|
||||||
|
path = '/?foo=4&bar=3&baz=2&qux=1'
|
||||||
|
request = webob.Request.blank(path)
|
||||||
|
if expect_val:
|
||||||
|
actual_val = api_common.get_filters(
|
||||||
|
request, attr_info,
|
||||||
|
is_filter_validation_supported=True)
|
||||||
|
self.assertEqual(expect_val, actual_val)
|
||||||
|
else:
|
||||||
|
self.assertRaises(
|
||||||
|
exc.HTTPBadRequest, api_common.get_filters, request, attr_info,
|
||||||
|
is_filter_validation_supported=True)
|
||||||
|
|
||||||
def test_attr_info_without_conversion(self):
|
def test_attr_info_without_conversion(self):
|
||||||
path = '/?foo=4&bar=3&baz=2&qux=1'
|
path = '/?foo=4&bar=3&baz=2&qux=1'
|
||||||
request = webob.Request.blank(path)
|
request = webob.Request.blank(path)
|
||||||
|
|
|
@ -158,6 +158,12 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
|
||||||
|
|
||||||
self._skip_native_pagination = not _is_native_pagination_support()
|
self._skip_native_pagination = not _is_native_pagination_support()
|
||||||
|
|
||||||
|
def _is_filter_validation_support():
|
||||||
|
return 'filter-validation' in (directory.get_plugin().
|
||||||
|
supported_extension_aliases)
|
||||||
|
|
||||||
|
self._skip_filter_validation = not _is_filter_validation_support()
|
||||||
|
|
||||||
def _is_native_sorting_support():
|
def _is_native_sorting_support():
|
||||||
native_sorting_attr_name = (
|
native_sorting_attr_name = (
|
||||||
"_%s__native_sorting_support" %
|
"_%s__native_sorting_support" %
|
||||||
|
@ -560,13 +566,13 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
|
||||||
return self.deserialize(self.fmt, res)
|
return self.deserialize(self.fmt, res)
|
||||||
|
|
||||||
def _list(self, resource, fmt=None, neutron_context=None,
|
def _list(self, resource, fmt=None, neutron_context=None,
|
||||||
query_params=None):
|
query_params=None, expected_code=webob.exc.HTTPOk.code):
|
||||||
fmt = fmt or self.fmt
|
fmt = fmt or self.fmt
|
||||||
req = self.new_list_request(resource, fmt, query_params)
|
req = self.new_list_request(resource, fmt, query_params)
|
||||||
if neutron_context:
|
if neutron_context:
|
||||||
req.environ['neutron.context'] = neutron_context
|
req.environ['neutron.context'] = neutron_context
|
||||||
res = req.get_response(self._api_for_resource(resource))
|
res = req.get_response(self._api_for_resource(resource))
|
||||||
self.assertEqual(webob.exc.HTTPOk.code, res.status_int)
|
self.assertEqual(expected_code, res.status_int)
|
||||||
return self.deserialize(fmt, res)
|
return self.deserialize(fmt, res)
|
||||||
|
|
||||||
def _fail_second_call(self, patched_plugin, orig, *args, **kwargs):
|
def _fail_second_call(self, patched_plugin, orig, *args, **kwargs):
|
||||||
|
@ -595,13 +601,16 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
|
||||||
self.assertEqual(items[1]['name'], 'test_1')
|
self.assertEqual(items[1]['name'], 'test_1')
|
||||||
|
|
||||||
def _test_list_resources(self, resource, items, neutron_context=None,
|
def _test_list_resources(self, resource, items, neutron_context=None,
|
||||||
query_params=None):
|
query_params=None,
|
||||||
|
expected_code=webob.exc.HTTPOk.code):
|
||||||
res = self._list('%ss' % resource,
|
res = self._list('%ss' % resource,
|
||||||
neutron_context=neutron_context,
|
neutron_context=neutron_context,
|
||||||
query_params=query_params)
|
query_params=query_params,
|
||||||
resource = resource.replace('-', '_')
|
expected_code=expected_code)
|
||||||
self.assertItemsEqual([i['id'] for i in res['%ss' % resource]],
|
if expected_code == webob.exc.HTTPOk.code:
|
||||||
[i[resource]['id'] for i in items])
|
resource = resource.replace('-', '_')
|
||||||
|
self.assertItemsEqual([i['id'] for i in res['%ss' % resource]],
|
||||||
|
[i[resource]['id'] for i in items])
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def network(self, name='net1',
|
def network(self, name='net1',
|
||||||
|
@ -5018,6 +5027,8 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
|
||||||
query_params=query_params)
|
query_params=query_params)
|
||||||
|
|
||||||
def test_list_subnets_filtering_by_unknown_filter(self):
|
def test_list_subnets_filtering_by_unknown_filter(self):
|
||||||
|
if self._skip_filter_validation:
|
||||||
|
self.skipTest("Plugin does not support filter validation")
|
||||||
with self.network() as network:
|
with self.network() as network:
|
||||||
with self.subnet(network=network,
|
with self.subnet(network=network,
|
||||||
gateway_ip='10.0.0.1',
|
gateway_ip='10.0.0.1',
|
||||||
|
@ -5028,11 +5039,13 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
|
||||||
subnets = (v1, v2)
|
subnets = (v1, v2)
|
||||||
query_params = 'admin_state_up=True'
|
query_params = 'admin_state_up=True'
|
||||||
self._test_list_resources('subnet', subnets,
|
self._test_list_resources('subnet', subnets,
|
||||||
query_params=query_params)
|
query_params=query_params,
|
||||||
|
expected_code=webob.exc.HTTPClientError.code)
|
||||||
# test with other value to check if we have the same results
|
# test with other value to check if we have the same results
|
||||||
query_params = 'admin_state_up=False'
|
query_params = 'admin_state_up=False'
|
||||||
self._test_list_resources('subnet', subnets,
|
self._test_list_resources('subnet', subnets,
|
||||||
query_params=query_params)
|
query_params=query_params,
|
||||||
|
expected_code=webob.exc.HTTPClientError.code)
|
||||||
|
|
||||||
def test_list_subnets_with_parameter(self):
|
def test_list_subnets_with_parameter(self):
|
||||||
with self.network() as network:
|
with self.network() as network:
|
||||||
|
|
|
@ -35,6 +35,7 @@ class MultiServiceCorePlugin(object):
|
||||||
|
|
||||||
|
|
||||||
class CorePluginWithAgentNotifiers(object):
|
class CorePluginWithAgentNotifiers(object):
|
||||||
|
supported_extension_aliases = []
|
||||||
agent_notifiers = {'l3': 'l3_agent_notifier',
|
agent_notifiers = {'l3': 'l3_agent_notifier',
|
||||||
'dhcp': 'dhcp_agent_notifier'}
|
'dhcp': 'dhcp_agent_notifier'}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
prelude: >
|
||||||
|
Perform validation on filter parameters on listing resources.
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Starting from this release, neutron server will perform validation on
|
||||||
|
filter parameters on list requests. Neutron will return a 400 response
|
||||||
|
if the request contains invalid filter parameters.
|
||||||
|
The list of valid parameters is documented in the neutron API reference.
|
||||||
|
|
||||||
|
Add an API extension ``filter-validation`` to indicate this new API
|
||||||
|
behavior. This extension can be disabled by operators via a config option.
|
||||||
|
upgrade:
|
||||||
|
- |
|
||||||
|
Prior to the upgrade, if a request contains an unknown or unsupported
|
||||||
|
parameter, the server will silently ignore the invalid input.
|
||||||
|
After the upgrade, the server will return a 400 Bad Request response
|
||||||
|
instead.
|
||||||
|
|
||||||
|
API users might observe that requests that received a successful response
|
||||||
|
now receive a failure response. If they encounter such experience,
|
||||||
|
they are suggested to confirm if the API extension ``filter-validation``
|
||||||
|
is present and validate filter parameters in their requests.
|
||||||
|
|
||||||
|
Operators can disable this feature if they want to maintain
|
||||||
|
backward-compatibility. If they choose to do that, the API extension
|
||||||
|
``filter-validation`` will not be present and the API behavior is
|
||||||
|
unchanged.
|
||||||
|
other:
|
||||||
|
- |
|
||||||
|
Each plugin can decide if it wants to support filter validation by
|
||||||
|
setting ``__filter_validation_support`` to True or False. If this field is
|
||||||
|
not set, the default value is False.
|
||||||
|
Right now, the ML2 plugin and all the in-tree service plugins support
|
||||||
|
filter validation. Out-of-tree plugins will have filter validation
|
||||||
|
disabled by default but they can turn it on if they choose to.
|
||||||
|
For filter validation to be supported, the core plugin and all the
|
||||||
|
services plugins in a deployment must support it.
|
Loading…
Reference in New Issue