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:
parent
6c58884030
commit
d655179182
@ -149,6 +149,13 @@ class UnsupportedCinderAPIVersion(NovaException):
|
|||||||
msg_fmt = _('Nova does not support Cinder API version %(version)s')
|
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):
|
class Forbidden(NovaException):
|
||||||
msg_fmt = _("Forbidden")
|
msg_fmt = _("Forbidden")
|
||||||
code = 403
|
code = 403
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from cinderclient import api_versions as cinder_api_versions
|
||||||
from cinderclient import exceptions as cinder_exception
|
from cinderclient import exceptions as cinder_exception
|
||||||
from keystoneclient import exceptions as keystone_exception
|
from keystoneclient import exceptions as keystone_exception
|
||||||
import mock
|
import mock
|
||||||
@ -717,3 +718,79 @@ class CinderApiTestCase(test.NoDBTestCase):
|
|||||||
my_func.side_effect = raised_exc
|
my_func.side_effect = raised_exc
|
||||||
|
|
||||||
self.assertRaises(expected_exc, wrapper(my_func), 'foo', 'bar', 'baz')
|
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)
|
||||||
|
@ -23,6 +23,7 @@ import copy
|
|||||||
import functools
|
import functools
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from cinderclient import api_versions as cinder_api_versions
|
||||||
from cinderclient import client as cinder_client
|
from cinderclient import client as cinder_client
|
||||||
from cinderclient import exceptions as cinder_exception
|
from cinderclient import exceptions as cinder_exception
|
||||||
from keystoneauth1 import exceptions as keystone_exception
|
from keystoneauth1 import exceptions as keystone_exception
|
||||||
@ -56,7 +57,37 @@ def reset_globals():
|
|||||||
_SESSION = None
|
_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
|
global _SESSION
|
||||||
|
|
||||||
if not _SESSION:
|
if not _SESSION:
|
||||||
@ -89,13 +120,18 @@ def cinderclient(context):
|
|||||||
raise exception.UnsupportedCinderAPIVersion(version=version)
|
raise exception.UnsupportedCinderAPIVersion(version=version)
|
||||||
|
|
||||||
if version == '2':
|
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 "
|
LOG.warning("The support for the Cinder API v2 is deprecated, please "
|
||||||
"upgrade to Cinder API v3.")
|
"upgrade to Cinder API v3.")
|
||||||
|
|
||||||
if version == '3':
|
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'
|
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,
|
return cinder_client.Client(version,
|
||||||
session=_SESSION,
|
session=_SESSION,
|
||||||
|
Loading…
Reference in New Issue
Block a user