Coverage for nova/virt/libvirt/imagebackend.py: 87%
681 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-17 15:08 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-17 15:08 +0000
1# Copyright 2012 Grid Dynamics
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.
16import abc
17import base64
18import contextlib
19import errno
20import functools
21import os
22import shutil
24from castellan import key_manager
25from oslo_concurrency import processutils
26from oslo_log import log as logging
27from oslo_serialization import jsonutils
28from oslo_service import loopingcall
29from oslo_utils import excutils
30from oslo_utils import fileutils
31from oslo_utils.imageutils import format_inspector
32from oslo_utils import strutils
33from oslo_utils import units
35import nova.conf
36from nova import exception
37from nova.i18n import _
38from nova.image import glance
39import nova.privsep.libvirt
40import nova.privsep.path
41from nova.storage import rbd_utils
42from nova import utils
43from nova.virt.disk import api as disk
44from nova.virt.image import model as imgmodel
45from nova.virt import images
46from nova.virt.libvirt import config as vconfig
47from nova.virt.libvirt.storage import dmcrypt
48from nova.virt.libvirt.storage import lvm
49from nova.virt.libvirt import utils as libvirt_utils
51CONF = nova.conf.CONF
53LOG = logging.getLogger(__name__)
54IMAGE_API = glance.API()
57# NOTE(neiljerram): Don't worry if this fails. This sometimes happens, with
58# EACCES (Permission Denied), when the base file is on an NFS client
59# filesystem. I don't understand why, but wonder if it's a similar problem as
60# the one that motivated using touch instead of utime in ec9d5e375e2. In any
61# case, IIUC, timing isn't the primary thing that the image cache manager uses
62# to determine when the base file is in use. The primary mechanism for that is
63# whether there is a matching disk file for a current instance. The timestamp
64# on the base file is only used when deciding whether to delete a base file
65# that is _not_ in use; so it is not a big deal if that deletion happens
66# slightly earlier, for an unused base file, because of one of these preceding
67# utime calls having failed.
68# NOTE(mdbooth): Only use this method for updating the utime of an image cache
69# entry during disk creation.
70# TODO(mdbooth): Remove or rework this when we understand the problem.
71def _update_utime_ignore_eacces(path):
72 try:
73 nova.privsep.path.utime(path)
74 except OSError as e:
75 with excutils.save_and_reraise_exception(logger=LOG) as ctxt:
76 if e.errno == errno.EACCES:
77 LOG.warning("Ignoring failure to update utime of %(path)s: "
78 "%(error)s", {'path': path, 'error': e})
79 ctxt.reraise = False
82class Image(metaclass=abc.ABCMeta):
84 SUPPORTS_CLONE = False
85 SUPPORTS_LUKS = False
87 def __init__(
88 self,
89 path,
90 source_type,
91 driver_format,
92 is_block_dev=False,
93 disk_info_mapping=None
94 ):
95 """Image initialization.
97 :param path: libvirt's representation of the path of this disk.
98 :param source_type: block or file
99 :param driver_format: raw or qcow2
100 :param is_block_dev:
101 :param disk_info_mapping: disk_info['mapping'][device] metadata
102 specific to this image generated by nova.virt.libvirt.blockinfo.
103 """
104 if (CONF.ephemeral_storage_encryption.enabled and 104 ↛ 106line 104 didn't jump to line 106 because the condition on line 104 was never true
105 not self._supports_encryption()):
106 msg = _('Incompatible settings: ephemeral storage encryption is '
107 'supported only for LVM images.')
108 raise exception.InternalError(msg)
110 self.path = path
112 self.source_type = source_type
113 self.driver_format = driver_format
114 self.driver_io = None
115 self.discard_mode = CONF.libvirt.hw_disk_discard
116 self.is_block_dev = is_block_dev
117 self.preallocate = False
119 self.disk_info_mapping = disk_info_mapping
121 # NOTE(dripton): We store lines of json (path, disk_format) in this
122 # file, for some image types, to prevent attacks based on changing the
123 # disk_format.
124 self.disk_info_path = None
126 # NOTE(mikal): We need a lock directory which is shared along with
127 # instance files, to cover the scenario where multiple compute nodes
128 # are trying to create a base file at the same time
129 self.lock_path = os.path.join(CONF.instances_path, 'locks')
131 def _supports_encryption(self):
132 """Used to test that the backend supports encryption.
133 Override in the subclass if backend supports encryption.
134 """
135 return False
137 @abc.abstractmethod
138 def create_image(
139 self, prepare_template, base, size, safe=False, *args, **kwargs):
140 """Create image from template.
142 Contains specific behavior for each image type.
144 :prepare_template: function, that creates template.
145 Should accept `target` argument.
146 :base: Template name
147 :size: Size of created image in bytes
148 :safe: True if image contains a safe filesystem
150 """
151 pass
153 @abc.abstractmethod
154 def resize_image(self, size):
155 """Resize image to size (in bytes).
157 :size: Desired size of image in bytes
159 """
160 pass
162 def libvirt_info(
163 self, cache_mode, extra_specs, boot_order=None, disk_unit=None,
164 ):
165 """Get `LibvirtConfigGuestDisk` filled for this image.
167 :cache_mode: Caching mode for this image
168 :extra_specs: Instance type extra specs dict.
169 :boot_order: Disk device boot order
170 """
171 if self.disk_info_mapping is None: 171 ↛ 172line 171 didn't jump to line 172 because the condition on line 171 was never true
172 raise AttributeError(
173 'Image must have disk_info_mapping to call libvirt_info()')
174 disk_bus = self.disk_info_mapping['bus']
175 info = vconfig.LibvirtConfigGuestDisk()
176 info.source_type = self.source_type
177 info.source_device = self.disk_info_mapping['type']
178 info.target_bus = disk_bus
179 info.target_dev = self.disk_info_mapping['dev']
180 info.driver_cache = cache_mode
181 info.driver_discard = self.discard_mode
182 info.driver_io = self.driver_io
183 info.driver_format = self.driver_format
184 if CONF.libvirt.virt_type in ('qemu', 'kvm'):
185 # the QEMU backend supports multiple backends, so tell libvirt
186 # which one to use
187 info.driver_name = 'qemu'
188 info.source_path = self.path
189 info.boot_order = boot_order
191 if (self.SUPPORTS_LUKS and 191 ↛ 196line 191 didn't jump to line 196 because the condition on line 191 was never true
192 self.disk_info_mapping and
193 self.disk_info_mapping.get('encrypted') and
194 self.disk_info_mapping.get('encryption_format') == 'luks'
195 ):
196 encryption = vconfig.LibvirtConfigGuestDiskEncryption()
197 secret = vconfig.LibvirtConfigGuestDiskEncryptionSecret()
198 secret.type = 'passphrase'
199 secret.uuid = self.disk_info_mapping.get('encryption_secret_uuid')
200 encryption.secret = secret
201 encryption.format = self.disk_info_mapping.get('encryption_format')
202 info.ephemeral_encryption = encryption
204 if disk_bus == 'scsi':
205 self.disk_scsi(info, disk_unit)
207 self.disk_qos(info, extra_specs)
209 return info
211 def disk_scsi(self, info, disk_unit):
212 # NOTE(melwitt): We set the device address unit number manually in the
213 # case of the virtio-scsi controller, in order to allow attachment of
214 # up to 256 devices. So, we should only be setting the address tag
215 # if we intend to set the unit number. Otherwise, we will let libvirt
216 # handle autogeneration of the address tag.
217 # See https://bugs.launchpad.net/nova/+bug/1792077 for details.
218 if disk_unit is not None:
219 # The driver is responsible to create the SCSI controller
220 # at index 0.
221 info.device_addr = vconfig.LibvirtConfigGuestDeviceAddressDrive()
222 info.device_addr.controller = 0
223 # In order to allow up to 256 disks handled by one
224 # virtio-scsi controller, the device addr should be
225 # specified.
226 info.device_addr.unit = disk_unit
228 def disk_qos(self, info, extra_specs):
229 tune_items = ['disk_read_bytes_sec', 'disk_read_iops_sec',
230 'disk_write_bytes_sec', 'disk_write_iops_sec',
231 'disk_total_bytes_sec', 'disk_total_iops_sec']
232 for key, value in extra_specs.items():
233 scope = key.split(':')
234 if len(scope) > 1 and scope[0] == 'quota':
235 if scope[1] in tune_items:
236 setattr(info, scope[1], value)
238 def libvirt_fs_info(self, target, driver_type=None):
239 """Get `LibvirtConfigGuestFilesys` filled for this image.
241 :target: target directory inside a container.
242 :driver_type: filesystem driver type, can be loop
243 nbd or ploop.
244 """
245 info = vconfig.LibvirtConfigGuestFilesys()
246 info.target_dir = target
248 if self.is_block_dev:
249 info.source_type = "block"
250 info.source_dev = self.path
251 else:
252 info.source_type = "file"
253 info.source_file = self.path
254 info.driver_format = self.driver_format
255 if driver_type:
256 info.driver_type = driver_type
257 else:
258 if self.driver_format == "raw":
259 info.driver_type = "loop"
260 else:
261 info.driver_type = "nbd"
263 return info
265 def exists(self):
266 return os.path.exists(self.path)
268 def cache(self, fetch_func, filename, size=None, safe=False, *args,
269 **kwargs):
270 """Creates image from template.
272 Ensures that template and image not already exists.
273 Ensures that base directory exists.
274 Synchronizes on template fetching.
276 :fetch_func: Function that creates the base image
277 Should accept `target` argument.
278 :filename: Name of the file in the image directory
279 :size: Size of created image in bytes (optional)
280 """
281 base_dir = os.path.join(CONF.instances_path,
282 CONF.image_cache.subdirectory_name)
283 if not os.path.exists(base_dir):
284 fileutils.ensure_tree(base_dir)
285 base = os.path.join(base_dir, filename)
287 @utils.synchronized(filename, external=True, lock_path=self.lock_path)
288 def fetch_func_sync(target, *args, **kwargs):
289 # NOTE(mdbooth): This method is called as a callback by the
290 # create_image() method of a specific backend. It assumes that
291 # target will be in the image cache, which is why it holds a
292 # lock, and does not overwrite an existing file. However,
293 # this is not true for all backends. Specifically Lvm writes
294 # directly to the target rather than to the image cache,
295 # and additionally it creates the target in advance.
296 # This guard is only relevant in the context of the lock if the
297 # target is in the image cache. If it isn't, we should
298 # call fetch_func. The lock we're holding is also unnecessary in
299 # that case, but it will not result in incorrect behaviour.
300 if target != base or not os.path.exists(target):
301 fetch_func(target=target, *args, **kwargs)
303 if not self.exists() or not os.path.exists(base):
304 self.create_image(
305 fetch_func_sync, base, size, safe=safe, *args,
306 **kwargs)
308 if size:
309 # create_image() only creates the base image if needed, so
310 # we cannot rely on it to exist here
311 if os.path.exists(base) and size > self.get_disk_size(base):
312 self.resize_image(size)
314 if (self.preallocate and self._can_fallocate() and
315 os.access(self.path, os.W_OK)):
316 processutils.execute('fallocate', '-n', '-l', size, self.path)
318 def _can_fallocate(self):
319 """Check once per class, whether fallocate(1) is available,
320 and that the instances directory supports fallocate(2).
321 """
322 can_fallocate = getattr(self.__class__, 'can_fallocate', None)
323 if can_fallocate is None:
324 test_path = self.path + '.fallocate_test'
325 _out, err = processutils.trycmd('fallocate', '-l', '1', test_path)
326 fileutils.delete_if_exists(test_path)
327 can_fallocate = not err
328 self.__class__.can_fallocate = can_fallocate
329 if not can_fallocate: 329 ↛ 330line 329 didn't jump to line 330 because the condition on line 329 was never true
330 LOG.warning('Unable to preallocate image at path: %(path)s',
331 {'path': self.path})
332 return can_fallocate
334 def verify_base_size(self, base, size, base_size=0):
335 """Check that the base image is not larger than size.
336 Since images can't be generally shrunk, enforce this
337 constraint taking account of virtual image size.
338 """
340 # Note(pbrady): The size and min_disk parameters of a glance
341 # image are checked against the instance size before the image
342 # is even downloaded from glance, but currently min_disk is
343 # adjustable and doesn't currently account for virtual disk size,
344 # so we need this extra check here.
345 # NOTE(cfb): Having a flavor that sets the root size to 0 and having
346 # nova effectively ignore that size and use the size of the
347 # image is considered a feature at this time, not a bug.
349 if size is None:
350 return
352 if size and not base_size:
353 base_size = self.get_disk_size(base)
355 if size < base_size:
356 LOG.error('%(base)s virtual size %(base_size)s '
357 'larger than flavor root disk size %(size)s',
358 {'base': base,
359 'base_size': base_size,
360 'size': size})
361 raise exception.FlavorDiskSmallerThanImage(
362 flavor_size=size, image_size=base_size)
364 def get_disk_size(self, name):
365 return disk.get_disk_size(name)
367 @abc.abstractmethod
368 def snapshot_extract(self, target, out_format):
369 """Extract a snapshot of the image.
371 This is used during cold (offline) snapshots. Live snapshots
372 while the guest is still running are handled separately.
374 :param target: The target path for the image snapshot.
375 :param out_format: The image snapshot format.
376 """
377 raise NotImplementedError()
379 def _get_driver_format(self):
380 return self.driver_format
382 def resolve_driver_format(self):
383 """Return the driver format for self.path.
385 First checks self.disk_info_path for an entry.
386 If it's not there, calls self._get_driver_format(), and then
387 stores the result in self.disk_info_path
389 See https://bugs.launchpad.net/nova/+bug/1221190
390 """
391 def _dict_from_line(line):
392 if not line:
393 return {}
394 try:
395 return jsonutils.loads(line)
396 except (TypeError, ValueError) as e:
397 msg = (_("Could not load line %(line)s, got error "
398 "%(error)s") %
399 {'line': line, 'error': e})
400 raise exception.InvalidDiskInfo(reason=msg)
402 @utils.synchronized(self.disk_info_path, external=False,
403 lock_path=self.lock_path)
404 def write_to_disk_info_file():
405 # Use os.open to create it without group or world write permission.
406 fd = os.open(self.disk_info_path, os.O_RDONLY | os.O_CREAT, 0o644)
407 with os.fdopen(fd, "r") as disk_info_file:
408 line = disk_info_file.read().rstrip()
409 dct = _dict_from_line(line)
411 if self.path in dct: 411 ↛ 412line 411 didn't jump to line 412 because the condition on line 411 was never true
412 msg = _("Attempted overwrite of an existing value.")
413 raise exception.InvalidDiskInfo(reason=msg)
414 dct.update({self.path: driver_format})
416 tmp_path = self.disk_info_path + ".tmp"
417 fd = os.open(tmp_path, os.O_WRONLY | os.O_CREAT, 0o644)
418 with os.fdopen(fd, "w") as tmp_file:
419 tmp_file.write('%s\n' % jsonutils.dumps(dct))
420 os.rename(tmp_path, self.disk_info_path)
422 try:
423 if (self.disk_info_path is not None and
424 os.path.exists(self.disk_info_path)):
425 with open(self.disk_info_path) as disk_info_file:
426 line = disk_info_file.read().rstrip()
427 dct = _dict_from_line(line)
428 for path, driver_format in dct.items():
429 if path == self.path:
430 return driver_format
431 driver_format = self._get_driver_format()
432 if self.disk_info_path is not None:
433 fileutils.ensure_tree(os.path.dirname(self.disk_info_path))
434 write_to_disk_info_file()
435 except OSError as e:
436 raise exception.DiskInfoReadWriteFail(reason=str(e))
437 return driver_format
439 @staticmethod
440 def is_shared_block_storage():
441 """True if the backend puts images on a shared block storage."""
442 return False
444 @staticmethod
445 def is_file_in_instance_path():
446 """True if the backend stores images in files under instance path."""
447 return False
449 def clone(self, context, image_id_or_uri):
450 """Clone an image.
452 Note that clone operation is backend-dependent. The backend may ask
453 the image API for a list of image "locations" and select one or more
454 of those locations to clone an image from.
456 :param image_id_or_uri: The ID or URI of an image to clone.
458 :raises: exception.ImageUnacceptable if it cannot be cloned
459 """
460 reason = _('clone() is not implemented')
461 raise exception.ImageUnacceptable(image_id=image_id_or_uri,
462 reason=reason)
464 def flatten(self):
465 """Flatten an image.
467 The implementation of this method is optional and therefore is
468 not an abstractmethod.
469 """
470 raise NotImplementedError('flatten() is not implemented')
472 def direct_snapshot(self, context, snapshot_name, image_format, image_id,
473 base_image_id):
474 """Prepare a snapshot for direct reference from glance.
476 The implementation of this method is optional and therefore is
477 not an abstractmethod.
479 :raises: exception.ImageUnacceptable if it cannot be
480 referenced directly in the specified image format
481 :returns: URL to be given to glance
482 """
483 raise NotImplementedError(_('direct_snapshot() is not implemented'))
485 def cleanup_direct_snapshot(self, location, also_destroy_volume=False,
486 ignore_errors=False):
487 """Performs any cleanup actions required after calling
488 direct_snapshot(), for graceful exception handling and the like.
490 This should be a no-op on any backend where it is not implemented.
491 """
492 pass
494 def _get_lock_name(self, base):
495 """Get an image's name of a base file."""
496 return os.path.split(base)[-1]
498 @abc.abstractmethod
499 def get_model(self, connection):
500 """Get the image information model
502 :returns: an instance of nova.virt.image.model.Image
503 """
504 raise NotImplementedError()
506 def import_file(self, instance, local_file, remote_name):
507 """Import an image from local storage into this backend.
509 Import a local file into the store used by this image type. Note that
510 this is a noop for stores using local disk (the local file is
511 considered "in the store").
513 If the image already exists it will be overridden by the new file
515 :param local_file: path to the file to import
516 :param remote_name: the name for the file in the store
517 """
519 # NOTE(mikal): this is a noop for now for all stores except RBD, but
520 # we should talk about if we want this functionality for everything.
521 pass
523 def create_snap(self, name):
524 """Create a snapshot on the image. A noop on backends that don't
525 support snapshots.
527 :param name: name of the snapshot
528 """
529 pass
531 def remove_snap(self, name, ignore_errors=False):
532 """Remove a snapshot on the image. A noop on backends that don't
533 support snapshots.
535 :param name: name of the snapshot
536 :param ignore_errors: don't log errors if the snapshot does not exist
537 """
538 pass
540 def rollback_to_snap(self, name):
541 """Rollback the image to the named snapshot. A noop on backends that
542 don't support snapshots.
544 :param name: name of the snapshot
545 """
546 pass
549class Flat(Image):
550 """The Flat backend uses either raw or qcow2 storage. It never uses
551 a backing store, so when using qcow2 it copies an image rather than
552 creating an overlay. By default it creates raw files, but will use qcow2
553 when creating a disk from a qcow2 if force_raw_images is not set in config.
554 """
556 def __init__(
557 self, instance=None, disk_name=None, path=None, disk_info_mapping=None
558 ):
559 self.disk_name = disk_name
560 path = (path or os.path.join(libvirt_utils.get_instance_path(instance),
561 disk_name))
562 super().__init__(
563 path, "file", "raw", is_block_dev=False,
564 disk_info_mapping=disk_info_mapping
565 )
567 self.preallocate = (
568 strutils.to_slug(CONF.preallocate_images) == 'space')
569 if self.preallocate:
570 self.driver_io = "native"
571 self.disk_info_path = os.path.join(os.path.dirname(path), 'disk.info')
572 self.correct_format()
574 def _get_driver_format(self):
575 try:
576 data = images.qemu_img_info(self.path)
577 return data.file_format
578 except exception.InvalidDiskInfo as e:
579 LOG.info('Failed to get image info from path %(path)s; '
580 'error: %(error)s',
581 {'path': self.path, 'error': e})
582 return 'raw'
584 def _supports_encryption(self):
585 # NOTE(dgenin): Kernel, ramdisk and disk.config are fetched using
586 # the Flat backend regardless of which backend is configured for
587 # ephemeral storage. Encryption for the Flat backend is not yet
588 # implemented so this loophole is necessary to allow other
589 # backends already supporting encryption to function. This can
590 # be removed once encryption for Flat is implemented.
591 if self.disk_name not in ['kernel', 'ramdisk', 'disk.config']:
592 return False
593 else:
594 return True
596 def correct_format(self):
597 if os.path.exists(self.path):
598 self.driver_format = self.resolve_driver_format()
600 def create_image(
601 self, prepare_template, base, size, safe=False, *args, **kwargs):
602 filename = self._get_lock_name(base)
604 @utils.synchronized(filename, external=True, lock_path=self.lock_path)
605 def copy_raw_image(base, target, size):
606 libvirt_utils.copy_image(base, target)
607 if size:
608 self.resize_image(size)
610 generating = 'image_id' not in kwargs
611 if generating:
612 if not self.exists():
613 # Generating image in place
614 prepare_template(target=self.path, *args, **kwargs)
616 # NOTE(plibeau): extend the disk in the case of image is not
617 # accessible anymore by the customer and the base image is
618 # available on source compute during the resize of the
619 # instance.
620 else:
621 if size: 621 ↛ 635line 621 didn't jump to line 635 because the condition on line 621 was always true
622 self.resize_image(size)
623 else:
624 if not os.path.exists(base): 624 ↛ 629line 624 didn't jump to line 629 because the condition on line 624 was always true
625 prepare_template(target=base, *args, **kwargs)
627 # NOTE(mikal): Update the mtime of the base file so the image
628 # cache manager knows it is in use.
629 _update_utime_ignore_eacces(base)
630 self.verify_base_size(base, size)
631 if not os.path.exists(self.path): 631 ↛ 635line 631 didn't jump to line 635 because the condition on line 631 was always true
632 with fileutils.remove_path_on_error(self.path):
633 copy_raw_image(base, self.path, size)
635 self.correct_format()
637 def resize_image(self, size):
638 image = imgmodel.LocalFileImage(self.path, self.driver_format)
639 disk.extend(image, size)
641 def snapshot_extract(self, target, out_format):
642 images.convert_image(self.path, target, self.driver_format, out_format)
644 @staticmethod
645 def is_file_in_instance_path():
646 return True
648 def get_model(self, connection):
649 return imgmodel.LocalFileImage(self.path,
650 imgmodel.FORMAT_RAW)
653class Qcow2(Image):
654 def __init__(
655 self, instance=None, disk_name=None, path=None, disk_info_mapping=None
656 ):
657 path = (path or os.path.join(libvirt_utils.get_instance_path(instance),
658 disk_name))
659 super().__init__(
660 path, "file", "qcow2", is_block_dev=False,
661 disk_info_mapping=disk_info_mapping
662 )
664 self.preallocate = (
665 strutils.to_slug(CONF.preallocate_images) == 'space')
666 if self.preallocate:
667 self.driver_io = "native"
668 self.disk_info_path = os.path.join(os.path.dirname(path), 'disk.info')
669 self.resolve_driver_format()
671 def create_image(
672 self, prepare_template, base, size, safe=False, *args, **kwargs):
673 filename = self._get_lock_name(base)
675 @utils.synchronized(filename, external=True, lock_path=self.lock_path)
676 def create_qcow2_image(base, target, size, safe=False):
677 libvirt_utils.create_image(
678 target, 'qcow2', size, backing_file=base, safe=safe)
680 # Download the unmodified base image unless we already have a copy.
681 if not os.path.exists(base):
682 prepare_template(target=base, *args, **kwargs)
684 # NOTE(danms): We need to perform safety checks on the base image
685 # before we inspect it for other attributes. We do this each time
686 # because additional safety checks could have been added since we
687 # downloaded the image.
688 # NOTE(sean-k-mooney) If the image was created by nova as a swap
689 # or ephemeral disk it is safe to skip the deep inspection.
690 if not CONF.workarounds.disable_deep_image_inspection and not safe:
691 inspector = images.get_image_format(base)
692 try:
693 inspector.safety_check()
694 except format_inspector.SafetyCheckFailed as e:
695 LOG.warning('Base image %s failed safety check: %s', base, e)
696 # NOTE(danms): This is the same exception as would be raised
697 # by qemu_img_info() if the disk format was unreadable or
698 # otherwise unsuitable.
699 raise exception.InvalidDiskInfo(
700 reason=_('Base image failed safety check'))
702 # NOTE(ankit): Update the mtime of the base file so the image
703 # cache manager knows it is in use.
704 _update_utime_ignore_eacces(base)
705 self.verify_base_size(base, size)
707 legacy_backing_size = None
708 legacy_base = base
710 # Determine whether an existing qcow2 disk uses a legacy backing by
711 # actually looking at the image itself and parsing the output of the
712 # backing file it expects to be using.
713 if os.path.exists(self.path):
714 backing_path = libvirt_utils.get_disk_backing_file(self.path)
715 if backing_path is not None:
716 backing_file = os.path.basename(backing_path)
717 backing_parts = backing_file.rpartition('_')
718 if backing_file != backing_parts[-1] and \ 718 ↛ 725line 718 didn't jump to line 725 because the condition on line 718 was always true
719 backing_parts[-1].isdigit():
720 legacy_backing_size = int(backing_parts[-1])
721 legacy_base += '_%d' % legacy_backing_size
722 legacy_backing_size *= units.Gi
724 # Create the legacy backing file if necessary.
725 if legacy_backing_size:
726 if not os.path.exists(legacy_base): 726 ↛ 733line 726 didn't jump to line 733 because the condition on line 726 was always true
727 with fileutils.remove_path_on_error(legacy_base):
728 libvirt_utils.copy_image(base, legacy_base)
729 image = imgmodel.LocalFileImage(legacy_base,
730 imgmodel.FORMAT_QCOW2)
731 disk.extend(image, legacy_backing_size)
733 if not os.path.exists(self.path):
734 with fileutils.remove_path_on_error(self.path):
735 create_qcow2_image(base, self.path, size, safe=safe)
737 def resize_image(self, size):
738 image = imgmodel.LocalFileImage(self.path, imgmodel.FORMAT_QCOW2)
739 disk.extend(image, size)
741 def snapshot_extract(self, target, out_format):
742 libvirt_utils.extract_snapshot(self.path, 'qcow2',
743 target,
744 out_format)
746 @staticmethod
747 def is_file_in_instance_path():
748 return True
750 def get_model(self, connection):
751 return imgmodel.LocalFileImage(self.path,
752 imgmodel.FORMAT_QCOW2)
755class Lvm(Image):
756 @staticmethod
757 def escape(filename):
758 return filename.replace('_', '__')
760 def __init__(
761 self, instance=None, disk_name=None, path=None,
762 disk_info_mapping=None
763 ):
764 self.ephemeral_key_uuid = instance.get('ephemeral_key_uuid')
766 if self.ephemeral_key_uuid is not None:
767 self.key_manager = key_manager.API(CONF)
768 else:
769 self.key_manager = None
771 if path:
772 if self.ephemeral_key_uuid is None: 772 ↛ 777line 772 didn't jump to line 777 because the condition on line 772 was always true
773 info = lvm.volume_info(path)
774 self.vg = info['VG']
775 self.lv = info['LV']
776 else:
777 self.vg = CONF.libvirt.images_volume_group
778 else:
779 if not CONF.libvirt.images_volume_group: 779 ↛ 780line 779 didn't jump to line 780 because the condition on line 779 was never true
780 raise RuntimeError(_('You should specify'
781 ' images_volume_group'
782 ' flag to use LVM images.'))
783 self.vg = CONF.libvirt.images_volume_group
784 self.lv = '%s_%s' % (instance.uuid,
785 self.escape(disk_name))
786 if self.ephemeral_key_uuid is None:
787 path = os.path.join('/dev', self.vg, self.lv)
788 else:
789 self.lv_path = os.path.join('/dev', self.vg, self.lv)
790 path = '/dev/mapper/' + dmcrypt.volume_name(self.lv)
792 super(Lvm, self).__init__(
793 path, "block", "raw", is_block_dev=True,
794 disk_info_mapping=disk_info_mapping
795 )
797 # TODO(sbauza): Remove the config option usage and default the
798 # LVM logical volume creation to preallocate the full size only.
799 self.sparse = CONF.libvirt.sparse_logical_volumes
800 self.preallocate = not self.sparse
802 if not self.sparse:
803 self.driver_io = "native"
805 def _supports_encryption(self):
806 return True
808 def _can_fallocate(self):
809 return False
811 def create_image(
812 self, prepare_template, base, size, safe=False, *args, **kwargs):
813 def encrypt_lvm_image():
814 dmcrypt.create_volume(self.path.rpartition('/')[2],
815 self.lv_path,
816 CONF.ephemeral_storage_encryption.cipher,
817 CONF.ephemeral_storage_encryption.key_size,
818 key)
820 filename = self._get_lock_name(base)
822 @utils.synchronized(filename, external=True, lock_path=self.lock_path)
823 def create_lvm_image(base, size):
824 base_size = disk.get_disk_size(base)
825 self.verify_base_size(base, size, base_size=base_size)
826 resize = size > base_size if size else False
827 size = size if resize else base_size
828 lvm.create_volume(self.vg, self.lv,
829 size, sparse=self.sparse)
830 if self.ephemeral_key_uuid is not None:
831 encrypt_lvm_image()
832 # NOTE: by calling convert_image_unsafe here we're
833 # telling qemu-img convert to do format detection on the input,
834 # because we don't know what the format is. For example,
835 # we might have downloaded a qcow2 image, or created an
836 # ephemeral filesystem locally, we just don't know here. Having
837 # audited this, all current sources have been sanity checked,
838 # either because they're locally generated, or because they have
839 # come from images.fetch_to_raw. However, this is major code smell.
840 images.convert_image_unsafe(base, self.path, self.driver_format,
841 run_as_root=True)
842 if resize:
843 disk.resize2fs(self.path, run_as_root=True)
845 generated = 'ephemeral_size' in kwargs
846 if self.ephemeral_key_uuid is not None:
847 if 'context' in kwargs: 847 ↛ 859line 847 didn't jump to line 859 because the condition on line 847 was always true
848 try:
849 # NOTE(dgenin): Key manager corresponding to the
850 # specific backend catches and reraises an
851 # an exception if key retrieval fails.
852 key = self.key_manager.get(kwargs['context'],
853 self.ephemeral_key_uuid).get_encoded()
854 except Exception:
855 with excutils.save_and_reraise_exception():
856 LOG.error("Failed to retrieve ephemeral "
857 "encryption key")
858 else:
859 raise exception.InternalError(
860 _("Instance disk to be encrypted but no context provided"))
861 # Generate images with specified size right on volume
862 if generated and size:
863 lvm.create_volume(self.vg, self.lv,
864 size, sparse=self.sparse)
865 with self.remove_volume_on_error(self.path):
866 if self.ephemeral_key_uuid is not None:
867 encrypt_lvm_image()
868 prepare_template(target=self.path, *args, **kwargs)
869 else:
870 if not os.path.exists(base): 870 ↛ 872line 870 didn't jump to line 872 because the condition on line 870 was always true
871 prepare_template(target=base, *args, **kwargs)
872 with self.remove_volume_on_error(self.path):
873 create_lvm_image(base, size)
875 # NOTE(nic): Resizing the image is already handled in create_image(),
876 # and migrate/resize is not supported with LVM yet, so this is a no-op
877 def resize_image(self, size):
878 pass
880 @contextlib.contextmanager
881 def remove_volume_on_error(self, path):
882 try:
883 yield
884 except Exception:
885 with excutils.save_and_reraise_exception():
886 if self.ephemeral_key_uuid is None:
887 lvm.remove_volumes([path])
888 else:
889 dmcrypt.delete_volume(path.rpartition('/')[2])
890 lvm.remove_volumes([self.lv_path])
892 def snapshot_extract(self, target, out_format):
893 images.convert_image(self.path, target, self.driver_format,
894 out_format, run_as_root=True)
896 def get_model(self, connection):
897 return imgmodel.LocalBlockImage(self.path)
900class Rbd(Image):
902 SUPPORTS_CLONE = True
904 def __init__(
905 self, instance=None, disk_name=None, path=None, disk_info_mapping=None
906 ):
907 if not CONF.libvirt.images_rbd_pool: 907 ↛ 908line 907 didn't jump to line 908 because the condition on line 907 was never true
908 raise RuntimeError(_('You should specify'
909 ' images_rbd_pool'
910 ' flag to use rbd images.'))
912 if path:
913 try:
914 self.rbd_name = path.split('/')[1]
915 except IndexError:
916 raise exception.InvalidDevicePath(path=path)
917 else:
918 self.rbd_name = '%s_%s' % (instance.uuid, disk_name)
920 self.driver = rbd_utils.RBDDriver()
922 path = 'rbd:%s/%s' % (self.driver.pool, self.rbd_name)
923 if self.driver.rbd_user:
924 path += ':id=' + self.driver.rbd_user
925 if self.driver.ceph_conf:
926 path += ':conf=' + self.driver.ceph_conf
928 super().__init__(
929 path, "block", "rbd", is_block_dev=False,
930 disk_info_mapping=disk_info_mapping
931 )
933 self.discard_mode = CONF.libvirt.hw_disk_discard
935 def libvirt_info(
936 self, cache_mode, extra_specs, boot_order=None, disk_unit=None
937 ):
938 """Get `LibvirtConfigGuestDisk` filled for this image.
940 :cache_mode: Caching mode for this image
941 :extra_specs: Instance type extra specs dict.
942 :boot_order: Disk device boot order
943 """
944 info = vconfig.LibvirtConfigGuestDisk()
945 disk_bus = self.disk_info_mapping['bus']
947 hosts, ports = self.driver.get_mon_addrs()
948 info.source_device = self.disk_info_mapping['type']
949 info.driver_format = 'raw'
950 info.driver_cache = cache_mode
951 info.driver_discard = self.discard_mode
952 info.target_bus = disk_bus
953 info.target_dev = self.disk_info_mapping['dev']
954 info.source_type = 'network'
955 info.source_protocol = 'rbd'
956 info.source_name = '%s/%s' % (self.driver.pool, self.rbd_name)
957 info.source_hosts = hosts
958 info.source_ports = ports
959 info.boot_order = boot_order
960 auth_enabled = (self.driver.rbd_user is not None)
961 if CONF.libvirt.rbd_secret_uuid: 961 ↛ 962line 961 didn't jump to line 962 because the condition on line 961 was never true
962 info.auth_secret_uuid = CONF.libvirt.rbd_secret_uuid
963 auth_enabled = True # Force authentication locally
964 if self.driver.rbd_user:
965 info.auth_username = self.driver.rbd_user
966 if auth_enabled:
967 info.auth_secret_type = 'ceph'
968 info.auth_secret_uuid = CONF.libvirt.rbd_secret_uuid
970 if disk_bus == 'scsi':
971 self.disk_scsi(info, disk_unit)
973 self.disk_qos(info, extra_specs)
975 return info
977 def _can_fallocate(self):
978 return False
980 def exists(self):
981 return self.driver.exists(self.rbd_name)
983 def get_disk_size(self, name):
984 """Returns the size of the virtual disk in bytes.
986 The name argument is ignored since this backend already knows
987 its name, and callers may pass a non-existent local file path.
988 """
989 return self.driver.size(self.rbd_name)
991 @staticmethod
992 def _remove_non_raw_cache_image(base):
993 # NOTE(boxiang): If the cache image file exists, we will check
994 # the format of it. Only raw format image is compatible for
995 # RBD image backend. If format is not raw, we will remove it
996 # at first. We limit force_raw_images to True this time. So
997 # the format of new cache image must be raw.
998 # We can remove this in 'U' version later.
999 if not os.path.exists(base):
1000 return True
1001 image_format = images.qemu_img_info(base)
1002 if image_format.file_format != 'raw':
1003 try:
1004 os.remove(base)
1005 except OSError as e:
1006 LOG.warning("Ignoring failure to remove %(path)s: "
1007 "%(error)s", {'path': base, 'error': e})
1009 def create_image(
1010 self, prepare_template, base, size, safe=False, *args, **kwargs):
1012 if not self.exists():
1013 self._remove_non_raw_cache_image(base)
1014 prepare_template(target=base, *args, **kwargs)
1016 # prepare_template() may have cloned the image into a new rbd
1017 # image already instead of downloading it locally
1018 if not self.exists():
1019 self.driver.import_image(base, self.rbd_name)
1020 self.verify_base_size(base, size)
1022 if size and size > self.get_disk_size(self.rbd_name):
1023 self.driver.resize(self.rbd_name, size)
1025 def resize_image(self, size):
1026 self.driver.resize(self.rbd_name, size)
1028 def snapshot_extract(self, target, out_format):
1029 images.convert_image(self.path, target, 'raw', out_format)
1031 @staticmethod
1032 def is_shared_block_storage():
1033 return True
1035 def copy_to_store(self, context, image_meta):
1036 store_name = CONF.libvirt.images_rbd_glance_store_name
1037 image_id = image_meta['id']
1038 try:
1039 IMAGE_API.copy_image_to_store(context, image_id, store_name)
1040 except exception.ImageBadRequest:
1041 # NOTE(danms): This means that we raced with another node to start
1042 # the copy. Fall through to polling the image for completion
1043 pass
1044 except exception.ImageImportImpossible as exc:
1045 # NOTE(danms): This means we can not do this operation at all,
1046 # so fold this into the kind of imagebackend failure that is
1047 # expected by our callers
1048 raise exception.ImageUnacceptable(image_id=image_id,
1049 reason=str(exc))
1051 def _wait_for_copy():
1052 image = IMAGE_API.get(context, image_id, include_locations=True)
1053 if store_name in image.get('os_glance_failed_import', []):
1054 # Our store is reported as failed
1055 raise loopingcall.LoopingCallDone('failed import')
1056 elif (store_name not in image.get('os_glance_importing_to_stores',
1057 []) and
1058 store_name in image['stores']):
1059 # No longer importing and our store is listed in the stores
1060 raise loopingcall.LoopingCallDone()
1061 else:
1062 LOG.debug('Glance reports copy of image %(image)s to '
1063 'rbd store %(store)s is still in progress',
1064 {'image': image_id,
1065 'store': store_name})
1066 return True
1068 LOG.info('Asking glance to copy image %(image)s to our '
1069 'rbd store %(store)s',
1070 {'image': image_id,
1071 'store': store_name})
1073 timer = loopingcall.FixedIntervalWithTimeoutLoopingCall(_wait_for_copy)
1075 # NOTE(danms): We *could* do something more complicated like try
1076 # to scale our polling interval based on image size. The problem with
1077 # that is that we do not get progress indication from Glance, so if
1078 # we scale our interval to something long, and happen to poll right
1079 # near the end of the copy, we will wait another long interval before
1080 # realizing that the copy is complete. A simple interval per compute
1081 # allows an operator to set this short on central/fast/inexpensive
1082 # computes, and longer on nodes that are remote/slow/expensive across
1083 # a slower link.
1084 interval = CONF.libvirt.images_rbd_glance_copy_poll_interval
1085 timeout = CONF.libvirt.images_rbd_glance_copy_timeout
1086 try:
1087 result = timer.start(interval=interval, timeout=timeout).wait()
1088 except loopingcall.LoopingCallTimeOut:
1089 raise exception.ImageUnacceptable(
1090 image_id=image_id,
1091 reason='Copy to store %(store)s timed out' % {
1092 'store': store_name})
1094 if result is not True:
1095 raise exception.ImageUnacceptable(
1096 image_id=image_id,
1097 reason=('Copy to store %(store)s unsuccessful '
1098 'because: %(reason)s') % {'store': store_name,
1099 'reason': result})
1101 LOG.info('Image %(image)s copied to rbd store %(store)s',
1102 {'image': image_id,
1103 'store': store_name})
1105 def clone(self, context, image_id_or_uri, copy_to_store=True):
1106 image_meta = IMAGE_API.get(context, image_id_or_uri,
1107 include_locations=True)
1108 locations = image_meta['locations']
1110 LOG.debug('Image locations are: %(locs)s', {'locs': locations})
1112 if image_meta.get('disk_format') not in ['raw', 'iso']: 1112 ↛ 1113line 1112 didn't jump to line 1113 because the condition on line 1112 was never true
1113 reason = _('Image is not raw format')
1114 raise exception.ImageUnacceptable(image_id=image_id_or_uri,
1115 reason=reason)
1117 for location in locations:
1118 if self.driver.is_cloneable(location, image_meta):
1119 LOG.debug('Selected location: %(loc)s', {'loc': location})
1120 return self.driver.clone(location, self.rbd_name)
1122 # Not clone-able in our ceph, so try to get glance to copy it for us
1123 # and then retry
1124 if CONF.libvirt.images_rbd_glance_store_name and copy_to_store:
1125 self.copy_to_store(context, image_meta)
1126 return self.clone(context, image_id_or_uri, copy_to_store=False)
1128 reason = _('No image locations are accessible')
1129 raise exception.ImageUnacceptable(image_id=image_id_or_uri,
1130 reason=reason)
1132 def flatten(self):
1133 # NOTE(vdrok): only flatten images if they are not already flattened,
1134 # meaning that parent info is present
1135 try:
1136 self.driver.parent_info(self.rbd_name, pool=self.driver.pool)
1137 except exception.ImageUnacceptable:
1138 LOG.debug(
1139 "Image %(img)s from pool %(pool)s has no parent info, "
1140 "consider it already flat", {
1141 'img': self.rbd_name, 'pool': self.driver.pool})
1142 else:
1143 self.driver.flatten(self.rbd_name, pool=self.driver.pool)
1145 def get_model(self, connection):
1146 secret = None
1147 if CONF.libvirt.rbd_secret_uuid: 1147 ↛ 1155line 1147 didn't jump to line 1155 because the condition on line 1147 was always true
1148 secretobj = connection.secretLookupByUUIDString(
1149 CONF.libvirt.rbd_secret_uuid)
1150 secret = base64.b64encode(secretobj.value())
1152 # Brackets are stripped from IPv6 addresses normally for libvirt XML,
1153 # but the servers list is for libguestfs, which needs the brackets
1154 # so the joined string is similar to '[::1]:6789'
1155 hosts, ports = self.driver.get_mon_addrs(strip_brackets=False)
1156 servers = [str(':'.join(k)) for k in zip(hosts, ports)]
1158 return imgmodel.RBDImage(self.rbd_name,
1159 self.driver.pool,
1160 self.driver.rbd_user,
1161 secret,
1162 servers)
1164 def import_file(self, instance, local_file, remote_name):
1165 name = '%s_%s' % (instance.uuid, remote_name)
1166 if self.exists():
1167 self.driver.remove_image(name)
1168 self.driver.import_image(local_file, name)
1170 def create_snap(self, name):
1171 return self.driver.create_snap(self.rbd_name, name)
1173 def remove_snap(self, name, ignore_errors=False):
1174 return self.driver.remove_snap(self.rbd_name, name, ignore_errors)
1176 def rollback_to_snap(self, name):
1177 return self.driver.rollback_to_snap(self.rbd_name, name)
1179 def _get_parent_pool(self, context, base_image_id, fsid):
1180 parent_pool = None
1181 try:
1182 # The easy way -- the image is an RBD clone, so use the parent
1183 # images' storage pool
1184 parent_pool, _im, _snap = self.driver.parent_info(self.rbd_name)
1185 except exception.ImageUnacceptable:
1186 # The hard way -- the image is itself a parent, so ask Glance
1187 # where it came from
1188 LOG.debug('No parent info for %s; asking the Image API where its '
1189 'store is', base_image_id)
1190 try:
1191 image_meta = IMAGE_API.get(context, base_image_id,
1192 include_locations=True)
1193 except Exception as e:
1194 LOG.debug('Unable to get image %(image_id)s; error: %(error)s',
1195 {'image_id': base_image_id, 'error': e})
1196 image_meta = {}
1198 # Find the first location that is in the same RBD cluster
1199 for location in image_meta.get('locations', []):
1200 try:
1201 parent_fsid, parent_pool, _im, _snap = \
1202 self.driver.parse_url(location['url'])
1203 if parent_fsid == fsid:
1204 break
1205 else:
1206 parent_pool = None
1207 except exception.ImageUnacceptable:
1208 continue
1210 if not parent_pool:
1211 raise exception.ImageUnacceptable(
1212 _('Cannot determine the parent storage pool for %s; '
1213 'cannot determine where to store images') %
1214 base_image_id)
1216 return parent_pool
1218 def direct_snapshot(self, context, snapshot_name, image_format,
1219 image_id, base_image_id):
1220 """Creates an RBD snapshot directly.
1221 """
1222 fsid = self.driver.get_fsid()
1223 # NOTE(nic): Nova has zero comprehension of how Glance's image store
1224 # is configured, but we can infer what storage pool Glance is using
1225 # by looking at the parent image. If using authx, write access should
1226 # be enabled on that pool for the Nova user
1227 parent_pool = self._get_parent_pool(context, base_image_id, fsid)
1229 # Snapshot the disk and clone it into Glance's storage pool. librbd
1230 # requires that snapshots be set to "protected" in order to clone them
1231 self.driver.create_snap(self.rbd_name, snapshot_name, protect=True)
1232 location = {'url': 'rbd://%(fsid)s/%(pool)s/%(image)s/%(snap)s' %
1233 dict(fsid=fsid,
1234 pool=self.driver.pool,
1235 image=self.rbd_name,
1236 snap=snapshot_name)}
1237 try:
1238 self.driver.clone(location, image_id, dest_pool=parent_pool)
1239 # Flatten the image, which detaches it from the source snapshot
1240 self.driver.flatten(image_id, pool=parent_pool)
1241 finally:
1242 # all done with the source snapshot, clean it up
1243 self.cleanup_direct_snapshot(location)
1245 # Glance makes a protected snapshot called 'snap' on uploaded
1246 # images and hands it out, so we'll do that too. The name of
1247 # the snapshot doesn't really matter, this just uses what the
1248 # glance-store rbd backend sets (which is not configurable).
1249 self.driver.create_snap(image_id, 'snap', pool=parent_pool,
1250 protect=True)
1251 return ('rbd://%(fsid)s/%(pool)s/%(image)s/snap' %
1252 dict(fsid=fsid, pool=parent_pool, image=image_id))
1254 def cleanup_direct_snapshot(self, location, also_destroy_volume=False,
1255 ignore_errors=False):
1256 """Unprotects and destroys the name snapshot.
1258 With also_destroy_volume=True, it will also cleanup/destroy the parent
1259 volume. This is useful for cleaning up when the target volume fails
1260 to snapshot properly.
1261 """
1262 if location:
1263 _fsid, _pool, _im, _snap = self.driver.parse_url(location['url'])
1264 self.driver.remove_snap(_im, _snap, pool=_pool, force=True,
1265 ignore_errors=ignore_errors)
1266 if also_destroy_volume:
1267 self.driver.destroy_volume(_im, pool=_pool)
1270class Ploop(Image):
1271 def __init__(
1272 self, instance=None, disk_name=None, path=None, disk_info_mapping=None
1273 ):
1274 path = (path or os.path.join(libvirt_utils.get_instance_path(instance),
1275 disk_name))
1276 super().__init__(
1277 path, "file", "ploop", is_block_dev=False,
1278 disk_info_mapping=disk_info_mapping
1279 )
1281 self.resolve_driver_format()
1283 # Create new ploop disk (in case of epehemeral) or
1284 # copy ploop disk from glance image
1285 def create_image(
1286 self, prepare_template, base, size, safe=False, *args, **kwargs):
1287 filename = os.path.basename(base)
1289 # Copy main file of ploop disk, restore DiskDescriptor.xml for it
1290 # and resize if necessary
1291 @utils.synchronized(filename, external=True, lock_path=self.lock_path)
1292 def _copy_ploop_image(base, target, size):
1293 # Ploop disk is a directory with data file(root.hds) and
1294 # DiskDescriptor.xml, so create this dir
1295 fileutils.ensure_tree(target)
1296 image_path = os.path.join(target, "root.hds")
1297 libvirt_utils.copy_image(base, image_path)
1298 nova.privsep.libvirt.ploop_restore_descriptor(target,
1299 image_path,
1300 self.pcs_format)
1301 if size: 1301 ↛ exitline 1301 didn't return from function '_copy_ploop_image' because the condition on line 1301 was always true
1302 self.resize_image(size)
1304 # Generating means that we create empty ploop disk
1305 generating = 'image_id' not in kwargs
1306 remove_func = functools.partial(fileutils.delete_if_exists,
1307 remove=shutil.rmtree)
1308 if generating:
1309 if os.path.exists(self.path): 1309 ↛ 1310line 1309 didn't jump to line 1310 because the condition on line 1309 was never true
1310 return
1311 with fileutils.remove_path_on_error(self.path, remove=remove_func):
1312 prepare_template(target=self.path, *args, **kwargs)
1313 else:
1314 # Create ploop disk from glance image
1315 if not os.path.exists(base): 1315 ↛ 1319line 1315 didn't jump to line 1319 because the condition on line 1315 was always true
1316 prepare_template(target=base, *args, **kwargs)
1317 else:
1318 # Disk already exists in cache, just update time
1319 _update_utime_ignore_eacces(base)
1320 self.verify_base_size(base, size)
1322 if os.path.exists(self.path): 1322 ↛ 1323line 1322 didn't jump to line 1323 because the condition on line 1322 was never true
1323 return
1325 # Get format for ploop disk
1326 if CONF.force_raw_images: 1326 ↛ 1329line 1326 didn't jump to line 1329 because the condition on line 1326 was always true
1327 self.pcs_format = "raw"
1328 else:
1329 image_meta = IMAGE_API.get(kwargs["context"],
1330 kwargs["image_id"])
1331 format = image_meta.get("disk_format")
1332 if format == "ploop":
1333 self.pcs_format = "expanded"
1334 elif format == "raw":
1335 self.pcs_format = "raw"
1336 else:
1337 reason = _("Ploop image backend doesn't support images in"
1338 " %s format. You should either set"
1339 " force_raw_images=True in config or upload an"
1340 " image in ploop or raw format.") % format
1341 raise exception.ImageUnacceptable(
1342 image_id=kwargs["image_id"],
1343 reason=reason)
1345 with fileutils.remove_path_on_error(self.path, remove=remove_func):
1346 _copy_ploop_image(base, self.path, size)
1348 def resize_image(self, size):
1349 image = imgmodel.LocalFileImage(self.path, imgmodel.FORMAT_PLOOP)
1350 disk.extend(image, size)
1352 def snapshot_extract(self, target, out_format):
1353 img_path = os.path.join(self.path, "root.hds")
1354 libvirt_utils.extract_snapshot(img_path,
1355 'parallels',
1356 target,
1357 out_format)
1359 def get_model(self, connection):
1360 return imgmodel.LocalFileImage(self.path, imgmodel.FORMAT_PLOOP)
1363class Backend(object):
1364 def __init__(self, use_cow):
1365 self.BACKEND = {
1366 'raw': Flat,
1367 'flat': Flat,
1368 'qcow2': Qcow2,
1369 'lvm': Lvm,
1370 'rbd': Rbd,
1371 'ploop': Ploop,
1372 'default': Qcow2 if use_cow else Flat
1373 }
1375 def backend(self, image_type=None):
1376 if not image_type:
1377 image_type = CONF.libvirt.images_type
1378 image = self.BACKEND.get(image_type)
1379 if not image: 1379 ↛ 1380line 1379 didn't jump to line 1380 because the condition on line 1379 was never true
1380 raise RuntimeError(_('Unknown image_type=%s') % image_type)
1381 return image
1383 def by_name(self, instance, name, image_type=None, disk_info_mapping=None):
1384 """Return an Image object for a disk with the given name.
1386 :param instance: the instance which owns this disk
1387 :param name: The name of the disk
1388 :param image_type: (Optional) Image type.
1389 Default is CONF.libvirt.images_type.
1390 :param disk_info_mapping: (Optional) Disk info mapping dict
1391 :return: An Image object for the disk with given name and instance.
1392 :rtype: Image
1393 """
1394 # NOTE(artom) To pass functional tests, wherein the code here is loaded
1395 # *before* any config with self.flags() is done, we need to have the
1396 # default inline in the method, and not in the kwarg declaration.
1397 image_type = image_type or CONF.libvirt.images_type
1398 backend = self.backend(image_type)
1399 return backend(
1400 instance=instance, disk_name=name,
1401 disk_info_mapping=disk_info_mapping)
1403 def by_libvirt_path(self, instance, path, image_type=None):
1404 """Return an Image object for a disk with the given libvirt path.
1406 :param instance: The instance which owns this disk.
1407 :param path: The libvirt representation of the image's path.
1408 :param image_type: (Optional) Image type.
1409 Default is CONF.libvirt.images_type.
1410 :return: An Image object for the given libvirt path.
1411 :rtype: Image
1412 """
1413 backend = self.backend(image_type)
1414 return backend(instance=instance, path=path)