diff --git a/cinder/api/microversions.py b/cinder/api/microversions.py index a724f3cdd99..b6476863ae6 100644 --- a/cinder/api/microversions.py +++ b/cinder/api/microversions.py @@ -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. diff --git a/cinder/api/openstack/api_version_request.py b/cinder/api/openstack/api_version_request.py index 381e04c1645..6df9ca7c5ab 100644 --- a/cinder/api/openstack/api_version_request.py +++ b/cinder/api/openstack/api_version_request.py @@ -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" diff --git a/cinder/api/openstack/rest_api_version_history.rst b/cinder/api/openstack/rest_api_version_history.rst index 22f8ca11fe2..becf74d39c2 100644 --- a/cinder/api/openstack/rest_api_version_history.rst +++ b/cinder/api/openstack/rest_api_version_history.rst @@ -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). diff --git a/cinder/api/v3/volumes.py b/cinder/api/v3/volumes.py index 4aa84801c9c..4509eeec295 100644 --- a/cinder/api/v3/volumes.py +++ b/cinder/api/v3/volumes.py @@ -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) diff --git a/cinder/tests/unit/api/v3/test_volumes.py b/cinder/tests/unit/api/v3/test_volumes.py index 9e19182ae5e..07184275d18 100644 --- a/cinder/tests/unit/api/v3/test_volumes.py +++ b/cinder/tests/unit/api/v3/test_volumes.py @@ -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( diff --git a/cinder/tests/unit/image/fake.py b/cinder/tests/unit/image/fake.py index ef439385b1f..cce77e2584d 100644 --- a/cinder/tests/unit/image/fake.py +++ b/cinder/tests/unit/image/fake.py @@ -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__() diff --git a/releasenotes/notes/bug-1560867-support-nova-specific-image-7yt6fd1173c4e3wd.yaml b/releasenotes/notes/bug-1560867-support-nova-specific-image-7yt6fd1173c4e3wd.yaml new file mode 100644 index 00000000000..18b19486d32 --- /dev/null +++ b/releasenotes/notes/bug-1560867-support-nova-specific-image-7yt6fd1173c4e3wd.yaml @@ -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).