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