]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Generic iSCSI copy volume<->image.
authorAvishay Traeger <avishay@il.ibm.com>
Wed, 16 Jan 2013 14:19:40 +0000 (16:19 +0200)
committerAvishay Traeger <avishay@il.ibm.com>
Mon, 28 Jan 2013 05:13:47 +0000 (07:13 +0200)
Implements a generic version of copy_volume_to_image and
copy_image_to_volume for iSCSI drivers.

Change-Id: Iff097629bcce9154829a7eb5aee0ea6302338b26
Implements: blueprint generic-iscsi-copy-vol-image

16 files changed:
cinder/image/image_utils.py
cinder/tests/test_volume.py
cinder/tests/test_volume_rpcapi.py
cinder/volume/api.py
cinder/volume/driver.py
cinder/volume/drivers/emc/emc_smis_iscsi.py
cinder/volume/drivers/lvm.py
cinder/volume/drivers/netapp.py
cinder/volume/drivers/nexenta/volume.py
cinder/volume/drivers/san/san.py
cinder/volume/drivers/windows.py
cinder/volume/drivers/xenapi/sm.py
cinder/volume/drivers/zadara.py
cinder/volume/manager.py
cinder/volume/rpcapi.py
etc/cinder/rootwrap.d/volume.filters

index f0d947f126229541ae763bbde939ef60d7825e08..dad7d42d8fd85562600af6f61b02ca7534714d16 100644 (file)
@@ -208,6 +208,10 @@ def fetch_to_raw(context, image_service,
             os.path.exists(FLAGS.image_conversion_dir)):
         os.makedirs(FLAGS.image_conversion_dir)
 
+    # NOTE(avishay): I'm not crazy about creating temp files which may be
+    # large and cause disk full errors which would confuse users.
+    # Unfortunately it seems that you can't pipe to 'qemu-img convert' because
+    # it seeks. Maybe we can think of something for a future version.
     fd, tmp = tempfile.mkstemp(dir=FLAGS.image_conversion_dir)
     os.close(fd)
     with utils.remove_path_on_error(tmp):
