Cinder introduced new attachment API flow in microversion 3.27 (also attachment_complete added in mv 3.44 and support for passing mode added in mv 3.54) which provides a clean interface to interact with cinder for attachments and is also required for multiattach volume support (Related future work). Nova uses it since a long time and is proven to be stable, this patch implements the same for glance. The create volume and delete volume calls are also moved to cinder_utils file to use the generic exception handler and keep similar code together for consistency. Partially Implements: blueprint attachment-api-and-multiattach-support Change-Id: I2758ed1d5b8e0981faa3eff6f83e1ce5975a01d2changes/00/782200/21
parent
98b4a0d4e7
commit
1178f113c4
@ -0,0 +1,194 @@
|
||||
# Copyright 2021 RedHat Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from cinderclient.apiclient import exceptions as apiclient_exception
|
||||
from cinderclient import exceptions as cinder_exception
|
||||
from keystoneauth1 import exceptions as keystone_exc
|
||||
from oslo_utils import excutils
|
||||
import retrying
|
||||
|
||||
from glance_store import exceptions
|
||||
from glance_store.i18n import _LE
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def handle_exceptions(method):
|
||||
"""Transforms the exception for the volume but keeps its traceback intact.
|
||||
"""
|
||||
def wrapper(self, ctx, volume_id, *args, **kwargs):
|
||||
try:
|
||||
res = method(self, ctx, volume_id, *args, **kwargs)
|
||||
except (keystone_exc.NotFound,
|
||||
cinder_exception.NotFound,
|
||||
cinder_exception.OverLimit) as e:
|
||||
raise exceptions.BackendException(str(e))
|
||||
return res
|
||||
return wrapper
|
||||
|
||||
|
||||
def _retry_on_internal_server_error(e):
|
||||
if isinstance(e, apiclient_exception.InternalServerError):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class API(object):
|
||||
"""API for interacting with the cinder."""
|
||||
|
||||
@handle_exceptions
|
||||
def create(self, client, size, name,
|
||||
volume_type=None, metadata=None):
|
||||
|
||||
kwargs = dict(volume_type=volume_type,
|
||||
metadata=metadata,
|
||||
name=name)
|
||||
|
||||
volume = client.volumes.create(size, **kwargs)
|
||||
return volume
|
||||
|
||||
@handle_exceptions
|
||||
def delete(self, client, volume_id):
|
||||
client.volumes.delete(volume_id)
|
||||
|
||||
@handle_exceptions
|
||||
def attachment_create(self, client, volume_id, connector=None,
|
||||
mountpoint=None, mode=None):
|
||||
"""Create a volume attachment. This requires microversion >= 3.54.
|
||||
|
||||
The attachment_create call was introduced in microversion 3.27. We
|
||||
need 3.54 as minimum here as we need attachment_complete to finish the
|
||||
attaching process and it which was introduced in version 3.44 and
|
||||
we also pass the attach mode which was introduced in version 3.54.
|
||||
|
||||
:param client: cinderclient object
|
||||
:param volume_id: UUID of the volume on which to create the attachment.
|
||||
:param connector: host connector dict; if None, the attachment will
|
||||
be 'reserved' but not yet attached.
|
||||
:param mountpoint: Optional mount device name for the attachment,
|
||||
e.g. "/dev/vdb". This is only used if a connector is provided.
|
||||
:param mode: The mode in which the attachment is made i.e.
|
||||
read only(ro) or read/write(rw)
|
||||
:returns: a dict created from the
|
||||
cinderclient.v3.attachments.VolumeAttachment object with a backward
|
||||
compatible connection_info dict
|
||||
"""
|
||||
if connector and mountpoint and 'mountpoint' not in connector:
|
||||
connector['mountpoint'] = mountpoint
|
||||
|
||||
try:
|
||||
attachment_ref = client.attachments.create(
|
||||
volume_id, connector, mode=mode)
|
||||
return attachment_ref
|
||||
except cinder_exception.ClientException as ex:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error(_LE('Create attachment failed for volume '
|
||||
'%(volume_id)s. Error: %(msg)s Code: %(code)s'),
|
||||
{'volume_id': volume_id,
|
||||
'msg': str(ex),
|
||||
'code': getattr(ex, 'code', None)})
|
||||
|
||||
@handle_exceptions
|
||||
def attachment_get(self, client, attachment_id):
|
||||
"""Gets a volume attachment.
|
||||
|
||||
:param client: cinderclient object
|
||||
:param attachment_id: UUID of the volume attachment to get.
|
||||
:returns: a dict created from the
|
||||
cinderclient.v3.attachments.VolumeAttachment object with a backward
|
||||
compatible connection_info dict
|
||||
"""
|
||||
try:
|
||||
attachment_ref = client.attachments.show(
|
||||
attachment_id)
|
||||
return attachment_ref
|
||||
except cinder_exception.ClientException as ex:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error(_LE('Show attachment failed for attachment '
|
||||
'%(id)s. Error: %(msg)s Code: %(code)s'),
|
||||
{'id': attachment_id,
|
||||
'msg': str(ex),
|
||||
'code': getattr(ex, 'code', None)})
|
||||
|
||||
@handle_exceptions
|
||||
def attachment_update(self, client, attachment_id, connector,
|
||||
mountpoint=None):
|
||||
"""Updates the connector on the volume attachment. An attachment
|
||||
without a connector is considered reserved but not fully attached.
|
||||
|
||||
:param client: cinderclient object
|
||||
:param attachment_id: UUID of the volume attachment to update.
|
||||
:param connector: host connector dict. This is required when updating
|
||||
a volume attachment. To terminate a connection, the volume
|
||||
attachment for that connection must be deleted.
|
||||
:param mountpoint: Optional mount device name for the attachment,
|
||||
e.g. "/dev/vdb". Theoretically this is optional per volume backend,
|
||||
but in practice it's normally required so it's best to always
|
||||
provide a value.
|
||||
:returns: a dict created from the
|
||||
cinderclient.v3.attachments.VolumeAttachment object with a backward
|
||||
compatible connection_info dict
|
||||
"""
|
||||
if mountpoint and 'mountpoint' not in connector:
|
||||
connector['mountpoint'] = mountpoint
|
||||
|
||||
try:
|
||||
attachment_ref = client.attachments.update(
|
||||
attachment_id, connector)
|
||||
return attachment_ref
|
||||
except cinder_exception.ClientException as ex:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error(_LE('Update attachment failed for attachment '
|
||||
'%(id)s. Error: %(msg)s Code: %(code)s'),
|
||||
{'id': attachment_id,
|
||||
'msg': str(ex),
|
||||
'code': getattr(ex, 'code', None)})
|
||||
|
||||
@handle_exceptions
|
||||
def attachment_complete(self, client, attachment_id):
|
||||
"""Marks a volume attachment complete.
|
||||
|
||||
This call should be used to inform Cinder that a volume attachment is
|
||||
fully connected on the host so Cinder can apply the necessary state
|
||||
changes to the volume info in its database.
|
||||
|
||||
:param client: cinderclient object
|
||||
:param attachment_id: UUID of the volume attachment to update.
|
||||
"""
|
||||
try:
|
||||
client.attachments.complete(attachment_id)
|
||||
except cinder_exception.ClientException as ex:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error(_LE('Complete attachment failed for attachment '
|
||||
'%(id)s. Error: %(msg)s Code: %(code)s'),
|
||||
{'id': attachment_id,
|
||||
'msg': str(ex),
|
||||
'code': getattr(ex, 'code', None)})
|
||||
|
||||
@handle_exceptions
|
||||
@retrying.retry(stop_max_attempt_number=5,
|
||||
retry_on_exception=_retry_on_internal_server_error)
|
||||
def attachment_delete(self, client, attachment_id):
|
||||
try:
|
||||
client.attachments.delete(attachment_id)
|
||||
except cinder_exception.ClientException as ex:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error(_LE('Delete attachment failed for attachment '
|
||||
'%(id)s. Error: %(msg)s Code: %(code)s'),
|
||||
{'id': attachment_id,
|
||||
'msg': str(ex),
|
||||
'code': getattr(ex, 'code', None)})
|
@ -0,0 +1,157 @@
|
||||
# Copyright 2021 RedHat Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from unittest import mock
|
||||
import uuid
|
||||
|
||||
from cinderclient.apiclient import exceptions as apiclient_exception
|
||||
from cinderclient import exceptions as cinder_exception
|
||||
from oslo_config import cfg
|
||||
from oslotest import base
|
||||
|
||||
from glance_store.common import cinder_utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class FakeObject(object):
|
||||
def __init__(self, **kwargs):
|
||||
for name, value in kwargs.items():
|
||||
setattr(self, name, value)
|
||||
|
||||
|
||||
class CinderUtilsTestCase(base.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(CinderUtilsTestCase, self).setUp()
|
||||
CONF.register_opt(cfg.DictOpt('enabled_backends'))
|
||||
CONF.set_override('enabled_backends', 'fake:cinder')
|
||||
self.volume_api = cinder_utils.API()
|
||||
self.fake_client = FakeObject(attachments=FakeObject(
|
||||
create=mock.MagicMock(), delete=mock.MagicMock(),
|
||||
complete=mock.MagicMock(), update=mock.MagicMock(),
|
||||
show=mock.MagicMock()))
|
||||
self.fake_vol_id = uuid.uuid4()
|
||||
self.fake_attach_id = uuid.uuid4()
|
||||
self.fake_connector = {
|
||||
'platform': 'x86_64', 'os_type': 'linux', 'ip': 'fake_ip',
|
||||
'host': 'fake_host', 'multipath': False,
|
||||
'initiator': 'fake_initiator', 'do_local_attach': False,
|
||||
'uuid': '3e1a7217-104e-41c1-b177-a37c491129a0',
|
||||
'system uuid': '98755544-c749-40ed-b30a-a1cb27b2a46d',
|
||||
'nqn': 'fake_nqn'}
|
||||
|
||||
def test_attachment_create(self):
|
||||
self.volume_api.attachment_create(self.fake_client, self.fake_vol_id)
|
||||
self.fake_client.attachments.create.assert_called_once_with(
|
||||
self.fake_vol_id, None, mode=None)
|
||||
|
||||
def test_attachment_create_with_connector_and_mountpoint(self):
|
||||
self.volume_api.attachment_create(
|
||||
self.fake_client, self.fake_vol_id,
|
||||
connector=self.fake_connector, mountpoint='fake_mountpoint')
|
||||
self.fake_connector['mountpoint'] = 'fake_mountpoint'
|
||||
self.fake_client.attachments.create.assert_called_once_with(
|
||||
self.fake_vol_id, self.fake_connector, mode=None)
|
||||
|
||||
def test_attachment_create_client_exception(self):
|
||||
self.fake_client.attachments.create.side_effect = (
|
||||
cinder_exception.ClientException(code=1))
|
||||
self.assertRaises(
|
||||
cinder_exception.ClientException,
|
||||
self.volume_api.attachment_create,
|
||||
self.fake_client, self.fake_vol_id)
|
||||
|
||||
def test_attachment_get(self):
|
||||
self.volume_api.attachment_get(self.fake_client, self.fake_attach_id)
|
||||
self.fake_client.attachments.show.assert_called_once_with(
|
||||
self.fake_attach_id)
|
||||
|
||||
def test_attachment_get_client_exception(self):
|
||||
self.fake_client.attachments.show.side_effect = (
|
||||
cinder_exception.ClientException(code=1))
|
||||
self.assertRaises(
|
||||
cinder_exception.ClientException,
|
||||
self.volume_api.attachment_get,
|
||||
self.fake_client, self.fake_attach_id)
|
||||
|
||||
def test_attachment_update(self):
|
||||
self.volume_api.attachment_update(self.fake_client,
|
||||
self.fake_attach_id,
|
||||
self.fake_connector)
|
||||
self.fake_client.attachments.update.assert_called_once_with(
|
||||
self.fake_attach_id, self.fake_connector)
|
||||
|
||||
def test_attachment_update_with_connector_and_mountpoint(self):
|
||||
self.volume_api.attachment_update(
|
||||
self.fake_client, self.fake_attach_id, self.fake_connector,
|
||||
mountpoint='fake_mountpoint')
|
||||
self.fake_connector['mountpoint'] = 'fake_mountpoint'
|
||||
self.fake_client.attachments.update.assert_called_once_with(
|
||||
self.fake_attach_id, self.fake_connector)
|
||||
|
||||
def test_attachment_update_client_exception(self):
|
||||
self.fake_client.attachments.update.side_effect = (
|
||||
cinder_exception.ClientException(code=1))
|
||||
self.assertRaises(
|
||||
cinder_exception.ClientException,
|
||||
self.volume_api.attachment_update,
|
||||
self.fake_client, self.fake_attach_id, self.fake_connector)
|
||||
|
||||
def test_attachment_complete(self):
|
||||
self.volume_api.attachment_complete(self.fake_client,
|
||||
self.fake_attach_id)
|
||||
self.fake_client.attachments.complete.assert_called_once_with(
|
||||
self.fake_attach_id)
|
||||
|
||||
def test_attachment_complete_client_exception(self):
|
||||
self.fake_client.attachments.complete.side_effect = (
|
||||
cinder_exception.ClientException(code=1))
|
||||
self.assertRaises(
|
||||
cinder_exception.ClientException,
|
||||
self.volume_api.attachment_complete,
|
||||
self.fake_client, self.fake_attach_id)
|
||||
|
||||
def test_attachment_delete(self):
|
||||
self.volume_api.attachment_delete(self.fake_client,
|
||||
self.fake_attach_id)
|
||||
self.fake_client.attachments.delete.assert_called_once_with(
|
||||
self.fake_attach_id)
|
||||
|
||||
def test_attachment_delete_client_exception(self):
|
||||
self.fake_client.attachments.delete.side_effect = (
|
||||
cinder_exception.ClientException(code=1))
|
||||
self.assertRaises(
|
||||
cinder_exception.ClientException,
|
||||
self.volume_api.attachment_delete,
|
||||
self.fake_client, self.fake_attach_id)
|
||||
|
||||
def test_attachment_delete_retries(self):
|
||||
# Make delete fail two times and succeed on the third attempt.
|
||||
self.fake_client.attachments.delete.side_effect = [
|
||||
apiclient_exception.InternalServerError(),
|
||||
apiclient_exception.InternalServerError(),
|
||||
lambda aid: 'foo']
|
||||
|
||||
# Make sure we get a clean result.
|
||||
self.assertIsNone(self.volume_api.attachment_delete(
|
||||
self.fake_client, self.fake_attach_id))
|
||||
|
||||
# Assert that we called delete three times due to the retry
|
||||
# decorator.
|
||||
self.fake_client.attachments.delete.assert_has_calls([
|
||||
mock.call(self.fake_attach_id),
|
||||
mock.call(self.fake_attach_id),
|
||||
mock.call(self.fake_attach_id)])
|
Loading…
Reference in new issue