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