Merge "Add compute API validation for when a volume_type is requested"

This commit is contained in:
Zuul 2018-10-12 05:56:47 +00:00 committed by Gerrit Code Review
commit 20bc0136d0
5 changed files with 236 additions and 0 deletions

View File

@ -106,6 +106,7 @@ CINDER_V3_ATTACH_MIN_COMPUTE_VERSION = 24
MIN_COMPUTE_MULTIATTACH = 27
MIN_COMPUTE_TRUSTED_CERTS = 31
MIN_COMPUTE_ABORT_QUEUED_LIVE_MIGRATION = 34
MIN_COMPUTE_VOLUME_TYPE = 36
# FIXME(danms): Keep a global cache of the cells we find the
# first time we look. This needs to be refreshed on a timer or
@ -1359,6 +1360,36 @@ class API(base.Base):
bdm.update_or_create()
@staticmethod
def _check_requested_volume_type(bdm, volume_type_id_or_name,
volume_types):
"""If we are specifying a volume type, we need to get the
volume type details from Cinder and make sure the ``volume_type``
is available.
"""
# NOTE(brinzhang): Verify that the specified volume type exists.
# And save the volume type name internally for consistency in the
# BlockDeviceMapping object.
for vol_type in volume_types:
if (volume_type_id_or_name == vol_type['id'] or
volume_type_id_or_name == vol_type['name']):
bdm.volume_type = vol_type['name']
break
else:
raise exception.VolumeTypeNotFound(
id_or_name=volume_type_id_or_name)
@staticmethod
def _check_compute_supports_volume_type(context):
# NOTE(brinzhang): Checking the minimum nova-compute service
# version across the deployment. Just make sure the volume
# type can be supported when the bdm.volume_type is requested.
min_compute_version = objects.service.get_minimum_version_all_cells(
context, ['nova-compute'])
if min_compute_version < MIN_COMPUTE_VOLUME_TYPE:
raise exception.VolumeTypeSupportNotYetAvailable()
def _validate_bdm(self, context, instance, instance_type,
block_device_mappings, supports_multiattach=False):
# Make sure that the boot indexes make sense.
@ -1379,7 +1410,32 @@ class API(base.Base):
instance=instance)
raise exception.InvalidBDMBootSequence()
volume_types = None
volume_type_is_supported = False
for bdm in block_device_mappings:
volume_type = bdm.volume_type
if volume_type:
if not volume_type_is_supported:
# The following method raises
# VolumeTypeSupportNotYetAvailable if the minimum
# nova-compute service version across the deployment is
# not new enough to support creating volumes with a
# specific type.
self._check_compute_supports_volume_type(context)
# Set the flag to avoid calling
# _check_compute_supports_volume_type more than once in
# this for loop.
volume_type_is_supported = True
if not volume_types:
# In order to reduce the number of hit cinder APIs,
# initialize our cache of volume types.
volume_types = self.volume_api.get_all_volume_types(
context)
# NOTE(brinzhang): Ensure the validity of volume_type.
self._check_requested_volume_type(bdm, volume_type,
volume_types)
# NOTE(vish): For now, just make sure the volumes are accessible.
# Additionally, check that the volume can be attached to this
# instance.

View File

@ -283,6 +283,14 @@ class MultiattachNotSupportedOldMicroversion(Invalid):
'compute API version 2.60.')
class VolumeTypeSupportNotYetAvailable(NovaException):
# This exception indicates the deployment is not yet new enough to support
# volume type, so a 409 HTTPConflict response is generally used
# for handling this in the API.
msg_fmt = _("Volume type support is not yet available.")
code = 409
class MultiattachToShelvedNotSupported(Invalid):
msg_fmt = _("Attaching multiattach volumes is not supported for "
"shelved-offloaded instances.")
@ -631,6 +639,10 @@ class VolumeNotFound(NotFound):
msg_fmt = _("Volume %(volume_id)s could not be found.")
class VolumeTypeNotFound(NotFound):
msg_fmt = _("Volume type %(id_or_name)s could not be found.")
class UndefinedRootBDM(NovaException):
msg_fmt = _("Undefined Block Device Mapping root: BlockDeviceMappingList "
"contains Block Device Mappings from multiple instances.")

