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

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. 

15 

16import abc 

17import base64 

18import contextlib 

19import errno 

20import functools 

21import os 

22import shutil 

23 

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 

34 

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 

50 

51CONF = nova.conf.CONF 

52 

53LOG = logging.getLogger(__name__) 

54IMAGE_API = glance.API() 

55 

56 

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 

80 

81 

82class Image(metaclass=abc.ABCMeta): 

83 

84 SUPPORTS_CLONE = False 

85 SUPPORTS_LUKS = False 

86 

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. 

96 

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) 

109 

110 self.path = path 

111 

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 

118 

119 self.disk_info_mapping = disk_info_mapping 

120 

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 

125 

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') 

130 

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 

136 

137 @abc.abstractmethod 

138 def create_image( 

139 self, prepare_template, base, size, safe=False, *args, **kwargs): 

140 """Create image from template. 

141 

142 Contains specific behavior for each image type. 

143 

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 

149 

150 """ 

151 pass 

152 

153 @abc.abstractmethod 

154 def resize_image(self, size): 

155 """Resize image to size (in bytes). 

156 

157 :size: Desired size of image in bytes 

158 

159 """ 

160 pass 

161 

162 def libvirt_info( 

163 self, cache_mode, extra_specs, boot_order=None, disk_unit=None, 

164 ): 

165 """Get `LibvirtConfigGuestDisk` filled for this image. 

166 

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 

190 

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 

203 

204 if disk_bus == 'scsi': 

205 self.disk_scsi(info, disk_unit) 

206 

207 self.disk_qos(info, extra_specs) 

208 

209 return info 

210 

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 

227 

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) 

237 

238 def libvirt_fs_info(self, target, driver_type=None): 

239 """Get `LibvirtConfigGuestFilesys` filled for this image. 

240 

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 

247 

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" 

262 

263 return info 

264 

265 def exists(self): 

266 return os.path.exists(self.path) 

267 

268 def cache(self, fetch_func, filename, size=None, safe=False, *args, 

269 **kwargs): 

270 """Creates image from template. 

271 

272 Ensures that template and image not already exists. 

273 Ensures that base directory exists. 

274 Synchronizes on template fetching. 

275 

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) 

286 

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) 

302 

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) 

307 

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) 

313 

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) 

317 

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 

333 

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 """ 

339 

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. 

348 

349 if size is None: 

350 return 

351 

352 if size and not base_size: 

353 base_size = self.get_disk_size(base) 

354 

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) 

363 

364 def get_disk_size(self, name): 

365 return disk.get_disk_size(name) 

366 

367 @abc.abstractmethod 

368 def snapshot_extract(self, target, out_format): 

369 """Extract a snapshot of the image. 

370 

371 This is used during cold (offline) snapshots. Live snapshots 

372 while the guest is still running are handled separately. 

373 

374 :param target: The target path for the image snapshot. 

375 :param out_format: The image snapshot format. 

376 """ 

377 raise NotImplementedError() 

378 

379 def _get_driver_format(self): 

380 return self.driver_format 

381 

382 def resolve_driver_format(self): 

383 """Return the driver format for self.path. 

384 

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 

388 

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) 

401 

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) 

410 

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}) 

415 

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) 

421 

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 

438 

439 @staticmethod 

440 def is_shared_block_storage(): 

441 """True if the backend puts images on a shared block storage.""" 

442 return False 

443 

444 @staticmethod 

445 def is_file_in_instance_path(): 

446 """True if the backend stores images in files under instance path.""" 

447 return False 

448 

449 def clone(self, context, image_id_or_uri): 

450 """Clone an image. 

451 

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. 

455 

456 :param image_id_or_uri: The ID or URI of an image to clone. 

457 

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) 

463 

464 def flatten(self): 

465 """Flatten an image. 

466 

467 The implementation of this method is optional and therefore is 

468 not an abstractmethod. 

469 """ 

470 raise NotImplementedError('flatten() is not implemented') 

471 

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. 

475 

476 The implementation of this method is optional and therefore is 

477 not an abstractmethod. 

478 

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')) 

484 

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. 

489 

490 This should be a no-op on any backend where it is not implemented. 

491 """ 

492 pass 

493 

494 def _get_lock_name(self, base): 

495 """Get an image's name of a base file.""" 

496 return os.path.split(base)[-1] 

497 

