summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Fried <efried@us.ibm.com>2018-03-28 15:45:26 -0500
committerEric Fried <efried@us.ibm.com>2018-03-29 21:55:40 +0000
commit307382f58d38778b480d2d030e427759a44c204b (patch)
tree0334e39205668494ad0608a89ce1c2176395099e
parent37b61e1279f70f3e41be3fbbbe8e850d98c024b3 (diff)
downloadnova-17.0.2.tar.gz
Use ksa session for cinder microversion check17.0.2
[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)
-rw-r--r--nova/tests/unit/volume/test_cinder.py20
-rw-r--r--nova/volume/cinder.py144
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,