@@ -229,6 +233,11 @@ def fetch_to_raw(context, image_service,
 
         # NOTE(jdg): I'm using qemu-img convert to write
         # to the volume regardless if it *needs* conversion or not
+        # TODO(avishay): We can speed this up by checking if the image is raw
+        # and if so, writing directly to the device. However, we need to keep
+        # check via 'qemu-img info' that what we copied was in fact a raw
+        # image and not a different format with a backing file, which may be
+        # malicious.
         LOG.debug("%s was %s, converting to raw" % (image_id, fmt))
         convert_image(tmp, dest, 'raw')
 
@@ -239,3 +248,36 @@ def fetch_to_raw(context, image_service,
                 reason=_("Converted to raw, but format is now %s") %
                 data.file_format)
         os.unlink(tmp)
+
+
+def upload_volume(context, image_service, image_meta, volume_path):
+    image_id = image_meta['id']
+    if (image_meta['disk_format'] == 'raw'):
+        LOG.debug("%s was raw, no need to convert to %s" %
+                  (image_id, image_meta['disk_format']))
+        with utils.temporary_chown(volume_path):
+            with utils.file_open(volume_path) as image_file:
+                image_service.update(context, image_id, {}, image_file)
+        return
+
+    if (FLAGS.image_conversion_dir and not
+            os.path.exists(FLAGS.image_conversion_dir)):
+        os.makedirs(FLAGS.image_conversion_dir)
+
+    fd, tmp = tempfile.mkstemp(dir=FLAGS.image_conversion_dir)
+    os.close(fd)
+    with utils.remove_path_on_error(tmp):
+        LOG.debug("%s was raw, converting to %s" %
+                  (image_id, image_meta['disk_format']))
+        convert_image(volume_path, tmp, image_meta['disk_format'])
+
+        data = qemu_img_info(tmp)
+        if data.file_format != image_meta['disk_format']:
+            raise exception.ImageUnacceptable(
+                image_id=image_id,
+                reason=_("Converted to %(f1)s, but format is now %(f2)s") %
+                {'f1': image_meta['disk_format'], 'f2': data.file_format})
+
+        with utils.file_open(tmp) as image_file:
+            image_service.update(context, image_id, {}, image_file)
+        os.unlink(tmp)
index 98b634db13f1ff2ca2f381c72a4733607b78cdb5..1f35c43f4edd27a9ff0d8d6082838591155390b5 100644 (file)
@@ -594,7 +594,11 @@ class VolumeTestCase(test.TestCase):
 
         self.stubs.Set(self.volume.driver, 'local_path', fake_local_path)
 
-        image_id = '70a599e0-31e7-49b7-b260-868f441e862b'
+        image_meta = {
+            'id': '70a599e0-31e7-49b7-b260-868f441e862b',
+            'container_format': 'bare',
+            'disk_format': 'raw'}
+
         # creating volume testdata
         volume_id = 1
         db.volume_create(self.context,
@@ -610,7 +614,7 @@ class VolumeTestCase(test.TestCase):
             # start test
             self.volume.copy_volume_to_image(self.context,
                                              volume_id,
-                                             image_id)
+                                             image_meta)
 
             volume = db.volume_get(self.context, volume_id)
             self.assertEqual(volume['status'], 'available')
@@ -628,8 +632,10 @@ class VolumeTestCase(test.TestCase):
 
         self.stubs.Set(self.volume.driver, 'local_path', fake_local_path)
 
-        #image_id = '70a599e0-31e7-49b7-b260-868f441e862b'
-        image_id = 'a440c04b-79fa-479c-bed1-0b816eaec379'
+        image_meta = {
+            'id': 'a440c04b-79fa-479c-bed1-0b816eaec379',
+            'container_format': 'bare',
+            'disk_format': 'raw'}
         # creating volume testdata
         volume_id = 1
         db.volume_create(
@@ -646,7 +652,7 @@ class VolumeTestCase(test.TestCase):
             # start test
             self.volume.copy_volume_to_image(self.context,
                                              volume_id,
-                                             image_id)
+                                             image_meta)
 
             volume = db.volume_get(self.context, volume_id)
             self.assertEqual(volume['status'], 'in-use')
@@ -664,7 +670,10 @@ class VolumeTestCase(test.TestCase):
 
         self.stubs.Set(self.volume.driver, 'local_path', fake_local_path)
 
-        image_id = 'aaaaaaaa-0000-0000-0000-000000000000'
+        image_meta = {
+            'id': 'aaaaaaaa-0000-0000-0000-000000000000',
+            'container_format': 'bare',
+            'disk_format': 'raw'}
         # creating volume testdata
         volume_id = 1
         db.volume_create(self.context,
@@ -681,7 +690,7 @@ class VolumeTestCase(test.TestCase):
                               self.volume.copy_volume_to_image,
                               self.context,
                               volume_id,
-                              image_id)
+                              image_meta)
 
             volume = db.volume_get(self.context, volume_id)
             self.assertEqual(volume['status'], 'available')
@@ -698,7 +707,9 @@ class VolumeTestCase(test.TestCase):
                 pass
 
             def show(self, context, image_id):
-                return {'size': 2 * 1024 * 1024 * 1024}
+                return {'size': 2 * 1024 * 1024 * 1024,
+                        'disk_format': 'raw',
+                        'container_format': 'bare'}
 
         image_id = '70a599e0-31e7-49b7-b260-868f441e862b'
 
@@ -722,7 +733,9 @@ class VolumeTestCase(test.TestCase):
                 pass
 
             def show(self, context, image_id):
-                return {'size': 2 * 1024 * 1024 * 1024 + 1}
+                return {'size': 2 * 1024 * 1024 * 1024 + 1,
+                        'disk_format': 'raw',
+                        'container_format': 'bare'}
 
         image_id = '70a599e0-31e7-49b7-b260-868f441e862b'
 
