Add new style volume attachment support to block_device.py

This plumbs in the new-style volume attachment support to the
virt driver block device code which does the actual work of
attaching the volume via the driver and completing the attachment
with cinder (the thing that makes the volume status "in-use").

At this point, none of the new flow is exercised outside of tests
because we are not yet setting the attachment_id when attaching
volumes, that comes in a later change to the API.

It's worth noting that when nova creates a volume during boot
from volume for a source_type=blank/image/snapshot BDM, we won't
have an attachment_id for that volume so we'll attach using the
old flow. For now this is OK, but might be something we need to
revisit in the future when we eventually want to remove the old
flow.

The structure here is to separate the old/new attach flows into
separate methods, despite quite a bit of copy/paste code in both
places. A single method using conditionals in different places
is arguably uglier (as seen in the test code) and we could likely
consolidate some of the common code into helper methods later.

Note that test_refresh_connection_info_with_attachment_id is
removed since it's now redundant with test_refresh_connection.

Co-Authored-By: Matt Riedemann <mriedem.os@gmail.com>

Part of blueprint cinder-new-attach-apis

Change-Id: Ib1b6b223c9d04579828d47607006ecd98b472e5a
This commit is contained in:
John Griffith 2017-12-05 18:39:57 -05:00 committed by Matt Riedemann
parent 82de8bc5ac
commit 2d68fbe565
2 changed files with 182 additions and 59 deletions

View File

