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.