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:
Ihar Hrachyshka 2016-06-14 16:16:06 +02:00
parent 7cff2287bb
commit 5e0878f476
12 changed files with 260 additions and 30 deletions

View File

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

View File

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

View File

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

View 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 {}

View 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 {}

View File

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

View File

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

View 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')

View File

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

View File

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

View File

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

View File

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