index 0b785387daf854a29ace1443bd10dddb152f51dc..5fdc8092afb31c2ca7312310ff2fcd616648b6b7 100644 (file)
@@ -150,7 +150,10 @@ class VolumeRpcAPITestCase(test.TestCase):
         self._test_volume_api('copy_volume_to_image',
                               rpc_method='cast',
                               volume=self.fake_volume,
-                              image_id='fake_image_id')
+                              image_meta={'id': 'fake_image_id',
+                                          'container_format': 'fake_type',
+                                          'disk_format': 'fake_type'},
+                              version='1.3')
 
     def test_initialize_connection(self):
         self._test_volume_api('initialize_connection',
index 050060ad35c32714f6752c4429a8d25f61702e29..0bba3a3360297dba6ed2d38d4b33879a75fb617f 100644 (file)
@@ -144,6 +144,14 @@ class API(base.Base):
             if image_size_in_gb > size:
                 msg = _('Size of specified image is larger than volume size.')
                 raise exception.InvalidInput(reason=msg)
+            #We use qemu-img to convert images to raw and so we can only
+            #support the intersection of what qemu-img and glance support
+            if (image_meta['container_format'] != 'bare' or
+                image_meta['disk_format'] not in ['raw', 'qcow2',
+                                                  'vmdk', 'vdi']):
+                msg = (_("Image format must be one of raw, qcow2, "
+                         "vmdk, or vdi."))
+                raise exception.InvalidInput(reason=msg)
 
         try:
             reservations = QUOTAS.reserve(context, volumes=1, gigabytes=size)
@@ -592,11 +600,21 @@ class API(base.Base):
         """Create a new image from the specified volume."""
         self._check_volume_availability(context, volume, force)
 
+        #We use qemu-img to convert raw images to the requested type
+        #and so we can only support the intersection of what qemu-img and
+        #glance support
+        if (metadata['container_format'] != 'bare' or
+                metadata['disk_format'] not in ['raw', 'qcow2',
+                                                'vmdk', 'vdi']):
+            msg = (_("Image format must be one of raw, qcow2, "
+                     "vmdk, or vdi."))
+            raise exception.InvalidInput(reason=msg)
+
         recv_metadata = self.image_service.create(context, metadata)
         self.update(context, volume, {'status': 'uploading'})
         self.volume_rpcapi.copy_volume_to_image(context,
                                                 volume,
-                                                recv_metadata['id'])
+                                                recv_metadata)
 
         response = {"id": volume['id'],
                     "updated_at": volume['updated_at'],
index 2e4de3648265b273b0de6dcf1b6182f7c93d1a43..5c5677be417a7f18b78b498ff727be733bff682c 100644 (file)
@@ -20,10 +20,12 @@ Drivers for volumes.
 
 """
 
+import os
 import time
 
 from cinder import exception
 from cinder import flags
+from cinder.image import image_utils
 from cinder.openstack.common import cfg
 from cinder.openstack.common import log as logging
 from cinder import utils
@@ -172,7 +174,7 @@ class VolumeDriver(object):
         """Fetch the image from image_service and write it to the volume."""
         raise NotImplementedError()
 
-    def copy_volume_to_image(self, context, volume, image_service, image_id):
+    def copy_volume_to_image(self, context, volume, image_service, image_meta):
         """Copy the volume to the specified image."""
         raise NotImplementedError()
 
@@ -287,19 +289,22 @@ class ISCSIDriver(VolumeDriver):
 
         return properties
 
-    def _run_iscsiadm(self, iscsi_properties, iscsi_command):
+    def _run_iscsiadm(self, iscsi_properties, iscsi_command, **kwargs):
+        check_exit_code = kwargs.pop('check_exit_code', 0)
         (out, err) = self._execute('iscsiadm', '-m', 'node', '-T',
                                    iscsi_properties['target_iqn'],
                                    '-p', iscsi_properties['target_portal'],
-                                   *iscsi_command, run_as_root=True)
+                                   *iscsi_command, run_as_root=True,
+                                   check_exit_code=check_exit_code)
         LOG.debug("iscsiadm %s: stdout=%s stderr=%s" %
                   (iscsi_command, out, err))
         return (out, err)
 
-    def _iscsiadm_update(self, iscsi_properties, property_key, property_value):
+    def _iscsiadm_update(self, iscsi_properties, property_key, property_value,
+                         **kwargs):
         iscsi_command = ('--op', 'update', '-n', property_key,
                          '-v', property_value)
-        return self._run_iscsiadm(iscsi_properties, iscsi_command)
+        return self._run_iscsiadm(iscsi_properties, iscsi_command, **kwargs)
 
     def initialize_connection(self, volume, connector):
         """Initializes the connection and returns connection info.
@@ -329,6 +334,115 @@ class ISCSIDriver(VolumeDriver):
     def terminate_connection(self, volume, connector, **kwargs):
         pass
 
+    def _get_iscsi_initiator(self):
+        """Get iscsi initiator name for this machine"""
+        # NOTE openiscsi stores initiator name in a file that
+        #      needs root permission to read.
+        contents = utils.read_file_as_root('/etc/iscsi/initiatorname.iscsi')
+        for l in contents.split('\n'):
+            if l.startswith('InitiatorName='):
+                return l[l.index('=') + 1:].strip()
+
+    def copy_image_to_volume(self, context, volume, image_service, image_id):
+        """Fetch the image from image_service and write it to the volume."""
+        LOG.debug(_('copy_image_to_volume %s.') % volume['name'])
+        initiator = self._get_iscsi_initiator()
+        connector = {}
+        connector['initiator'] = initiator
+
+        iscsi_properties, volume_path = self._attach_volume(
+            context, volume, connector)
+
+        try:
+            image_utils.fetch_to_raw(context,
+                                     image_service,
+                                     image_id,
+                                     volume_path)
+        finally:
+            self.terminate_connection(volume, connector)
+
+    def copy_volume_to_image(self, context, volume, image_service, image_meta):
+        """Copy the volume to the specified image."""
+        LOG.debug(_('copy_volume_to_image %s.') % volume['name'])
+        initiator = self._get_iscsi_initiator()
+        connector = {}
+        connector['initiator'] = initiator
+
+        iscsi_properties, volume_path = self._attach_volume(
+            context, volume, connector)
+
+        try:
+            image_utils.upload_volume(context,
+                                      image_service,
+                                      image_meta,
+                                      volume_path)
+        finally:
+            self.terminate_connection(volume, connector)
+
+    def _attach_volume(self, context, volume, connector):
+        """Attach the volume."""
+        iscsi_properties = None
+        host_device = None
+        init_conn = self.initialize_connection(volume, connector)
+        iscsi_properties = init_conn['data']
+
+        # code "inspired by" nova/virt/libvirt/volume.py
+        try:
+            self._run_iscsiadm(iscsi_properties, ())
+        except exception.ProcessExecutionError as exc:
+            # iscsiadm returns 21 for "No records found" after version 2.0-871
+            if exc.exit_code in [21, 255]:
+                self._run_iscsiadm(iscsi_properties, ('--op', 'new'))
+            else:
+                raise
+
+        if iscsi_properties.get('auth_method'):
+            self._iscsiadm_update(iscsi_properties,
+                                  "node.session.auth.authmethod",
+                                  iscsi_properties['auth_method'])
+            self._iscsiadm_update(iscsi_properties,
+                                  "node.session.auth.username",
+                                  iscsi_properties['auth_username'])
+            self._iscsiadm_update(iscsi_properties,
+                                  "node.session.auth.password",
+                                  iscsi_properties['auth_password'])
+
+        # NOTE(vish): If we have another lun on the same target, we may
+        #             have a duplicate login
+        self._run_iscsiadm(iscsi_properties, ("--login",),
+                           check_exit_code=[0, 255])
+
+        self._iscsiadm_update(iscsi_properties, "node.startup", "automatic")
+
+        host_device = ("/dev/disk/by-path/ip-%s-iscsi-%s-lun-%s" %
+                       (iscsi_properties['target_portal'],
+                        iscsi_properties['target_iqn'],
+                        iscsi_properties.get('target_lun', 0)))
+
+        tries = 0
+        while not os.path.exists(host_device):
+            if tries >= FLAGS.num_iscsi_scan_tries:
+                raise exception.CinderException(
+                    _("iSCSI device not found at %s") % (host_device))
+
+            LOG.warn(_("ISCSI volume not yet found at: %(host_device)s. "
+                     "Will rescan & retry.  Try number: %(tries)s") %
+                     locals())
+
+            # The rescan isn't documented as being necessary(?), but it helps
+            self._run_iscsiadm(iscsi_properties, ("--rescan"))
+
+            tries = tries + 1
+            if not os.path.exists(host_device):
+                time.sleep(tries ** 2)
+
+        if tries != 0:
+            LOG.debug(_("Found iSCSI node %(host_device)s "
+                      "(after %(tries)s rescans)") %
+                      locals())
+
+        return iscsi_properties, host_device
+
 
 class FakeISCSIDriver(ISCSIDriver):
     """Logs calls instead of executing."""
index 0ffe0e42b75f9cb9f1806ed085abaf8f266cba74..936d17e708307c44ddbefd1b4848ee3ea6a43438 100644 (file)
@@ -269,7 +269,7 @@ class EMCSMISISCSIDriver(driver.ISCSIDriver):
 
         return iscsi_properties, host_device
 
-    def copy_volume_to_image(self, context, volume, image_service, image_id):
+    def copy_volume_to_image(self, context, volume, image_service, image_meta):
         """Copy the volume to the specified image."""
         LOG.debug(_('copy_volume_to_image %s.') % volume['name'])
         initiator = get_iscsi_initiator()
@@ -281,7 +281,8 @@ class EMCSMISISCSIDriver(driver.ISCSIDriver):
 
         with utils.temporary_chown(volume_path):
             with utils.file_open(volume_path) as volume_file:
-                image_service.update(context, image_id, {}, volume_file)
+                image_service.update(context, image_meta['id'], {},
+                                     volume_file)
 
         self.terminate_connection(volume, connector)
 
index b3bde19b8f85520ab2b7afa2be8e7615c4920ab0..bc353cbffcf46a9501ce73d48a08802d0f2c951d 100644 (file)
@@ -237,12 +237,12 @@ class LVMVolumeDriver(driver.VolumeDriver):
                                  image_id,
                                  self.local_path(volume))
 
