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:
parent
602da5b06b
commit
949291c3e5
@ -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)
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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'],
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user