]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Copy custom properties to image from volume
authorPranaliDeore <pranali.deore@nttdata.com>
Tue, 6 May 2014 04:04:45 +0000 (04:04 +0000)
committerPranaliDeore <pranali.deore@nttdata.com>
Wed, 11 Jun 2014 11:06:42 +0000 (11:06 +0000)
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

cinder/image/glance.py
cinder/tests/api/contrib/test_volume_actions.py
cinder/tests/api/v2/stubs.py
cinder/tests/policy.json
cinder/volume/api.py
etc/cinder/cinder.conf.sample

index e0e6ab257736d4ff1867457526a4ec34a1448b04..105df91055bbbb9a57e81f1207f3e6316868c97f 100644 (file)
@@ -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__)
index 86eb12320843933d0395629bcee2d56efcb2de55..6a8f5b7a29babefef9e21ff3c5d541daba1466dd 100644 (file)
@@ -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)
index dc269a67d99e7995f8be713cbfb19c49b5fde8dd..47c9384f3a21cdb16f76977a381f9ecf589db5a6 100644 (file)
@@ -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,
index 4347c2c8f5ba75e3df019a52674f736865c85187..f84cd775f6b9982d195bf3890739cae730c433ec 100644 (file)
@@ -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"]],
index 33e5dc8a4bdf068345a689c8465e134a70cfaa93..aaaa07456c31577f79067f928e18cf835f792a67 100644 (file)
@@ -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'})
index 4784a5696e3db155e0e9eda20f422e1b916abe54..561d3f55a769df3ff83444e912c47b42c61d6255 100644 (file)
 # 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)