From 5e0878f476d7116bc4e0c29c7929718f379b36c2 Mon Sep 17 00:00:00 2001 From: Ihar Hrachyshka Date: Tue, 14 Jun 2016 16:16:06 +0200 Subject: [PATCH] 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 --- neutron/api/__init__.py | 15 ----- neutron/api/extensions.py | 37 ++++++++++- neutron/conf/common.py | 5 +- neutron/extensions/pagination.py | 50 +++++++++++++++ neutron/extensions/sorting.py | 50 +++++++++++++++ neutron/tests/contrib/hooks/api_extensions | 2 + neutron/tests/tempest/api/base.py | 4 +- neutron/tests/tempest/api/test_extensions.py | 39 +++++++++++ neutron/tests/tempest/config.py | 9 +-- .../services/network/json/network_client.py | 9 +++ neutron/tests/unit/api/test_extensions.py | 64 +++++++++++++++++++ ...agination-extensions-e66e99e2a8f5e563.yaml | 6 ++ 12 files changed, 260 insertions(+), 30 deletions(-) create mode 100644 neutron/extensions/pagination.py create mode 100644 neutron/extensions/sorting.py create mode 100644 neutron/tests/tempest/api/test_extensions.py create mode 100644 releasenotes/notes/sorting-pagination-extensions-e66e99e2a8f5e563.yaml diff --git a/neutron/api/__init__.py b/neutron/api/__init__.py index 57fa91b62af..e69de29bb2d 100644 --- a/neutron/api/__init__.py +++ b/neutron/api/__init__.py @@ -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 diff --git a/neutron/api/extensions.py b/neutron/api/extensions.py index 93feff9b6ce..381df4162c6 100644 --- a/neutron/api/extensions.py +++ b/neutron/api/extensions.py @@ -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)) diff --git a/neutron/conf/common.py b/neutron/conf/common.py index b1e9a64204c..3c07faaf737 100644 --- a/neutron/conf/common.py +++ b/neutron/conf/common.py @@ -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 " diff --git a/neutron/extensions/pagination.py b/neutron/extensions/pagination.py new file mode 100644 index 00000000000..4f2c14fb2f3 --- /dev/null +++ b/neutron/extensions/pagination.py @@ -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 {} diff --git a/neutron/extensions/sorting.py b/neutron/extensions/sorting.py new file mode 100644 index 00000000000..bcfef471180 --- /dev/null +++ b/neutron/extensions/sorting.py @@ -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 {} diff --git a/neutron/tests/contrib/hooks/api_extensions b/neutron/tests/contrib/hooks/api_extensions index 8ce15b7b583..e0317cd9183 100644 --- a/neutron/tests/contrib/hooks/api_extensions +++ b/neutron/tests/contrib/hooks/api_extensions @@ -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, \ diff --git a/neutron/tests/tempest/api/base.py b/neutron/tests/tempest/api/base.py index 84d91dfd038..9ee1ba4be62 100644 --- a/neutron/tests/tempest/api/base.py +++ b/neutron/tests/tempest/api/base.py @@ -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 diff --git a/neutron/tests/tempest/api/test_extensions.py b/neutron/tests/tempest/api/test_extensions.py new file mode 100644 index 00000000000..1defb337bdc --- /dev/null +++ b/neutron/tests/tempest/api/test_extensions.py @@ -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') diff --git a/neutron/tests/tempest/config.py b/neutron/tests/tempest/config.py index bad100063a6..f50fa3954f2 100644 --- a/neutron/tests/tempest/config.py +++ b/neutron/tests/tempest/config.py @@ -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 diff --git a/neutron/tests/tempest/services/network/json/network_client.py b/neutron/tests/tempest/services/network/json/network_client.py index 12ba685a9c8..9c8cb056519 100644 --- a/neutron/tests/tempest/services/network/json/network_client.py +++ b/neutron/tests/tempest/services/network/json/network_client.py @@ -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) diff --git a/neutron/tests/unit/api/test_extensions.py b/neutron/tests/unit/api/test_extensions.py index d769dd8bda8..ddf91816ee2 100644 --- a/neutron/tests/unit/api/test_extensions.py +++ b/neutron/tests/unit/api/test_extensions.py @@ -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): diff --git a/releasenotes/notes/sorting-pagination-extensions-e66e99e2a8f5e563.yaml b/releasenotes/notes/sorting-pagination-extensions-e66e99e2a8f5e563.yaml new file mode 100644 index 00000000000..5a3d7636fad --- /dev/null +++ b/releasenotes/notes/sorting-pagination-extensions-e66e99e2a8f5e563.yaml @@ -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.