498 @abc.abstractmethod 

499 def get_model(self, connection): 

500 """Get the image information model 

501 

502 :returns: an instance of nova.virt.image.model.Image 

503 """ 

504 raise NotImplementedError() 

505 

506 def import_file(self, instance, local_file, remote_name): 

507 """Import an image from local storage into this backend. 

508 

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"). 

512 

513 If the image already exists it will be overridden by the new file 

514 

515 :param local_file: path to the file to import 

516 :param remote_name: the name for the file in the store 

517 """ 

518 

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 

522 

523 def create_snap(self, name): 

524 """Create a snapshot on the image. A noop on backends that don't 

525 support snapshots. 

526 

527 :param name: name of the snapshot 

528 """ 

529 pass 

530 

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. 

534 

535 :param name: name of the snapshot 

536 :param ignore_errors: don't log errors if the snapshot does not exist 

537 """ 

538 pass 

539 

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. 

543 

544 :param name: name of the snapshot 

545 """ 

546 pass 

547 

548 

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 """ 

555 

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 ) 

566 

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() 

573 

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' 

583 

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 

595 

596 def correct_format(self): 

597 if os.path.exists(self.path): 

598 self.driver_format = self.resolve_driver_format() 

599 

600 def create_image( 

601 self, prepare_template, base, size, safe=False, *args, **kwargs): 

602 filename = self._get_lock_name(base) 

603 

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) 

609 

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) 

615 

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) 

626 

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) 

634 

635 self.correct_format() 

636 

637 def resize_image(self, size): 

638 image = imgmodel.LocalFileImage(self.path, self.driver_format) 

639 disk.extend(image, size) 

640 

641 def snapshot_extract(self, target, out_format): 

642 images.convert_image(self.path, target, self.driver_format, out_format) 

643 

644 @staticmethod 

645 def is_file_in_instance_path(): 

646 return True 

647 

648 def get_model(self, connection): 

649 return imgmodel.LocalFileImage(self.path, 

650 imgmodel.FORMAT_RAW) 

651 

652 

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 ) 

663 

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() 

670 

671 def create_image( 

672 self, prepare_template, base, size, safe=False, *args, **kwargs): 

673 filename = self._get_lock_name(base) 

674 

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) 

679 

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) 

683 

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')) 

701 

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) 

706 

707 legacy_backing_size = None 

708 legacy_base = base 

709 

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 

723 

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) 

732 

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) 

736 

737 def resize_image(self, size): 

738 image = imgmodel.LocalFileImage(self.path, imgmodel.FORMAT_QCOW2) 

739 disk.extend(image, size) 

740 

741 def snapshot_extract(self, target, out_format): 

742 libvirt_utils.extract_snapshot(self.path, 'qcow2', 

743 target, 

744 out_format) 

745 

746 @staticmethod 

747 def is_file_in_instance_path(): 

748 return True 

749 

750 def get_model(self, connection): 

751 return imgmodel.LocalFileImage(self.path, 

752 imgmodel.FORMAT_QCOW2) 

753 

754 

755class Lvm(Image): 

756 @staticmethod 

757 def escape(filename): 

758 return filename.replace('_', '__') 

759 

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') 

765 

766 if self.ephemeral_key_uuid is not None: 

767 self.key_manager = key_manager.API(CONF) 

768 else: 

769 self.key_manager = None 

770 

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) 

791 

792 super(Lvm, self).__init__( 

793 path, "block", "raw", is_block_dev=True, 

794 disk_info_mapping=disk_info_mapping 

795 ) 

796 

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 

801 

802 if not self.sparse: 

803 self.driver_io = "native" 

804 

805 def _supports_encryption(self): 

806 return True 

807 

808 def _can_fallocate(self): 

809 return False 

810 

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) 

819 

820 filename = self._get_lock_name(base) 

821 

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) 

844 

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) 

874 

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 

879 

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]) 

891 

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) 

895 

896 def get_model(self, connection): 

897 return imgmodel.LocalBlockImage(self.path) 

898 

899 

900class Rbd(Image): 

901 

902 SUPPORTS_CLONE = True 

903 

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.')) 

911 

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) 

919 

920 self.driver = rbd_utils.RBDDriver() 

921 

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 

927 

928 super().__init__( 

929 path, "block", "rbd", is_block_dev=False, 

930 disk_info_mapping=disk_info_mapping 

931 ) 

932 

933 self.discard_mode = CONF.libvirt.hw_disk_discard 

934 

935 def libvirt_info( 

936 self, cache_mode, extra_specs, boot_order=None, disk_unit=None 

937 ): 

938 """Get `LibvirtConfigGuestDisk` filled for this image. 

