Added API extensions to detect sorting/pagination features
Those features are available only when allow_sorting and allow_pagination options are enabled (the current default is False). They don't depend on plugin support, because when plugins don't implement them natively, emulated mode is applied by API router itself. So to make it plugin agnostic, we introduce a way to register custom per-extension checks to override support detection for cases like that one. Now that we have a way to detect support for those features via API, there is little reason to keep tempest configuration options to enable those features. Instead, just inspect [network-feature-enabled] api_extensions option in tempest.conf. Now that DEFAULT_ALLOW_SORTING/DEFAULT_ALLOW_PAGINATION constants are used in a single place only (in allow_sorting/allow_pagination definitions), removed them and replaced with a literal. Added first in-tree API tests for /extensions entry point. DocImpact Update API documentation to cover new extensions. APIImpact Document the new extensions. Related-Bug: #1566514 Change-Id: I0aaaa037a8ad52060a68dd75c0a1accc6add238e
This commit is contained in:
parent
7cff2287bb
commit
5e0878f476
@ -1,15 +0,0 @@
|
||||
# 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.
|
||||
|
||||
# Default values for advanced API features
|
||||
DEFAULT_ALLOW_SORTING = False
|
||||
DEFAULT_ALLOW_PAGINATION = False
|
@ -38,6 +38,27 @@ from neutron import wsgi
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
EXTENSION_SUPPORTED_CHECK_MAP = {}
|
||||
_PLUGIN_AGNOSTIC_EXTENSIONS = set()
|
||||
|
||||
|
||||
def register_custom_supported_check(alias, f, plugin_agnostic=False):
|
||||
'''Register a custom function to determine if extension is supported.
|
||||
|
||||
Consequent calls for the same alias replace the registered function.
|
||||
|
||||
:param alias: API extension alias name
|
||||
:param f: custom check function that returns True if extension is supported
|
||||
:param plugin_agnostic: if False, don't require a plugin to claim support
|
||||
with supported_extension_aliases. If True, a plugin must claim the
|
||||
extension is supported.
|
||||
'''
|
||||
|
||||
EXTENSION_SUPPORTED_CHECK_MAP[alias] = f
|
||||
if plugin_agnostic:
|
||||
_PLUGIN_AGNOSTIC_EXTENSIONS.add(alias)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class PluginInterface(object):
|
||||
|
||||
@ -598,8 +619,14 @@ class PluginAwareExtensionManager(ExtensionManager):
|
||||
"""Check if an extension is supported by any plugin."""
|
||||
extension_is_valid = super(PluginAwareExtensionManager,
|
||||
self)._check_extension(extension)
|
||||
return (extension_is_valid and
|
||||
self._plugins_support(extension) and
|
||||
if not extension_is_valid:
|
||||
return False
|
||||
|
||||
alias = extension.get_alias()
|
||||
if alias in EXTENSION_SUPPORTED_CHECK_MAP:
|
||||
return EXTENSION_SUPPORTED_CHECK_MAP[alias]()
|
||||
|
||||
return (self._plugins_support(extension) and
|
||||
self._plugins_implement_interface(extension))
|
||||
|
||||
def _plugins_support(self, extension):
|
||||
@ -650,6 +677,11 @@ class PluginAwareExtensionManager(ExtensionManager):
|
||||
aliases = set()
|
||||
for plugin in self.plugins.values():
|
||||
aliases |= self.get_plugin_supported_extension_aliases(plugin)
|
||||
aliases |= {
|
||||
alias
|
||||
for alias, func in EXTENSION_SUPPORTED_CHECK_MAP.items()
|
||||
if func()
|
||||
}
|
||||
return aliases
|
||||
|
||||
@classmethod
|
||||
@ -660,6 +692,7 @@ class PluginAwareExtensionManager(ExtensionManager):
|
||||
"""Check if an extension supported by a plugin has been loaded."""
|
||||
plugin_extensions = self.get_supported_extension_aliases()
|
||||
missing_aliases = plugin_extensions - set(self.extensions)
|
||||
missing_aliases -= _PLUGIN_AGNOSTIC_EXTENSIONS
|
||||
if missing_aliases:
|
||||
raise exceptions.ExtensionsNotFound(
|
||||
extensions=list(missing_aliases))
|
||||
|
@ -18,7 +18,6 @@ from oslo_config import cfg
|
||||
from oslo_service import wsgi
|
||||
|
||||
from neutron._i18n import _
|
||||
from neutron import api
|
||||
from neutron.common import constants
|
||||
from neutron.common import utils
|
||||
|
||||
@ -54,9 +53,9 @@ core_opts = [
|
||||
"removed in the Ocata release.")),
|
||||
cfg.BoolOpt('allow_bulk', default=True,
|
||||
help=_("Allow the usage of the bulk API")),
|
||||
cfg.BoolOpt('allow_pagination', default=api.DEFAULT_ALLOW_PAGINATION,
|
||||
cfg.BoolOpt('allow_pagination', default=False,
|
||||
help=_("Allow the usage of the pagination")),
|
||||
cfg.BoolOpt('allow_sorting', default=api.DEFAULT_ALLOW_SORTING,
|
||||
cfg.BoolOpt('allow_sorting', default=False,
|
||||
help=_("Allow the usage of the sorting")),
|
||||
cfg.StrOpt('pagination_max_limit', default="-1",
|
||||
help=_("The maximum number of items returned in a single "
|
||||
|
50
neutron/extensions/pagination.py
Normal file
50
neutron/extensions/pagination.py
Normal file
@ -0,0 +1,50 @@
|
||||
# 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 oslo_config import cfg
|
||||
|
||||
from neutron.api import extensions
|
||||
|
||||
|
||||
_ALIAS = 'pagination'
|
||||
|
||||
|
||||
class Pagination(extensions.ExtensionDescriptor):
|
||||
"""Fake extension that indicates that pagination is enabled."""
|
||||
|
||||
extensions.register_custom_supported_check(
|
||||
_ALIAS, lambda: cfg.CONF.allow_pagination, plugin_agnostic=True
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_name(cls):
|
||||
return "Pagination support"
|
||||
|
||||
@classmethod
|
||||
def get_alias(cls):
|
||||
return _ALIAS
|
||||
|
||||
@classmethod
|
||||
def get_description(cls):
|
||||
return "Extension that indicates that pagination is enabled."
|
||||
|
||||
@classmethod
|
||||
def get_updated(cls):
|
||||
return "2016-06-12T00:00:00-00:00"
|
||||
|
||||
@classmethod
|
||||
def get_resources(cls):
|
||||
return []
|
||||
|
||||
def get_extended_resources(self, version):
|
||||
return {}
|
50
neutron/extensions/sorting.py
Normal file
50
neutron/extensions/sorting.py
Normal file
@ -0,0 +1,50 @@
|
||||
# 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 oslo_config import cfg
|
||||
|
||||
from neutron.api import extensions
|
||||
|
||||
|
||||
_ALIAS = 'sorting'
|
||||
|
||||
|
||||
class Sorting(extensions.ExtensionDescriptor):
|
||||
"""Fake extension that indicates that sorting is enabled."""
|
||||
|
||||
extensions.register_custom_supported_check(
|
||||
_ALIAS, lambda: cfg.CONF.allow_sorting, plugin_agnostic=True
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_name(cls):
|
||||
return "Sorting support"
|
||||
|
||||
@classmethod
|
||||
def get_alias(cls):
|
||||
return _ALIAS
|
||||
|
||||
@classmethod
|
||||
def get_description(cls):
|
||||
return "Extension that indicates that sorting is enabled."
|
||||
|
||||
@classmethod
|
||||
def get_updated(cls):
|
||||
return "2016-06-12T00:00:00-00:00"
|
||||
|
||||
@classmethod
|
||||
def get_resources(cls):
|
||||
return []
|
||||
|
||||
def get_extended_resources(self, version):
|
||||
return {}
|
@ -21,6 +21,7 @@ NETWORK_API_EXTENSIONS="
|
||||
net-mtu, \
|
||||
network-ip-availability, \
|
||||
network_availability_zone, \
|
||||
pagination, \
|
||||
port-security, \
|
||||
provider, \
|
||||
qos, \
|
||||
@ -31,6 +32,7 @@ NETWORK_API_EXTENSIONS="
|
||||
router_availability_zone, \
|
||||
security-group, \
|
||||
service-type, \
|
||||
sorting, \
|
||||
standard-attr-description, \
|
||||
subnet_allocation, \
|
||||
tag, \
|
||||
|
@ -471,7 +471,7 @@ class BaseAdminNetworkTest(BaseNetworkTest):
|
||||
def _require_sorting(f):
|
||||
@functools.wraps(f)
|
||||
def inner(self, *args, **kwargs):
|
||||
if not CONF.neutron_plugin_options.validate_sorting:
|
||||
if not test.is_extension_enabled("sorting", "network"):
|
||||
self.skipTest('Sorting feature is required')
|
||||
return f(self, *args, **kwargs)
|
||||
return inner
|
||||
@ -480,7 +480,7 @@ def _require_sorting(f):
|
||||
def _require_pagination(f):
|
||||
@functools.wraps(f)
|
||||
def inner(self, *args, **kwargs):
|
||||
if not CONF.neutron_plugin_options.validate_pagination:
|
||||
if not test.is_extension_enabled("pagination", "network"):
|
||||
self.skipTest('Pagination feature is required')
|
||||
return f(self, *args, **kwargs)
|
||||
return inner
|
||||
|
39
neutron/tests/tempest/api/test_extensions.py
Normal file
39
neutron/tests/tempest/api/test_extensions.py
Normal file
@ -0,0 +1,39 @@
|
||||
# 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 tempest import test
|
||||
|
||||
from neutron.tests.tempest.api import base
|
||||
from neutron.tests.tempest import config
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
class ExtensionsTest(base.BaseNetworkTest):
|
||||
|
||||
def _test_list_extensions_includes(self, ext):
|
||||
body = self.client.list_extensions()
|
||||
extensions = {ext_['alias'] for ext_ in body['extensions']}
|
||||
self.assertNotEmpty(extensions, "Extension list returned is empty")
|
||||
ext_enabled = test.is_extension_enabled(ext, "network")
|
||||
if ext_enabled:
|
||||
self.assertIn(ext, extensions)
|
||||
else:
|
||||
self.assertNotIn(ext, extensions)
|
||||
|
||||
@test.idempotent_id('262420b7-a4bb-4a3e-b4b5-e73bad18df8c')
|
||||
def test_list_extensions_sorting(self):
|
||||
self._test_list_extensions_includes('sorting')
|
||||
|
||||
@test.idempotent_id('19db409e-a23f-445d-8bc8-ca3d64c84706')
|
||||
def test_list_extensions_pagination(self):
|
||||
self._test_list_extensions_includes('pagination')
|
@ -12,7 +12,6 @@
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from neutron import api
|
||||
from tempest import config
|
||||
|
||||
|
||||
@ -23,13 +22,7 @@ NeutronPluginOptions = [
|
||||
cfg.BoolOpt('specify_floating_ip_address_available',
|
||||
default=True,
|
||||
help='Allow passing an IP Address of the floating ip when '
|
||||
'creating the floating ip'),
|
||||
cfg.BoolOpt('validate_pagination',
|
||||
default=api.DEFAULT_ALLOW_PAGINATION,
|
||||
help='Validate pagination'),
|
||||
cfg.BoolOpt('validate_sorting',
|
||||
default=api.DEFAULT_ALLOW_SORTING,
|
||||
help='Validate sorting')]
|
||||
'creating the floating ip')]
|
||||
|
||||
# TODO(amuller): Redo configuration options registration as part of the planned
|
||||
# transition to the Tempest plugin architecture
|
||||
|
@ -791,3 +791,12 @@ class NetworkClientJSON(service_client.RestClient):
|
||||
self.expected_success(201, resp.status)
|
||||
body = jsonutils.loads(body)
|
||||
return service_client.ResponseBody(resp, body)
|
||||
|
||||
def list_extensions(self, **filters):
|
||||
uri = self.get_uri("extensions")
|
||||
if filters:
|
||||
uri = '?'.join([uri, urlparse.urlencode(filters)])
|
||||
resp, body = self.get(uri)
|
||||
body = {'extensions': self.deserialize_list(body)}
|
||||
self.expected_success(200, resp.status)
|
||||
return service_client.ResponseBody(resp, body)
|
||||
|
@ -14,7 +14,9 @@
|
||||
# under the License.
|
||||
|
||||
import abc
|
||||
import copy
|
||||
|
||||
import fixtures
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
@ -49,6 +51,27 @@ _get_path = test_base._get_path
|
||||
extensions_path = ':'.join(neutron.tests.unit.extensions.__path__)
|
||||
|
||||
|
||||
class CustomExtensionCheckMapMemento(fixtures.Fixture):
|
||||
"""Create a copy of the custom extension support check map so it can be
|
||||
restored during test cleanup.
|
||||
"""
|
||||
|
||||
def _setUp(self):
|
||||
self._map_contents_backup = copy.deepcopy(
|
||||
extensions.EXTENSION_SUPPORTED_CHECK_MAP
|
||||
)
|
||||
self._plugin_agnostic_extensions_backup = set(
|
||||
extensions._PLUGIN_AGNOSTIC_EXTENSIONS
|
||||
)
|
||||
self.addCleanup(self._restore)
|
||||
|
||||
def _restore(self):
|
||||
extensions.EXTENSION_SUPPORTED_CHECK_MAP = self._map_contents_backup
|
||||
extensions._PLUGIN_AGNOSTIC_EXTENSIONS = (
|
||||
self._plugin_agnostic_extensions_backup
|
||||
)
|
||||
|
||||
|
||||
class ExtensionsTestApp(base_wsgi.Router):
|
||||
|
||||
def __init__(self, options=None):
|
||||
@ -793,6 +816,47 @@ class PluginAwareExtensionManagerTest(base.BaseTestCase):
|
||||
extensions.PluginAwareExtensionManager,
|
||||
'', plugin_info)
|
||||
|
||||
def test_custom_supported_implementation(self):
|
||||
self.useFixture(CustomExtensionCheckMapMemento())
|
||||
|
||||
class FakePlugin(object):
|
||||
pass
|
||||
|
||||
class FakeExtension(ext_stubs.StubExtension):
|
||||
extensions.register_custom_supported_check(
|
||||
'stub_extension', lambda: True, plugin_agnostic=True
|
||||
)
|
||||
|
||||
ext = FakeExtension()
|
||||
|
||||
plugin_info = {constants.CORE: FakePlugin()}
|
||||
ext_mgr = extensions.PluginAwareExtensionManager('', plugin_info)
|
||||
ext_mgr.add_extension(ext)
|
||||
self.assertIn("stub_extension", ext_mgr.extensions)
|
||||
|
||||
extensions.register_custom_supported_check(
|
||||
'stub_extension', lambda: False, plugin_agnostic=True
|
||||
)
|
||||
ext_mgr = extensions.PluginAwareExtensionManager('', plugin_info)
|
||||
ext_mgr.add_extension(ext)
|
||||
self.assertNotIn("stub_extension", ext_mgr.extensions)
|
||||
|
||||
def test_custom_supported_implementation_plugin_specific(self):
|
||||
self.useFixture(CustomExtensionCheckMapMemento())
|
||||
|
||||
class FakePlugin(object):
|
||||
pass
|
||||
|
||||
class FakeExtension(ext_stubs.StubExtension):
|
||||
extensions.register_custom_supported_check(
|
||||
'stub_plugin_extension', lambda: True, plugin_agnostic=False
|
||||
)
|
||||
|
||||
plugin_info = {constants.CORE: FakePlugin()}
|
||||
self.assertRaises(
|
||||
exceptions.ExtensionsNotFound,
|
||||
extensions.PluginAwareExtensionManager, '', plugin_info)
|
||||
|
||||
|
||||
class ExtensionControllerTest(testlib_api.WebTestCase):
|
||||
|
||||
|
@ -0,0 +1,6 @@
|
||||
---
|
||||
features:
|
||||
- New API extensions, 'sorting' and 'pagination', have been added to allow
|
||||
API users to detect if sorting and pagination features are enabled. These
|
||||
features are controlled by ``allow_sorting`` and ``allow_pagination``
|
||||
configuration options.
|
Loading…
x
Reference in New Issue
Block a user