-    def copy_volume_to_image(self, context, volume, image_service, image_id):
+    def copy_volume_to_image(self, context, volume, image_service, image_meta):
         """Copy the volume to the specified image."""
-        volume_path = self.local_path(volume)
-        with utils.temporary_chown(volume_path):
-            with utils.file_open(volume_path) as volume_file:
-                image_service.update(context, image_id, {}, volume_file)
+        image_utils.upload_volume(context,
+                                  image_service,
+                                  image_meta,
+                                  self.local_path(volume))
 
     def clone_image(self, volume, image_location):
         return False
index 3ce77c5c2191580d6cda6abf4bc50345221f7160..eb613cb76bd8b6d3d6b9c08ad453301cebf6126c 100644 (file)
@@ -1307,7 +1307,7 @@ class NetAppCmodeISCSIDriver(driver.ISCSIDriver):
         """Fetch the image from image_service and write it to the volume."""
         raise NotImplementedError()
 
-    def copy_volume_to_image(self, context, volume, image_service, image_id):
+    def copy_volume_to_image(self, context, volume, image_service, image_meta):
         """Copy the volume to the specified image."""
         raise NotImplementedError()
 
index a384f34274b55d701d1f63ed5d2a61884fecbe0c..5af2084e95d66e4169f39a0283a18c9e9d89e158 100644 (file)
@@ -284,7 +284,7 @@ class NexentaDriver(driver.ISCSIDriver):  # pylint: disable=R0921
         """Fetch the image from image_service and write it to the volume."""
         raise NotImplementedError()
 
