1# Copyright (c) 2015 Parallels IP Holdings GmbH
2# All Rights Reserved.
3#
4#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5#    not use this file except in compliance with the License. You may obtain
6#    a copy of the License at
7#
8#         http://www.apache.org/licenses/LICENSE-2.0
9#
10#    Unless required by applicable law or agreed to in writing, software
11#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13#    License for the specific language governing permissions and limitations
14#    under the License.
15
16import collections
17import errno
18import json
19import os
20import re
21
22from os_brick.remotefs import remotefs
23from oslo_concurrency import processutils as putils
24from oslo_config import cfg
25from oslo_log import log as logging
26from oslo_utils import imageutils
27from oslo_utils import units
28
29from cinder import exception
30from cinder.i18n import _
31from cinder.image import image_utils
32from cinder import interface
33from cinder import utils
34from cinder.volume import configuration
35from cinder.volume.drivers import remotefs as remotefs_drv
36
37LOG = logging.getLogger(__name__)
38
39vzstorage_opts = [
40    cfg.StrOpt('vzstorage_shares_config',
41               default='/etc/cinder/vzstorage_shares',
42               help='File with the list of available vzstorage shares.'),
43    cfg.BoolOpt('vzstorage_sparsed_volumes',
44                default=True,
45                help=('Create volumes as sparsed files which take no space '
46                      'rather than regular files when using raw format, '
47                      'in which case volume creation takes lot of time.')),
48    cfg.FloatOpt('vzstorage_used_ratio',
49                 default=0.95,
50                 help=('Percent of ACTUAL usage of the underlying volume '
51                       'before no new volumes can be allocated to the volume '
52                       'destination.')),
53    cfg.StrOpt('vzstorage_mount_point_base',
54               default='$state_path/mnt',
55               help=('Base dir containing mount points for '
56                     'vzstorage shares.')),
57    cfg.ListOpt('vzstorage_mount_options',
58                help=('Mount options passed to the vzstorage client. '
59                      'See section of the pstorage-mount man page '
60                      'for details.')),
61    cfg.StrOpt('vzstorage_default_volume_format',
62               default='raw',
63               help=('Default format that will be used when creating volumes '
64                     'if no volume format is specified.')),
65]
66
67CONF = cfg.CONF
68CONF.register_opts(vzstorage_opts, group=configuration.SHARED_CONF_GROUP)
69
70PLOOP_BASE_DELTA_NAME = 'root.hds'
71DISK_FORMAT_RAW = 'raw'
72DISK_FORMAT_QCOW2 = 'qcow2'
73DISK_FORMAT_PLOOP = 'ploop'
74
75
76class PloopDevice(object):
77    """Setup a ploop device for ploop image
78
79    This class is for mounting ploop devices using with statement:
80    with PloopDevice('/vzt/private/my-ct/harddisk.hdd') as dev_path:
81    # do something
82
83    :param path: A path to ploop harddisk dir
84    :param snapshot_id: Snapshot id to mount
85    :param execute: execute helper
86    """
87
88    def __init__(self, path, snapshot_id=None, read_only=True,
89                 execute=putils.execute):
90        self.path = path
91        self.snapshot_id = snapshot_id
92        self.read_only = read_only
93        self.execute = execute
94
95    def __enter__(self):
96        self.dd_path = os.path.join(self.path, 'DiskDescriptor.xml')
97        cmd = ['ploop', 'mount', self.dd_path]
98
99        if self.snapshot_id:
100            cmd.append('-u')
101            cmd.append(self.snapshot_id)
102
103        if self.read_only:
104            cmd.append('-r')
105
106        out, err = self.execute(*cmd, run_as_root=True)
107
108        m = re.search(r'dev=(\S+)', out)
109        if not m:
110            raise Exception('Invalid output from ploop mount: %s' % out)
111
112        self.ploop_dev = m.group(1)
113
114        return self.ploop_dev
115
116    def _umount(self):
117        self.execute('ploop', 'umount', self.dd_path, run_as_root=True)
118
119    def __exit__(self, type, value, traceback):
120        self._umount()
121
122
123@interface.volumedriver
124class VZStorageDriver(remotefs_drv.RemoteFSSnapDriver):
125    """Cinder driver for Virtuozzo Storage.
126
127    Creates volumes as files on the mounted vzstorage cluster.
128
129    Version history:
130        1.0     - Initial driver.
131        1.1     - Supports vz:volume_format in vendor properties.
132    """
133    VERSION = '1.1'
134    CI_WIKI_NAME = "Virtuozzo_Storage_CI"
135
136    SHARE_FORMAT_REGEX = r'(?:(\S+):\/)?([a-zA-Z0-9_-]+)(?::(\S+))?'
137
138    def __init__(self, execute=putils.execute, *args, **kwargs):
139        self.driver_volume_type = 'vzstorage'
140        self.driver_prefix = 'vzstorage'
141        self.volume_backend_name = 'Virtuozzo_Storage'
142        self._remotefsclient = None
143        super(VZStorageDriver, self).__init__(*args, **kwargs)
144        self.configuration.append_config_values(vzstorage_opts)
145        self._execute_as_root = False
146        root_helper = utils.get_root_helper()
147        # base bound to instance is used in RemoteFsConnector.
148        self.base = self.configuration.vzstorage_mount_point_base
149        opts = self.configuration.vzstorage_mount_options
150
151        self._remotefsclient = remotefs.VZStorageRemoteFSClient(
152            'vzstorage', root_helper, execute=execute,
153            vzstorage_mount_point_base=self.base,
154            vzstorage_mount_options=opts)
155
156    def _update_volume_stats(self):
157        super(VZStorageDriver, self)._update_volume_stats()
158        self._stats['vendor_name'] = 'Virtuozzo'
159
160    def _init_vendor_properties(self):
161        namespace = 'vz'
162        properties = {}
163
164        self._set_property(
165            properties,
166            "%s:volume_format" % namespace,
167            "Volume format",
168            _("Specifies volume format."),
169            "string",
170            enum=["qcow2", "ploop", "raw"],
171            default=self.configuration.vzstorage_default_volume_format)
172
173        return properties, namespace
174
175    def _qemu_img_info(self, path, volume_name):
176        qemu_img_cache = path + ".qemu_img_info"
177        is_cache_outdated = True
178        if os.path.isdir(path):
179            # Ploop disks stored along with metadata xml as directories
180            # qemu-img should explore base data file inside
181            path = os.path.join(path, PLOOP_BASE_DELTA_NAME)
182        if os.path.isfile(qemu_img_cache):
183            info_tm = os.stat(qemu_img_cache).st_mtime
184            snap_tm = os.stat(path).st_mtime
185            if info_tm >= snap_tm:
186                is_cache_outdated = False
187        if is_cache_outdated:
188            LOG.debug("Cached qemu-img info %s not present or outdated,"
189                      " refresh", qemu_img_cache)
190            ret = super(VZStorageDriver, self)._qemu_img_info_base(
191                path, volume_name,
192                self.configuration.vzstorage_mount_point_base)
193            # We need only backing_file and file_format
194            d = {'file_format': ret.file_format,
195                 'backing_file': ret.backing_file}
196            with open(qemu_img_cache, "w") as f:
197                json.dump(d, f)
198        else:
199            ret = imageutils.QemuImgInfo()
200            with open(qemu_img_cache, "r") as f:
201                cached_data = json.load(f)
202            ret.file_format = cached_data['file_format']
203            ret.backing_file = cached_data['backing_file']
204        return ret
205
206    @remotefs_drv.locked_volume_id_operation
207    def initialize_connection(self, volume, connector):
208        """Allow connection to connector and return connection info.
209
210        :param volume: volume reference
211        :param connector: connector reference
212        """
213        # Find active image
214        active_file = self.get_active_image_from_info(volume)
215
216        data = {'export': volume.provider_location,
217                'format': self.get_volume_format(volume),
218                'name': active_file,
219                }
220
221        return {
222            'driver_volume_type': self.driver_volume_type,
223            'data': data,
224            'mount_point_base': self._get_mount_point_base(),
225        }
226
227    def do_setup(self, context):
228        """Any initialization the volume driver does while starting."""
229        super(VZStorageDriver, self).do_setup(context)
230
231        config = self.configuration.vzstorage_shares_config
232        if not os.path.exists(config):
233            msg = (_("VzStorage config file at %(config)s doesn't exist.") %
234                   {'config': config})
235            LOG.error(msg)
236            raise exception.VzStorageException(msg)
237
238        if not os.path.isabs(self.base):
239            msg = _("Invalid mount point base: %s.") % self.base
240            LOG.error(msg)
241            raise exception.VzStorageException(msg)
242
243        used_ratio = self.configuration.vzstorage_used_ratio
244        if not ((used_ratio > 0) and (used_ratio <= 1)):
245            msg = _("VzStorage config 'vzstorage_used_ratio' invalid. "
246                    "Must be > 0 and <= 1.0: %s.") % used_ratio
247            LOG.error(msg)
248            raise exception.VzStorageException(msg)
249
250        self.shares = {}
251
252        # Check if mount.fuse.pstorage is installed on this system;
253        # note that we don't need to be root to see if the package
254        # is installed.
255        package = 'mount.fuse.pstorage'
256        try:
257            self._execute(package, check_exit_code=False,
258                          run_as_root=False)
259        except OSError as exc:
260            if exc.errno == errno.ENOENT:
261                msg = _('%s is not installed.') % package
262                raise exception.VzStorageException(msg)
263            else:
264                raise
265
266        self.configuration.nas_secure_file_operations = 'true'
267        self.configuration.nas_secure_file_permissions = 'true'
268
269    def _ensure_share_mounted(self, share):
270        m = re.search(self.SHARE_FORMAT_REGEX, share)
271        if not m:
272            msg = (_("Invalid Virtuozzo Storage share specification: %r. "
273                     "Must be: [MDS1[,MDS2],...:/]<CLUSTER NAME>[:PASSWORD].")
274                   % share)
275            raise exception.VzStorageException(msg)
276        cluster_name = m.group(2)
277
278        if share in self.shares:
279            mnt_flags = json.loads(self.shares[share])
280        else:
281            mnt_flags = []
282
283        if '-l' not in mnt_flags:
284            # If logging path is not specified in shares config
285            # set up logging to non-default path, so that it will
286            # be possible to mount the same cluster to another mount
287            # point by hand with default options.
288            mnt_flags.extend([
289                '-l', '/var/log/vstorage/%s/cinder.log.gz' % cluster_name])
290
291        self._remotefsclient.mount(share, mnt_flags)
292
293    def _find_share(self, volume):
294        """Choose VzStorage share among available ones for given volume size.
295
296        For instances with more than one share that meets the criteria, the
297        first suitable share will be selected.
298
299        :param volume: the volume to be created.
300        """
301
302        if not self._mounted_shares:
303            raise exception.VzStorageNoSharesMounted()
304
305        for share in self._mounted_shares:
306            if self._is_share_eligible(share, volume.size):
307                break
308        else:
309            raise exception.VzStorageNoSuitableShareFound(
310                volume_size=volume.size)
311
312        LOG.debug('Selected %s as target VzStorage share.', share)
313
314        return share
315
316    def _is_share_eligible(self, vz_share, volume_size_in_gib):
317        """Verifies VzStorage share is eligible to host volume with given size.
318
319        :param vz_share: vzstorage share
320        :param volume_size_in_gib: int size in GB
321        """
322
323        used_ratio = self.configuration.vzstorage_used_ratio
324        volume_size = volume_size_in_gib * units.Gi
325
326        total_size, available, allocated = self._get_capacity_info(vz_share)
327
328        if (allocated + volume_size) // total_size > used_ratio:
329            LOG.debug('_is_share_eligible: %s is above '
330                      'vzstorage_used_ratio.', vz_share)
331            return False
332
333        return True
334
335    def choose_volume_format(self, volume):
336        volume_format = None
337        volume_type = volume.volume_type
338
339        # Retrieve volume format from volume metadata
340        if 'volume_format' in volume.metadata:
341            volume_format = volume.metadata['volume_format']
342
343        # If volume format wasn't found in metadata, use
344        # volume type extra specs
345        if not volume_format and volume_type:
346            extra_specs = volume_type.extra_specs or {}
347            if 'vz:volume_format' in extra_specs:
348                volume_format = extra_specs['vz:volume_format']
349
350        # If volume format is still undefined, return default
351        # volume format from backend configuration
352        return (volume_format or
353                self.configuration.vzstorage_default_volume_format)
354
355    def get_volume_format(self, volume):
356        active_file = self.get_active_image_from_info(volume)
357        active_file_path = os.path.join(self._local_volume_dir(volume),
358                                        active_file)
359        img_info = self._qemu_img_info(active_file_path, volume.name)
360        return image_utils.from_qemu_img_disk_format(img_info.file_format)
361
362    def _create_ploop(self, volume_path, volume_size):
363        os.mkdir(volume_path)
364        try:
365            self._execute('ploop', 'init', '-s', '%sG' % volume_size,
366                          os.path.join(volume_path, PLOOP_BASE_DELTA_NAME),
367                          run_as_root=True)
368        except putils.ProcessExecutionError:
369            os.rmdir(volume_path)
370            raise
371
372    def _do_create_volume(self, volume):
373        """Create a volume on given vzstorage share.
374
375        :param volume: volume reference
376        """
377        volume_format = self.choose_volume_format(volume)
378        volume_path = self.local_path(volume)
379        volume_size = volume.size
380
381        LOG.debug("Creating new volume at %s.", volume_path)
382
383        if os.path.exists(volume_path):
384            msg = _('File already exists at %s.') % volume_path
385            LOG.error(msg)
386            raise exception.InvalidVolume(reason=msg)
387
388        if volume_format == DISK_FORMAT_PLOOP:
389            self._create_ploop(volume_path, volume_size)
390        elif volume_format == DISK_FORMAT_QCOW2:
391            self._create_qcow2_file(volume_path, volume_size)
392        elif self.configuration.vzstorage_sparsed_volumes:
393            self._create_sparsed_file(volume_path, volume_size)
394        else:
395            self._create_regular_file(volume_path, volume_size)
396
397        info_path = self._local_path_volume_info(volume)
398        snap_info = {'active': os.path.basename(volume_path)}
399        self._write_info_file(info_path, snap_info)
400
401        # Query qemu-img info to cache the output
402        self._qemu_img_info(volume_path, volume.name)
403
404    def _delete(self, path):
405        self._execute('rm', '-rf', path, run_as_root=True)
406
407    @remotefs_drv.locked_volume_id_operation
408    def extend_volume(self, volume, size_gb):
409        LOG.info('Extending volume %s.', volume.id)
410        volume_format = self.get_volume_format(volume)
411        self._extend_volume(volume, size_gb, volume_format)
412
413    def _extend_volume(self, volume, size_gb, volume_format):
414        volume_path = self.local_path(volume)
415
416        self._check_extend_volume_support(volume, size_gb)
417        LOG.info('Resizing file to %sG...', size_gb)
418
419        self._do_extend_volume(volume_path, size_gb, volume_format)
420
421    def _do_extend_volume(self, volume_path, size_gb, volume_format):
422        if volume_format == DISK_FORMAT_PLOOP:
423            self._execute('ploop', 'resize', '-s',
424                          '%dG' % size_gb,
425                          os.path.join(volume_path, 'DiskDescriptor.xml'),
426                          run_as_root=True)
427        else:
428            image_utils.resize_image(volume_path, size_gb)
429            if not self._is_file_size_equal(volume_path, size_gb):
430                raise exception.ExtendVolumeError(
431                    reason='Resizing image file failed.')
432
433    def _check_extend_volume_support(self, volume, size_gb):
434        volume_path = self.local_path(volume)
435        active_file = self.get_active_image_from_info(volume)
436        active_file_path = os.path.join(self._local_volume_dir(volume),
437                                        active_file)
438
439        if active_file_path != volume_path:
440            msg = _('Extend volume is only supported for this '
441                    'driver when no snapshots exist.')
442            raise exception.InvalidVolume(msg)
443
444        extend_by = int(size_gb) - volume.size
445        if not self._is_share_eligible(volume.provider_location,
446                                       extend_by):
447            raise exception.ExtendVolumeError(reason='Insufficient space to '
448                                              'extend volume %s to %sG.'
449                                              % (volume.id, size_gb))
450
451    def _is_file_size_equal(self, path, size):
452        """Checks if file size at path is equal to size."""
453        data = image_utils.qemu_img_info(path)
454        virt_size = data.virtual_size / units.Gi
455        return virt_size == size
456
457    def _recreate_ploop_desc(self, image_dir, image_file):
458        self._delete(os.path.join(image_dir, 'DiskDescriptor.xml'))
459
460        self._execute('ploop', 'restore-descriptor', image_dir, image_file)
461
462    def copy_image_to_volume(self, context, volume, image_service, image_id):
463        """Fetch the image from image_service and write it to the volume."""
464        volume_format = self.get_volume_format(volume)
465        qemu_volume_format = image_utils.fixup_disk_format(volume_format)
466        image_path = self.local_path(volume)
467        if volume_format == DISK_FORMAT_PLOOP:
468            image_path = os.path.join(image_path, PLOOP_BASE_DELTA_NAME)
469
470        image_utils.fetch_to_volume_format(
471            context, image_service, image_id,
472            image_path, qemu_volume_format,
473            self.configuration.volume_dd_blocksize)
474
475        if volume_format == DISK_FORMAT_PLOOP:
476            self._recreate_ploop_desc(self.local_path(volume), image_path)
477
478        self._do_extend_volume(self.local_path(volume),
479                               volume.size,
480                               volume_format)
481        # Query qemu-img info to cache the output
482        self._qemu_img_info(self.local_path(volume), volume.name)
483
484    def _copy_volume_from_snapshot(self, snapshot, volume, volume_size):
485        """Copy data from snapshot to destination volume.
486
487        This is done with a qemu-img convert to raw/qcow2 from the snapshot
488        qcow2.
489        """
490
491        info_path = self._local_path_volume_info(snapshot.volume)
492        snap_info = self._read_info_file(info_path)
493        vol_dir = self._local_volume_dir(snapshot.volume)
494        out_format = self.choose_volume_format(volume)
495        qemu_out_format = image_utils.fixup_disk_format(out_format)
496        volume_format = self.get_volume_format(snapshot.volume)
497        volume_path = self.local_path(volume)
498
499        if volume_format in (DISK_FORMAT_QCOW2, DISK_FORMAT_RAW):
500            forward_file = snap_info[snapshot.id]
501            forward_path = os.path.join(vol_dir, forward_file)
502
503            # Find the file which backs this file, which represents the point
504            # when this snapshot was created.
505            img_info = self._qemu_img_info(forward_path,
506                                           snapshot.volume.name)
507            path_to_snap_img = os.path.join(vol_dir, img_info.backing_file)
508
509            LOG.debug("_copy_volume_from_snapshot: will copy "
510                      "from snapshot at %s.", path_to_snap_img)
511
512            image_utils.convert_image(path_to_snap_img,
513                                      volume_path,
514                                      qemu_out_format)
515        elif volume_format == DISK_FORMAT_PLOOP:
516            with PloopDevice(self.local_path(snapshot.volume),
517                             snapshot.id,
518                             execute=self._execute) as dev:
519                base_file = os.path.join(volume_path, 'root.hds')
520                image_utils.convert_image(dev,
521                                          base_file,
522                                          qemu_out_format)
523        else:
524            msg = _("Unsupported volume format %s") % volume_format
525            raise exception.InvalidVolume(msg)
526
527        self._extend_volume(volume, volume_size, out_format)
528        # Query qemu-img info to cache the output
529        img_info = self._qemu_img_info(volume_path, volume.name)
530
531    @remotefs_drv.locked_volume_id_operation
532    def delete_volume(self, volume):
533        """Deletes a logical volume."""
534        if not volume.provider_location:
535            msg = (_('Volume %s does not have provider_location '
536                     'specified, skipping.') % volume.name)
537            LOG.error(msg)
538            return
539
540        self._ensure_share_mounted(volume.provider_location)
541        volume_dir = self._local_volume_dir(volume)
542        mounted_path = os.path.join(volume_dir,
543                                    self.get_active_image_from_info(volume))
544        if os.path.exists(mounted_path):
545            self._delete(mounted_path)
546            self._delete(mounted_path + ".qemu_img_info")
547        else:
548            LOG.info("Skipping deletion of volume %s "
549                     "as it does not exist.", mounted_path)
550
551        info_path = self._local_path_volume_info(volume)
552        self._delete(info_path)
553
554    def _get_desc_path(self, volume):
555        return os.path.join(self.local_path(volume), 'DiskDescriptor.xml')
556
557    def _create_snapshot_ploop(self, snapshot):
558        status = snapshot.volume.status
559        if status != 'available':
560            msg = (_('Volume status must be available for '
561                     'snapshot %(id)s. (is %(status)s)') %
562                   {'id': snapshot.id, 'status': status})
563            raise exception.InvalidVolume(msg)
564
565        info_path = self._local_path_volume_info(snapshot.volume)
566        snap_info = self._read_info_file(info_path)
567        self._execute('ploop', 'snapshot', '-u', '{%s}' % snapshot.id,
568                      self._get_desc_path(snapshot.volume),
569                      run_as_root=True)
570        snap_file = os.path.join('volume-%s' % snapshot.volume.id, snapshot.id)
571        snap_info[snapshot.id] = snap_file
572        self._write_info_file(info_path, snap_info)
573
574    def _delete_snapshot_ploop(self, snapshot):
575        status = snapshot.volume.status
576        if status != 'available':
577            msg = (_('Volume status must be available for '
578                     'snapshot %(id)s. (is %(status)s)') %
579                   {'id': snapshot.id, 'status': status})
580            raise exception.InvalidVolume(msg)
581
582        info_path = self._local_path_volume_info(snapshot.volume)
583        snap_info = self._read_info_file(info_path)
584        self._execute('ploop', 'snapshot-delete', '-u', '{%s}' % snapshot.id,
585                      self._get_desc_path(snapshot.volume),
586                      run_as_root=True)
587        snap_info.pop(snapshot.id, None)
588        self._write_info_file(info_path, snap_info)
589
590    def _create_snapshot(self, snapshot):
591        volume_format = self.get_volume_format(snapshot.volume)
592        if volume_format == DISK_FORMAT_PLOOP:
593            self._create_snapshot_ploop(snapshot)
594        else:
595            super(VZStorageDriver, self)._create_snapshot(snapshot)
596
597    def _do_create_snapshot(self, snapshot, backing_filename,
598                            new_snap_path):
599        super(VZStorageDriver, self)._do_create_snapshot(snapshot,
600                                                         backing_filename,
601                                                         new_snap_path)
602        # Cache qemu-img info for created snapshot
603        self._qemu_img_info(new_snap_path, snapshot.volume.name)
604
605    def _delete_snapshot_qcow2(self, snapshot):
606        info_path = self._local_path_volume_info(snapshot.volume)
607        snap_info = self._read_info_file(info_path, empty_if_missing=True)
608        if snapshot.id not in snap_info:
609            LOG.warning("Snapshot %s doesn't exist in snap_info",
610                        snapshot.id)
611            return
612
613        snap_file = os.path.join(self._local_volume_dir(snapshot.volume),
614                                 snap_info[snapshot.id])
615        active_file = os.path.join(self._local_volume_dir(snapshot.volume),
616                                   snap_info['active'])
617        higher_file = self._get_higher_image_path(snapshot)
618        if higher_file:
619            higher_file = os.path.join(self._local_volume_dir(snapshot.volume),
620                                       higher_file)
621        elif active_file != snap_file:
622            msg = (_("Expected higher file exists for snapshot %s") %
623                   snapshot.id)
624            raise exception.VzStorageException(msg)
625
626        img_info = self._qemu_img_info(snap_file, snapshot.volume.name)
627        base_file = os.path.join(self._local_volume_dir(snapshot.volume),
628                                 img_info.backing_file)
629
630        super(VZStorageDriver, self)._delete_snapshot(snapshot)
631
632        def _qemu_info_cache(fn):
633            return fn + ".qemu_img_info"
634
635        def _update_backing_file(info_src, info_dst):
636            with open(info_src, 'r') as fs, open(info_dst, 'r') as fd:
637                src = json.load(fs)
638                dst = json.load(fd)
639            dst['backing_file'] = src['backing_file']
640            with open(info_dst, 'w') as fdw:
641                json.dump(dst, fdw)
642
643        if snap_file != active_file:
644            # mv snap_file.info higher_file.info
645            _update_backing_file(
646                _qemu_info_cache(snap_file),
647                _qemu_info_cache(higher_file))
648            self._delete(_qemu_info_cache(snap_file))
649        elif snapshot.volume.status == 'in-use':
650            # mv base_file.info snap_file.info
651            _update_backing_file(
652                _qemu_info_cache(base_file),
653                _qemu_info_cache(snap_file))
654            self._delete(_qemu_info_cache(base_file))
655        else:
656            # rm snap_file.info
657            self._delete(_qemu_info_cache(snap_file))
658
659    def _delete_snapshot(self, snapshot):
660        volume_format = self.get_volume_format(snapshot.volume)
661        if volume_format == DISK_FORMAT_PLOOP:
662            self._delete_snapshot_ploop(snapshot)
663        else:
664            self._delete_snapshot_qcow2(snapshot)
665
666    def _copy_volume_to_image(self, context, volume, image_service,
667                              image_meta):
668        """Copy the volume to the specified image."""
669
670        volume_format = self.get_volume_format(volume)
671        if volume_format == DISK_FORMAT_PLOOP:
672            with PloopDevice(self.local_path(volume),
673                             execute=self._execute) as dev:
674                image_utils.upload_volume(context,
675                                          image_service,
676                                          image_meta,
677                                          dev,
678                                          volume_format='raw')
679        else:
680            super(VZStorageDriver, self)._copy_volume_to_image(context, volume,
681                                                               image_service,
682                                                               image_meta)
683
684    def _create_cloned_volume_ploop(self, volume, src_vref):
685        LOG.info('Cloning volume %(src)s to volume %(dst)s',
686                 {'src': src_vref.id,
687                  'dst': volume.id})
688
689        if src_vref.status != 'available':
690            msg = _("Volume status must be 'available'.")
691            raise exception.InvalidVolume(msg)
692
693        volume_name = CONF.volume_name_template % volume.id
694
695        # Create fake snapshot object
696        snap_attrs = ['volume_name', 'size', 'volume_size', 'name',
697                      'volume_id', 'id', 'volume']
698        Snapshot = collections.namedtuple('Snapshot', snap_attrs)
699
700        temp_snapshot = Snapshot(id=src_vref.id,
701                                 volume_name=volume_name,
702                                 size=src_vref.size,
703                                 volume_size=src_vref.size,
704                                 name='clone-snap-%s' % src_vref.id,
705                                 volume_id=src_vref.id,
706                                 volume=src_vref)
707
708        self._create_snapshot_ploop(temp_snapshot)
709        try:
710            volume.provider_location = src_vref.provider_location
711            info_path = self._local_path_volume_info(volume)
712            snap_info = {'active': 'volume-%s' % volume.id}
713            self._write_info_file(info_path, snap_info)
714            self._copy_volume_from_snapshot(temp_snapshot,
715                                            volume,
716                                            volume.size)
717
718        finally:
719            self.delete_snapshot(temp_snapshot)
720
721        return {'provider_location': src_vref.provider_location}
722
723    def _create_cloned_volume(self, volume, src_vref, context):
724        """Creates a clone of the specified volume."""
725        volume_format = self.get_volume_format(src_vref)
726        if volume_format == DISK_FORMAT_PLOOP:
727            return self._create_cloned_volume_ploop(volume, src_vref)
728        else:
729            return super(VZStorageDriver, self)._create_cloned_volume(volume,
730                                                                      src_vref)
731