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 20eaaee233)
This commit is contained in:
Eric Fried 2018-03-28 15:45:26 -05:00
parent 37b61e1279
commit 307382f58d
2 changed files with 131 additions and 41 deletions

View File

@ -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')

View File

@ -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,