Support cinder_img_volume_type in image metadata
This patch adds a feature to specify Cinder volume type via cinder_img_volume_type parameter in glance image metadata. In some cases, between image and hypervisor, storage backend are tightly connected in each other. For example, if users want to boot a VMware instance from an image, this image has to be configured some VMware specific parameters in previous, and then Nova can deploy an instance on to proper hypervisor based on the image metadata properties. Currently, Cinder handles few image metadata properties but volume type is not included. If volume type can be retrieved from image metadata which is configured by cloud admin, user doesn't need to care volume type and also storage backends. And then appropriate volume type will be chosen automatically based on the information of cinder_img_volume_type in the image metadata during volume creation. If user has enough knowledge about image, hypervisor and storage backend, user also can specify 'volume_type' via CLI or API as in the past. Priority of volume type related parameters are shown below. 1. volume_type (via API or CLI) 2. cinder_img_volume_type (via glance image metadata) 3. default_volume_type (via cinder.conf) DocImpact: Add usage of this parameter to 'Manage volumes' section in Cloud Administrator Guide Change-Id: I62f02d817d84d3a7b651db36d7297299b1af2fe3
This commit is contained in:
parent
d957f90b15
commit
dc12ecd1ea
@ -14,6 +14,7 @@
|
||||
# under the License.
|
||||
""" Tests for create_volume TaskFlow """
|
||||
|
||||
import ddt
|
||||
import mock
|
||||
|
||||
from oslo_config import cfg
|
||||
@ -35,6 +36,7 @@ from cinder.volume.flows.manager import create_volume as create_volume_manager
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CreateVolumeFlowTestCase(test.TestCase):
|
||||
|
||||
def time_inc(self):
|
||||
@ -341,6 +343,237 @@ class CreateVolumeFlowTestCase(test.TestCase):
|
||||
'cgsnapshot_id': None, }
|
||||
self.assertEqual(expected_result, result)
|
||||
|
||||
@mock.patch('cinder.volume.volume_types.is_encrypted')
|
||||
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
|
||||
@mock.patch('cinder.volume.volume_types.get_default_volume_type')
|
||||
@mock.patch('cinder.volume.volume_types.get_volume_type_by_name')
|
||||
@mock.patch('cinder.volume.flows.api.create_volume.'
|
||||
'ExtractVolumeRequestTask.'
|
||||
'_get_volume_type_id')
|
||||
def test_extract_image_volume_type_from_image(
|
||||
self,
|
||||
fake_get_type_id,
|
||||
fake_get_vol_type,
|
||||
fake_get_def_vol_type,
|
||||
fake_get_qos,
|
||||
fake_is_encrypted):
|
||||
|
||||
image_volume_type = 'type_from_image'
|
||||
fake_image_service = fake_image.FakeImageService()
|
||||
image_id = 6
|
||||
image_meta = {}
|
||||
image_meta['id'] = image_id
|
||||
image_meta['status'] = 'active'
|
||||
image_meta['size'] = 1
|
||||
image_meta['properties'] = {}
|
||||
image_meta['properties']['cinder_img_volume_type'] = image_volume_type
|
||||
fake_image_service.create(self.ctxt, image_meta)
|
||||
fake_key_manager = mock_key_mgr.MockKeyManager()
|
||||
|
||||
task = create_volume.ExtractVolumeRequestTask(
|
||||
fake_image_service,
|
||||
{'nova'})
|
||||
|
||||
fake_is_encrypted.return_value = False
|
||||
fake_get_type_id.return_value = 1
|
||||
fake_get_vol_type.return_value = image_volume_type
|
||||
fake_get_def_vol_type.return_value = 'fake_vol_type'
|
||||
fake_get_qos.return_value = {'qos_specs': None}
|
||||
result = task.execute(self.ctxt,
|
||||
size=1,
|
||||
snapshot=None,
|
||||
image_id=image_id,
|
||||
source_volume=None,
|
||||
availability_zone='nova',
|
||||
volume_type=None,
|
||||
metadata=None,
|
||||
key_manager=fake_key_manager,
|
||||
source_replica=None,
|
||||
consistencygroup=None,
|
||||
cgsnapshot=None)
|
||||
expected_result = {'size': 1,
|
||||
'snapshot_id': None,
|
||||
'source_volid': None,
|
||||
'availability_zone': 'nova',
|
||||
'volume_type': image_volume_type,
|
||||
'volume_type_id': 1,
|
||||
'encryption_key_id': None,
|
||||
'qos_specs': None,
|
||||
'source_replicaid': None,
|
||||
'consistencygroup_id': None,
|
||||
'cgsnapshot_id': None, }
|
||||
self.assertEqual(expected_result, result)
|
||||
|
||||
@mock.patch('cinder.db.volume_type_get_by_name')
|
||||
@mock.patch('cinder.volume.volume_types.is_encrypted')
|
||||
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
|
||||
@mock.patch('cinder.volume.volume_types.get_default_volume_type')
|
||||
@mock.patch('cinder.volume.flows.api.create_volume.'
|
||||
'ExtractVolumeRequestTask.'
|
||||
'_get_volume_type_id')
|
||||
def test_extract_image_volume_type_from_image_invalid_type(
|
||||
self,
|
||||
fake_get_type_id,
|
||||
fake_get_def_vol_type,
|
||||
fake_get_qos,
|
||||
fake_is_encrypted,
|
||||
fake_db_get_vol_type):
|
||||
|
||||
image_volume_type = 'invalid'
|
||||
fake_image_service = fake_image.FakeImageService()
|
||||
image_id = 7
|
||||
image_meta = {}
|
||||
image_meta['id'] = image_id
|
||||
image_meta['status'] = 'active'
|
||||
image_meta['size'] = 1
|
||||
image_meta['properties'] = {}
|
||||
image_meta['properties']['cinder_img_volume_type'] = image_volume_type
|
||||
fake_image_service.create(self.ctxt, image_meta)
|
||||
fake_key_manager = mock_key_mgr.MockKeyManager()
|
||||
|
||||
task = create_volume.ExtractVolumeRequestTask(
|
||||
fake_image_service,
|
||||
{'nova'})
|
||||
|
||||
fake_is_encrypted.return_value = False
|
||||
fake_get_type_id.return_value = 1
|
||||
fake_get_def_vol_type.return_value = 'fake_vol_type'
|
||||
fake_db_get_vol_type.side_effect = (
|
||||
exception.VolumeTypeNotFoundByName(volume_type_name='invalid'))
|
||||
fake_get_qos.return_value = {'qos_specs': None}
|
||||
result = task.execute(self.ctxt,
|
||||
size=1,
|
||||
snapshot=None,
|
||||
image_id=image_id,
|
||||
source_volume=None,
|
||||
availability_zone='nova',
|
||||
volume_type=None,
|
||||
metadata=None,
|
||||
key_manager=fake_key_manager,
|
||||
source_replica=None,
|
||||
consistencygroup=None,
|
||||
cgsnapshot=None)
|
||||
expected_result = {'size': 1,
|
||||
'snapshot_id': None,
|
||||
'source_volid': None,
|
||||
'availability_zone': 'nova',
|
||||
'volume_type': 'fake_vol_type',
|
||||
'volume_type_id': 1,
|
||||
'encryption_key_id': None,
|
||||
'qos_specs': None,
|
||||
'source_replicaid': None,
|
||||
'consistencygroup_id': None,
|
||||
'cgsnapshot_id': None, }
|
||||
self.assertEqual(expected_result, result)
|
||||
|
||||
@mock.patch('cinder.db.volume_type_get_by_name')
|
||||
@mock.patch('cinder.volume.volume_types.is_encrypted')
|
||||
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
|
||||
@mock.patch('cinder.volume.volume_types.get_default_volume_type')
|
||||
@mock.patch('cinder.volume.flows.api.create_volume.'
|
||||
'ExtractVolumeRequestTask.'
|
||||
'_get_volume_type_id')
|
||||
@ddt.data((8, None), (9, {'cinder_img_volume_type': None}))
|
||||
@ddt.unpack
|
||||
def test_extract_image_volume_type_from_image_properties_error(
|
||||
self,
|
||||
image_id,
|
||||
fake_img_properties,
|
||||
fake_get_type_id,
|
||||
fake_get_def_vol_type,
|
||||
fake_get_qos,
|
||||
fake_is_encrypted,
|
||||
fake_db_get_vol_type):
|
||||
|
||||
fake_image_service = fake_image.FakeImageService()
|
||||
image_meta = {}
|
||||
image_meta['id'] = image_id
|
||||
image_meta['status'] = 'active'
|
||||
image_meta['size'] = 1
|
||||
image_meta['properties'] = fake_img_properties
|
||||
fake_image_service.create(self.ctxt, image_meta)
|
||||
fake_key_manager = mock_key_mgr.MockKeyManager()
|
||||
|
||||
task = create_volume.ExtractVolumeRequestTask(
|
||||
fake_image_service,
|
||||
{'nova'})
|
||||
|
||||
fake_is_encrypted.return_value = False
|
||||
fake_get_type_id.return_value = 1
|
||||
fake_get_def_vol_type.return_value = 'fake_vol_type'
|
||||
fake_get_qos.return_value = {'qos_specs': None}
|
||||
result = task.execute(self.ctxt,
|
||||
size=1,
|
||||
snapshot=None,
|
||||
image_id=image_id,
|
||||
source_volume=None,
|
||||
availability_zone='nova',
|
||||
volume_type=None,
|
||||
metadata=None,
|
||||
key_manager=fake_key_manager,
|
||||
source_replica=None,
|
||||
consistencygroup=None,
|
||||
cgsnapshot=None)
|
||||
expected_result = {'size': 1,
|
||||
'snapshot_id': None,
|
||||
'source_volid': None,
|
||||
'availability_zone': 'nova',
|
||||
'volume_type': 'fake_vol_type',
|
||||
'volume_type_id': 1,
|
||||
'encryption_key_id': None,
|
||||
'qos_specs': None,
|
||||
'source_replicaid': None,
|
||||
'consistencygroup_id': None,
|
||||
'cgsnapshot_id': None, }
|
||||
self.assertEqual(expected_result, result)
|
||||
|
||||
@mock.patch('cinder.db.volume_type_get_by_name')
|
||||
@mock.patch('cinder.volume.volume_types.is_encrypted')
|
||||
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
|
||||
@mock.patch('cinder.volume.volume_types.get_default_volume_type')
|
||||
@mock.patch('cinder.volume.flows.api.create_volume.'
|
||||
'ExtractVolumeRequestTask.'
|
||||
'_get_volume_type_id')
|
||||
def test_extract_image_volume_type_from_image_invalid_input(
|
||||
self,
|
||||
fake_get_type_id,
|
||||
fake_get_def_vol_type,
|
||||
fake_get_qos,
|
||||
fake_is_encrypted,
|
||||
fake_db_get_vol_type):
|
||||
|
||||
fake_image_service = fake_image.FakeImageService()
|
||||
image_id = 10
|
||||
image_meta = {}
|
||||
image_meta['id'] = image_id
|
||||
image_meta['status'] = 'inactive'
|
||||
fake_image_service.create(self.ctxt, image_meta)
|
||||
fake_key_manager = mock_key_mgr.MockKeyManager()
|
||||
|
||||
task = create_volume.ExtractVolumeRequestTask(
|
||||
fake_image_service,
|
||||
{'nova'})
|
||||
|
||||
fake_is_encrypted.return_value = False
|
||||
fake_get_type_id.return_value = 1
|
||||
fake_get_def_vol_type.return_value = 'fake_vol_type'
|
||||
fake_get_qos.return_value = {'qos_specs': None}
|
||||
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
task.execute,
|
||||
self.ctxt,
|
||||
size=1,
|
||||
snapshot=None,
|
||||
image_id=image_id,
|
||||
source_volume=None,
|
||||
availability_zone='nova',
|
||||
volume_type=None,
|
||||
metadata=None,
|
||||
key_manager=fake_key_manager,
|
||||
source_replica=None,
|
||||
consistencygroup=None,
|
||||
cgsnapshot=None)
|
||||
|
||||
|
||||
class CreateVolumeFlowManagerTestCase(test.TestCase):
|
||||
|
||||
|
@ -223,6 +223,49 @@ class ExtractVolumeRequestTask(flow_utils.CinderTask):
|
||||
msg = msg % {'volume_size': size, 'min_disk': min_disk}
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
def _get_image_volume_type(self, context, image_id):
|
||||
"""Get cinder_img_volume_type property from the image metadata."""
|
||||
|
||||
# Check image existence
|
||||
if image_id is None:
|
||||
return None
|
||||
|
||||
image_meta = self.image_service.show(context, image_id)
|
||||
|
||||
# check whether image is active
|
||||
if image_meta['status'] != 'active':
|
||||
msg = (_('Image %(image_id)s is not active.') %
|
||||
{'image_id': image_id})
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
# Retrieve 'cinder_img_volume_type' property from glance image
|
||||
# metadata.
|
||||
image_volume_type = "cinder_img_volume_type"
|
||||
properties = image_meta.get('properties')
|
||||
if properties:
|
||||
try:
|
||||
img_vol_type = properties.get(image_volume_type)
|
||||
if img_vol_type is None:
|
||||
return None
|
||||
volume_type = volume_types.get_volume_type_by_name(
|
||||
context,
|
||||
img_vol_type)
|
||||
except exception.VolumeTypeNotFoundByName:
|
||||
LOG.warning(_LW("Failed to retrieve volume_type from image "
|
||||
"metadata. '%(img_vol_type)s' doesn't match "
|
||||
"any volume types."),
|
||||
{'img_vol_type': img_vol_type})
|
||||
return None
|
||||
|
||||
LOG.debug("Retrieved volume_type from glance image metadata. "
|
||||
"image_id: %(image_id)s, "
|
||||
"image property: %(image_volume_type)s, "
|
||||
"volume_type: %(volume_type)s." %
|
||||
{'image_id': image_id,
|
||||
'image_volume_type': image_volume_type,
|
||||
'volume_type': volume_type})
|
||||
return volume_type
|
||||
|
||||
@staticmethod
|
||||
def _check_metadata_properties(metadata=None):
|
||||
"""Checks that the volume metadata properties are valid."""
|
||||
@ -384,7 +427,9 @@ class ExtractVolumeRequestTask(flow_utils.CinderTask):
|
||||
# This strategy avoids any dependency upon the encrypted volume type.
|
||||
def_vol_type = volume_types.get_default_volume_type()
|
||||
if not volume_type and not source_volume and not snapshot:
|
||||
volume_type = def_vol_type
|
||||
image_volume_type = self._get_image_volume_type(context, image_id)
|
||||
volume_type = (image_volume_type if image_volume_type else
|
||||
def_vol_type)
|
||||
|
||||
# When creating a clone of a replica (replication test), we can't
|
||||
# use the volume type of the replica, therefore, we use the default.
|
||||
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Support cinder_img_volume_type property in glance image metadata to specify volume type.
|
Loading…
Reference in New Issue
Block a user