Pass multiattach flag to reserve_block_device_name

This is similar to how we use reserve_block_device_name
for tagged attach in that we can fail fast in the API if
trying to attach a multiattach volume to an instance on
a compute host that can't support it, either because the
compute service is too old or the virt driver doesn't
support the multiattach capability yet.

A later change will build on this to pass the multiattach
flag from the API when a multiattach volume is requested
in a new microversion, similar to how 2.49 works.

Part of blueprint multi-attach-volume

Change-Id: Ieec2bbd8a23c861c89bf9922517fa6a5562f1937
This commit is contained in:
Matt Riedemann 2018-01-04 15:48:07 -05:00 committed by Ildiko Vancsa
parent ecd19ce45b
commit 1b53028c9c
8 changed files with 140 additions and 21 deletions

View File

@ -337,6 +337,7 @@ class VolumeAttachmentController(wsgi.Controller):
raise exc.HTTPNotFound(explanation=e.format_message())
except (exception.InstanceIsLocked,
exception.DevicePathInUse) as e:
# TODO(mriedem): Need to handle MultiattachNotSupportedByVirtDriver
raise exc.HTTPConflict(explanation=e.format_message())
except exception.InstanceInvalidState as state_error:
common.raise_http_conflict_for_instance_invalid_state(state_error,

View File

@ -3654,6 +3654,7 @@ class API(base.Base):
# the same time. When db access is removed from
# compute, the bdm will be created here and we will
# have to make sure that they are assigned atomically.
# TODO(mriedem): Handle multiattach here.
volume_bdm = self.compute_rpcapi.reserve_block_device_name(
context, instance, device, volume_id, disk_bus=disk_bus,
device_type=device_type, tag=tag)

View File

@ -485,7 +485,7 @@ class ComputeVirtAPI(virtapi.VirtAPI):
class ComputeManager(manager.Manager):
"""Manages the running instances from creation to destruction."""
target = messaging.Target(version='4.19')
target = messaging.Target(version='4.20')
# How long to wait in seconds before re-issuing a shutdown
# signal to an instance during power off. The overall
@ -5177,12 +5177,18 @@ class ComputeManager(manager.Manager):
@reverts_task_state
@wrap_instance_fault
def reserve_block_device_name(self, context, instance, device,
volume_id, disk_bus, device_type, tag=None):
volume_id, disk_bus, device_type, tag=None,
multiattach=False):
if (tag and not
self.driver.capabilities.get('supports_tagged_attach_volume',
False)):
raise exception.VolumeTaggedAttachNotSupported()
if (multiattach and not
self.driver.capabilities.get('supports_multiattach', False)):
raise exception.MultiattachNotSupportedByVirtDriver(
volume_id=volume_id)
@utils.synchronized(instance.uuid)
def do_reserve():
bdms = (

View File

@ -337,6 +337,7 @@ class ComputeAPI(object):
* 4.19 - build_and_run_instance() now gets a 'host_list' parameter
representing potential alternate hosts for retries within a
cell.
* 4.20 - Add multiattach argument to reserve_block_device_name().
'''
VERSION_ALIASES = {
@ -965,13 +966,24 @@ class ComputeAPI(object):
return cctxt.call(ctxt, 'get_host_uptime')
def reserve_block_device_name(self, ctxt, instance, device, volume_id,
disk_bus=None, device_type=None, tag=None):
disk_bus=None, device_type=None, tag=None,
multiattach=False):
kw = {'instance': instance, 'device': device,
'volume_id': volume_id, 'disk_bus': disk_bus,
'device_type': device_type, 'tag': tag}
version = '4.15'
'device_type': device_type, 'tag': tag,
'multiattach': multiattach}
version = '4.20'
client = self.router.client(ctxt)
if not client.can_send_version(version):
if multiattach:
# NOTE(mriedem): Reserve attempted with a multiattach volume,
# but the compute isn't new enough to handle that value so we
# need to fail since the compute is too old to support it.
raise exception.MultiattachSupportNotYetAvailable()
version = '4.15'
kw.pop('multiattach')
if not client.can_send_version(version):
if tag:
# NOTE(artom) Reserve attempted with a device role tag, but

View File

@ -260,6 +260,14 @@ class MultiattachNotSupportedByVirtDriver(NovaException):
code = 409
class MultiattachSupportNotYetAvailable(NovaException):
# This exception indicates the deployment is not yet new enough to support
# multiattach volumes, so a 409 HTTPConflict response is generally used
# for handling this in the API.
msg_fmt = _("Multiattach volume support is not yet available.")
code = 409
class VolumeNotCreated(NovaException):
msg_fmt = _("Volume %(volume_id)s did not finish being created"
" even after we waited %(seconds)s seconds or %(attempts)s"

View File

@ -31,7 +31,7 @@ LOG = logging.getLogger(__name__)
# NOTE(danms): This is the global service version counter
SERVICE_VERSION = 26
SERVICE_VERSION = 27
# NOTE(danms): This is our SERVICE_VERSION history. The idea is that any
@ -122,6 +122,9 @@ SERVICE_VERSION_HISTORY = (
{'compute_rpc': '4.18'},
# Version 26: Adds a 'host_list' parameter to build_and_run_instance()
{'compute_rpc': '4.19'},
# Version 27: Compute RPC version 4.20; adds multiattach argument to
# reserve_block_device_name().
{'compute_rpc': '4.20'},
)

View File

@ -410,6 +410,34 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
'fake_device', 'fake_volume_id', 'fake_disk_bus',
'fake_device_type', tag='foo')
@mock.patch.object(objects.BlockDeviceMapping, 'create')
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid',
return_value=objects.BlockDeviceMappingList())
def test_reserve_block_device_name_multiattach(self, mock_get,
mock_create):
"""Tests the case that multiattach=True and the driver supports it."""
instance = fake_instance.fake_instance_obj(self.context)
with test.nested(
mock.patch.object(self.compute,
'_get_device_name_for_instance',
return_value='/dev/vda'),
mock.patch.dict(self.compute.driver.capabilities,
supports_multiattach=True)):
self.compute.reserve_block_device_name(
self.context, instance, device=None, volume_id=uuids.volume_id,
disk_bus=None, device_type=None, multiattach=True)
@mock.patch.object(compute_utils, 'add_instance_fault_from_exc')
def test_reserve_block_device_name_multiattach_raises(self, _):
with mock.patch.dict(self.compute.driver.capabilities,
supports_multiattach=False):
self.assertRaises(exception.MultiattachNotSupportedByVirtDriver,
self.compute.reserve_block_device_name,
self.context,
fake_instance.fake_instance_obj(self.context),
'fake_device', 'fake_volume_id', 'fake_disk_bus',
'fake_device_type', multiattach=True)
@mock.patch.object(objects.Instance, 'save')
@mock.patch.object(time, 'sleep')
def test_allocate_network_succeeds_after_retries(

View File

@ -535,10 +535,66 @@ class ComputeRpcAPITestCase(test.NoDBTestCase):
self._test_compute_api('reserve_block_device_name', 'call',
instance=self.fake_instance_obj, device='device',
volume_id='id', disk_bus='ide', device_type='cdrom',
tag='foo', version='4.15',
tag='foo', multiattach=True, version='4.20',
_return_value=objects_block_dev.BlockDeviceMapping())
def test_reserve_block_device_name_raises(self):
ctxt = context.RequestContext('fake_user', 'fake_project')
instance = self.fake_instance_obj
rpcapi = compute_rpcapi.ComputeAPI()
cctxt_mock = mock.Mock()
mock_client = mock.Mock()
rpcapi.router.client = mock.Mock()
rpcapi.router.client.return_value = mock_client
with test.nested(
mock.patch.object(mock_client, 'can_send_version',
side_effect=[False, False]),
mock.patch.object(mock_client, 'prepare',
return_value=cctxt_mock)
) as (
can_send_mock, prepare_mock
):
self.assertRaises(exception.TaggedAttachmentNotSupported,
rpcapi.reserve_block_device_name, ctxt, instance,
'fake_device', 'fake_volume_id', tag='foo')
can_send_calls = [mock.call('4.20'), mock.call('4.15')]
can_send_mock.assert_has_calls(can_send_calls)
def test_reserve_block_device_name_downgrades_version(self):
ctxt = context.RequestContext('fake_user', 'fake_project')
instance = self.fake_instance_obj
rpcapi = compute_rpcapi.ComputeAPI()
call_mock = mock.Mock()
cctxt_mock = mock.Mock(call=call_mock)
mock_client = mock.Mock()
rpcapi.router.client = mock.Mock()
rpcapi.router.client.return_value = mock_client
with test.nested(
mock.patch.object(mock_client, 'can_send_version',
side_effect=[False, False]),
mock.patch.object(mock_client, 'prepare',
return_value=cctxt_mock)
) as (
can_send_mock, prepare_mock
):
rpcapi.reserve_block_device_name(ctxt, instance, 'fake_device',
'fake_volume_id')
can_send_calls = [mock.call('4.20'), mock.call('4.15')]
can_send_mock.assert_has_calls(can_send_calls)
prepare_mock.assert_called_once_with(server=instance['host'],
version='4.0')
call_mock.assert_called_once_with(ctxt, 'reserve_block_device_name',
instance=instance,
device='fake_device',
volume_id='fake_volume_id',
disk_bus=None, device_type=None)
def test_reserve_block_device_name_raises_no_multiattach(self):
"""Tests that if multiattach=True but the compute service is too
old for the multiattach argument, an error is raised from the RPC
client.
"""
ctxt = context.RequestContext('fake_user', 'fake_project')
instance = self.fake_instance_obj
rpcapi = compute_rpcapi.ComputeAPI()
@ -554,12 +610,16 @@ class ComputeRpcAPITestCase(test.NoDBTestCase):
) as (
can_send_mock, prepare_mock
):
self.assertRaises(exception.TaggedAttachmentNotSupported,
self.assertRaises(exception.MultiattachSupportNotYetAvailable,
rpcapi.reserve_block_device_name, ctxt, instance,
'fake_device', 'fake_volume_id', tag='foo')
can_send_mock.assert_called_once_with('4.15')
'fake_device', 'fake_volume_id',
multiattach=True)
can_send_mock.assert_called_once_with('4.20')
def test_reserve_block_device_name_downgrades_version(self):
def test_reserve_block_device_name_downgrades_version_multiattach(self):
"""Tests that if multiattach=False and the compute service is too
old for the multiattach argument, it's removed from the RPC call.
"""
ctxt = context.RequestContext('fake_user', 'fake_project')
instance = self.fake_instance_obj
rpcapi = compute_rpcapi.ComputeAPI()
@ -570,23 +630,23 @@ class ComputeRpcAPITestCase(test.NoDBTestCase):
rpcapi.router.client.return_value = mock_client
with test.nested(
mock.patch.object(mock_client, 'can_send_version',
return_value=False),
side_effect=[False, True]),
mock.patch.object(mock_client, 'prepare',
return_value=cctxt_mock)
) as (
can_send_mock, prepare_mock
):
rpcapi.reserve_block_device_name(ctxt, instance, 'fake_device',
'fake_volume_id')
rpcapi.reserve_block_device_name(
ctxt, instance, 'fake_device', 'fake_volume_id', tag='foo')
can_send_mock.assert_called_once_with('4.15')
can_send_calls = [mock.call('4.20'), mock.call('4.15')]
can_send_mock.assert_has_calls(can_send_calls)
prepare_mock.assert_called_once_with(server=instance['host'],
version='4.0')
call_mock.assert_called_once_with(ctxt, 'reserve_block_device_name',
instance=instance,
device='fake_device',
volume_id='fake_volume_id',
disk_bus=None, device_type=None)
version='4.15')
call_mock.assert_called_once_with(
ctxt, 'reserve_block_device_name', instance=instance,
device='fake_device', volume_id='fake_volume_id', disk_bus=None,
device_type=None, tag='foo')
def test_refresh_instance_security_rules(self):
expected_args = {'instance': self.fake_instance_obj}