-    def copy_volume_to_image(self, context, volume, image_service, image_id):
+    def copy_volume_to_image(self, context, volume, image_service, image_meta):
         """Copy the volume to the specified image."""
         raise NotImplementedError()
 
index ec5f65195f57c02dd609ae2aa08e96fa2c74fae5..b6b442c1661eb3226dbdbfffc6857f1da73c00a4 100644 (file)
@@ -154,14 +154,6 @@ class SanISCSIDriver(ISCSIDriver):
         if not FLAGS.san_ip:
             raise exception.InvalidInput(reason=_("san_ip must be set"))
 
-    def copy_image_to_volume(self, context, volume, image_service, image_id):
-        """Fetch the image from image_service and write it to the volume."""
-        raise NotImplementedError()
-
-    def copy_volume_to_image(self, context, volume, image_service, image_id):
-        """Copy the volume to the specified image."""
-        raise NotImplementedError()
-
     def create_cloned_volume(self, volume, src_vref):
         """Create a cloen of the specified volume."""
         raise NotImplementedError()
index ac6a531d67c5f88a6d8550e33da8359f84cfe352..4c3a07026b193200653da3f0015004dbd071c068 100644 (file)
@@ -239,6 +239,6 @@ class WindowsDriver(driver.ISCSIDriver):
         """Fetch the image from image_service and write it to the volume."""
         raise NotImplementedError()
 
-    def copy_volume_to_image(self, context, volume, image_service, image_id):
+    def copy_volume_to_image(self, context, volume, image_service, image_meta):
         """Copy the volume to the specified image."""
         raise NotImplementedError()
index 6c1709152f865804c5fd417ee41eb27127317b3a..337a3d8519780d4229e1cec6cfff1228c993d32d 100644 (file)
@@ -144,5 +144,5 @@ class XenAPINFSDriver(driver.VolumeDriver):
     def copy_image_to_volume(self, context, volume, image_service, image_id):
         raise NotImplementedError()
 
-    def copy_volume_to_image(self, context, volume, image_service, image_id):
+    def copy_volume_to_image(self, context, volume, image_service, image_meta):
         raise NotImplementedError()
index 912b67117767a99a6208b798d337634cd38824c3..6af373c154c0161a27da1c232d043a2303ea66f5 100644 (file)
@@ -483,7 +483,7 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
         """Fetch the image from image_service and write it to the volume."""
         raise NotImplementedError()
 
