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.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)

View 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'

View 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',

View 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'],

View 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."""

View 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)

View 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

View 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()

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."""
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()

View 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()

View 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()

View 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()

View 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()

View 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:

View 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',

View 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