From 307382f58d38778b480d2d030e427759a44c204b Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Wed, 28 Mar 2018 15:45:26 -0500 Subject: [PATCH] Use ksa session for cinder microversion check [1] added a method to validate availability of a desired version of the cinder API. This method called into cinderclient.client.get_highest_client_server_version to (unsurprisingly) discover the highest available version to compare against. However, that routine uses a raw requests.get to access the version document from the server. This breaks when the endpoint URL is using HTTPS, because nothing sets up the cert info for that call. With this change, we work around the issue by duplicating the logic from get_highest_client_server_version, but doing the version discovery via the same keystoneauth1 session that's configured for use with the client itself, thus inheriting any SSL configuration as appropriate. [1] https://review.openstack.org/#/c/469579/ Change-Id: I4de355195281009a5979710d7f14ae8ea11d10e0 Closes-Bug: #1752152 (cherry picked from commit 20eaaee2334957eb8739ecca524a1c4aa9f246e9) --- nova/tests/unit/volume/test_cinder.py | 20 ++-- nova/volume/cinder.py | 152 ++++++++++++++++++++------ 2 files changed, 131 insertions(+), 41 deletions(-) diff --git a/nova/tests/unit/volume/test_cinder.py b/nova/tests/unit/volume/test_cinder.py index cfc8fa4ec509..e286336ecb37 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 9337e71e9ad1..cd513f71914b 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,36 +74,16 @@ def _load_auth_plugin(conf): raise cinder_exception.Unauthorized(401, message=err_msg) -def _check_microversion(url, microversion): - """Checks to see if the requested microversion is supported by the current - version of python-cinderclient and the volume API endpoint. - - :param url: Cinder API endpoint URL. - :param microversion: Requested microversion. If not available at the given - API endpoint URL, a CinderAPIVersionNotAvailable exception is raised. - :returns: The microversion if it is available. This can be used to - 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)) - # 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. - return microversion - raise exception.CinderAPIVersionNotAvailable(version=microversion) - - -def _get_cinderclient_parameters(context): - global _ADMIN_AUTH +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' @@ -110,9 +92,104 @@ def _get_cinderclient_parameters(context): if context.is_admin and not context.auth_token: if not _ADMIN_AUTH: _ADMIN_AUTH = _load_auth_plugin(CONF) - auth = _ADMIN_AUTH + return _ADMIN_AUTH else: - auth = service_auth.get_auth_plugin(context) + 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. + :returns: The microversion if it is available. This can be used to + construct the cinder v3 client object. + :raises: CinderAPIVersionNotAvailable if the microversion is not available. + """ + 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. + return microversion + raise exception.CinderAPIVersionNotAvailable(version=microversion) + + +def _get_cinderclient_parameters(context): + _load_session() + + auth = _get_auth(context) url = None @@ -132,13 +209,13 @@ def _get_cinderclient_parameters(context): def is_microversion_supported(context, microversion): - - _, _, url = _get_cinderclient_parameters(context) - - _check_microversion(url, 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) -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,