-    def copy_volume_to_image(self, context, volume, image_service, image_id):
+    def copy_volume_to_image(self, context, volume, image_service, image_meta):
         """Copy the volume to the specified image."""
         raise NotImplementedError()
 
index 72e7cd396500ee1f6bb9fc2d2995ae9b0352404b..3d9bf8d97a5332597fb1dcf1bc934de05c26c7ea 100644 (file)
@@ -103,7 +103,7 @@ MAPPING = {
 class VolumeManager(manager.SchedulerDependentManager):
     """Manages attachable block storage devices."""
 
-    RPC_API_VERSION = '1.2'
+    RPC_API_VERSION = '1.3'
 
     def __init__(self, volume_driver=None, *args, **kwargs):
         """Load the driver from the one specified in args, or from flags."""
@@ -234,7 +234,7 @@ class VolumeManager(manager.SchedulerDependentManager):
                                                           volume_ref['id'],
                                                           key, value)
 
-            #copy the image onto the volume.
+            # Copy the image onto the volume.
             self._copy_image_to_volume(context, volume_ref, image_id)
         self._notify_about_volume_usage(context, volume_ref, "create.end")
         return volume_ref['id']
@@ -421,16 +421,21 @@ class VolumeManager(manager.SchedulerDependentManager):
                 payload['message'] = unicode(error)
                 self.db.volume_update(context, volume_id, {'status': 'error'})
 
-    def copy_volume_to_image(self, context, volume_id, image_id):
-        """Uploads the specified volume to Glance."""
-        payload = {'volume_id': volume_id, 'image_id': image_id}
+    def copy_volume_to_image(self, context, volume_id, image_meta):
+        """Uploads the specified volume to Glance.
+
+        image_meta is a dictionary containing the following keys:
+        'id', 'container_format', 'disk_format'
+
+        """
+        payload = {'volume_id': volume_id, 'image_id': image_meta['id']}
         try:
             volume = self.db.volume_get(context, volume_id)
             self.driver.ensure_export(context.elevated(), volume)
-            image_service, image_id = glance.get_remote_image_service(context,
-                                                                      image_id)
+            image_service, image_id = \
+                glance.get_remote_image_service(context, image_meta['id'])
             self.driver.copy_volume_to_image(context, volume, image_service,
-                                             image_id)
+                                             image_meta)
             LOG.debug(_("Uploaded volume %(volume_id)s to "
                         "image (%(image_id)s) successfully") % locals())
         except Exception, error:
index 0f7621016f56a156378b9fb3e4c109990a050fcf..d9f72f0a06688aaf7da9f85546f0d1a04ba71a55 100644 (file)
@@ -35,6 +35,7 @@ class VolumeAPI(cinder.openstack.common.rpc.proxy.RpcProxy):
         1.0 - Initial version.
         1.1 - Adds clone volume option to create_volume.
         1.2 - Add publish_service_capabilities() method.
+        1.3 - Pass all image metadata (not just ID) in copy_volume_to_image
     '''
 
     BASE_RPC_API_VERSION = '1.0'
@@ -91,13 +92,14 @@ class VolumeAPI(cinder.openstack.common.rpc.proxy.RpcProxy):
                                                  self.topic,
                                                  volume['host']))
 
-    def copy_volume_to_image(self, ctxt, volume, image_id):
+    def copy_volume_to_image(self, ctxt, volume, image_meta):
         self.cast(ctxt, self.make_msg('copy_volume_to_image',
                                       volume_id=volume['id'],
-                                      image_id=image_id),
+                                      image_meta=image_meta),
                   topic=rpc.queue_get_for(ctxt,
                                           self.topic,
-                                          volume['host']))
+                                          volume['host']),
+                  version='1.3')
 
     def initialize_connection(self, ctxt, volume, connector):
         return self.call(ctxt, self.make_msg('initialize_connection',
index 68a57b1df864345da471a278ac78e6d6c2ea3a72..51e972defd79fdaf78f97b2a2f9b92142cc052d3 100644 (file)
@@ -42,6 +42,9 @@ ln: CommandFilter, /bin/ln, root
 qemu-img: CommandFilter, /usr/bin/qemu-img, root
 env: CommandFilter, /usr/bin/env, root
 
+# cinder/volume/driver.py: utils.read_file_as_root()
+cat: CommandFilter, /bin/cat, root
+
 # cinder/volume/nfs.py
 stat: CommandFilter, /usr/bin/stat, root
 mount: CommandFilter, /bin/mount, root