diff --git a/cinder/tests/unit/test_vzstorage.py b/cinder/tests/unit/test_vzstorage.py index 29ca1e577..b5962ebea 100644 --- a/cinder/tests/unit/test_vzstorage.py +++ b/cinder/tests/unit/test_vzstorage.py @@ -284,7 +284,7 @@ class VZStorageTestCase(test.TestCase): self.snap, self.vol, self.vol['size']) drv._extend_volume.assert_called_once_with( - self.vol, self.vol['size']) + self.vol, self.vol['size'], 'raw') mock_convert_image.assert_called_once_with( self._FAKE_VOLUME_PATH, self._FAKE_VOLUME_PATH[:-1], 'raw') diff --git a/cinder/volume/drivers/vzstorage.py b/cinder/volume/drivers/vzstorage.py index fcecd0b68..1216587d4 100644 --- a/cinder/volume/drivers/vzstorage.py +++ b/cinder/volume/drivers/vzstorage.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import collections import errno import json import os @@ -57,11 +58,67 @@ vzstorage_opts = [ help=('Mount options passed to the vzstorage client. ' 'See section of the pstorage-mount man page ' 'for details.')), + cfg.StrOpt('vzstorage_default_volume_format', + default='raw', + help=('Default format that will be used when creating volumes ' + 'if no volume format is specified.')), ] CONF = cfg.CONF CONF.register_opts(vzstorage_opts) +PLOOP_BASE_DELTA_NAME = 'root.hds' +DISK_FORMAT_RAW = 'raw' +DISK_FORMAT_QCOW2 = 'qcow2' +DISK_FORMAT_PLOOP = 'parallels' + + +class PloopDevice(object): + """Setup a ploop device for parallels image + + This class is for mounting ploop devices using with statement: + with PloopDevice('/parallels/my-vm/harddisk.hdd') as dev_path: + # do something + + :param path: A path to parallels harddisk dir + :param snapshot_id: Snapshot id to mount + :param execute: execute helper + """ + + def __init__(self, path, snapshot_id=None, read_only=True, + execute=putils.execute): + self.path = path + self.snapshot_id = snapshot_id + self.read_only = read_only + self.execute = execute + + def __enter__(self): + self.dd_path = os.path.join(self.path, 'DiskDescriptor.xml') + cmd = ['ploop', 'mount', self.dd_path] + + if self.snapshot_id: + cmd.append('-u') + cmd.append(self.snapshot_id) + + if self.read_only: + cmd.append('-r') + + out, err = self.execute(*cmd, run_as_root=True) + + m = re.search('dev=(\S+)', out) + if not m: + raise Exception('Invalid output from ploop mount: %s' % out) + + self.ploop_dev = m.group(1) + + return self.ploop_dev + + def _umount(self): + self.execute('ploop', 'umount', self.dd_path, run_as_root=True) + + def __exit__(self, type, value, traceback): + self._umount() + @interface.volumedriver class VZStorageDriver(remotefs_drv.RemoteFSSnapDriver): @@ -110,13 +167,9 @@ class VZStorageDriver(remotefs_drv.RemoteFSSnapDriver): """ # Find active image active_file = self.get_active_image_from_info(volume) - active_file_path = os.path.join(self._local_volume_dir(volume), - active_file) - info = self._qemu_img_info(active_file_path, volume.name) - fmt = info.file_format data = {'export': volume.provider_location, - 'format': fmt, + 'format': self.get_volume_format(volume), 'name': active_file, } @@ -228,25 +281,92 @@ class VZStorageDriver(remotefs_drv.RemoteFSSnapDriver): return True + def choose_volume_format(self, volume): + vol_type = volume.volume_type + if vol_type: + extra_specs = vol_type.extra_specs or {} + else: + extra_specs = {} + + extra_specs.update(volume.metadata or {}) + + return (extra_specs.get('volume_format') or + self.configuration.vzstorage_default_volume_format) + + def get_volume_format(self, volume): + info_path = self._local_path_volume_info(volume) + snap_info = self._read_info_file(info_path) + return snap_info['volume_format'] + + def _create_ploop(self, volume_path, volume_size): + os.mkdir(volume_path) + try: + self._execute('ploop', 'init', '-s', '%sG' % volume_size, + os.path.join(volume_path, PLOOP_BASE_DELTA_NAME), + run_as_root=True) + except putils.ProcessExecutionError: + os.rmdir(volume_path) + raise + + def _do_create_volume(self, volume): + """Create a volume on given smbfs_share. + + :param volume: volume reference + """ + volume_format = self.choose_volume_format(volume) + volume_path = self.local_path(volume) + volume_size = volume.size + + LOG.debug("Creating new volume at %s.", volume_path) + + if os.path.exists(volume_path): + msg = _('File already exists at %s.') % volume_path + LOG.error(msg) + raise exception.InvalidVolume(reason=msg) + + if volume_format == 'parallels': + self._create_ploop(volume_path, volume_size) + elif volume_format == 'qcow2': + self._create_qcow2_file(volume_path, volume_size) + elif self.configuration.vzstorage_sparsed_volumes: + self._create_sparsed_file(volume_path, volume_size) + else: + self._create_regular_file(volume_path, volume_size) + + info_path = self._local_path_volume_info(volume) + snap_info = {'volume_format': volume_format, + 'active': 'volume-%s' % volume.id} + self._write_info_file(info_path, snap_info) + + def _delete(self, path): + self._execute('rm', '-rf', path, run_as_root=True) + @remotefs_drv.locked_volume_id_operation def extend_volume(self, volume, size_gb): LOG.info(_LI('Extending volume %s.'), volume.id) - self._extend_volume(volume, size_gb) + volume_format = self.get_volume_format(volume) + self._extend_volume(volume, size_gb, volume_format) - def _extend_volume(self, volume, size_gb): + def _extend_volume(self, volume, size_gb, volume_format): volume_path = self.local_path(volume) self._check_extend_volume_support(volume, size_gb) LOG.info(_LI('Resizing file to %sG...'), size_gb) - self._do_extend_volume(volume_path, size_gb) + self._do_extend_volume(volume_path, size_gb, volume_format) - def _do_extend_volume(self, volume_path, size_gb): - image_utils.resize_image(volume_path, size_gb) - - if not self._is_file_size_equal(volume_path, size_gb): - raise exception.ExtendVolumeError( - reason='Resizing image file failed.') + def _do_extend_volume(self, volume_path, size_gb, volume_format): + if volume_format == "parallels": + self._execute('ploop', 'grow', '-s', + '%dG' % size_gb, + os.path.join(volume_path, 'DiskDescriptor.xml'), + run_as_root=True) + volume_path = os.path.join(volume_path, PLOOP_BASE_DELTA_NAME) + else: + image_utils.resize_image(volume_path, size_gb) + if not self._is_file_size_equal(volume_path, size_gb): + raise exception.ExtendVolumeError( + reason='Resizing image file failed.') def _check_extend_volume_support(self, volume, size_gb): volume_path = self.local_path(volume) @@ -272,6 +392,30 @@ class VZStorageDriver(remotefs_drv.RemoteFSSnapDriver): virt_size = data.virtual_size / units.Gi return virt_size == size + def _recreate_ploop_desc(self, image_dir, image_file): + self._delete(os.path.join(image_dir, 'DiskDescriptor.xml')) + + self._execute('ploop', 'restore-descriptor', image_dir, image_file) + + def copy_image_to_volume(self, context, volume, image_service, image_id): + """Fetch the image from image_service and write it to the volume.""" + volume_format = self.get_volume_format(volume) + image_path = self.local_path(volume) + if volume_format == DISK_FORMAT_PLOOP: + image_path = os.path.join(image_path, PLOOP_BASE_DELTA_NAME) + + image_utils.fetch_to_volume_format( + context, image_service, image_id, + image_path, volume_format, + self.configuration.volume_dd_blocksize) + + if volume_format == DISK_FORMAT_PLOOP: + self._recreate_ploop_desc(self.local_path(volume), image_path) + + self._do_extend_volume(self.local_path(volume), + volume.size, + volume_format) + def _copy_volume_from_snapshot(self, snapshot, volume, volume_size): """Copy data from snapshot to destination volume. @@ -279,34 +423,46 @@ class VZStorageDriver(remotefs_drv.RemoteFSSnapDriver): qcow2. """ - LOG.debug("_copy_volume_from_snapshot: snapshot: %(snap)s, " - "volume: %(vol)s, volume_size: %(size)s.", - {'snap': snapshot.id, - 'vol': volume.id, - 'size': volume_size, - }) - info_path = self._local_path_volume_info(snapshot.volume) snap_info = self._read_info_file(info_path) vol_dir = self._local_volume_dir(snapshot.volume) - out_format = "raw" + out_format = self.choose_volume_format(volume) + volume_format = self.get_volume_format(snapshot.volume) + volume_path = self.local_path(volume) - forward_file = snap_info[snapshot.id] - forward_path = os.path.join(vol_dir, forward_file) + if volume_format in (DISK_FORMAT_QCOW2, DISK_FORMAT_RAW): + forward_file = snap_info[snapshot.id] + forward_path = os.path.join(vol_dir, forward_file) - # Find the file which backs this file, which represents the point - # when this snapshot was created. - img_info = self._qemu_img_info(forward_path, - snapshot.volume.name) - path_to_snap_img = os.path.join(vol_dir, img_info.backing_file) + # Find the file which backs this file, which represents the point + # when this snapshot was created. + img_info = self._qemu_img_info(forward_path, + snapshot.volume.name) + path_to_snap_img = os.path.join(vol_dir, img_info.backing_file) - LOG.debug("_copy_volume_from_snapshot: will copy " - "from snapshot at %s.", path_to_snap_img) + LOG.debug("_copy_volume_from_snapshot: will copy " + "from snapshot at %s.", path_to_snap_img) - image_utils.convert_image(path_to_snap_img, - self.local_path(volume), - out_format) - self._extend_volume(volume, volume_size) + image_utils.convert_image(path_to_snap_img, + volume_path, + out_format) + elif volume_format == DISK_FORMAT_PLOOP: + with PloopDevice(self.local_path(snapshot.volume), + snapshot.id, + execute=self._execute) as dev: + image_utils.convert_image(dev, volume_path, out_format) + else: + msg = _("Unsupported volume format %s") % volume_format + raise exception.InvalidVolume(msg) + + if out_format == DISK_FORMAT_PLOOP: + img_path = os.path.join(volume_path, 'root.hds') + os.rename(volume_path, volume_path + '.tmp') + os.mkdir(volume_path) + os.rename(volume_path + '.tmp', img_path) + self._execute('ploop', 'restore-descriptor', volume_path, img_path) + + self._extend_volume(volume, volume_size, out_format) @remotefs_drv.locked_volume_id_operation def delete_volume(self, volume): @@ -315,7 +471,8 @@ class VZStorageDriver(remotefs_drv.RemoteFSSnapDriver): msg = (_('Volume %s does not have provider_location ' 'specified, skipping.') % volume.name) LOG.error(msg) - raise exception.VzStorageException(msg) + return +# raise exception.VzStorageException(msg) self._ensure_share_mounted(volume.provider_location) volume_dir = self._local_volume_dir(volume) @@ -329,3 +486,120 @@ class VZStorageDriver(remotefs_drv.RemoteFSSnapDriver): info_path = self._local_path_volume_info(volume) self._delete(info_path) + + def _get_desc_path(self, volume): + return os.path.join(self.local_path(volume), 'DiskDescriptor.xml') + + def _create_snapshot_ploop(self, snapshot): + status = snapshot.volume.status + if status != 'available': + msg = (_('Volume status must be available for ' + 'snapshot %(id)s. (is %(status)s)') % + {'id': snapshot.id, 'status': status}) + raise exception.InvalidVolume(msg) + + info_path = self._local_path_volume_info(snapshot.volume) + snap_info = self._read_info_file(info_path) + self._execute('ploop', 'snapshot', '-u', '{%s}' % snapshot.id, + self._get_desc_path(snapshot.volume), + run_as_root=True) + snap_file = os.path.join('volume-%s' % snapshot.volume.id, snapshot.id) + snap_info[snapshot.id] = snap_file + self._write_info_file(info_path, snap_info) + + def _delete_snapshot_ploop(self, snapshot): + status = snapshot.volume.status + if status != 'available': + msg = (_('Volume status must be available for ' + 'snapshot %(id)s. (is %(status)s)') % + {'id': snapshot.id, 'status': status}) + raise exception.InvalidVolume(msg) + + info_path = self._local_path_volume_info(snapshot.volume) + snap_info = self._read_info_file(info_path) + self._execute('ploop', 'snapshot-delete', '-u', '{%s}' % snapshot.id, + self._get_desc_path(snapshot.volume), + run_as_root=True) + snap_info.pop(snapshot.id) + self._write_info_file(info_path, snap_info) + + def create_snapshot(self, snapshot): + volume_format = self.get_volume_format(snapshot.volume) + if volume_format == 'parallels': + self._create_snapshot_ploop(snapshot) + else: + super(VZStorageDriver, self)._create_snapshot(snapshot) + + def delete_snapshot(self, snapshot): + volume_format = self.get_volume_format(snapshot.volume) + if volume_format == 'parallels': + self._delete_snapshot_ploop(snapshot) + else: + super(VZStorageDriver, self)._delete_snapshot(snapshot) + + def _copy_volume_to_image(self, context, volume, image_service, + image_meta): + """Copy the volume to the specified image.""" + + volume_format = self.get_volume_format(volume) + if volume_format == DISK_FORMAT_PLOOP: + with PloopDevice(self.local_path(volume), + execute=self._execute) as dev: + image_utils.upload_volume(context, + image_service, + image_meta, + dev, + volume_format='raw') + else: + super(VZStorageDriver, self)._copy_volume_to_image(context, volume, + image_service, + image_meta) + + def _create_cloned_volume(self, volume, src_vref): + LOG.info(_LI('Cloning volume %(src)s to volume %(dst)s'), + {'src': src_vref.id, + 'dst': volume.id}) + + if src_vref.status != 'available': + msg = _("Volume status must be 'available'.") + raise exception.InvalidVolume(msg) + + volume_name = CONF.volume_name_template % volume.id + + # Create fake snapshot object + snap_attrs = ['volume_name', 'size', 'volume_size', 'name', + 'volume_id', 'id', 'volume'] + Snapshot = collections.namedtuple('Snapshot', snap_attrs) + + temp_snapshot = Snapshot(id=src_vref.id, + volume_name=volume_name, + size=src_vref.size, + volume_size=src_vref.size, + name='clone-snap-%s' % src_vref.id, + volume_id=src_vref.id, + volume=volume) + + self._create_snapshot_ploop(temp_snapshot) + try: + volume.provider_location = src_vref.provider_location + info_path = self._local_path_volume_info(volume) + snap_info = {'volume_format': DISK_FORMAT_PLOOP, + 'active': 'volume-%s' % volume.id} + self._write_info_file(info_path, snap_info) + self._copy_volume_from_snapshot(temp_snapshot, + volume, + volume.size) + + finally: + self.delete_snapshot(temp_snapshot) + + return {'provider_location': src_vref.provider_location} + + @remotefs_drv.locked_volume_id_operation + def create_cloned_volume(self, vol, src_vref): + """Creates a clone of the specified volume.""" + volume_format = self.get_volume_format(src_vref) + if volume_format == 'parallels': + self._create_cloned_volume(vol, src_vref) + else: + super(VZStorageDriver, self)._create_cloned_volume(vol, src_vref) diff --git a/etc/cinder/rootwrap.d/volume.filters b/etc/cinder/rootwrap.d/volume.filters index c4083cbb1..fd4f3166b 100644 --- a/etc/cinder/rootwrap.d/volume.filters +++ b/etc/cinder/rootwrap.d/volume.filters @@ -202,6 +202,7 @@ vgc-cluster: CommandFilter, vgc-cluster, root # cinder/volume/drivers/vzstorage.py pstorage-mount: CommandFilter, pstorage-mount, root pstorage: CommandFilter, pstorage, root +ploop: CommandFilter, ploop, root # initiator/connector.py: drv_cfg: CommandFilter, /opt/emc/scaleio/sdc/bin/drv_cfg, root, /opt/emc/scaleio/sdc/bin/drv_cfg, --query_guid