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):
# 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')
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)
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,
# 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')
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(
# 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')
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,
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')
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'
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'
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',
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)
"""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'],
"""
+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
"""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()
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.
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."""
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()
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)
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
"""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()
"""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()
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()
"""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()
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()
"""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()
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."""
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']
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:
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'
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',
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