@ -32,6 +32,10 @@ from nova.volume import cinder
class TestDriverBlockDevice(test.NoDBTestCase):
# This is used to signal if we're dealing with a new style volume
# attachment (Cinder v3.44 flow).
attachment_id = None
driver_classes = {
'swap': driver_block_device.DriverSwapBlockDevice,
'ephemeral': driver_block_device.DriverEphemeralBlockDevice,
@ -101,7 +105,6 @@ class TestDriverBlockDevice(test.NoDBTestCase):
'boot_index': 0})
volume_driver_bdm = {
'attachment_id': None,
'mount_device': '/dev/sda1',
'connection_info': {"fake": "connection_info"},
'delete_on_termination': False,
@ -130,7 +133,6 @@ class TestDriverBlockDevice(test.NoDBTestCase):
'boot_index': -1})
snapshot_driver_bdm = {
'attachment_id': None,
'mount_device': '/dev/sda2',
'connection_info': {"fake": "connection_info"},
'delete_on_termination': True,
@ -159,7 +161,6 @@ class TestDriverBlockDevice(test.NoDBTestCase):
'boot_index': -1})
image_driver_bdm = {
'attachment_id': None,
'mount_device': '/dev/sda2',
'connection_info': {"fake": "connection_info"},
'delete_on_termination': True,
@ -188,7 +189,6 @@ class TestDriverBlockDevice(test.NoDBTestCase):
'boot_index': -1})
blank_driver_bdm = {
'attachment_id': None,
'mount_device': '/dev/sda2',
'connection_info': {"fake": "connection_info"},
'delete_on_termination': True,
@ -222,6 +222,14 @@ class TestDriverBlockDevice(test.NoDBTestCase):
self.blank_bdm = fake_block_device.fake_bdm_object(
self.context, self.blank_bdm_dict)
# Set the attachment_id on our fake class variables which we have
# to do in setUp so that any attachment_id set by a subclass will
# be used properly.
for name in ('volume', 'snapshot', 'image', 'blank'):
for attr in ('%s_bdm', '%s_driver_bdm'):
bdm = getattr(self, attr % name)
bdm['attachment_id'] = self.attachment_id
def test_no_device_raises(self):
for name, cls in self.driver_classes.items():
bdm = fake_block_device.fake_bdm_object(
@ -443,13 +451,20 @@ class TestDriverBlockDevice(test.NoDBTestCase):
fake_volume,
instance=instance).AndRaise(
test.TestingException)
# The @update_db decorator will save any changes.
driver_bdm._bdm_obj.save().AndReturn(None)
return instance, expected_conn_info
self.virt_driver.get_volume_connector(instance).AndReturn(connector)
self.volume_api.initialize_connection(
elevated_context, fake_volume['id'],
connector).AndReturn(connection_info)
if self.attachment_id is None:
self.volume_api.initialize_connection(
elevated_context, fake_volume['id'],
connector).AndReturn(connection_info)
else:
self.volume_api.attachment_update(
elevated_context, self.attachment_id, connector).AndReturn(
{'connection_info': connection_info})
if driver_attach:
encryptors.get_encryption_metadata(
elevated_context, self.volume_api, fake_volume['id'],
@ -468,34 +483,52 @@ class TestDriverBlockDevice(test.NoDBTestCase):
disk_bus=bdm_dict['disk_bus'],
device_type=bdm_dict['device_type'],
encryption=enc_data).AndRaise(test.TestingException)
self.volume_api.terminate_connection(
elevated_context, fake_volume['id'],
connector).AndReturn(None)
if self.attachment_id is None:
self.volume_api.terminate_connection(
elevated_context, fake_volume['id'],
connector).AndReturn(None)
else:
self.volume_api.attachment_delete(
elevated_context, self.attachment_id).AndReturn(None)
# The @update_db decorator will save any changes.
driver_bdm._bdm_obj.save().AndReturn(None)
return instance, expected_conn_info
if volume_attach:
# save updates before marking the volume as in-use
driver_bdm._bdm_obj.save().AndReturn(None)
if not fail_volume_attach:
self.volume_api.attach(elevated_context, fake_volume['id'],
uuids.uuid, bdm_dict['device_name'],
mode=access_mode).AndReturn(None)
if self.attachment_id is None:
self.volume_api.attach(elevated_context, fake_volume['id'],
uuids.uuid, bdm_dict['device_name'],
mode=access_mode).AndReturn(None)
else:
self.volume_api.attachment_complete(
elevated_context, self.attachment_id).AndReturn(None)
else:
self.volume_api.attach(elevated_context, fake_volume['id'],
uuids.uuid, bdm_dict['device_name'],
mode=access_mode).AndRaise(
test.TestingException)
if driver_attach:
self.virt_driver.detach_volume(
if self.attachment_id is None:
self.volume_api.attach(elevated_context, fake_volume['id'],
uuids.uuid, bdm_dict['device_name'],
mode=access_mode).AndRaise(
test.TestingException)
if driver_attach:
self.virt_driver.detach_volume(
expected_conn_info, instance,
bdm_dict['device_name'],
encryption=enc_data).AndReturn(None)
self.volume_api.terminate_connection(
self.volume_api.terminate_connection(
elevated_context, fake_volume['id'],
connector).AndReturn(None)
self.volume_api.detach(elevated_context,
fake_volume['id']).AndReturn(None)
self.volume_api.detach(elevated_context,
fake_volume['id']).AndReturn(None)
else:
self.volume_api.attachment_complete(
elevated_context, self.attachment_id).AndRaise(
test.TestingException)
self.volume_api.attachment_delete(
elevated_context, self.attachment_id).AndReturn(None)
# The @update_db decorator will save any changes.
driver_bdm._bdm_obj.save().AndReturn(None)
return instance, expected_conn_info
@ -648,10 +681,16 @@ class TestDriverBlockDevice(test.NoDBTestCase):
self.mox.StubOutWithMock(test_bdm._bdm_obj, 'save')
self.virt_driver.get_volume_connector(instance).AndReturn(connector)
self.volume_api.initialize_connection(
self.context, test_bdm.volume_id,
connector).AndReturn(connection_info)
if self.attachment_id is None:
self.virt_driver.get_volume_connector(instance).AndReturn(
connector)
self.volume_api.initialize_connection(
self.context, test_bdm.volume_id,
connector).AndReturn(connection_info)
else:
self.volume_api.attachment_get(
self.context, self.attachment_id).AndReturn(
{'connection_info': connection_info})
test_bdm._bdm_obj.save().AndReturn(None)
self.mox.ReplayAll()
@ -661,36 +700,14 @@ class TestDriverBlockDevice(test.NoDBTestCase):
self.assertThat(test_bdm['connection_info'],
matchers.DictMatches(expected_conn_info))
def test_refresh_connection_info_with_attachment_id(self):
"""Tests refreshing connection info when the DriverVolumeBlockDevice
has a new style attachment_id set, which is a call to attachment_get
rather than initialize_connection.
"""
test_bdm = self.driver_classes['volume'](self.volume_bdm)
test_bdm['attachment_id'] = uuids.attachment_id
connection_info = {'data': {'multipath_id': 'fake_multipath_id'}}
fake_attachment = dict(connection_info=connection_info)
expected_conn_info = {'data': {'multipath_id': 'fake_multipath_id'},
'serial': self.volume_bdm.volume_id}
with mock.patch('nova.volume.cinder.API') as volume_api:
with mock.patch.object(test_bdm, 'save') as bdm_save:
volume_api.attachment_get.return_value = fake_attachment
test_bdm.refresh_connection_info(
self.context, mock.sentinel.instance, volume_api,
mock.sentinel.virt_driver)
volume_api.attachment_get.assert_called_once_with(
self.context, uuids.attachment_id)
self.assertDictEqual(expected_conn_info, test_bdm['connection_info'])
bdm_save.assert_called_once_with()
def test_snapshot_attach_no_volume(self):
no_volume_snapshot = self.snapshot_bdm_dict.copy()
no_volume_snapshot['volume_id'] = None
test_bdm = self.driver_classes['snapshot'](
fake_block_device.fake_bdm_object(
self.context, no_volume_snapshot))
# When we create a volume, we attach it using the old flow.
self.attachment_id = None
snapshot = {'id': 'fake-volume-id-1',
'attach_status': 'detached'}
@ -721,6 +738,8 @@ class TestDriverBlockDevice(test.NoDBTestCase):
test_bdm = self.driver_classes['snapshot'](
fake_block_device.fake_bdm_object(
self.context, no_volume_snapshot))
# When we create a volume, we attach it using the old flow.
self.attachment_id = None
snapshot = {'id': 'fake-volume-id-1',
'attach_status': 'detached'}
@ -809,6 +828,8 @@ class TestDriverBlockDevice(test.NoDBTestCase):
test_bdm = self.driver_classes['image'](
fake_block_device.fake_bdm_object(
self.context, no_volume_image))
# When we create a volume, we attach it using the old flow.
self.attachment_id = None
image = {'id': 'fake-image-id-1'}
volume = {'id': 'fake-volume-id-2',
@ -836,6 +857,8 @@ class TestDriverBlockDevice(test.NoDBTestCase):
test_bdm = self.driver_classes['image'](
fake_block_device.fake_bdm_object(
self.context, no_volume_image))
# When we create a volume, we attach it using the old flow.
self.attachment_id = None
image = {'id': 'fake-image-id-1'}
volume = {'id': 'fake-volume-id-2',
@ -1131,3 +1154,10 @@ class TestDriverBlockDevice(test.NoDBTestCase):
# can't assert_not_called if the method isn't in the spec.
self.assertFalse(hasattr(test_eph, 'refresh_connection_info'))
self.assertFalse(hasattr(test_swap, 'refresh_connection_info'))
class TestDriverBlockDeviceNewFlow(TestDriverBlockDevice):
"""Virt block_device tests for the Cinder 3.44 volume attach flow
where a volume BDM has an attachment_id.
"""
attachment_id = uuids.attachment_id

View File

@ -358,17 +358,11 @@ class DriverVolumeBlockDevice(DriverBlockDevice):
else:
volume_api.attachment_delete(context, self['attachment_id'])
@update_db
def attach(self, context, instance, volume_api, virt_driver,
do_driver_attach=False, **kwargs):
volume = volume_api.get(context, self.volume_id)
volume_api.check_availability_zone(context, volume,
instance=instance)
def _legacy_volume_attach(self, context, volume, connector, instance,
volume_api, virt_driver,
do_driver_attach=False):
volume_id = volume['id']
context = context.elevated()
connector = virt_driver.get_volume_connector(instance)
connection_info = volume_api.initialize_connection(context,
volume_id,
connector)
@ -434,6 +428,96 @@ class DriverVolumeBlockDevice(DriverBlockDevice):
# happen, the detach request will be ignored.
volume_api.detach(context, volume_id)
def _volume_attach(self, context, volume, connector, instance,
volume_api, virt_driver, attachment_id,
do_driver_attach=False):
# This is where we actually (finally) make a call down to the device
# driver and actually create/establish the connection. We'll go from
# here to block driver-->os-brick and back up.
volume_id = volume['id']
if self.volume_size is None:
self.volume_size = volume.get('size')
LOG.debug("Updating existing volume attachment record: %s",
attachment_id, instance=instance)
connection_info = volume_api.attachment_update(
context, attachment_id, connector)['connection_info']
if 'serial' not in connection_info:
connection_info['serial'] = self.volume_id
self._preserve_multipath_id(connection_info)
if do_driver_attach:
encryption = encryptors.get_encryption_metadata(
context, volume_api, volume_id, connection_info)
try:
virt_driver.attach_volume(
context, connection_info, instance,
self['mount_device'], disk_bus=self['disk_bus'],
device_type=self['device_type'], encryption=encryption)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception("Driver failed to attach volume "
"%(volume_id)s at %(mountpoint)s",
{'volume_id': volume_id,
'mountpoint': self['mount_device']},
instance=instance)
volume_api.attachment_delete(context,
attachment_id)
# NOTE(mriedem): save our current state so connection_info is in
# the database before the volume status goes to 'in-use' because
# after that we can detach and connection_info is required for
# detach.
# TODO(mriedem): Technically for the new flow, we shouldn't have to
# rely on the BlockDeviceMapping.connection_info since it's stored
# with the attachment in Cinder (see refresh_connection_info).
# Therefore we should phase out code that relies on the
# BDM.connection_info and get it from Cinder if it's needed.
self['connection_info'] = connection_info
self.save()
try:
# This marks the volume as "in-use".
volume_api.attachment_complete(context, attachment_id)
except Exception:
with excutils.save_and_reraise_exception():
if do_driver_attach:
# Disconnect the volume from the host.
try:
virt_driver.detach_volume(connection_info,
instance,
self['mount_device'],
encryption=encryption)
except Exception:
LOG.warning("Driver failed to detach volume "
"%(volume_id)s at %(mount_point)s.",
{'volume_id': volume_id,
'mount_point': self['mount_device']},
exc_info=True, instance=instance)
# Delete the attachment to mark the volume as "available".
volume_api.attachment_delete(context, self['attachment_id'])
@update_db
def attach(self, context, instance, volume_api, virt_driver,
do_driver_attach=False, **kwargs):
volume = volume_api.get(context, self.volume_id)
volume_api.check_availability_zone(context, volume,
instance=instance)
context = context.elevated()
connector = virt_driver.get_volume_connector(instance)
if not self['attachment_id']:
self._legacy_volume_attach(context, volume, connector, instance,
volume_api, virt_driver,
do_driver_attach)
else:
self._volume_attach(context, volume, connector, instance,
volume_api, virt_driver,
self['attachment_id'],
do_driver_attach)
@update_db
def refresh_connection_info(self, context, instance,
volume_api, virt_driver):
@ -502,6 +586,9 @@ class DriverSnapshotBlockDevice(DriverVolumeBlockDevice):
self.volume_id = vol['id']
# TODO(mriedem): Create an attachment to reserve the volume and
# make us go down the new-style attach flow.
# Call the volume attach now
super(DriverSnapshotBlockDevice, self).attach(
context, instance, volume_api, virt_driver)
@ -524,6 +611,9 @@ class DriverImageBlockDevice(DriverVolumeBlockDevice):
self.volume_id = vol['id']
# TODO(mriedem): Create an attachment to reserve the volume and
# make us go down the new-style attach flow.
super(DriverImageBlockDevice, self).attach(
context, instance, volume_api, virt_driver)
@ -545,6 +635,9 @@ class DriverBlankBlockDevice(DriverVolumeBlockDevice):
self.volume_id = vol['id']
# TODO(mriedem): Create an attachment to reserve the volume and
# make us go down the new-style attach flow.
super(DriverBlankBlockDevice, self).attach(
context, instance, volume_api, virt_driver)