Use microversions for new style volume attachments

This adds the ability to do version discovery with Cinder to
know if we can make requests at a specific microversion, like
3.27 in the case of creating/updating/deleting volume attachments.

We will use this to attempt an attachment_create at 3.27 when
attaching a volume to an instance and if 3.27 is not available
then the calling code will need to handle CinderAPIVersionNotAvailable
and fallback to the old style attachment flow.

For new style attachments, everything will be keyed off the
BlockDeviceMapping.attachment_id field being set if we successfully
created the attachment using 3.27, so the attachment_create method
is really the only one that should care about microversions initially.

Part of blueprint cinder-new-attach-apis

Change-Id: I9b6ab9f71cc58d4514127f5ea61789f187487828
This commit is contained in:
Matt Riedemann 2017-05-31 12:37:55 -04:00
parent 6c58884030
commit d655179182
3 changed files with 123 additions and 3 deletions

View File

@ -149,6 +149,13 @@ class UnsupportedCinderAPIVersion(NovaException):
msg_fmt = _('Nova does not support Cinder API version %(version)s')
class CinderAPIVersionNotAvailable(NovaException):
"""Used to indicate that a requested Cinder API version, generally a
microversion, is not available.
"""
msg_fmt = _('Cinder API version %(version)s is not available.')
class Forbidden(NovaException):
msg_fmt = _("Forbidden")
code = 403

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from cinderclient import api_versions as cinder_api_versions
from cinderclient import exceptions as cinder_exception
from keystoneclient import exceptions as keystone_exception
import mock
@ -717,3 +718,79 @@ class CinderApiTestCase(test.NoDBTestCase):
my_func.side_effect = raised_exc
self.assertRaises(expected_exc, wrapper(my_func), 'foo', 'bar', 'baz')
class CinderClientTestCase(test.NoDBTestCase):
"""Used to test constructing a cinder client object at various versions."""
def setUp(self):
super(CinderClientTestCase, self).setUp()
cinder.reset_globals()
self.ctxt = context.RequestContext('fake-user', 'fake-project')
# Mock out the keystoneauth stuff.
self.mock_session = mock.Mock(
autospec='keystoneauth1.loading.session.Session')
load_session = mock.patch('keystoneauth1.loading.'
'load_session_from_conf_options',
return_value=self.mock_session).start()
self.addCleanup(load_session.stop)
@mock.patch('cinderclient.client.get_volume_api_from_url',
return_value='3')
def test_create_v3_client_no_microversion(self, get_volume_api):
"""Tests that creating a v3 client, which is the default, and without
specifying a microversion will default to 3.0 as the version to use.
"""
client = cinder.cinderclient(self.ctxt)
self.assertEqual(cinder_api_versions.APIVersion('3.0'),
client.api_version)
get_volume_api.assert_called_once_with(
self.mock_session.get_endpoint.return_value)
@mock.patch('cinderclient.client.get_volume_api_from_url',
return_value='2')
def test_create_v2_client_with_microversion_fails(self, get_volume_api):
"""Tests that requesting a microversion against a v2 client will raise
an exception.
"""
self.assertRaises(exception.CinderAPIVersionNotAvailable,
cinder.cinderclient, self.ctxt, microversion='3.27')
get_volume_api.assert_called_once_with(
self.mock_session.get_endpoint.return_value)
@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):
"""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.
"""
self.assertRaises(exception.CinderAPIVersionNotAvailable,
cinder.cinderclient, self.ctxt, microversion='3.27')
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)
@mock.patch('cinderclient.client.get_highest_client_server_version',
return_value=float(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,
get_volume_api,
get_highest_version):
"""Tests that creating a v3 client and requesting a microversion that
is available in the server and supported by the client will result in
creating a Client object with the requested microversion.
"""
client = cinder.cinderclient(self.ctxt, microversion='3.27')
self.assertEqual(cinder_api_versions.APIVersion('3.27'),
client.api_version)
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)

View File

@ -23,6 +23,7 @@ import copy
import functools
import sys
from cinderclient import api_versions as cinder_api_versions
from cinderclient import client as cinder_client
from cinderclient import exceptions as cinder_exception
from keystoneauth1 import exceptions as keystone_exception
@ -56,7 +57,37 @@ def reset_globals():
_SESSION = None
def cinderclient(context):
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 cinderclient(context, microversion=None):
"""Constructs a cinder client object for making API requests.
:param context: The nova request context for auth.
:param microversion: Optional microversion to check against the client.
This implies that Cinder v3 is required for any calls that require a
microversion. If the microversion is not available, this method will
raise an CinderAPIVersionNotAvailable exception.
"""
global _SESSION
if not _SESSION:
@ -89,13 +120,18 @@ def cinderclient(context):
raise exception.UnsupportedCinderAPIVersion(version=version)
if version == '2':
if microversion is not None:
# The Cinder v2 API does not support microversions.
raise exception.CinderAPIVersionNotAvailable(version=microversion)
LOG.warning("The support for the Cinder API v2 is deprecated, please "
"upgrade to Cinder API v3.")
if version == '3':
# TODO(ildikov): Add microversion support for picking up the new
# attach/detach API that was added in 3.27.
version = '3.0'
# Check to see a specific microversion is requested and if so, can it
# be handled by the backing server.
if microversion is not None:
version = _check_microversion(url, microversion)
return cinder_client.Client(version,
session=_SESSION,