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:
@@ -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.
|
||||
Reference in New Issue
Block a user