Support create a volume from image snapshot

The instance boot from volume, snapshot the instance will create
a image in glance. The image size is 0, it just a link to the
volume snapshot. When the user create volume from the snapshot
image, in the current version, it will create a blank volume. so
the user is troubled. This patch fix it.

The scheme: when the user create volume from snapshot image, the
api create interface will parse out the snapshot image corresponds
to the volume snapshot id. Then create a volume from the volume
snapshot. So, the user can use this way to restore the instance's
bootable volume.

Change-Id: Ic44dfae8bb187a5aa9c2c1f50f7b5dc0ef0f28f8
Closes-Bug: #1560867
This commit is contained in:
zhengyao1 2016-06-20 17:06:12 +08:00 committed by TommyLike
parent 80558687d0
commit 25dd8109df
7 changed files with 105 additions and 7 deletions

View File

@ -129,6 +129,8 @@ NEW_ATTACH_COMPLETION = '3.44'
SUPPORT_COUNT_INFO = '3.45'
SUPPORT_NOVA_IMAGE = '3.46'
def get_mv_header(version):
"""Gets a formatted HTTP microversion header.

View File

@ -109,6 +109,7 @@ REST_API_VERSION_HISTORY = """
* 3.44 - Add attachment-complete.
* 3.45 - Add ``count`` field to volume, backup and snapshot list and
detail APIs.
* 3.46 - Support create volume by Nova specific image (0 size image).
"""
# The minimum and maximum versions of the API supported
@ -116,7 +117,7 @@ REST_API_VERSION_HISTORY = """
# minimum version of the API supported.
# Explicitly using /v2 endpoints will still work
_MIN_API_VERSION = "3.0"
_MAX_API_VERSION = "3.45"
_MAX_API_VERSION = "3.46"
_LEGACY_API_VERSION2 = "2.0"
UPDATED = "2017-09-19T20:18:14Z"

View File

@ -377,3 +377,7 @@ user documentation.
3.45
----
Add ``count`` field to volume, backup and snapshot list and detail APIs.
3.46
----
Support create volume by Nova specific image (0 size image).

View File

