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_COUNT_INFO = '3.45'
SUPPORT_NOVA_IMAGE = '3.46'
def get_mv_header(version): def get_mv_header(version):
"""Gets a formatted HTTP microversion header. """Gets a formatted HTTP microversion header.

View File

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

View File

@ -377,3 +377,7 @@ user documentation.
3.45 3.45
---- ----
Add ``count`` field to volume, backup and snapshot list and detail APIs. 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 exception
from cinder import group as group_api from cinder import group as group_api
from cinder.i18n import _ from cinder.i18n import _
from cinder.image import glance
from cinder import objects from cinder import objects
from cinder.policies import volumes as policy from cinder.policies import volumes as policy
from cinder import utils from cinder import utils
@ -193,6 +194,31 @@ class VolumeController(volumes_v2.VolumeController):
except exception.VolumeSizeExceedsAvailableQuota as e: except exception.VolumeSizeExceedsAvailableQuota as e:
raise exc.HTTPForbidden(explanation=six.text_type(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) @wsgi.response(http_client.ACCEPTED)
def create(self, req, body): def create(self, req, body):
"""Creates a new volume. """Creates a new volume.
@ -297,6 +323,17 @@ class VolumeController(volumes_v2.VolumeController):
# Not found exception will be handled at the wsgi level # Not found exception will be handled at the wsgi level
kwargs['group'] = self.group_api.get(context, group_id) 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) size = volume.get('size', None)
if size is None and kwargs['snapshot'] is not None: if size is None and kwargs['snapshot'] is not None:
size = kwargs['snapshot']['volume_size'] size = kwargs['snapshot']['volume_size']
@ -305,12 +342,6 @@ class VolumeController(volumes_v2.VolumeController):
LOG.info("Create volume of %s GB", size) 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['availability_zone'] = volume.get('availability_zone', None)
kwargs['scheduler_hints'] = volume.get('scheduler_hints', None) kwargs['scheduler_hints'] = volume.get('scheduler_hints', None)
multiattach = volume.get('multiattach', False) 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 fakes as v2_fakes
from cinder.tests.unit.api.v2 import test_volumes as v2_test_volumes 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 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.tests.unit import utils as test_utils
from cinder import utils from cinder import utils
from cinder.volume import api as volume_api from cinder.volume import api as volume_api
@ -48,6 +49,7 @@ class VolumeApiTest(test.TestCase):
super(VolumeApiTest, self).setUp() super(VolumeApiTest, self).setUp()
self.ext_mgr = extensions.ExtensionManager() self.ext_mgr = extensions.ExtensionManager()
self.ext_mgr.extensions = {} self.ext_mgr.extensions = {}
fake_image.mock_image_service(self)
self.controller = volumes.VolumeController(self.ext_mgr) self.controller = volumes.VolumeController(self.ext_mgr)
self.flags(host='fake') self.flags(host='fake')
@ -259,6 +261,40 @@ class VolumeApiTest(test.TestCase):
req.api_version_request = mv.get_api_version(version) req.api_version_request = mv.get_api_version(version)
return req 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): def test_volumes_summary_in_unsupport_version(self):
"""Function call to test summary volumes API in unsupported version""" """Function call to test summary volumes API in unsupported version"""
req = self._fake_volumes_summary_request( req = self._fake_volumes_summary_request(

View File

@ -147,6 +147,24 @@ class _FakeImageService(object):
'auto_disk_config': 'True'}, 'auto_disk_config': 'True'},
'size': 1234000000} '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, image1)
self.create(None, image2) self.create(None, image2)
self.create(None, image3) self.create(None, image3)
@ -154,6 +172,7 @@ class _FakeImageService(object):
self.create(None, image5) self.create(None, image5)
self.create(None, image6) self.create(None, image6)
self.create(None, image7) self.create(None, image7)
self.create(None, image8)
self._imagedata = {} self._imagedata = {}
self.temp_images = mock.MagicMock() self.temp_images = mock.MagicMock()
super(_FakeImageService, self).__init__() 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).