From: PranaliDeore Date: Tue, 6 May 2014 04:04:45 +0000 (+0000) Subject: Copy custom properties to image from volume X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=dcf4b10cd1e16b28ab388def658e77cc9a34d766;p=openstack-build%2Fcinder-build.git Copy custom properties to image from volume Presently after copying an image to volume, all properties of the image are getting copied properly but while creating image back from volume, it doesn't copy custom properties to the image. At present in volume-glance-metadata table all the properties of volume are stored as key and value. Because of this it is difficult to differentiate between core and custom properties. To overcome this, I have added a new option 'glance_core_properties' in cinder.conf. This option defines all core properties of an image. This way, it's easy to separate core and custom properties from the glance_volume_metadata and add custom property to the newly created image. For Example: glance_core_properties = 'checksum', 'container_format', 'disk_format', 'image_name', 'image_id', 'min_disk', 'min_ram', 'name', 'size' DocImpact: Added 'glance_core_properties' to distinguish the core and custom properties as discussed above in cinder.conf blueprint: restrict-uploading-volume-to-image Change-Id: I786edbc6e54b3d06ef679a71e22676d2f88e7307 --- diff --git a/cinder/image/glance.py b/cinder/image/glance.py index e0e6ab257..105df9105 100644 --- a/cinder/image/glance.py +++ b/cinder/image/glance.py @@ -42,8 +42,16 @@ glance_opts = [ 'via the direct_url. Currently supported schemes: ' '[file].'), ] +glance_core_properties = [ + cfg.ListOpt('glance_core_properties', + default=['checksum', 'container_format', + 'disk_format', 'image_name', 'image_id', + 'min_disk', 'min_ram', 'name', 'size'], + help='Default core properties of image') +] CONF = cfg.CONF CONF.register_opts(glance_opts) +CONF.register_opts(glance_core_properties) CONF.import_opt('glance_api_version', 'cinder.common.config') LOG = logging.getLogger(__name__) diff --git a/cinder/tests/api/contrib/test_volume_actions.py b/cinder/tests/api/contrib/test_volume_actions.py index 86eb12320..6a8f5b7a2 100644 --- a/cinder/tests/api/contrib/test_volume_actions.py +++ b/cinder/tests/api/contrib/test_volume_actions.py @@ -17,17 +17,22 @@ import json import uuid import mock +from oslo.config import cfg from oslo import messaging import webob from cinder.api.contrib import volume_actions from cinder import exception +from cinder.image.glance import GlanceImageService from cinder.openstack.common import jsonutils from cinder import test from cinder.tests.api import fakes from cinder.tests.api.v2 import stubs from cinder import volume from cinder.volume import api as volume_api +from cinder.volume import rpcapi as volume_rpcapi + +CONF = cfg.CONF class VolumeActionsTest(test.TestCase): @@ -456,6 +461,40 @@ class VolumeImageActionsTest(test.TestCase): self.stubs.Set(volume_api.API, 'get', stub_volume_get) + def _get_os_volume_upload_image(self): + vol = { + "container_format": 'bare', + "disk_format": 'raw', + "updated_at": datetime.datetime(1, 1, 1, 1, 1, 1), + "image_name": 'image_name', + "is_public": False, + "force": True} + body = {"os-volume_upload_image": vol} + + return body + + def fake_image_service_create(self, *args): + ret = { + 'status': u'queued', + 'name': u'image_name', + 'deleted': False, + 'container_format': u'bare', + 'created_at': datetime.datetime(1, 1, 1, 1, 1, 1), + 'disk_format': u'raw', + 'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1), + 'id': 1, + 'min_ram': 0, + 'checksum': None, + 'min_disk': 0, + 'is_public': False, + 'deleted_at': None, + 'properties': {u'x_billing_code_license': u'246254365'}, + 'size': 0} + return ret + + def fake_rpc_copy_volume_to_image(self, *args): + pass + def test_copy_volume_to_image(self): self.stubs.Set(volume_api.API, "copy_volume_to_image", @@ -610,3 +649,214 @@ class VolumeImageActionsTest(test.TestCase): req, id, body) + + def test_copy_volume_to_image_with_protected_prop(self): + """Test create image from volume with protected properties.""" + id = 1 + + def fake_get_volume_image_metadata(*args): + meta_dict = { + "volume_id": id, + "key": "x_billing_code_license", + "value": "246254365"} + return meta_dict + + # Need to mock get_volume_image_metadata, create, + # update and copy_volume_to_image + with mock.patch.object(volume_api.API, "get_volume_image_metadata") \ + as mock_get_volume_image_metadata: + mock_get_volume_image_metadata.side_effect = \ + fake_get_volume_image_metadata + + with mock.patch.object(GlanceImageService, "create") \ + as mock_create: + mock_create.side_effect = self.fake_image_service_create + + with mock.patch.object(volume_api.API, "update") \ + as mock_update: + mock_update.side_effect = stubs.stub_volume_update + + with mock.patch.object(volume_rpcapi.VolumeAPI, + "copy_volume_to_image") \ + as mock_copy_volume_to_image: + mock_copy_volume_to_image.side_effect = \ + self.fake_rpc_copy_volume_to_image + + req = fakes.HTTPRequest.blank( + '/v2/tenant1/volumes/%s/action' % id) + body = self._get_os_volume_upload_image() + res_dict = self.controller._volume_upload_image(req, + id, + body) + expected_res = { + 'os-volume_upload_image': { + 'id': id, + 'updated_at': datetime.datetime(1900, 1, 1, + 1, 1, 1), + 'status': 'uploading', + 'display_description': 'displaydesc', + 'size': 1, + 'volume_type': {'name': 'vol_type_name'}, + 'image_id': 1, + 'container_format': 'bare', + 'disk_format': 'raw', + 'image_name': 'image_name' + } + } + + self.assertDictMatch(res_dict, expected_res) + + def test_copy_volume_to_image_without_glance_metadata(self): + """Test create image from volume if volume is created without image. + + In this case volume glance metadata will not be available for this + volume. + """ + id = 1 + + def fake_get_volume_image_metadata_raise(*args): + raise exception.GlanceMetadataNotFound(id=id) + + # Need to mock get_volume_image_metadata, create, + # update and copy_volume_to_image + with mock.patch.object(volume_api.API, "get_volume_image_metadata") \ + as mock_get_volume_image_metadata: + mock_get_volume_image_metadata.side_effect = \ + fake_get_volume_image_metadata_raise + + with mock.patch.object(GlanceImageService, "create") \ + as mock_create: + mock_create.side_effect = self.fake_image_service_create + + with mock.patch.object(volume_api.API, "update") \ + as mock_update: + mock_update.side_effect = stubs.stub_volume_update + + with mock.patch.object(volume_rpcapi.VolumeAPI, + "copy_volume_to_image") \ + as mock_copy_volume_to_image: + mock_copy_volume_to_image.side_effect = \ + self.fake_rpc_copy_volume_to_image + + req = fakes.HTTPRequest.blank( + '/v2/tenant1/volumes/%s/action' % id) + body = self._get_os_volume_upload_image() + res_dict = self.controller._volume_upload_image(req, + id, + body) + expected_res = { + 'os-volume_upload_image': { + 'id': id, + 'updated_at': datetime.datetime(1900, 1, 1, + 1, 1, 1), + 'status': 'uploading', + 'display_description': 'displaydesc', + 'size': 1, + 'volume_type': {'name': 'vol_type_name'}, + 'image_id': 1, + 'container_format': 'bare', + 'disk_format': 'raw', + 'image_name': 'image_name' + } + } + + self.assertDictMatch(res_dict, expected_res) + + def test_copy_volume_to_image_without_protected_prop(self): + """Test protected property is not defined with the root image.""" + id = 1 + + def fake_get_volume_image_metadata(*args): + return [] + + # Need to mock get_volume_image_metadata, create, + # update and copy_volume_to_image + with mock.patch.object(volume_api.API, "get_volume_image_metadata") \ + as mock_get_volume_image_metadata: + mock_get_volume_image_metadata.side_effect = \ + fake_get_volume_image_metadata + + with mock.patch.object(GlanceImageService, "create") \ + as mock_create: + mock_create.side_effect = self.fake_image_service_create + + with mock.patch.object(volume_api.API, "update") \ + as mock_update: + mock_update.side_effect = stubs.stub_volume_update + + with mock.patch.object(volume_rpcapi.VolumeAPI, + "copy_volume_to_image") \ + as mock_copy_volume_to_image: + mock_copy_volume_to_image.side_effect = \ + self.fake_rpc_copy_volume_to_image + + req = fakes.HTTPRequest.blank( + '/v2/tenant1/volumes/%s/action' % id) + + body = self._get_os_volume_upload_image() + res_dict = self.controller._volume_upload_image(req, + id, + body) + expected_res = { + 'os-volume_upload_image': { + 'id': id, + 'updated_at': datetime.datetime(1900, 1, 1, + 1, 1, 1), + 'status': 'uploading', + 'display_description': 'displaydesc', + 'size': 1, + 'volume_type': {'name': 'vol_type_name'}, + 'image_id': 1, + 'container_format': 'bare', + 'disk_format': 'raw', + 'image_name': 'image_name' + } + } + + self.assertDictMatch(res_dict, expected_res) + + def test_copy_volume_to_image_without_core_prop(self): + """Test glance_core_properties defined in cinder.conf is empty.""" + id = 1 + + # Need to mock create, update, copy_volume_to_image + with mock.patch.object(GlanceImageService, "create") \ + as mock_create: + mock_create.side_effect = self.fake_image_service_create + + with mock.patch.object(volume_api.API, "update") \ + as mock_update: + mock_update.side_effect = stubs.stub_volume_update + + with mock.patch.object(volume_rpcapi.VolumeAPI, + "copy_volume_to_image") \ + as mock_copy_volume_to_image: + mock_copy_volume_to_image.side_effect = \ + self.fake_rpc_copy_volume_to_image + + CONF.set_override('glance_core_properties', []) + + req = fakes.HTTPRequest.blank( + '/v2/tenant1/volumes/%s/action' % id) + + body = self._get_os_volume_upload_image() + res_dict = self.controller._volume_upload_image(req, + id, + body) + expected_res = { + 'os-volume_upload_image': { + 'id': id, + 'updated_at': datetime.datetime(1900, 1, 1, + 1, 1, 1), + 'status': 'uploading', + 'display_description': 'displaydesc', + 'size': 1, + 'volume_type': {'name': 'vol_type_name'}, + 'image_id': 1, + 'container_format': 'bare', + 'disk_format': 'raw', + 'image_name': 'image_name' + } + } + + self.assertDictMatch(res_dict, expected_res) diff --git a/cinder/tests/api/v2/stubs.py b/cinder/tests/api/v2/stubs.py index dc269a67d..47c9384f3 100644 --- a/cinder/tests/api/v2/stubs.py +++ b/cinder/tests/api/v2/stubs.py @@ -40,6 +40,7 @@ def stub_volume(id, **kwargs): 'name': 'vol name', 'display_name': 'displayname', 'display_description': 'displaydesc', + 'updated_at': datetime.datetime(1900, 1, 1, 1, 1, 1), 'created_at': datetime.datetime(1, 1, 1, 1, 1, 1), 'snapshot_id': None, 'source_volid': None, diff --git a/cinder/tests/policy.json b/cinder/tests/policy.json index 4347c2c8f..f84cd775f 100644 --- a/cinder/tests/policy.json +++ b/cinder/tests/policy.json @@ -34,6 +34,7 @@ "volume:migrate_volume_completion": [["rule:admin_api"]], "volume:update_readonly_flag": [], "volume:retype": [], + "volume:copy_volume_to_image": [], "volume_extension:volume_admin_actions:reset_status": [["rule:admin_api"]], "volume_extension:snapshot_admin_actions:reset_status": [["rule:admin_api"]], diff --git a/cinder/volume/api.py b/cinder/volume/api.py index 33e5dc8a4..aaaa07456 100644 --- a/cinder/volume/api.py +++ b/cinder/volume/api.py @@ -57,6 +57,8 @@ volume_same_az_opt = cfg.BoolOpt('cloned_volume_same_az', CONF = cfg.CONF CONF.register_opt(volume_host_opt) CONF.register_opt(volume_same_az_opt) + +CONF.import_opt('glance_core_properties', 'cinder.image.glance') CONF.import_opt('storage_availability_zone', 'cinder.volume.manager') LOG = logging.getLogger(__name__) @@ -722,6 +724,25 @@ class API(base.Base): def copy_volume_to_image(self, context, volume, metadata, force): """Create a new image from the specified volume.""" self._check_volume_availability(volume, force) + glance_core_properties = CONF.glance_core_properties + if glance_core_properties: + try: + volume_image_metadata = self.get_volume_image_metadata(context, + volume) + custom_property_set = (set(volume_image_metadata).difference + (set(glance_core_properties))) + if custom_property_set: + metadata.update(dict(properties=dict((custom_property, + volume_image_metadata + [custom_property]) + for custom_property + in custom_property_set))) + except exception.GlanceMetadataNotFound: + # If volume is not created from image, No glance metadata + # would be available for that volume in + # volume glance metadata table + + pass recv_metadata = self.image_service.create(context, metadata) self.update(context, volume, {'status': 'uploading'}) diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index 4784a5696..561d3f55a 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -664,6 +664,9 @@ # Options defined in cinder.image.glance # +# Default core properties of image (list value) +#glance_core_properties=checksum,container_format,disk_format,image_name,image_id,min_disk,min_ram,name,size + # A list of url schemes that can be downloaded directly via # the direct_url. Currently supported schemes: [file]. (list # value)