Update detach_volume() with versionedobjects
The following patch updates detach_volume API to use volume versionedobjects. Changes were made to be backwards compatible with older RPC clients. It only includes changes to the core cinder code. Changes in the drivers are left to each driver maintainer to update. We want to update both the attach_volume() and detach_volume() with versionedobjects. There are 3 patches in the chain. The patch sequence is: - Update attach_volume() with versionedojbects - Update detach_volume() with versionedobjects - Update test cases related to attach_volume and detach_volume in file cinder/tests/unit/test_volume.py Co-Authored-By: Thang Pham <thang.g.pham@gmail.com> Co-Authored-By: lisali <xiaoyan.li@intel.com> Partial-Implements: blueprint cinder-objects Change-Id: Ib153342b20f5b3e74d2815cb422b411f5690443f
This commit is contained in:
parent
f0998f72b6
commit
324fae75e2
@ -1608,44 +1608,50 @@ def volume_detached(context, volume_id, attachment_id):
|
||||
"""
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
attachment = None
|
||||
try:
|
||||
attachment = volume_attachment_get(context, attachment_id,
|
||||
session=session)
|
||||
except exception.VolumeAttachmentNotFound:
|
||||
pass
|
||||
attachment_updates = None
|
||||
attachment = None
|
||||
|
||||
# If this is already detached, attachment will be None
|
||||
if attachment:
|
||||
now = timeutils.utcnow()
|
||||
attachment['attach_status'] = fields.VolumeAttachStatus.DETACHED
|
||||
attachment['detach_time'] = now
|
||||
attachment['deleted'] = True
|
||||
attachment['deleted_at'] = now
|
||||
attachment_updates = {
|
||||
'attach_status': fields.VolumeAttachStatus.DETACHED,
|
||||
'detach_time': now,
|
||||
'deleted': True,
|
||||
'deleted_at': now,
|
||||
'updated_at':
|
||||
literal_column('updated_at'),
|
||||
}
|
||||
attachment.update(attachment_updates)
|
||||
attachment.save(session=session)
|
||||
del attachment_updates['updated_at']
|
||||
|
||||
attachment_list = volume_attachment_get_all_by_volume_id(
|
||||
context, volume_id, session=session)
|
||||
remain_attachment = False
|
||||
if attachment_list and len(attachment_list) > 0:
|
||||
remain_attachment = True
|
||||
|
||||
volume_ref = _volume_get(context, volume_id, session=session)
|
||||
if not remain_attachment:
|
||||
volume_ref = _volume_get(context, volume_id,
|
||||
session=session)
|
||||
volume_updates = {'updated_at': literal_column('updated_at')}
|
||||
if not volume_ref.volume_attachment:
|
||||
# Hide status update from user if we're performing volume migration
|
||||
# or uploading it to image
|
||||
if ((not volume_ref['migration_status'] and
|
||||
not (volume_ref['status'] == 'uploading')) or
|
||||
volume_ref['migration_status'] in ('success', 'error')):
|
||||
volume_ref['status'] = 'available'
|
||||
if ((not volume_ref.migration_status and
|
||||
not (volume_ref.status == 'uploading')) or
|
||||
volume_ref.migration_status in ('success', 'error')):
|
||||
volume_updates['status'] = 'available'
|
||||
|
||||
volume_ref['attach_status'] = fields.VolumeAttachStatus.DETACHED
|
||||
volume_ref.save(session=session)
|
||||
volume_updates['attach_status'] = (
|
||||
fields.VolumeAttachStatus.DETACHED)
|
||||
else:
|
||||
# Volume is still attached
|
||||
volume_ref['status'] = 'in-use'
|
||||
volume_ref['attach_status'] = fields.VolumeAttachStatus.ATTACHED
|
||||
volume_ref.save(session=session)
|
||||
volume_updates['status'] = 'in-use'
|
||||
volume_updates['attach_status'] = (
|
||||
fields.VolumeAttachStatus.ATTACHED)
|
||||
|
||||
volume_ref.update(volume_updates)
|
||||
volume_ref.save(session=session)
|
||||
del volume_updates['updated_at']
|
||||
return (volume_updates, attachment_updates)
|
||||
|
||||
|
||||
@require_context
|
||||
|
@ -517,6 +517,25 @@ class Volume(cleanable.CinderCleanableObject, base.CinderObject,
|
||||
self.save()
|
||||
return attachment
|
||||
|
||||
def finish_detach(self, attachment_id):
|
||||
with self.obj_as_admin():
|
||||
volume_updates, attachment_updates = (
|
||||
db.volume_detached(self._context, self.id, attachment_id))
|
||||
db.volume_admin_metadata_delete(self._context, self.id,
|
||||
'attached_mode')
|
||||
self.admin_metadata.pop('attached_mode', None)
|
||||
# Remove attachment in volume only when this field is loaded.
|
||||
if attachment_updates and self.obj_attr_is_set('volume_attachment'):
|
||||
for i, attachment in enumerate(self.volume_attachment):
|
||||
if attachment.id == attachment_id:
|
||||
del self.volume_attachment.objects[i]
|
||||
break
|
||||
|
||||
self.update(volume_updates)
|
||||
self.obj_reset_changes(
|
||||
list(volume_updates.keys()) +
|
||||
['volume_attachment', 'admin_metadata'])
|
||||
|
||||
|
||||
@base.CinderObjectRegistry.register
|
||||
class VolumeList(base.ObjectListBase, base.CinderObject):
|
||||
|
@ -435,6 +435,73 @@ class TestVolume(test_objects.BaseObjectsTestCase):
|
||||
True)
|
||||
self.assertEqual('rw', volume.admin_metadata['attached_mode'])
|
||||
|
||||
@mock.patch('cinder.db.volume_admin_metadata_delete')
|
||||
@mock.patch('cinder.db.sqlalchemy.api.volume_detached')
|
||||
@mock.patch('cinder.objects.volume_attachment.VolumeAttachmentList.'
|
||||
'get_all_by_volume_id')
|
||||
def test_volume_detached_with_attachment(
|
||||
self, volume_attachment_get,
|
||||
volume_detached,
|
||||
metadata_delete):
|
||||
va_objs = [objects.VolumeAttachment(context=self.context, id=i)
|
||||
for i in [fake.OBJECT_ID, fake.OBJECT2_ID, fake.OBJECT3_ID]]
|
||||
# As changes are not saved, we need reset it here. Later changes
|
||||
# will be checked.
|
||||
for obj in va_objs:
|
||||
obj.obj_reset_changes()
|
||||
va_list = objects.VolumeAttachmentList(context=self.context,
|
||||
objects=va_objs)
|
||||
va_list.obj_reset_changes()
|
||||
volume_attachment_get.return_value = va_list
|
||||
admin_context = context.get_admin_context()
|
||||
volume = fake_volume.fake_volume_obj(
|
||||
admin_context,
|
||||
volume_attachment=va_list,
|
||||
volume_admin_metadata=[{'key': 'attached_mode',
|
||||
'value': 'rw'}])
|
||||
self.assertEqual(3, len(volume.volume_attachment))
|
||||
volume_detached.return_value = ({'status': 'in-use'},
|
||||
{'attached_mode': 'rw'})
|
||||
with mock.patch.object(admin_context, 'elevated') as mock_elevated:
|
||||
mock_elevated.return_value = admin_context
|
||||
volume.finish_detach(fake.OBJECT_ID)
|
||||
volume_detached.assert_called_once_with(admin_context,
|
||||
volume.id,
|
||||
fake.OBJECT_ID)
|
||||
metadata_delete.assert_called_once_with(admin_context,
|
||||
volume.id,
|
||||
'attached_mode')
|
||||
self.assertEqual('in-use', volume.status)
|
||||
self.assertEqual({}, volume.cinder_obj_get_changes())
|
||||
self.assertEqual(2, len(volume.volume_attachment))
|
||||
self.assertIsNone(volume.admin_metadata.get('attached_mode'))
|
||||
|
||||
@mock.patch('cinder.db.volume_admin_metadata_delete')
|
||||
@mock.patch('cinder.db.sqlalchemy.api.volume_detached')
|
||||
@mock.patch('cinder.objects.volume_attachment.VolumeAttachmentList.'
|
||||
'get_all_by_volume_id')
|
||||
def test_volume_detached_without_attachment(
|
||||
self, volume_attachment_get, volume_detached, metadata_delete):
|
||||
admin_context = context.get_admin_context()
|
||||
volume = fake_volume.fake_volume_obj(
|
||||
admin_context,
|
||||
volume_admin_metadata=[{'key': 'attached_mode',
|
||||
'value': 'rw'}])
|
||||
self.assertFalse(volume.obj_attr_is_set('volume_attachment'))
|
||||
volume_detached.return_value = ({'status': 'in-use'}, None)
|
||||
with mock.patch.object(admin_context, 'elevated') as mock_elevated:
|
||||
mock_elevated.return_value = admin_context
|
||||
volume.finish_detach(fake.OBJECT_ID)
|
||||
metadata_delete.assert_called_once_with(admin_context,
|
||||
volume.id,
|
||||
'attached_mode')
|
||||
volume_detached.assert_called_once_with(admin_context,
|
||||
volume.id,
|
||||
fake.OBJECT_ID)
|
||||
self.assertEqual('in-use', volume.status)
|
||||
self.assertEqual({}, volume.cinder_obj_get_changes())
|
||||
self.assertFalse(volume_attachment_get.called)
|
||||
|
||||
|
||||
class TestVolumeList(test_objects.BaseObjectsTestCase):
|
||||
@mock.patch('cinder.db.volume_get_all')
|
||||
|
@ -387,33 +387,101 @@ class DBAPIVolumeTestCase(BaseTest):
|
||||
'instance_uuid': instance_uuid,
|
||||
'attach_status': fields.VolumeAttachStatus.ATTACHING, }
|
||||
attachment = db.volume_attach(self.ctxt, values)
|
||||
db.volume_attached(self.ctxt, attachment['id'],
|
||||
db.volume_attached(self.ctxt, attachment.id,
|
||||
instance_uuid,
|
||||
None, '/tmp')
|
||||
db.volume_detached(self.ctxt, volume['id'], attachment['id'])
|
||||
volume = db.volume_get(self.ctxt, volume['id'])
|
||||
volume_updates, attachment_updates = (
|
||||
db.volume_detached(self.ctxt, volume.id, attachment.id))
|
||||
expected_attachment = {
|
||||
'attach_status': fields.VolumeAttachStatus.DETACHED,
|
||||
'detach_time': mock.ANY,
|
||||
'deleted': True,
|
||||
'deleted_at': mock.ANY, }
|
||||
self.assertDictEqual(expected_attachment, attachment_updates)
|
||||
expected_volume = {
|
||||
'status': 'available',
|
||||
'attach_status': fields.VolumeAttachStatus.DETACHED, }
|
||||
self.assertDictEqual(expected_volume, volume_updates)
|
||||
volume = db.volume_get(self.ctxt, volume.id)
|
||||
self.assertRaises(exception.VolumeAttachmentNotFound,
|
||||
db.volume_attachment_get,
|
||||
self.ctxt,
|
||||
attachment['id'])
|
||||
self.assertEqual('available', volume['status'])
|
||||
attachment.id)
|
||||
self.assertEqual('available', volume.status)
|
||||
|
||||
def test_volume_detached_two_attachments(self):
|
||||
volume = db.volume_create(self.ctxt, {})
|
||||
instance_uuid = fake.INSTANCE_ID
|
||||
values = {'volume_id': volume.id,
|
||||
'instance_uuid': instance_uuid,
|
||||
'attach_status': fields.VolumeAttachStatus.ATTACHING, }
|
||||
attachment = db.volume_attach(self.ctxt, values)
|
||||
values2 = {'volume_id': volume.id,
|
||||
'instance_uuid': fake.OBJECT_ID,
|
||||
'attach_status': fields.VolumeAttachStatus.ATTACHING, }
|
||||
db.volume_attach(self.ctxt, values2)
|
||||
db.volume_attached(self.ctxt, attachment.id,
|
||||
instance_uuid,
|
||||
None, '/tmp')
|
||||
volume_updates, attachment_updates = (
|
||||
db.volume_detached(self.ctxt, volume.id, attachment.id))
|
||||
expected_attachment = {
|
||||
'attach_status': fields.VolumeAttachStatus.DETACHED,
|
||||
'detach_time': mock.ANY,
|
||||
'deleted': True,
|
||||
'deleted_at': mock.ANY, }
|
||||
self.assertDictEqual(expected_attachment, attachment_updates)
|
||||
expected_volume = {
|
||||
'status': 'in-use',
|
||||
'attach_status': fields.VolumeAttachStatus.ATTACHED, }
|
||||
self.assertDictEqual(expected_volume, volume_updates)
|
||||
volume = db.volume_get(self.ctxt, volume.id)
|
||||
self.assertRaises(exception.VolumeAttachmentNotFound,
|
||||
db.volume_attachment_get,
|
||||
self.ctxt,
|
||||
attachment.id)
|
||||
self.assertEqual('in-use', volume.status)
|
||||
|
||||
def test_volume_detached_invalid_attachment(self):
|
||||
volume = db.volume_create(self.ctxt, {})
|
||||
# detach it again
|
||||
volume_updates, attachment_updates = (
|
||||
db.volume_detached(self.ctxt, volume.id, fake.ATTACHMENT_ID))
|
||||
self.assertIsNone(attachment_updates)
|
||||
expected_volume = {
|
||||
'status': 'available',
|
||||
'attach_status': fields.VolumeAttachStatus.DETACHED, }
|
||||
self.assertDictEqual(expected_volume, volume_updates)
|
||||
volume = db.volume_get(self.ctxt, volume.id)
|
||||
self.assertEqual('available', volume.status)
|
||||
|
||||
def test_volume_detached_from_host(self):
|
||||
volume = db.volume_create(self.ctxt, {})
|
||||
host_name = 'fake_host'
|
||||
values = {'volume_id': volume['id'],
|
||||
values = {'volume_id': volume.id,
|
||||
'attach_host': host_name,
|
||||
'attach_status': fields.VolumeAttachStatus.ATTACHING, }
|
||||
attachment = db.volume_attach(self.ctxt, values)
|
||||
db.volume_attached(self.ctxt, attachment['id'],
|
||||
db.volume_attached(self.ctxt, attachment.id,
|
||||
None, host_name, '/tmp')
|
||||
db.volume_detached(self.ctxt, volume['id'], attachment['id'])
|
||||
volume = db.volume_get(self.ctxt, volume['id'])
|
||||
volume_updates, attachment_updates = (
|
||||
db.volume_detached(self.ctxt, volume.id, attachment.id))
|
||||
expected_attachment = {
|
||||
'attach_status': fields.VolumeAttachStatus.DETACHED,
|
||||
'detach_time': mock.ANY,
|
||||
'deleted': True,
|
||||
'deleted_at': mock.ANY}
|
||||
self.assertDictEqual(expected_attachment, attachment_updates)
|
||||
expected_volume = {
|
||||
'status': 'available',
|
||||
'attach_status': fields.VolumeAttachStatus.DETACHED, }
|
||||
self.assertDictEqual(expected_volume, volume_updates)
|
||||
volume = db.volume_get(self.ctxt, volume.id)
|
||||
self.assertRaises(exception.VolumeAttachmentNotFound,
|
||||
db.volume_attachment_get,
|
||||
self.ctxt,
|
||||
attachment['id'])
|
||||
self.assertEqual('available', volume['status'])
|
||||
attachment.id)
|
||||
self.assertEqual('available', volume.status)
|
||||
|
||||
def test_volume_get(self):
|
||||
volume = db.volume_create(self.ctxt, {})
|
||||
|
@ -2087,7 +2087,7 @@ class VolumeTestCase(base.BaseVolumeTestCase):
|
||||
with volume.obj_as_admin():
|
||||
volume.admin_metadata['readonly'] = True
|
||||
volume.save()
|
||||
volume_id = volume['id']
|
||||
volume_id = volume.id
|
||||
self.volume.create_volume(self.user_context,
|
||||
volume=volume)
|
||||
volume_passed = volume if volume_object else None
|
||||
@ -2120,10 +2120,12 @@ class VolumeTestCase(base.BaseVolumeTestCase):
|
||||
self.assertRaises(exception.VolumeAttached,
|
||||
self.volume.delete_volume,
|
||||
self.context,
|
||||
volume)
|
||||
self.volume.detach_volume(self.context, volume_id, attachment['id'])
|
||||
vol = db.volume_get(self.context, volume_id)
|
||||
self.assertEqual('available', vol['status'])
|
||||
volume=volume)
|
||||
self.volume.detach_volume(self.context, volume_id,
|
||||
attachment.id,
|
||||
volume=volume_passed)
|
||||
vol = objects.Volume.get_by_id(self.context, volume_id)
|
||||
self.assertEqual('available', vol.status)
|
||||
|
||||
self.volume.delete_volume(self.context, volume)
|
||||
self.assertRaises(exception.VolumeNotFound,
|
||||
|
@ -439,20 +439,26 @@ class VolumeRpcAPITestCase(test.TestCase):
|
||||
mode='rw',
|
||||
version=version)
|
||||
|
||||
def test_detach_volume(self):
|
||||
@ddt.data('3.0', '3.4')
|
||||
@mock.patch('oslo_messaging.RPCClient.can_send_version')
|
||||
def test_detach_volume(self, version, can_send_version):
|
||||
can_send_version.return_value = (version == '3.4')
|
||||
self._test_volume_api('detach_volume',
|
||||
rpc_method='call',
|
||||
volume=self.fake_volume_obj,
|
||||
attachment_id='fake_uuid',
|
||||
version="3.0")
|
||||
version=version)
|
||||
|
||||
def test_detach_volume_cluster(self):
|
||||
@ddt.data('3.0', '3.4')
|
||||
@mock.patch('oslo_messaging.RPCClient.can_send_version')
|
||||
def test_detach_volume_cluster(self, version, can_send_version):
|
||||
can_send_version.return_value = (version == '3.4')
|
||||
self._set_cluster()
|
||||
self._test_volume_api('detach_volume',
|
||||
rpc_method='call',
|
||||
volume=self.fake_volume_obj,
|
||||
attachment_id='fake_uuid',
|
||||
version="3.0")
|
||||
version=version)
|
||||
|
||||
def test_copy_volume_to_image(self):
|
||||
self._test_volume_api('copy_volume_to_image',
|
||||
|
@ -1022,29 +1022,33 @@ class VolumeManager(manager.CleanableManager,
|
||||
return attachment
|
||||
|
||||
@coordination.synchronized('{volume_id}-{f_name}')
|
||||
def detach_volume(self, context, volume_id, attachment_id=None):
|
||||
def detach_volume(self, context, volume_id, attachment_id=None,
|
||||
volume=None):
|
||||
"""Updates db to show volume is detached."""
|
||||
# TODO(vish): refactor this into a more general "unreserve"
|
||||
volume = self.db.volume_get(context, volume_id)
|
||||
attachment = None
|
||||
# FIXME(lixiaoy1): Remove this in v4.0 of RPC API.
|
||||
if volume is None:
|
||||
# For older clients, mimic the old behavior and look up the volume
|
||||
# by its volume_id.
|
||||
volume = objects.Volume.get_by_id(context, volume_id)
|
||||
|
||||
if attachment_id:
|
||||
try:
|
||||
attachment = self.db.volume_attachment_get(context,
|
||||
attachment_id)
|
||||
attachment = objects.VolumeAttachment.get_by_id(context,
|
||||
attachment_id)
|
||||
except exception.VolumeAttachmentNotFound:
|
||||
LOG.info(_LI("Volume detach called, but volume not attached."),
|
||||
resource=volume)
|
||||
# We need to make sure the volume status is set to the correct
|
||||
# status. It could be in detaching status now, and we don't
|
||||
# want to leave it there.
|
||||
self.db.volume_detached(context, volume_id, attachment_id)
|
||||
volume.finish_detach(attachment_id)
|
||||
return
|
||||
else:
|
||||
# We can try and degrade gracefully here by trying to detach
|
||||
# a volume without the attachment_id here if the volume only has
|
||||
# one attachment. This is for backwards compatibility.
|
||||
attachments = self.db.volume_attachment_get_all_by_volume_id(
|
||||
context, volume_id)
|
||||
attachments = volume.volume_attachment
|
||||
if len(attachments) > 1:
|
||||
# There are more than 1 attachments for this volume
|
||||
# we have to have an attachment id.
|
||||
@ -1059,10 +1063,9 @@ class VolumeManager(manager.CleanableManager,
|
||||
# so set the status to available and move on.
|
||||
LOG.info(_LI("Volume detach called, but volume not attached."),
|
||||
resource=volume)
|
||||
self.db.volume_update(
|
||||
context, volume_id, {
|
||||
'status': 'available',
|
||||
'attach_status': fields.VolumeAttachStatus.DETACHED})
|
||||
volume.status = 'available'
|
||||
volume.attach_status = fields.VolumeAttachStatus.DETACHED
|
||||
volume.save()
|
||||
return
|
||||
|
||||
self._notify_about_volume_usage(context, volume, "detach.start")
|
||||
@ -1092,7 +1095,6 @@ class VolumeManager(manager.CleanableManager,
|
||||
|
||||
# We're going to remove the export here
|
||||
# (delete the iscsi target)
|
||||
volume = self.db.volume_get(context, volume_id)
|
||||
try:
|
||||
utils.require_driver_initialized(self.driver)
|
||||
self.driver.remove_export(context.elevated(), volume)
|
||||
@ -1108,11 +1110,7 @@ class VolumeManager(manager.CleanableManager,
|
||||
raise exception.RemoveExportException(volume=volume_id,
|
||||
reason=six.text_type(ex))
|
||||
|
||||
self.db.volume_detached(context.elevated(), volume_id,
|
||||
attachment.get('id'))
|
||||
self.db.volume_admin_metadata_delete(context.elevated(), volume_id,
|
||||
'attached_mode')
|
||||
|
||||
volume.finish_detach(attachment.id)
|
||||
self._notify_about_volume_usage(context, volume, "detach.end")
|
||||
LOG.info(_LI("Detach volume completed successfully."), resource=volume)
|
||||
|
||||
|
@ -114,9 +114,10 @@ class VolumeAPI(rpc.RPCAPI):
|
||||
3.2 - Adds support for sending objects over RPC in
|
||||
get_backup_device().
|
||||
3.3 - Adds support for sending objects over RPC in attach_volume().
|
||||
3.4 - Adds support for sending objects over RPC in detach_volume().
|
||||
"""
|
||||
|
||||
RPC_API_VERSION = '3.3'
|
||||
RPC_API_VERSION = '3.4'
|
||||
RPC_DEFAULT_VERSION = '3.0'
|
||||
TOPIC = constants.VOLUME_TOPIC
|
||||
BINARY = 'cinder-volume'
|
||||
@ -201,9 +202,13 @@ class VolumeAPI(rpc.RPCAPI):
|
||||
return cctxt.call(ctxt, 'attach_volume', **msg_args)
|
||||
|
||||
def detach_volume(self, ctxt, volume, attachment_id):
|
||||
cctxt = self._get_cctxt(volume.service_topic_queue)
|
||||
return cctxt.call(ctxt, 'detach_volume', volume_id=volume.id,
|
||||
attachment_id=attachment_id)
|
||||
msg_args = {'volume_id': volume.id,
|
||||
'attachment_id': attachment_id,
|
||||
'volume': volume}
|
||||
cctxt = self._get_cctxt(volume.service_topic_queue, ('3.4', '3.0'))
|
||||
if not self.client.can_send_version('3.4'):
|
||||
msg_args.pop('volume')
|
||||
return cctxt.call(ctxt, 'detach_volume', **msg_args)
|
||||
|
||||
def copy_volume_to_image(self, ctxt, volume, image_meta):
|
||||
cctxt = self._get_cctxt(volume['host'])
|
||||
|
Loading…
Reference in New Issue
Block a user