diff options
-rw-r--r-- | nova/tests/unit/volume/test_cinder.py | 20 | ||||
-rw-r--r-- | nova/volume/cinder.py | 144 |
2 files changed, 127 insertions, 37 deletions
diff --git a/nova/tests/unit/volume/test_cinder.py b/nova/tests/unit/volume/test_cinder.py index cfc8fa4ec5..e286336ecb 100644 --- a/nova/tests/unit/volume/test_cinder.py +++ b/nova/tests/unit/volume/test_cinder.py @@ -1007,13 +1007,14 @@ class CinderClientTestCase(test.NoDBTestCase): get_volume_api.assert_called_once_with( self.mock_session.get_endpoint.return_value) + @mock.patch('nova.volume.cinder._get_highest_client_server_version', + # Fake the case that cinder is really old. + return_value=cinder_api_versions.APIVersion('2.0')) @mock.patch('cinderclient.client.get_volume_api_from_url', return_value='3') - @mock.patch('cinderclient.client.get_highest_client_server_version', - return_value=2.0) # Fake the case that cinder is really old. def test_create_v3_client_with_microversion_too_new(self, - get_highest_version, - get_volume_api): + get_volume_api, + get_highest_version): """Tests that creating a v3 client and requesting a microversion that is either too new for the server (or client) to support raises an exception. @@ -1023,10 +1024,11 @@ class CinderClientTestCase(test.NoDBTestCase): get_volume_api.assert_called_once_with( self.mock_session.get_endpoint.return_value) get_highest_version.assert_called_once_with( - self.mock_session.get_endpoint.return_value) + self.ctxt, self.mock_session.get_endpoint.return_value) - @mock.patch('cinderclient.client.get_highest_client_server_version', - return_value=cinder_api_versions.MAX_VERSION) + @mock.patch('nova.volume.cinder._get_highest_client_server_version', + return_value=cinder_api_versions.APIVersion( + cinder_api_versions.MAX_VERSION)) @mock.patch('cinderclient.client.get_volume_api_from_url', return_value='3') def test_create_v3_client_with_microversion_available(self, @@ -1042,9 +1044,9 @@ class CinderClientTestCase(test.NoDBTestCase): get_volume_api.assert_called_once_with( self.mock_session.get_endpoint.return_value) get_highest_version.assert_called_once_with( - self.mock_session.get_endpoint.return_value) + self.ctxt, self.mock_session.get_endpoint.return_value) - @mock.patch('cinderclient.client.get_highest_client_server_version', + @mock.patch('nova.volume.cinder._get_highest_client_server_version', new_callable=mock.NonCallableMock) # asserts not called @mock.patch('cinderclient.client.get_volume_api_from_url', return_value='3') diff --git a/nova/volume/cinder.py b/nova/volume/cinder.py index 9337e71e9a..cd513f7191 100644 --- a/nova/volume/cinder.py +++ b/nova/volume/cinder.py @@ -29,10 +29,12 @@ from cinderclient import exceptions as cinder_exception from keystoneauth1 import exceptions as keystone_exception from keystoneauth1 import loading as ks_loading from oslo_log import log as logging +from oslo_serialization import jsonutils from oslo_utils import encodeutils from oslo_utils import excutils from oslo_utils import strutils import six +from six.moves import urllib from nova import availability_zones as az import nova.conf @@ -72,10 +74,103 @@ def _load_auth_plugin(conf): raise cinder_exception.Unauthorized(401, message=err_msg) -def _check_microversion(url, microversion): +def _load_session(): + global _SESSION + + if not _SESSION: + _SESSION = ks_loading.load_session_from_conf_options( + CONF, nova.conf.cinder.cinder_group.name) + + +def _get_auth(context): + global _ADMIN_AUTH + # NOTE(lixipeng): Auth token is none when call + # cinder API from compute periodic tasks, context + # from them generated from 'context.get_admin_context' + # which only set is_admin=True but is without token. + # So add load_auth_plugin when this condition appear. + if context.is_admin and not context.auth_token: + if not _ADMIN_AUTH: + _ADMIN_AUTH = _load_auth_plugin(CONF) + return _ADMIN_AUTH + else: + return service_auth.get_auth_plugin(context) + + +# NOTE(efried): Bug #1752152 +# This method is copied/adapted from cinderclient.client.get_server_version so +# we can use _SESSION.get rather than a raw requests.get to retrieve the +# version document. This enables HTTPS by gleaning cert info from the session +# config. +def _get_server_version(context, url): + """Queries the server via the naked endpoint and gets version info. + + :param context: The nova request context for auth. + :param url: url of the cinder endpoint + :returns: APIVersion object for min and max version supported by + the server + """ + min_version = "2.0" + current_version = "2.0" + + _load_session() + auth = _get_auth(context) + + try: + u = urllib.parse.urlparse(url) + version_url = None + + # NOTE(andreykurilin): endpoint URL has at least 2 formats: + # 1. The classic (legacy) endpoint: + # http://{host}:{optional_port}/v{1 or 2 or 3}/{project-id} + # http://{host}:{optional_port}/v{1 or 2 or 3} + # 3. Under wsgi: + # http://{host}:{optional_port}/volume/v{1 or 2 or 3} + for ver in ['v1', 'v2', 'v3']: + if u.path.endswith(ver) or "/{0}/".format(ver) in u.path: + path = u.path[:u.path.rfind(ver)] + version_url = '%s://%s%s' % (u.scheme, u.netloc, path) + break + + if not version_url: + # NOTE(andreykurilin): probably, it is one of the next cases: + # * https://volume.example.com/ + # * https://example.com/volume + # leave as is without cropping. + version_url = url + + response = _SESSION.get(version_url, auth=auth) + data = jsonutils.loads(response.text) + versions = data['versions'] + for version in versions: + if '3.' in version['version']: + min_version = version['min_version'] + current_version = version['version'] + break + except cinder_exception.ClientException as e: + LOG.warning("Error in server version query:%s\n" + "Returning APIVersion 2.0", six.text_type(e.message)) + return (cinder_api_versions.APIVersion(min_version), + cinder_api_versions.APIVersion(current_version)) + + +# NOTE(efried): Bug #1752152 +# This method is copied/adapted from +# cinderclient.client.get_highest_client_server_version. See note on +# _get_server_version. +def _get_highest_client_server_version(context, url): + """Returns highest APIVersion supported version by client and server.""" + min_server, max_server = _get_server_version(context, url) + max_client = cinder_api_versions.APIVersion( + cinder_api_versions.MAX_VERSION) + return min(max_server, max_client) + + +def _check_microversion(context, url, microversion): """Checks to see if the requested microversion is supported by the current version of python-cinderclient and the volume API endpoint. + :param context: The nova request context for auth. :param url: Cinder API endpoint URL. :param microversion: Requested microversion. If not available at the given API endpoint URL, a CinderAPIVersionNotAvailable exception is raised. @@ -83,10 +178,7 @@ def _check_microversion(url, microversion): construct the cinder v3 client object. :raises: CinderAPIVersionNotAvailable if the microversion is not available. """ - max_api_version = cinder_client.get_highest_client_server_version(url) - # get_highest_client_server_version returns a float which we need to cast - # to a str and create an APIVersion object to do our version comparison. - max_api_version = cinder_api_versions.APIVersion(str(max_api_version)) + max_api_version = _get_highest_client_server_version(context, url) # Check if the max_api_version matches the requested minimum microversion. if max_api_version.matches(microversion): # The requested microversion is supported by the client and the server. @@ -95,24 +187,9 @@ def _check_microversion(url, microversion): def _get_cinderclient_parameters(context): - global _ADMIN_AUTH - global _SESSION - - if not _SESSION: - _SESSION = ks_loading.load_session_from_conf_options( - CONF, nova.conf.cinder.cinder_group.name) + _load_session() - # NOTE(lixipeng): Auth token is none when call - # cinder API from compute periodic tasks, context - # from them generated from 'context.get_admin_context' - # which only set is_admin=True but is without token. - # So add load_auth_plugin when this condition appear. - if context.is_admin and not context.auth_token: - if not _ADMIN_AUTH: - _ADMIN_AUTH = _load_auth_plugin(CONF) - auth = _ADMIN_AUTH - else: - auth = service_auth.get_auth_plugin(context) + auth = _get_auth(context) url = None @@ -132,13 +209,13 @@ def _get_cinderclient_parameters(context): def is_microversion_supported(context, microversion): + # NOTE(efried): Work around bug #1752152. Call the cinderclient() builder + # in a way that just does a microversion check. + cinderclient(context, microversion=microversion, check_only=True) - _, _, url = _get_cinderclient_parameters(context) - - _check_microversion(url, microversion) - -def cinderclient(context, microversion=None, skip_version_check=False): +def cinderclient(context, microversion=None, skip_version_check=False, + check_only=False): """Constructs a cinder client object for making API requests. :param context: The nova request context for auth. @@ -150,6 +227,14 @@ def cinderclient(context, microversion=None, skip_version_check=False): requested, the version discovery check is skipped and the microversion is used directly. This should only be used if a previous check for the same microversion was successful. + :param check_only: If True, don't build the actual client; just do the + setup and version checking. + :raises: UnsupportedCinderAPIVersion if a major version other than 3 is + requested. + :raises: CinderAPIVersionNotAvailable if microversion checking is requested + and the specified microversion is higher than what the service can + handle. + :returns: A cinderclient.client.Client wrapper, unless check_only is False. """ endpoint_override = None @@ -173,7 +258,10 @@ def cinderclient(context, microversion=None, skip_version_check=False): if skip_version_check: version = microversion else: - version = _check_microversion(url, microversion) + version = _check_microversion(context, url, microversion) + + if check_only: + return return cinder_client.Client(version, session=_SESSION, |