View File

@ -4481,6 +4481,92 @@ class _ComputeAPIUnitTestMixIn(object):
self.context, objects.Instance(), objects.Flavor(),
bdms)
@mock.patch.object(objects.service, 'get_minimum_version_all_cells',
return_value=compute_api.MIN_COMPUTE_VOLUME_TYPE)
def test_validate_bdm_with_volume_type_name_is_specified(
self, mock_get_min_version):
"""Test _check_requested_volume_type and
_check_compute_supports_volume_type methods are used.
"""
instance = self._create_instance_obj()
instance_type = self._create_flavor()
volume_type = 'fake_lvm_1'
volume_types = [{'id': 'fake_volume_type_id_1', 'name': 'fake_lvm_1'},
{'id': 'fake_volume_type_id_2', 'name': 'fake_lvm_2'}]
bdm1 = objects.BlockDeviceMapping(
**fake_block_device.AnonFakeDbBlockDeviceDict(
{
'uuid': uuids.image_id,
'source_type': 'image',
'destination_type': 'volume',
'device_name': 'vda',
'boot_index': 0,
'volume_size': 3,
'volume_type': 'fake_lvm_1'
}))
bdm2 = objects.BlockDeviceMapping(
**fake_block_device.AnonFakeDbBlockDeviceDict(
{
'uuid': uuids.image_id,
'source_type': 'snapshot',
'destination_type': 'volume',
'device_name': 'vdb',
'boot_index': 1,
'volume_size': 3,
'volume_type': 'fake_lvm_1'
}))
bdms = [bdm1, bdm2]
with test.nested(
mock.patch.object(cinder.API, 'get_all_volume_types',
return_value=volume_types),
mock.patch.object(compute_api.API,
'_check_compute_supports_volume_type'),
mock.patch.object(compute_api.API,
'_check_requested_volume_type')) as (
get_all_vol_types, vol_type_supported, vol_type_requested):
self.compute_api._validate_bdm(self.context, instance,
instance_type, bdms)
vol_type_supported.assert_called_once_with(self.context)
get_all_vol_types.assert_called_once_with(self.context)
vol_type_requested.assert_any_call(bdms[0], volume_type,
volume_types)
vol_type_requested.assert_any_call(bdms[1], volume_type,
volume_types)
@mock.patch.object(objects.service, 'get_minimum_version_all_cells',
return_value=compute_api.MIN_COMPUTE_VOLUME_TYPE)
def test_the_specified_volume_type_id_assignment_to_name(
self, mock_get_min_version):
"""Test _check_requested_volume_type method is called, if the user
is using the volume type ID, assign volume_type to volume type name.
"""
volume_type = 'fake_volume_type_id_1'
volume_types = [{'id': 'fake_volume_type_id_1', 'name': 'fake_lvm_1'},
{'id': 'fake_volume_type_id_2', 'name': 'fake_lvm_2'}]
bdms = [objects.BlockDeviceMapping(
**fake_block_device.AnonFakeDbBlockDeviceDict(
{
'uuid': uuids.image_id,
'source_type': 'image',
'destination_type': 'volume',
'device_name': 'vda',
'boot_index': 0,
'volume_size': 3,
'volume_type': 'fake_volume_type_id_1'
}))]
self.compute_api._check_requested_volume_type(bdms[0],
volume_type,
volume_types)
self.assertEqual(bdms[0].volume_type, volume_types[0]['name'])
def _test_provision_instances_with_cinder_error(self,
expected_exception):
@mock.patch('nova.compute.utils.check_num_instances_quota')
@ -6233,6 +6319,41 @@ class ComputeAPIUnitTestCase(_ComputeAPIUnitTestMixIn, test.NoDBTestCase):
self.compute_api.attach_volume,
self.context, instance, uuids.volumeid)
@mock.patch('nova.objects.service.get_minimum_version_all_cells',
return_value=compute_api.MIN_COMPUTE_VOLUME_TYPE - 1)
def test_check_compute_supports_volume_type_new_inst_old_compute(
self, get_min_version):
"""Tests that _check_compute_supports_volume_type fails if trying to
specify a volume type to create a new instance but the nova-compute
service version are not all upgraded yet.
"""
self.assertRaises(exception.VolumeTypeSupportNotYetAvailable,
self.compute_api._check_compute_supports_volume_type,
self.context)
@mock.patch('nova.objects.service.get_minimum_version_all_cells',
return_value=compute_api.MIN_COMPUTE_VOLUME_TYPE)
def test_validate_bdm_check_volume_type_raise_not_found(
self, get_min_version):
"""Tests that _validate_bdm will fail if the requested volume type
name or id does not match the volume types in Cinder.
"""
volume_types = [{'id': 'fake_volume_type_id_1', 'name': 'fake_lvm_1'},
{'id': 'fake_volume_type_id_2', 'name': 'fake_lvm_2'}]
bdm = objects.BlockDeviceMapping(
**fake_block_device.FakeDbBlockDeviceDict(
{
'uuid': uuids.image_id,
'source_type': 'image',
'destination_type': 'volume',
'device_name': 'vda',
'boot_index': 0,
'volume_size': 3,
'volume_type': 'lvm-1'}))
self.assertRaises(exception.VolumeTypeNotFound,
self.compute_api._check_requested_volume_type,
bdm, 'lvm-1', volume_types)
@mock.patch.object(neutron_api.API, 'has_substr_port_filtering_extension')
@mock.patch.object(neutron_api.API, 'list_ports')
@mock.patch.object(objects.BuildRequestList, 'get_by_filters',

View File

@ -70,6 +70,12 @@ class FakeSnapshot(object):
self.project_id = 'fake_project'
class FakeVolumeType(object):
def __init__(self, volume_type_name, volume_type_id):
self.id = volume_type_id
self.name = volume_type_name
class FakeAttachment(object):
def __init__(self):
@ -895,6 +901,27 @@ class CinderApiTestCase(test.NoDBTestCase):
mock_volume_snapshots.update_snapshot_status.assert_called_once_with(
'snapshot_id', {'status': 'error', 'progress': '90%'})
@mock.patch('nova.volume.cinder.cinderclient')
def test_get_all_volume_types(self, mock_cinderclient):
volume_type1 = FakeVolumeType('lvm_1', 'volume_type_id1')
volume_type2 = FakeVolumeType('lvm_2', 'volume_type_id2')
volume_type_list = [volume_type1, volume_type2]
mock_volume_types = mock.MagicMock()
mock_cinderclient.return_value = mock.MagicMock(
volume_types=mock_volume_types)
mock_volume_types.list.return_value = volume_type_list
volume_types = self.api.get_all_volume_types(self.ctx)
self.assertEqual(2, len(volume_types))
self.assertEqual(['volume_type_id1', 'volume_type_id2'],
[vol_type['id'] for vol_type in volume_types])
self.assertEqual(['lvm_1', 'lvm_2'],
[vol_type['name'] for vol_type in volume_types])
mock_cinderclient.assert_called_once_with(self.ctx)
mock_volume_types.list.assert_called_once_with()
@mock.patch('nova.volume.cinder.cinderclient')
def test_get_volume_encryption_metadata(self, mock_cinderclient):
mock_volumes = mock.MagicMock()

View File

@ -328,6 +328,16 @@ def _untranslate_volume_summary_view(context, vol):
return d
def _untranslate_volume_type_view(volume_type):
"""Maps keys for volume type view."""
v = {}
v['id'] = volume_type.id
v['name'] = volume_type.name
return v
def _untranslate_snapshot_summary_view(context, snapshot):
"""Maps keys for snapshots summary view."""
d = {}
@ -686,6 +696,16 @@ class API(object):
def delete_snapshot(self, context, snapshot_id):
cinderclient(context).volume_snapshots.delete(snapshot_id)
@translate_cinder_exception
def get_all_volume_types(self, context):
items = cinderclient(context).volume_types.list()
rvals = []
for item in items:
rvals.append(_untranslate_volume_type_view(item))
return rvals
@translate_cinder_exception
def get_volume_encryption_metadata(self, context, volume_id):
return cinderclient(context).volumes.get_encryption_metadata(volume_id)