@ -28,6 +28,7 @@ from cinder.api.v3.views import volumes as volume_views_v3
from cinder import exception
from cinder import group as group_api
from cinder.i18n import _
from cinder.image import glance
from cinder import objects
from cinder.policies import volumes as policy
from cinder import utils
@ -193,6 +194,31 @@ class VolumeController(volumes_v2.VolumeController):
except exception.VolumeSizeExceedsAvailableQuota as e:
raise exc.HTTPForbidden(explanation=six.text_type(e))
def _get_image_snapshot(self, context, image_uuid):
image_snapshot = None
if image_uuid:
image_service = glance.get_default_image_service()
image_meta = image_service.show(context, image_uuid)
if image_meta is not None:
bdms = image_meta.get('properties', {}).get(
'block_device_mapping', [])
if bdms:
boot_bdm = [bdm for bdm in bdms if (
bdm.get('source_type') == 'snapshot' and
bdm.get('boot_index') == 0)]
if boot_bdm:
try:
image_snapshot = self.volume_api.get_snapshot(
context, boot_bdm[0].get('snapshot_id'))
return image_snapshot
except exception.NotFound:
explanation = _(
'Nova specific image is found, but boot '
'volume snapshot id:%s not found.'
) % boot_bdm[0].get('snapshot_id')
raise exc.HTTPNotFound(explanation=explanation)
return image_snapshot
@wsgi.response(http_client.ACCEPTED)
def create(self, req, body):
"""Creates a new volume.
@ -297,6 +323,17 @@ class VolumeController(volumes_v2.VolumeController):
# Not found exception will be handled at the wsgi level
kwargs['group'] = self.group_api.get(context, group_id)
if self.ext_mgr.is_loaded('os-image-create'):
image_ref = volume.get('imageRef')
if image_ref is not None:
image_uuid = self._image_uuid_from_ref(image_ref, context)
image_snapshot = self._get_image_snapshot(context, image_uuid)
if (req_version.matches(mv.get_api_version(
mv.SUPPORT_NOVA_IMAGE)) and image_snapshot):
kwargs['snapshot'] = image_snapshot
else:
kwargs['image_id'] = image_uuid
size = volume.get('size', None)
if size is None and kwargs['snapshot'] is not None:
size = kwargs['snapshot']['volume_size']
@ -305,12 +342,6 @@ class VolumeController(volumes_v2.VolumeController):
LOG.info("Create volume of %s GB", size)
if self.ext_mgr.is_loaded('os-image-create'):
image_ref = volume.get('imageRef')
if image_ref is not None:
image_uuid = self._image_uuid_from_ref(image_ref, context)
kwargs['image_id'] = image_uuid
kwargs['availability_zone'] = volume.get('availability_zone', None)
kwargs['scheduler_hints'] = volume.get('scheduler_hints', None)
multiattach = volume.get('multiattach', False)

View File

@ -34,6 +34,7 @@ from cinder.tests.unit.api import fakes
from cinder.tests.unit.api.v2 import fakes as v2_fakes
from cinder.tests.unit.api.v2 import test_volumes as v2_test_volumes
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit.image import fake as fake_image
from cinder.tests.unit import utils as test_utils
from cinder import utils
from cinder.volume import api as volume_api
@ -48,6 +49,7 @@ class VolumeApiTest(test.TestCase):
super(VolumeApiTest, self).setUp()
self.ext_mgr = extensions.ExtensionManager()
self.ext_mgr.extensions = {}
fake_image.mock_image_service(self)
self.controller = volumes.VolumeController(self.ext_mgr)
self.flags(host='fake')
@ -259,6 +261,40 @@ class VolumeApiTest(test.TestCase):
req.api_version_request = mv.get_api_version(version)
return req
@mock.patch.object(db.sqlalchemy.api, '_volume_type_get_full',
autospec=True)
@mock.patch.object(volume_api.API, 'get_snapshot', autospec=True)
@mock.patch.object(volume_api.API, 'create', autospec=True)
@mock.patch(
'cinder.api.openstack.wsgi.Controller.validate_name_and_description')
def test_volume_create_with_snapshot_image(self, mock_validate, create,
get_snapshot, volume_type_get):
create.side_effect = v2_fakes.fake_volume_api_create
get_snapshot.side_effect = v2_fakes.fake_snapshot_get
volume_type_get.side_effect = v2_fakes.fake_volume_type_get
self.ext_mgr.extensions = {'os-image-create': 'fake'}
vol = self._vol_in_request_body(
image_id="b0a599e0-41d7-3582-b260-769f443c862a")
snapshot_id = fake.SNAPSHOT_ID
ex = self._expected_vol_from_controller(snapshot_id=snapshot_id)
body = {"volume": vol}
req = fakes.HTTPRequest.blank('/v3/volumes')
req.headers = mv.get_mv_header(mv.SUPPORT_NOVA_IMAGE)
req.api_version_request = mv.get_api_version(mv.SUPPORT_NOVA_IMAGE)
res_dict = self.controller.create(req, body)
self.assertEqual(ex, res_dict)
context = req.environ['cinder.context']
get_snapshot.assert_called_once_with(self.controller.volume_api,
context, snapshot_id)
kwargs = self._expected_volume_api_create_kwargs(
v2_fakes.fake_snapshot(snapshot_id))
create.assert_called_once_with(
self.controller.volume_api, context,
vol['size'], v2_fakes.DEFAULT_VOL_NAME,
v2_fakes.DEFAULT_VOL_DESCRIPTION, **kwargs)
def test_volumes_summary_in_unsupport_version(self):
"""Function call to test summary volumes API in unsupported version"""
req = self._fake_volumes_summary_request(

View File

@ -147,6 +147,24 @@ class _FakeImageService(object):
'auto_disk_config': 'True'},
'size': 1234000000}
image8 = {'id': 'b0a599e0-41d7-3582-b260-769f443c862a',
'name': 'fakeimage8',
'created_at': timestamp,
'updated_at': timestamp,
'deleted_at': None,
'deleted': False,
'status': 'active',
'is_public': False,
'container_format': 'bare',
'disk_format': 'raw',
'properties':
{'block_device_mapping': [
{'boot_index': 0, 'source_type': 'snapshot',
'snapshot_id': fake_constants.SNAPSHOT_ID}],
'ramdisk_id': 'nokernel',
'architecture': 'x86_64',
'auto_disk_config': 'True'}}
self.create(None, image1)
self.create(None, image2)
self.create(None, image3)
@ -154,6 +172,7 @@ class _FakeImageService(object):
self.create(None, image5)
self.create(None, image6)
self.create(None, image7)
self.create(None, image8)
self._imagedata = {}
self.temp_images = mock.MagicMock()
super(_FakeImageService, self).__init__()

View File

@ -0,0 +1,5 @@
---
fixes:
- Fix the bug that Cinder can't support creating volume
from Nova specific image which only includes ``snapshot-id``
metadata (Bug #1560867).