diff --git a/openstack_dashboard/api/cinder.py b/openstack_dashboard/api/cinder.py index 5a69f872e7..5242a936c3 100644 --- a/openstack_dashboard/api/cinder.py +++ b/openstack_dashboard/api/cinder.py @@ -231,7 +231,7 @@ def cinderclient(request_auth_params, version=None): return c -def get_microversion(request, feature): +def get_microversion(request, features): for service_name in ('volume', 'volumev2', 'volumev3'): try: cinder_url = base.url_for(request, service_name) @@ -241,8 +241,8 @@ def get_microversion(request, feature): else: return None min_ver, max_ver = cinder_client.get_server_version(cinder_url) - return (microversions.get_microversion_for_feature( - 'cinder', feature, api_versions.APIVersion, min_ver, max_ver)) + return (microversions.get_microversion_for_features( + 'cinder', features, api_versions.APIVersion, min_ver, max_ver)) def _replace_v2_parameters(data): @@ -1061,7 +1061,7 @@ def pool_list(request, detailed=False): @profiler.trace def message_list(request, search_opts=None): - version = get_microversion(request, 'message_list') + version = get_microversion(request, ['message_list']) if version is None: LOG.warning("insufficient microversion for message_list") return [] diff --git a/openstack_dashboard/api/microversions.py b/openstack_dashboard/api/microversions.py index ea744d508a..2a770d8b17 100644 --- a/openstack_dashboard/api/microversions.py +++ b/openstack_dashboard/api/microversions.py @@ -42,17 +42,31 @@ MICROVERSION_FEATURES = { # NOTE(robcresswell): Since each client implements their own wrapper class for # API objects, we'll need to allow that to be passed in. In the future this # should be replaced by some common handling in Oslo. -def get_microversion_for_feature(service, feature, wrapper_class, - min_ver, max_ver): - """Retrieves that highest known functional microversion for a feature""" +def get_microversion_for_features(service, features, wrapper_class, + min_ver, max_ver): + """Retrieves that highest known functional microversion for features""" + if not features: + return None + # Convert a single feature string into a list for backward compatiblity. + if isinstance(features, str): + features = [features] try: service_features = MICROVERSION_FEATURES[service] except KeyError: LOG.debug("'%s' could not be found in the MICROVERSION_FEATURES dict", service) return None - feature_versions = service_features[feature] - for version in reversed(feature_versions): + + feature_versions = set(service_features[features[0]]) + for feature in features[1:]: + feature_versions &= set(service_features[feature]) + if not feature_versions: + return None + + # Sort version candidates from larger versins + feature_versions = sorted(feature_versions, reverse=True, + key=lambda v: [int(i) for i in v.split('.')]) + for version in feature_versions: microversion = wrapper_class(version) if microversion.matches(min_ver, max_ver): return microversion diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 00a2c147db..b58f2782e9 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -58,15 +58,15 @@ CACERT = getattr(settings, 'OPENSTACK_SSL_CACERT', None) @memoized -def get_microversion(request, feature): +def get_microversion(request, features): client = novaclient(request) min_ver, max_ver = api_versions._get_server_version_range(client) - return (microversions.get_microversion_for_feature( - 'nova', feature, api_versions.APIVersion, min_ver, max_ver)) + return (microversions.get_microversion_for_features( + 'nova', features, api_versions.APIVersion, min_ver, max_ver)) -def is_feature_available(request, feature): - return bool(get_microversion(request, feature)) +def is_feature_available(request, features): + return bool(get_microversion(request, features)) class VNCConsole(base.APIDictWrapper): diff --git a/openstack_dashboard/api/rest/nova.py b/openstack_dashboard/api/rest/nova.py index 7647b20518..27fea2ff7e 100644 --- a/openstack_dashboard/api/rest/nova.py +++ b/openstack_dashboard/api/rest/nova.py @@ -51,7 +51,7 @@ class Features(generic.View): @rest_utils.ajax() def get(self, request, name): """Check if a specified feature is supported.""" - return api.nova.is_feature_available(request, name) + return api.nova.is_feature_available(request, [name]) @urls.register diff --git a/openstack_dashboard/test/api_tests/microversions_tests.py b/openstack_dashboard/test/api_tests/microversions_tests.py new file mode 100644 index 0000000000..0cf79407ca --- /dev/null +++ b/openstack_dashboard/test/api_tests/microversions_tests.py @@ -0,0 +1,108 @@ +# 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. + +import unittest + +import mock + +from openstack_dashboard.api import microversions + + +class _VersionWrapper(object): + + def __init__(self, version): + self.version = version + + def matches(self, min_ver, max_ver): + return min_ver <= self.version <= max_ver + + +class MicroversionsTests(unittest.TestCase): + + def _test_get_microversion(self, min_ver, max_ver, + features=None, service=None, + feature_versions=None): + if feature_versions is None: + feature_versions = {'myservice': {'feature_a': ['2.3', '2.5']}} + if features is None: + features = ['feature_a'] + if service is None: + service = 'myservice' + with mock.patch.object(microversions, 'MICROVERSION_FEATURES', + feature_versions): + return microversions.get_microversion_for_features( + service, features, _VersionWrapper, min_ver, max_ver) + + def test_get_microversion(self): + ret = self._test_get_microversion('2.1', '2.5') + self.assertIsInstance(ret, _VersionWrapper) + self.assertEqual('2.5', ret.version) + + def test_get_microversion_second_version(self): + ret = self._test_get_microversion('2.1', '2.4') + self.assertIsInstance(ret, _VersionWrapper) + self.assertEqual('2.3', ret.version) + + def test_get_microversion_out_of_range(self): + ret = self._test_get_microversion('2.1', '2.2') + self.assertIsNone(ret) + + def test_get_microversion_string_feature(self): + ret = self._test_get_microversion('2.1', '2.5', 'feature_a') + self.assertIsInstance(ret, _VersionWrapper) + # NOTE: ret.version depends on a wrapper class. + self.assertEqual('2.5', ret.version) + + def test_get_microversion_multiple_features(self): + feature_versions = {'myservice': {'feature_a': ['2.3', '2.5', '2.7'], + 'feature_b': ['2.5', '2.7', '2.8']}} + ret = self._test_get_microversion( + '2.1', '2.9', ['feature_a', 'feature_b'], + feature_versions=feature_versions) + self.assertIsInstance(ret, _VersionWrapper) + self.assertEqual('2.7', ret.version) + + def test_get_microversion_multiple_features_second_largest(self): + feature_versions = {'myservice': {'feature_a': ['2.3', '2.5', '2.7'], + 'feature_b': ['2.5', '2.7', '2.8']}} + ret = self._test_get_microversion( + '2.1', '2.6', ['feature_a', 'feature_b'], + feature_versions=feature_versions) + self.assertIsInstance(ret, _VersionWrapper) + self.assertEqual('2.5', ret.version) + + def test_get_microversion_multiple_features_out_of_range(self): + feature_versions = {'myservice': {'feature_a': ['2.3', '2.5', '2.7'], + 'feature_b': ['2.5', '2.7', '2.8']}} + ret = self._test_get_microversion( + '2.1', '2.4', ['feature_a', 'feature_b'], + feature_versions=feature_versions) + self.assertIsNone(ret) + + def test_get_microversion_multiple_features_no_common_version(self): + feature_versions = {'myservice': {'feature_a': ['2.3', '2.5', '2.7'], + 'feature_b': ['2.6', '2.8']}} + ret = self._test_get_microversion( + '2.1', '2.9', ['feature_a', 'feature_b'], + feature_versions=feature_versions) + self.assertIsNone(ret) + + def test_get_microversion_version_number_sort(self): + feature_versions = {'myservice': {'feature_a': ['2.3', '2.20', '2.2']}} + ret = self._test_get_microversion('2.1', '2.30', + feature_versions=feature_versions) + self.assertIsInstance(ret, _VersionWrapper) + self.assertEqual('2.20', ret.version) + + def test_get_microversion_undefined_service(self): + ret = self._test_get_microversion('2.1', '2.5', service='notfound') + self.assertIsNone(ret)