939 

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'] 

946 

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 

969 

970 if disk_bus == 'scsi': 

971 self.disk_scsi(info, disk_unit) 

972 

973 self.disk_qos(info, extra_specs) 

974 

975 return info 

976 

977 def _can_fallocate(self): 

978 return False 

979 

980 def exists(self): 

981 return self.driver.exists(self.rbd_name) 

982 

983 def get_disk_size(self, name): 

984 """Returns the size of the virtual disk in bytes. 

985 

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) 

990 

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}) 

1008 

1009 def create_image( 

1010 self, prepare_template, base, size, safe=False, *args, **kwargs): 

1011 

1012 if not self.exists(): 

1013 self._remove_non_raw_cache_image(base) 

1014 prepare_template(target=base, *args, **kwargs) 

1015 

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) 

1021 

1022 if size and size > self.get_disk_size(self.rbd_name): 

1023 self.driver.resize(self.rbd_name, size) 

1024 

1025 def resize_image(self, size): 

1026 self.driver.resize(self.rbd_name, size) 

1027 

1028 def snapshot_extract(self, target, out_format): 

1029 images.convert_image(self.path, target, 'raw', out_format) 

1030 

1031 @staticmethod 

1032 def is_shared_block_storage(): 

1033 return True 

1034 

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)) 

1050 

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 

1067 

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}) 

1072 

1073 timer = loopingcall.FixedIntervalWithTimeoutLoopingCall(_wait_for_copy) 

1074 

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}) 

1093 

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}) 

1100 

1101 LOG.info('Image %(image)s copied to rbd store %(store)s', 

1102 {'image': image_id, 

1103 'store': store_name}) 

1104 

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'] 

1109 

1110 LOG.debug('Image locations are: %(locs)s', {'locs': locations}) 

1111 

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) 

1116 

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) 

1121 

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) 

1127 

1128 reason = _('No image locations are accessible') 

1129 raise exception.ImageUnacceptable(image_id=image_id_or_uri, 

1130 reason=reason) 

1131 

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) 

1144 

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()) 

1151 

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)] 

1157 

1158 return imgmodel.RBDImage(self.rbd_name, 

1159 self.driver.pool, 

1160 self.driver.rbd_user, 

1161 secret, 

1162 servers) 

1163 

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) 

1169 

1170 def create_snap(self, name): 

1171 return self.driver.create_snap(self.rbd_name, name) 

1172 

1173 def remove_snap(self, name, ignore_errors=False): 

1174 return self.driver.remove_snap(self.rbd_name, name, ignore_errors) 

1175 

1176 def rollback_to_snap(self, name): 

1177 return self.driver.rollback_to_snap(self.rbd_name, name) 

1178 

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 = {} 

1197 

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 

1209 

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) 

1215 

1216 return parent_pool 

1217 

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) 

1228 

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) 

1244 

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)) 

1253 

1254 def cleanup_direct_snapshot(self, location, also_destroy_volume=False, 

1255 ignore_errors=False): 

1256 """Unprotects and destroys the name snapshot. 

1257 

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) 

1268 

1269 

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 ) 

1280 

1281 self.resolve_driver_format() 

1282 

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) 

1288 

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) 

1303 

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) 

1321 

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 

1324 

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) 

1344 

1345 with fileutils.remove_path_on_error(self.path, remove=remove_func): 

1346 _copy_ploop_image(base, self.path, size) 

1347 

1348 def resize_image(self, size): 

1349 image = imgmodel.LocalFileImage(self.path, imgmodel.FORMAT_PLOOP) 

1350 disk.extend(image, size) 

1351 

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) 

1358 

1359 def get_model(self, connection): 

1360 return imgmodel.LocalFileImage(self.path, imgmodel.FORMAT_PLOOP) 

1361 

1362 

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 } 

1374 

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 

1382 

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. 

1385 

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) 

1402 

1403 def by_libvirt_path(self, instance, path, image_type=None): 

1404 """Return an Image object for a disk with the given libvirt path. 

1405 

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)