Generic iSCSI copy volume<->image.

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
This commit is contained in:
Avishay Traeger 2013-01-16 16:19:40 +02:00
parent 602da5b06b
commit 949291c3e5
16 changed files with 240 additions and 47 deletions

View File

@ -208,6 +208,10 @@ def fetch_to_raw(context, image_service,
os.path.exists(FLAGS.image_conversion_dir)): os.path.exists(FLAGS.image_conversion_dir)):
os.makedirs(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) fd, tmp = tempfile.mkstemp(dir=FLAGS.image_conversion_dir)
os.close(fd) os.close(fd)
with utils.remove_path_on_error(tmp): 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 # NOTE(jdg): I'm using qemu-img convert to write
# to the volume regardless if it *needs* conversion or not # 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)) LOG.debug("%s was %s, converting to raw" % (image_id, fmt))
convert_image(tmp, dest, 'raw') 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") % reason=_("Converted to raw, but format is now %s") %
data.file_format) data.file_format)
os.unlink(tmp) 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)

View File

@ -594,7 +594,11 @@ class VolumeTestCase(test.TestCase):
self.stubs.Set(self.volume.driver, 'local_path', fake_local_path) 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 # creating volume testdata
volume_id = 1 volume_id = 1
db.volume_create(self.context, db.volume_create(self.context,
@ -610,7 +614,7 @@ class VolumeTestCase(test.TestCase):
# start test # start test
self.volume.copy_volume_to_image(self.context, self.volume.copy_volume_to_image(self.context,
volume_id, volume_id,
image_id) image_meta)
volume = db.volume_get(self.context, volume_id) volume = db.volume_get(self.context, volume_id)
self.assertEqual(volume['status'], 'available') self.assertEqual(volume['status'], 'available')
@ -628,8 +632,10 @@ class VolumeTestCase(test.TestCase):
self.stubs.Set(self.volume.driver, 'local_path', fake_local_path) self.stubs.Set(self.volume.driver, 'local_path', fake_local_path)
#image_id = '70a599e0-31e7-49b7-b260-868f441e862b' image_meta = {
image_id = 'a440c04b-79fa-479c-bed1-0b816eaec379' 'id': 'a440c04b-79fa-479c-bed1-0b816eaec379',
'container_format': 'bare',
'disk_format': 'raw'}
# creating volume testdata # creating volume testdata
volume_id = 1 volume_id = 1
db.volume_create( db.volume_create(
@ -646,7 +652,7 @@ class VolumeTestCase(test.TestCase):
# start test # start test
self.volume.copy_volume_to_image(self.context, self.volume.copy_volume_to_image(self.context,
volume_id, volume_id,
image_id) image_meta)
volume = db.volume_get(self.context, volume_id) volume = db.volume_get(self.context, volume_id)
self.assertEqual(volume['status'], 'in-use') 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) 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 # creating volume testdata
volume_id = 1 volume_id = 1
db.volume_create(self.context, db.volume_create(self.context,
@ -681,7 +690,7 @@ class VolumeTestCase(test.TestCase):
self.volume.copy_volume_to_image, self.volume.copy_volume_to_image,
self.context, self.context,
volume_id, volume_id,
image_id) image_meta)
volume = db.volume_get(self.context, volume_id) volume = db.volume_get(self.context, volume_id)
self.assertEqual(volume['status'], 'available') self.assertEqual(volume['status'], 'available')
@ -698,7 +707,9 @@ class VolumeTestCase(test.TestCase):
pass pass
def show(self, context, image_id): 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' image_id = '70a599e0-31e7-49b7-b260-868f441e862b'
@ -722,7 +733,9 @@ class VolumeTestCase(test.TestCase):
pass pass
def show(self, context, image_id): 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' image_id = '70a599e0-31e7-49b7-b260-868f441e862b'

View File

@ -150,7 +150,10 @@ class VolumeRpcAPITestCase(test.TestCase):
self._test_volume_api('copy_volume_to_image', self._test_volume_api('copy_volume_to_image',
rpc_method='cast', rpc_method='cast',
volume=self.fake_volume, 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): def test_initialize_connection(self):
self._test_volume_api('initialize_connection', self._test_volume_api('initialize_connection',

View File

@ -144,6 +144,14 @@ class API(base.Base):
if image_size_in_gb > size: if image_size_in_gb > size:
msg = _('Size of specified image is larger than volume size.') msg = _('Size of specified image is larger than volume size.')
raise exception.InvalidInput(reason=msg) 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: try:
reservations = QUOTAS.reserve(context, volumes=1, gigabytes=size) reservations = QUOTAS.reserve(context, volumes=1, gigabytes=size)
@ -592,11 +600,21 @@ class API(base.Base):
"""Create a new image from the specified volume.""" """Create a new image from the specified volume."""
self._check_volume_availability(context, volume, force) 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) recv_metadata = self.image_service.create(context, metadata)
self.update(context, volume, {'status': 'uploading'}) self.update(context, volume, {'status': 'uploading'})
self.volume_rpcapi.copy_volume_to_image(context, self.volume_rpcapi.copy_volume_to_image(context,
volume, volume,
recv_metadata['id']) recv_metadata)
response = {"id": volume['id'], response = {"id": volume['id'],
"updated_at": volume['updated_at'], "updated_at": volume['updated_at'],

View File

@ -20,10 +20,12 @@ Drivers for volumes.
""" """
import os
import time import time
from cinder import exception from cinder import exception
from cinder import flags from cinder import flags
from cinder.image import image_utils
from cinder.openstack.common import cfg from cinder.openstack.common import cfg
from cinder.openstack.common import log as logging from cinder.openstack.common import log as logging
from cinder import utils from cinder import utils
@ -172,7 +174,7 @@ class VolumeDriver(object):
"""Fetch the image from image_service and write it to the volume.""" """Fetch the image from image_service and write it to the volume."""
raise NotImplementedError() 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.""" """Copy the volume to the specified image."""
raise NotImplementedError() raise NotImplementedError()
@ -287,19 +289,22 @@ class ISCSIDriver(VolumeDriver):
return properties 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', (out, err) = self._execute('iscsiadm', '-m', 'node', '-T',
iscsi_properties['target_iqn'], iscsi_properties['target_iqn'],
'-p', iscsi_properties['target_portal'], '-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" % LOG.debug("iscsiadm %s: stdout=%s stderr=%s" %
(iscsi_command, out, err)) (iscsi_command, out, err))
return (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, iscsi_command = ('--op', 'update', '-n', property_key,
'-v', property_value) '-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): def initialize_connection(self, volume, connector):
"""Initializes the connection and returns connection info. """Initializes the connection and returns connection info.
@ -329,6 +334,115 @@ class ISCSIDriver(VolumeDriver):
def terminate_connection(self, volume, connector, **kwargs): def terminate_connection(self, volume, connector, **kwargs):
pass 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): class FakeISCSIDriver(ISCSIDriver):
"""Logs calls instead of executing.""" """Logs calls instead of executing."""

View File

@ -269,7 +269,7 @@ class EMCSMISISCSIDriver(driver.ISCSIDriver):
return iscsi_properties, host_device 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.""" """Copy the volume to the specified image."""
LOG.debug(_('copy_volume_to_image %s.') % volume['name']) LOG.debug(_('copy_volume_to_image %s.') % volume['name'])
initiator = get_iscsi_initiator() initiator = get_iscsi_initiator()
@ -281,7 +281,8 @@ class EMCSMISISCSIDriver(driver.ISCSIDriver):
with utils.temporary_chown(volume_path): with utils.temporary_chown(volume_path):
with utils.file_open(volume_path) as volume_file: 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) self.terminate_connection(volume, connector)

View File

@ -237,12 +237,12 @@ class LVMVolumeDriver(driver.VolumeDriver):
image_id, image_id,
self.local_path(volume)) 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.""" """Copy the volume to the specified image."""
volume_path = self.local_path(volume) image_utils.upload_volume(context,
with utils.temporary_chown(volume_path): image_service,
with utils.file_open(volume_path) as volume_file: image_meta,
image_service.update(context, image_id, {}, volume_file) self.local_path(volume))
def clone_image(self, volume, image_location): def clone_image(self, volume, image_location):
return False return False

View File

@ -1307,7 +1307,7 @@ class NetAppCmodeISCSIDriver(driver.ISCSIDriver):
"""Fetch the image from image_service and write it to the volume.""" """Fetch the image from image_service and write it to the volume."""
raise NotImplementedError() 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.""" """Copy the volume to the specified image."""
raise NotImplementedError() raise NotImplementedError()

View File

@ -284,7 +284,7 @@ class NexentaDriver(driver.ISCSIDriver): # pylint: disable=R0921
"""Fetch the image from image_service and write it to the volume.""" """Fetch the image from image_service and write it to the volume."""
raise NotImplementedError() 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.""" """Copy the volume to the specified image."""
raise NotImplementedError() raise NotImplementedError()

View File

@ -154,14 +154,6 @@ class SanISCSIDriver(ISCSIDriver):
if not FLAGS.san_ip: if not FLAGS.san_ip:
raise exception.InvalidInput(reason=_("san_ip must be set")) 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): def create_cloned_volume(self, volume, src_vref):
"""Create a cloen of the specified volume.""" """Create a cloen of the specified volume."""
raise NotImplementedError() raise NotImplementedError()

View File

@ -239,6 +239,6 @@ class WindowsDriver(driver.ISCSIDriver):
"""Fetch the image from image_service and write it to the volume.""" """Fetch the image from image_service and write it to the volume."""
raise NotImplementedError() 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.""" """Copy the volume to the specified image."""
raise NotImplementedError() raise NotImplementedError()

View File

@ -144,5 +144,5 @@ class XenAPINFSDriver(driver.VolumeDriver):
def copy_image_to_volume(self, context, volume, image_service, image_id): def copy_image_to_volume(self, context, volume, image_service, image_id):
raise NotImplementedError() 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() raise NotImplementedError()

View File

@ -483,7 +483,7 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
"""Fetch the image from image_service and write it to the volume.""" """Fetch the image from image_service and write it to the volume."""
raise NotImplementedError() 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.""" """Copy the volume to the specified image."""
raise NotImplementedError() raise NotImplementedError()

View File

@ -103,7 +103,7 @@ MAPPING = {
class VolumeManager(manager.SchedulerDependentManager): class VolumeManager(manager.SchedulerDependentManager):
"""Manages attachable block storage devices.""" """Manages attachable block storage devices."""
RPC_API_VERSION = '1.2' RPC_API_VERSION = '1.3'
def __init__(self, volume_driver=None, *args, **kwargs): def __init__(self, volume_driver=None, *args, **kwargs):
"""Load the driver from the one specified in args, or from flags.""" """Load the driver from the one specified in args, or from flags."""
@ -234,7 +234,7 @@ class VolumeManager(manager.SchedulerDependentManager):
volume_ref['id'], volume_ref['id'],
key, value) 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._copy_image_to_volume(context, volume_ref, image_id)
self._notify_about_volume_usage(context, volume_ref, "create.end") self._notify_about_volume_usage(context, volume_ref, "create.end")
return volume_ref['id'] return volume_ref['id']
@ -421,16 +421,21 @@ class VolumeManager(manager.SchedulerDependentManager):
payload['message'] = unicode(error) payload['message'] = unicode(error)
self.db.volume_update(context, volume_id, {'status': 'error'}) self.db.volume_update(context, volume_id, {'status': 'error'})
def copy_volume_to_image(self, context, volume_id, image_id): def copy_volume_to_image(self, context, volume_id, image_meta):
"""Uploads the specified volume to Glance.""" """Uploads the specified volume to Glance.
payload = {'volume_id': volume_id, 'image_id': image_id}
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: try:
volume = self.db.volume_get(context, volume_id) volume = self.db.volume_get(context, volume_id)
self.driver.ensure_export(context.elevated(), volume) self.driver.ensure_export(context.elevated(), volume)
image_service, image_id = glance.get_remote_image_service(context, image_service, image_id = \
image_id) glance.get_remote_image_service(context, image_meta['id'])
self.driver.copy_volume_to_image(context, volume, image_service, self.driver.copy_volume_to_image(context, volume, image_service,
image_id) image_meta)
LOG.debug(_("Uploaded volume %(volume_id)s to " LOG.debug(_("Uploaded volume %(volume_id)s to "
"image (%(image_id)s) successfully") % locals()) "image (%(image_id)s) successfully") % locals())
except Exception, error: except Exception, error:

View File

@ -35,6 +35,7 @@ class VolumeAPI(cinder.openstack.common.rpc.proxy.RpcProxy):
1.0 - Initial version. 1.0 - Initial version.
1.1 - Adds clone volume option to create_volume. 1.1 - Adds clone volume option to create_volume.
1.2 - Add publish_service_capabilities() method. 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' BASE_RPC_API_VERSION = '1.0'
@ -91,13 +92,14 @@ class VolumeAPI(cinder.openstack.common.rpc.proxy.RpcProxy):
self.topic, self.topic,
volume['host'])) 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', self.cast(ctxt, self.make_msg('copy_volume_to_image',
volume_id=volume['id'], volume_id=volume['id'],
image_id=image_id), image_meta=image_meta),
topic=rpc.queue_get_for(ctxt, topic=rpc.queue_get_for(ctxt,
self.topic, self.topic,
volume['host'])) volume['host']),
version='1.3')
def initialize_connection(self, ctxt, volume, connector): def initialize_connection(self, ctxt, volume, connector):
return self.call(ctxt, self.make_msg('initialize_connection', return self.call(ctxt, self.make_msg('initialize_connection',

View File

@ -42,6 +42,9 @@ ln: CommandFilter, /bin/ln, root
qemu-img: CommandFilter, /usr/bin/qemu-img, root qemu-img: CommandFilter, /usr/bin/qemu-img, root
env: CommandFilter, /usr/bin/env, 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 # cinder/volume/nfs.py
stat: CommandFilter, /usr/bin/stat, root stat: CommandFilter, /usr/bin/stat, root
mount: CommandFilter, /bin/mount, root mount: CommandFilter, /bin/mount, root