Coverage for nova/image/glance.py: 93%

582 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-24 11:16 +0000

1# Copyright 2010 OpenStack Foundation 

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 

16"""Implementation of an image service that uses Glance as the backend.""" 

17 

18import copy 

19import inspect 

20import itertools 

21import os 

22import random 

23import re 

24import stat 

25import sys 

26import time 

27import urllib.parse as urlparse 

28 

29import cryptography 

30from cursive import certificate_utils 

31from cursive import exception as cursive_exception 

32from cursive import signature_utils 

33import glanceclient 

34from glanceclient.common import utils as glance_utils 

35import glanceclient.exc 

36from glanceclient.v2 import schemas 

37from keystoneauth1 import loading as ks_loading 

38from oslo_log import log as logging 

39from oslo_serialization import jsonutils 

40from oslo_utils import excutils 

41from oslo_utils import timeutils 

42 

43import nova.conf 

44from nova import exception 

45from nova import objects 

46from nova.objects import fields 

47from nova import profiler 

48from nova import service_auth 

49from nova import utils 

50 

51 

52LOG = logging.getLogger(__name__) 

53CONF = nova.conf.CONF 

54 

55_SESSION = None 

56 

57 

58def _session_and_auth(context): 

59 # Session is cached, but auth needs to be pulled from context each time. 

60 global _SESSION 

61 

62 if not _SESSION: 

63 _SESSION = ks_loading.load_session_from_conf_options( 

64 CONF, nova.conf.glance.glance_group.name) 

65 

66 auth = service_auth.get_auth_plugin(context) 

67 

68 return _SESSION, auth 

69 

70 

71def _glanceclient_from_endpoint(context, endpoint, version): 

72 sess, auth = _session_and_auth(context) 

73 

74 return glanceclient.Client(version, session=sess, auth=auth, 

75 endpoint_override=endpoint, 

76 global_request_id=context.global_id) 

77 

78 

79def generate_glance_url(context): 

80 """Return a random glance url from the api servers we know about.""" 

81 return next(get_api_servers(context)) 

82 

83 

84def _endpoint_from_image_ref(image_href): 

85 """Return the image_ref and guessed endpoint from an image url. 

86 

87 :param image_href: href of an image 

88 :returns: a tuple of the form (image_id, endpoint_url) 

89 """ 

90 parts = image_href.split('/') 

91 image_id = parts[-1] 

92 # the endpoint is everything in the url except the last 3 bits 

93 # which are version, 'images', and image_id 

94 endpoint = '/'.join(parts[:-3]) 

95 return (image_id, endpoint) 

96 

97 

98def generate_identity_headers(context, status='Confirmed'): 

99 return { 

100 'X-Auth-Token': getattr(context, 'auth_token', None), 

101 'X-User-Id': getattr(context, 'user_id', None), 

102 'X-Tenant-Id': getattr(context, 'project_id', None), 

103 'X-Roles': ','.join(getattr(context, 'roles', [])), 

104 'X-Identity-Status': status, 

105 } 

106 

107 

108def get_api_servers(context): 

109 """Shuffle a list of service endpoints and return an iterator that will 

110 cycle through the list, looping around to the beginning if necessary. 

111 """ 

112 # NOTE(efried): utils.get_ksa_adapter().get_endpoint() is the preferred 

113 # mechanism for endpoint discovery. Only use `api_servers` if you really 

114 # need to shuffle multiple endpoints. 

115 if CONF.glance.api_servers: 

116 api_servers = CONF.glance.api_servers 

117 random.shuffle(api_servers) 

118 else: 

119 sess, auth = _session_and_auth(context) 

120 ksa_adap = utils.get_ksa_adapter( 

121 nova.conf.glance.DEFAULT_SERVICE_TYPE, 

122 ksa_auth=auth, ksa_session=sess, 

123 min_version='2.0', max_version='2.latest') 

124 endpoint = utils.get_endpoint(ksa_adap) 

125 if endpoint: 125 ↛ 131line 125 didn't jump to line 131 because the condition on line 125 was always true

126 # NOTE(mriedem): Due to python-glanceclient bug 1707995 we have 

127 # to massage the endpoint URL otherwise it won't work properly. 

128 # We can't use glanceclient.common.utils.strip_version because 

129 # of bug 1748009. 

130 endpoint = re.sub(r'/v\d+(\.\d+)?/?$', '/', endpoint) 

131 api_servers = [endpoint] 

132 

133 return itertools.cycle(api_servers) 

134 

135 

136class GlanceClientWrapper(object): 

137 """Glance client wrapper class that implements retries.""" 

138 

139 def __init__(self, context=None, endpoint=None): 

140 version = 2 

141 if endpoint is not None: 

142 self.client = self._create_static_client(context, 

143 endpoint, 

144 version) 

145 else: 

146 self.client = None 

147 self.api_servers = None 

148 

149 def _create_static_client(self, context, endpoint, version): 

150 """Create a client that we'll use for every call.""" 

151 self.api_server = str(endpoint) 

152 return _glanceclient_from_endpoint(context, endpoint, version) 

153 

154 def _create_onetime_client(self, context, version): 

155 """Create a client that will be used for one call.""" 

156 if self.api_servers is None: 

157 self.api_servers = get_api_servers(context) 

158 self.api_server = next(self.api_servers) 

159 return _glanceclient_from_endpoint(context, self.api_server, version) 

160 

161 def call(self, context, version, method, controller=None, args=None, 

162 kwargs=None): 

163 """Call a glance client method. If we get a connection error, 

164 retry the request according to CONF.glance.num_retries. 

165 

166 :param context: RequestContext to use 

167 :param version: Numeric version of the *Glance API* to use 

168 :param method: string method name to execute on the glanceclient 

169 :param controller: optional string name of the client controller to 

170 use. Default (None) is to use the 'images' 

171 controller 

172 :param args: optional iterable of arguments to pass to the 

173 glanceclient method, splatted as positional args 

174 :param kwargs: optional dict of arguments to pass to the glanceclient, 

175 splatted into named arguments 

176 """ 

177 args = args or [] 

178 kwargs = kwargs or {} 

179 retry_excs = (glanceclient.exc.ServiceUnavailable, 

180 glanceclient.exc.InvalidEndpoint, 

181 glanceclient.exc.CommunicationError, 

182 IOError) 

183 num_attempts = 1 + CONF.glance.num_retries 

184 controller_name = controller or 'images' 

185 

186 for attempt in range(1, num_attempts + 1): 186 ↛ exitline 186 didn't return from function 'call' because the loop on line 186 didn't complete

187 client = self.client or self._create_onetime_client(context, 

188 version) 

189 try: 

190 controller = getattr(client, controller_name) 

191 result = getattr(controller, method)(*args, **kwargs) 

192 if inspect.isgenerator(result): 

193 # Convert generator results to a list, so that we can 

194 # catch any potential exceptions now and retry the call. 

195 return list(result) 

196 return result 

197 except retry_excs as e: 

198 if attempt < num_attempts: 

199 extra = "retrying" 

200 else: 

201 extra = 'done trying' 

202 

203 LOG.exception("Error contacting glance server " 

204 "'%(server)s' for '%(method)s', " 

205 "%(extra)s.", 

206 {'server': self.api_server, 

207 'method': method, 'extra': extra}) 

208 if attempt == num_attempts: 

209 raise exception.GlanceConnectionFailed( 

210 server=str(self.api_server), reason=str(e)) 

211 time.sleep(1) 

212 

213 

214class GlanceImageServiceV2(object): 

215 """Provides storage and retrieval of disk image objects within Glance.""" 

216 

217 def __init__(self, client=None): 

218 self._client = client or GlanceClientWrapper() 

219 # NOTE(danms): This used to be built from a list of external modules 

220 # that were loaded at runtime. Preserve this list for implementations 

221 # to be added here. 

222 self._download_handlers = {} 

223 

224 if CONF.glance.enable_rbd_download: 

225 self._download_handlers['rbd'] = self.rbd_download 

226 

227 def rbd_download(self, context, url_parts, dst_path, metadata=None): 

228 """Use an explicit rbd call to download an image. 

229 

230 :param context: The `nova.context.RequestContext` object for the 

231 request 

232 :param url_parts: Parts of URL pointing to the image location 

233 :param dst_path: Filepath to transfer the image file to. 

234 :param metadata: Image location metadata (currently unused) 

235 """ 

236 

237 # avoid circular import 

238 from nova.storage import rbd_utils 

239 try: 

240 # Parse the RBD URL from url_parts, it should consist of 4 

241 # sections and be in the format of: 

242 # <cluster_uuid>/<pool_name>/<image_uuid>/<snapshot_name> 

243 url_path = str(urlparse.unquote(url_parts.path)) 

244 cluster_uuid, pool_name, image_uuid, snapshot_name = ( 

245 url_path.split('/')) 

246 except ValueError as e: 

247 msg = f"Invalid RBD URL format: {e}" 

248 LOG.error(msg) 

249 raise nova.exception.InvalidParameterValue(msg) 

250 

251 rbd_driver = rbd_utils.RBDDriver( 

252 user=CONF.glance.rbd_user, 

253 pool=CONF.glance.rbd_pool, 

254 ceph_conf=CONF.glance.rbd_ceph_conf, 

255 connect_timeout=CONF.glance.rbd_connect_timeout) 

256 

257 try: 

258 LOG.debug("Attempting to export RBD image: " 

259 "[pool_name: %s] [image_uuid: %s] " 

260 "[snapshot_name: %s] [dst_path: %s]", 

261 pool_name, image_uuid, snapshot_name, dst_path) 

262 

263 rbd_driver.export_image(dst_path, image_uuid, 

264 snapshot_name, pool_name) 

265 except Exception as e: 

266 LOG.error("Error during RBD image export: %s", e) 

267 raise nova.exception.CouldNotFetchImage(image_id=image_uuid) 

268 

269 def show(self, context, image_id, include_locations=False, 

270 show_deleted=True): 

271 """Returns a dict with image data for the given opaque image id. 

272 

273 :param context: The context object to pass to image client 

274 :param image_id: The UUID of the image 

275 :param include_locations: (Optional) include locations in the returned 

276 dict of information if the image service API 

277 supports it. If the image service API does 

278 not support the locations attribute, it will 

279 still be included in the returned dict, as an 

280 empty list. 

281 :param show_deleted: (Optional) show the image even the status of 

282 image is deleted. 

283 """ 

284 try: 

285 image = self._client.call(context, 2, 'get', args=(image_id,)) 

286 except Exception: 

287 _reraise_translated_image_exception(image_id) 

288 

289 if not show_deleted and getattr(image, 'deleted', False): 

290 raise exception.ImageNotFound(image_id=image_id) 

291 

292 if not _is_image_available(context, image): 

293 raise exception.ImageNotFound(image_id=image_id) 

294 

295 image = _translate_from_glance(image, 

296 include_locations=include_locations) 

297 if include_locations: 

298 locations = image.get('locations', None) or [] 

299 du = image.get('direct_url', None) 

300 if du: 300 ↛ 301line 300 didn't jump to line 301 because the condition on line 300 was never true

301 locations.append({'url': du, 'metadata': {}}) 

302 image['locations'] = locations 

303 

304 return image 

305 

306 def _get_transfer_method(self, scheme): 

307 """Returns a transfer method for scheme, or None.""" 

308 try: 

309 return self._download_handlers[scheme] 

310 except KeyError: 

311 return None 

312 

313 def detail(self, context, **kwargs): 

314 """Calls out to Glance for a list of detailed image information.""" 

315 params = _extract_query_params_v2(kwargs) 

316 try: 

317 images = self._client.call(context, 2, 'list', kwargs=params) 

318 except Exception: 

319 _reraise_translated_exception() 

320 

321 _images = [] 

322 for image in images: 

323 if _is_image_available(context, image): 

324 _images.append(_translate_from_glance(image)) 

325 

326 return _images 

327 

328 @staticmethod 

329 def _safe_fsync(fh): 

330 """Performs os.fsync on a filehandle only if it is supported. 

331 

332 fsync on a pipe, FIFO, or socket raises OSError with EINVAL. This 

333 method discovers whether the target filehandle is one of these types 

334 and only performs fsync if it isn't. 

335 

336 :param fh: Open filehandle (not a path or fileno) to maybe fsync. 

337 """ 

338 fileno = fh.fileno() 

339 mode = os.fstat(fileno).st_mode 

340 # A pipe answers True to S_ISFIFO 

341 if not any(check(mode) for check in (stat.S_ISFIFO, stat.S_ISSOCK)): 

342 os.fsync(fileno) 

343 

344 def _try_special_handlers(self, context, image_id, dst_path, verifier): 

345 image = self.show(context, image_id, include_locations=True) 

346 for entry in image.get('locations', []): 

347 loc_url = entry['url'] 

348 loc_meta = entry['metadata'] 

349 o = urlparse.urlparse(loc_url) 

350 xfer_method = self._get_transfer_method(o.scheme) 

351 if not xfer_method: 

352 continue 

353 

354 try: 

355 xfer_method(context, o, dst_path, loc_meta) 

356 LOG.info("Successfully transferred using %s", o.scheme) 

357 

358 if not verifier: 

359 return True 

360 

361 # Load chunks from the downloaded image file 

362 # for verification 

363 with open(dst_path, 'rb') as fh: 

364 downloaded_length = os.path.getsize(dst_path) 

365 image_chunks = glance_utils.IterableWithLength(fh, 

366 downloaded_length) 

367 self._verify_and_write(context, image_id, verifier, 

368 image_chunks, None, None) 

369 return True 

370 except Exception: 

371 LOG.exception("Download image error") 

372 

373 return False 

374 

375 def download(self, context, image_id, data=None, dst_path=None, 

376 trusted_certs=None): 

377 """Calls out to Glance for data and writes data.""" 

378 # First, try to get the verifier, so we do not even start to download 

379 # the image and then fail on the metadata 

380 verifier = self._get_verifier(context, image_id, trusted_certs) 

381 

382 # Second, try to delegate image download to a special handler 

383 if (self._download_handlers and dst_path is not None): 

384 if self._try_special_handlers(context, image_id, dst_path, 

385 verifier): 

386 return 

387 

388 # By default (or if direct download has failed), use glance client call 

389 # to fetch the image and fill image_chunks 

390 try: 

391 image_chunks = self._client.call( 

392 context, 2, 'data', args=(image_id,)) 

393 except Exception: 

394 _reraise_translated_image_exception(image_id) 

395 

396 if image_chunks.wrapped is None: 

397 # None is a valid return value, but there's nothing we can do with 

398 # a image with no associated data 

399 raise exception.ImageUnacceptable(image_id=image_id, 

400 reason='Image has no associated data') 

401 

402 return self._verify_and_write(context, image_id, verifier, 

403 image_chunks, data, dst_path) 

404 

405 def _verify_and_write(self, context, image_id, verifier, 

406 image_chunks, data, dst_path): 

407 """Perform image signature verification and save the image file if 

408 needed. 

409 

410 This function writes the content of the image_chunks iterator either to 

411 a file object provided by the data parameter or to a filepath provided 

412 by dst_path parameter. If none of them are provided then no data will 

413 be written out but instead image_chunks iterator is returned. 

414 

415 :param image_id: The UUID of the image 

416 :param verifier: An instance of a 'cursive.verifier' 

417 :param image_chunks An iterator pointing to the image data 

418 :param data: File object to use when writing the image. 

419 If passed as None and dst_path is provided, new file is opened. 

420 :param dst_path: Filepath to transfer the image file to. 

421 :returns an iterable with image data, or nothing. Iterable is returned 

422 only when data param is None and dst_path is not provided (assuming 

423 the caller wants to process the data by itself). 

424 

425 """ 

426 

427 close_file = False 

428 if data is None and dst_path: 

429 data = open(dst_path, 'wb') 

430 close_file = True 

431 

432 write_image = True 

433 if data is None: 

434 write_image = False 

435 

436 try: 

437 # Exit early if we do not need write nor verify 

438 if verifier is None and not write_image: 

439 return image_chunks 

440 

441 for chunk in image_chunks: 

442 if verifier: 

443 verifier.update(chunk) 

444 if write_image: 

445 data.write(chunk) 

446 if verifier: 

447 verifier.verify() 

448 LOG.info('Image signature verification succeeded ' 

449 'for image %s', image_id) 

450 except cryptography.exceptions.InvalidSignature: 

451 if write_image: 

452 data.truncate(0) 

453 with excutils.save_and_reraise_exception(): 

454 LOG.error('Image signature verification failed ' 

455 'for image %s', image_id) 

456 except Exception as ex: 

457 if write_image: 457 ↛ 462line 457 didn't jump to line 462 because the condition on line 457 was always true

458 with excutils.save_and_reraise_exception(): 

459 LOG.error("Error writing to %(path)s: %(exception)s", 

460 {'path': dst_path, 'exception': ex}) 

461 else: 

462 with excutils.save_and_reraise_exception(): 

463 LOG.error("Error during image verification: %s", ex) 

464 

465 finally: 

466 if close_file: 

467 # Ensure that the data is pushed all the way down to 

468 # persistent storage. This ensures that in the event of a 

469 # subsequent host crash we don't have running instances 

470 # using a corrupt backing file. 

471 data.flush() 

472 self._safe_fsync(data) 

473 data.close() 

474 

475 if data is None: 

476 return image_chunks 

477 

478 def _get_verifier(self, context, image_id, trusted_certs): 

479 verifier = None 

480 

481 # Use the default certs if the user didn't provide any (and there are 

482 # default certs configured). 

483 if (not trusted_certs and CONF.glance.enable_certificate_validation and 

484 CONF.glance.default_trusted_certificate_ids): 

485 trusted_certs = objects.TrustedCerts( 

486 ids=CONF.glance.default_trusted_certificate_ids) 

487 

488 # Verify image signature if feature is enabled or trusted 

489 # certificates were provided 

490 if trusted_certs or CONF.glance.verify_glance_signatures: 

491 image_meta_dict = self.show(context, image_id, 

492 include_locations=False) 

493 image_meta = objects.ImageMeta.from_dict(image_meta_dict) 

494 img_signature = image_meta.properties.get('img_signature') 

495 img_sig_hash_method = image_meta.properties.get( 

496 'img_signature_hash_method' 

497 ) 

498 img_sig_cert_uuid = image_meta.properties.get( 

499 'img_signature_certificate_uuid' 

500 ) 

501 img_sig_key_type = image_meta.properties.get( 

502 'img_signature_key_type' 

503 ) 

504 try: 

505 verifier = signature_utils.get_verifier( 

506 context=context, 

507 img_signature_certificate_uuid=img_sig_cert_uuid, 

508 img_signature_hash_method=img_sig_hash_method, 

509 img_signature=img_signature, 

510 img_signature_key_type=img_sig_key_type, 

511 ) 

512 except cursive_exception.SignatureVerificationError: 

513 with excutils.save_and_reraise_exception(): 

514 LOG.error('Image signature verification failed ' 

515 'for image: %s', image_id) 

516 # Validate image signature certificate if trusted certificates 

517 # were provided 

518 # NOTE(jackie-truong): Certificate validation will occur if 

519 # trusted_certs are provided, even if the certificate validation 

520 # feature is disabled. This is to provide safety for the user. 

521 # We may want to consider making this a "soft" check in the future. 

522 if trusted_certs: 

523 _verify_certs(context, img_sig_cert_uuid, trusted_certs) 

524 elif CONF.glance.enable_certificate_validation: 

525 msg = ('Image signature certificate validation enabled, ' 

526 'but no trusted certificate IDs were provided. ' 

527 'Unable to validate the certificate used to ' 

528 'verify the image signature.') 

529 LOG.warning(msg) 

530 raise exception.CertificateValidationFailed(msg) 

531 else: 

532 LOG.debug('Certificate validation was not performed. A list ' 

533 'of trusted image certificate IDs must be provided ' 

534 'in order to validate an image certificate.') 

535 

536 return verifier 

537 

538 def create(self, context, image_meta, data=None): 

539 """Store the image data and return the new image object.""" 

540 # Here we workaround the situation when user wants to activate an 

541 # empty image right after the creation. In Glance v1 api (and 

542 # therefore in Nova) it is enough to set 'size = 0'. v2 api 

543 # doesn't allow this hack - we have to send an upload request with 

544 # empty data. 

545 force_activate = data is None and image_meta.get('size') == 0 

546 

547 # The "instance_owner" property is set in the API if a user, who is 

548 # not the owner of an instance, is creating the image, e.g. admin 

549 # snapshots or shelves another user's instance. This is used to add 

550 # member access to the image for the instance owner. 

551 sharing_member_id = image_meta.get('properties', {}).pop( 

552 'instance_owner', None) 

553 sent_service_image_meta = _translate_to_glance(image_meta) 

554 

555 try: 

556 image = self._create_v2(context, sent_service_image_meta, 

557 data, force_activate, 

558 sharing_member_id=sharing_member_id) 

559 except glanceclient.exc.HTTPException: 

560 _reraise_translated_exception() 

561 

562 return _translate_from_glance(image) 

563 

564 def _add_location(self, context, image_id, location): 

565 # 'show_multiple_locations' must be enabled in glance api conf file. 

566 try: 

567 return self._client.call( 

568 context, 2, 'add_location', args=(image_id, location, {})) 

569 except glanceclient.exc.HTTPBadRequest: 

570 _reraise_translated_exception() 

571 

572 def _add_image_member(self, context, image_id, member_id): 

573 """Grant access to another project that does not own the image 

574 

575 :param context: nova auth RequestContext where context.project_id is 

576 the owner of the image 

577 :param image_id: ID of the image on which to grant access 

578 :param member_id: ID of the member project to grant access to the 

579 image; this should not be the owner of the image 

580 :returns: A Member schema object of the created image member 

581 """ 

582 try: 

583 return self._client.call( 

584 context, 2, 'create', controller='image_members', 

585 args=(image_id, member_id)) 

586 except glanceclient.exc.HTTPBadRequest: 

587 _reraise_translated_exception() 

588 

589 def _upload_data(self, context, image_id, data): 

590 # NOTE(aarents) offload upload in a native thread as it can block 

591 # coroutine in busy environment. 

592 utils.tpool_execute(self._client.call, 

593 context, 2, 'upload', 

594 args=(image_id, data)) 

595 

596 return self._client.call(context, 2, 'get', args=(image_id,)) 

597 

598 def _get_image_create_disk_format_default(self, context): 

599 """Gets an acceptable default image disk_format based on the schema. 

600 """ 

601 # These preferred disk formats are in order: 

602 # 1. we want qcow2 if possible (at least for backward compat) 

603 # 2. vhd for hyperv 

604 # 3. vmdk for vmware 

605 # 4. raw should be universally accepted 

606 preferred_disk_formats = ( 

607 fields.DiskFormat.QCOW2, 

608 fields.DiskFormat.VHD, 

609 fields.DiskFormat.VMDK, 

610 fields.DiskFormat.RAW, 

611 ) 

612 

613 # Get the image schema - note we don't cache this value since it could 

614 # change under us. This looks a bit funky, but what it's basically 

615 # doing is calling glanceclient.v2.Client.schemas.get('image'). 

616 image_schema = self._client.call( 

617 context, 2, 'get', args=('image',), controller='schemas') 

618 # get the disk_format schema property from the raw schema 

619 disk_format_schema = ( 

620 image_schema.raw()['properties'].get('disk_format') if image_schema 

621 else {} 

622 ) 

623 if disk_format_schema and 'enum' in disk_format_schema: 

624 supported_disk_formats = disk_format_schema['enum'] 

625 # try a priority ordered list 

626 for preferred_format in preferred_disk_formats: 

627 if preferred_format in supported_disk_formats: 

628 return preferred_format 

629 # alright, let's just return whatever is available 

630 LOG.debug('Unable to find a preferred disk_format for image ' 

631 'creation with the Image Service v2 API. Using: %s', 

632 supported_disk_formats[0]) 

633 return supported_disk_formats[0] 

634 

635 LOG.warning('Unable to determine disk_format schema from the ' 

636 'Image Service v2 API. Defaulting to ' 

637 '%(preferred_disk_format)s.', 

638 {'preferred_disk_format': preferred_disk_formats[0]}) 

639 return preferred_disk_formats[0] 

640 

641 def _create_v2(self, context, sent_service_image_meta, data=None, 

642 force_activate=False, sharing_member_id=None): 

643 # Glance v1 allows image activation without setting disk and 

644 # container formats, v2 doesn't. It leads to the dirtiest workaround 

645 # where we have to hardcode this parameters. 

646 if force_activate: 

647 data = '' 

648 # NOTE(danms): If we are using this terrible hack to upload 

649 # zero-length data to activate the image, we cannot claim it 

650 # is some format other than 'raw'. If the caller asked for 

651 # something specific, that's a bug. Otherwise, we must force 

652 # disk_format=raw. 

653 if 'disk_format' not in sent_service_image_meta: 

654 sent_service_image_meta['disk_format'] = 'raw' 

655 elif sent_service_image_meta['disk_format'] != 'raw': 655 ↛ 659line 655 didn't jump to line 659 because the condition on line 655 was always true

656 raise exception.ImageBadRequest( 

657 'Unable to force activate with disk_format=%s' % ( 

658 sent_service_image_meta['disk_format'])) 

659 if 'container_format' not in sent_service_image_meta: 659 ↛ 662line 659 didn't jump to line 662 because the condition on line 659 was always true

660 sent_service_image_meta['container_format'] = 'bare' 

661 

662 location = sent_service_image_meta.pop('location', None) 

663 image = self._client.call( 

664 context, 2, 'create', kwargs=sent_service_image_meta) 

665 image_id = image['id'] 

666 

667 # Sending image location in a separate request. 

668 if location: 

669 image = self._add_location(context, image_id, location) 

670 

671 # Add image membership in a separate request. 

672 if sharing_member_id: 

673 LOG.debug('Adding access for member %s to image %s', 

674 sharing_member_id, image_id) 

675 self._add_image_member(context, image_id, sharing_member_id) 

676 

677 # If we have some data we have to send it in separate request and 

678 # update the image then. 

679 if data is not None: 

680 image = self._upload_data(context, image_id, data) 

681 

682 return image 

683 

684 def update(self, context, image_id, image_meta, data=None, 

685 purge_props=True): 

686 """Modify the given image with the new data.""" 

687 sent_service_image_meta = _translate_to_glance(image_meta) 

688 # NOTE(bcwaldon): id is not an editable field, but it is likely to be 

689 # passed in by calling code. Let's be nice and ignore it. 

690 sent_service_image_meta.pop('id', None) 

691 sent_service_image_meta['image_id'] = image_id 

692 

693 try: 

694 if purge_props: 

695 # In Glance v2 we have to explicitly set prop names 

696 # we want to remove. 

697 all_props = set(self.show( 

698 context, image_id)['properties'].keys()) 

699 props_to_update = set( 

700 image_meta.get('properties', {}).keys()) 

701 remove_props = list(all_props - props_to_update) 

702 sent_service_image_meta['remove_props'] = remove_props 

703 

704 image = self._update_v2(context, sent_service_image_meta, data) 

705 except Exception: 

706 _reraise_translated_image_exception(image_id) 

707 

708 return _translate_from_glance(image) 

709 

710 def _update_v2(self, context, sent_service_image_meta, data=None): 

711 location = sent_service_image_meta.pop('location', None) 

712 image_id = sent_service_image_meta['image_id'] 

713 image = self._client.call( 

714 context, 2, 'update', kwargs=sent_service_image_meta) 

715 

716 # Sending image location in a separate request. 

717 if location: 

718 image = self._add_location(context, image_id, location) 

719 

720 # If we have some data we have to send it in separate request and 

721 # update the image then. 

722 if data is not None: 

723 image = self._upload_data(context, image_id, data) 

724 

725 return image 

726 

727 def delete(self, context, image_id): 

728 """Delete the given image. 

729 

730 :raises: ImageNotFound if the image does not exist. 

731 :raises: NotAuthorized if the user is not an owner. 

732 :raises: ImageNotAuthorized if the user is not authorized. 

733 :raises: ImageDeleteConflict if the image is conflicted to delete. 

734 

735 """ 

736 try: 

737 self._client.call(context, 2, 'delete', args=(image_id,)) 

738 except glanceclient.exc.NotFound: 

739 raise exception.ImageNotFound(image_id=image_id) 

740 except glanceclient.exc.HTTPForbidden: 

741 raise exception.ImageNotAuthorized(image_id=image_id) 

742 except glanceclient.exc.HTTPConflict as exc: 

743 raise exception.ImageDeleteConflict(reason=str(exc)) 

744 return True 

745 

746 def image_import_copy(self, context, image_id, stores): 

747 """Copy an image to another store using image_import. 

748 

749 This triggers the Glance image_import API with an opinionated 

750 method of 'copy-image' to a list of stores. This will initiate 

751 a copy of the image from one of the existing stores to the 

752 stores provided. 

753 

754 :param context: The RequestContext 

755 :param image_id: The image to copy 

756 :param stores: A list of stores to copy the image to 

757 

758 :raises: ImageNotFound if the image does not exist. 

759 :raises: ImageNotAuthorized if the user is not permitted to 

760 import/copy this image 

761 :raises: ImageImportImpossible if the image cannot be imported 

762 for workflow reasons (not active, etc) 

763 :raises: ImageBadRequest if the image is already in the requested 

764 store (which may be a race) 

765 """ 

766 try: 

767 self._client.call(context, 2, 'image_import', args=(image_id,), 

768 kwargs={'method': 'copy-image', 

769 'stores': stores}) 

770 except glanceclient.exc.NotFound: 

771 raise exception.ImageNotFound(image_id=image_id) 

772 except glanceclient.exc.HTTPForbidden: 

773 raise exception.ImageNotAuthorized(image_id=image_id) 

774 except glanceclient.exc.HTTPConflict as exc: 

775 raise exception.ImageImportImpossible(image_id=image_id, 

776 reason=str(exc)) 

777 except glanceclient.exc.HTTPBadRequest as exc: 

778 raise exception.ImageBadRequest(image_id=image_id, 

779 response=str(exc)) 

780 

781 

782def _extract_query_params_v2(params): 

783 _params = {} 

784 accepted_params = ('filters', 'marker', 'limit', 

785 'page_size', 'sort_key', 'sort_dir') 

786 for param in accepted_params: 

787 if params.get(param): 

788 _params[param] = params.get(param) 

789 

790 # ensure filters is a dict 

791 _params.setdefault('filters', {}) 

792 # NOTE(vish): don't filter out private images 

793 _params['filters'].setdefault('is_public', 'none') 

794 

795 # adopt filters to be accepted by glance v2 api 

796 filters = _params['filters'] 

797 new_filters = {} 

798 

799 for filter_ in filters: 

800 # remove 'property-' prefix from filters by custom properties 

801 if filter_.startswith('property-'): 

802 new_filters[filter_.lstrip('property-')] = filters[filter_] 

803 elif filter_ == 'changes-since': 

804 # convert old 'changes-since' into new 'updated_at' filter 

805 updated_at = 'gte:' + filters['changes-since'] 

806 new_filters['updated_at'] = updated_at 

807 elif filter_ == 'is_public': 

808 # convert old 'is_public' flag into 'visibility' filter 

809 # omit the filter if is_public is None 

810 is_public = filters['is_public'] 

811 if is_public.lower() in ('true', '1'): 

812 new_filters['visibility'] = 'public' 

813 elif is_public.lower() in ('false', '0'): 813 ↛ 814line 813 didn't jump to line 814 because the condition on line 813 was never true

814 new_filters['visibility'] = 'private' 

815 else: 

816 new_filters[filter_] = filters[filter_] 

817 

818 _params['filters'] = new_filters 

819 

820 return _params 

821 

822 

823def _is_image_available(context, image): 

824 """Check image availability. 

825 

826 This check is needed in case Nova and Glance are deployed 

827 without authentication turned on. 

828 """ 

829 # The presence of an auth token implies this is an authenticated 

830 # request and we need not handle the noauth use-case. 

831 if hasattr(context, 'auth_token') and context.auth_token: 

832 return True 

833 

834 def _is_image_public(image): 

835 # NOTE(jaypipes) V2 Glance API replaced the is_public attribute 

836 # with a visibility attribute. We do this here to prevent the 

837 # glanceclient for a V2 image model from throwing an 

838 # exception from warlock when trying to access an is_public 

839 # attribute. 

840 if hasattr(image, 'visibility'): 840 ↛ 843line 840 didn't jump to line 843 because the condition on line 840 was always true

841 return str(image.visibility).lower() == 'public' 

842 else: 

843 return image.is_public 

844 

845 if context.is_admin or _is_image_public(image): 

846 return True 

847 

848 properties = image.properties 

849 

850 if context.project_id and ('owner_id' in properties): 

851 return str(properties['owner_id']) == str(context.project_id) 

852 

853 if context.project_id and ('project_id' in properties): 

854 return str(properties['project_id']) == str(context.project_id) 

855 

856 try: 

857 user_id = properties['user_id'] 

858 except KeyError: 

859 return False 

860 

861 return str(user_id) == str(context.user_id) 

862 

863 

864def _translate_to_glance(image_meta): 

865 image_meta = _convert_to_string(image_meta) 

866 image_meta = _remove_read_only(image_meta) 

867 image_meta = _convert_to_v2(image_meta) 

868 return image_meta 

869 

870 

871def _convert_to_v2(image_meta): 

872 output = {} 

873 for name, value in image_meta.items(): 

874 if name == 'properties': 

875 for prop_name, prop_value in value.items(): 

876 # if allow_additional_image_properties is disabled we can't 

877 # define kernel_id and ramdisk_id as None, so we have to omit 

878 # these properties if they are not set. 

879 if prop_name in ('kernel_id', 'ramdisk_id') and \ 

880 prop_value is not None and \ 

881 prop_value.strip().lower() in ('none', ''): 

882 continue 

883 # in glance only string and None property values are allowed, 

884 # v1 client accepts any values and converts them to string, 

885 # v2 doesn't - so we have to take care of it. 

886 elif prop_value is None or isinstance(prop_value, str): 

887 output[prop_name] = prop_value 

888 else: 

889 output[prop_name] = str(prop_value) 

890 

891 elif name in ('min_ram', 'min_disk'): 

892 output[name] = int(value) 

893 elif name == 'is_public': 

894 output['visibility'] = 'public' if value else 'private' 

895 elif name in ('size', 'deleted'): 

896 continue 

897 else: 

898 output[name] = value 

899 return output 

900 

901 

902def _translate_from_glance(image, include_locations=False): 

903 image_meta = _extract_attributes_v2( 

904 image, include_locations=include_locations) 

905 

906 image_meta = _convert_timestamps_to_datetimes(image_meta) 

907 image_meta = _convert_from_string(image_meta) 

908 return image_meta 

909 

910 

911def _convert_timestamps_to_datetimes(image_meta): 

912 """Returns image with timestamp fields converted to datetime objects.""" 

913 for attr in ['created_at', 'updated_at', 'deleted_at']: 

914 if image_meta.get(attr): 

915 image_meta[attr] = timeutils.parse_isotime(image_meta[attr]) 

916 return image_meta 

917 

918 

919# NOTE(bcwaldon): used to store non-string data in glance metadata 

920def _json_loads(properties, attr): 

921 prop = properties[attr] 

922 if isinstance(prop, str): 922 ↛ exitline 922 didn't return from function '_json_loads' because the condition on line 922 was always true

923 properties[attr] = jsonutils.loads(prop) 

924 

925 

926def _json_dumps(properties, attr): 

927 prop = properties[attr] 

928 if not isinstance(prop, str): 928 ↛ exitline 928 didn't return from function '_json_dumps' because the condition on line 928 was always true

929 properties[attr] = jsonutils.dumps(prop) 

930 

931 

932_CONVERT_PROPS = ('block_device_mapping', 'mappings') 

933 

934 

935def _convert(method, metadata): 

936 metadata = copy.deepcopy(metadata) 

937 properties = metadata.get('properties') 

938 if properties: 

939 for attr in _CONVERT_PROPS: 

940 if attr in properties: 

941 method(properties, attr) 

942 

943 return metadata 

944 

945 

946def _convert_from_string(metadata): 

947 return _convert(_json_loads, metadata) 

948 

949 

950def _convert_to_string(metadata): 

951 return _convert(_json_dumps, metadata) 

952 

953 

954def _extract_attributes(image, include_locations=False): 

955 # TODO(mfedosin): Remove this function once we move to glance V2 

956 # completely. 

957 # NOTE(hdd): If a key is not found, base.Resource.__getattr__() may perform 

958 # a get(), resulting in a useless request back to glance. This list is 

959 # therefore sorted, with dependent attributes as the end 

960 # 'deleted_at' depends on 'deleted' 

961 # 'checksum' depends on 'status' == 'active' 

962 IMAGE_ATTRIBUTES = ['size', 'disk_format', 'owner', 

963 'container_format', 'status', 'id', 

964 'name', 'created_at', 'updated_at', 

965 'deleted', 'deleted_at', 'checksum', 

966 'min_disk', 'min_ram', 'is_public', 

967 'direct_url', 'locations'] 

968 

969 queued = getattr(image, 'status') == 'queued' 

970 queued_exclude_attrs = ['disk_format', 'container_format'] 

971 include_locations_attrs = ['direct_url', 'locations'] 

972 output = {} 

973 

974 for attr in IMAGE_ATTRIBUTES: 

975 if attr == 'deleted_at' and not output['deleted']: 

976 output[attr] = None 

977 elif attr == 'checksum' and output['status'] != 'active': 

978 output[attr] = None 

979 # image may not have 'name' attr 

980 elif attr == 'name': 

981 output[attr] = getattr(image, attr, None) 

982 # NOTE(liusheng): queued image may not have these attributes and 'name' 

983 elif queued and attr in queued_exclude_attrs: 983 ↛ 984line 983 didn't jump to line 984 because the condition on line 983 was never true

984 output[attr] = getattr(image, attr, None) 

985 # NOTE(mriedem): Only get location attrs if including locations. 

986 elif attr in include_locations_attrs: 

987 if include_locations: 

988 output[attr] = getattr(image, attr, None) 

989 # NOTE(mdorman): 'size' attribute must not be 'None', so use 0 instead 

990 elif attr == 'size': 

991 # NOTE(mriedem): A snapshot image may not have the size attribute 

992 # set so default to 0. 

993 output[attr] = getattr(image, attr, 0) or 0 

994 else: 

995 # NOTE(xarses): Anything that is caught with the default value 

996 # will result in an additional lookup to glance for said attr. 

997 # Notable attributes that could have this issue: 

998 # disk_format, container_format, name, deleted, checksum 

999 output[attr] = getattr(image, attr, None) 

1000 

1001 output['properties'] = getattr(image, 'properties', {}) 

1002 

1003 return output 

1004 

1005 

1006def _extract_attributes_v2(image, include_locations=False): 

1007 include_locations_attrs = ['direct_url', 'locations'] 

1008 omit_attrs = ['self', 'schema', 'protected', 'virtual_size', 'file', 

1009 'tags'] 

1010 raw_schema = image.schema 

1011 schema = schemas.Schema(raw_schema) 

1012 output = {'properties': {}, 'deleted': False, 'deleted_at': None, 

1013 'disk_format': None, 'container_format': None, 'name': None, 

1014 'checksum': None} 

1015 for name, value in image.items(): 

1016 if (name in omit_attrs or 

1017 name in include_locations_attrs and not include_locations): 

1018 continue 

1019 elif name == 'visibility': 

1020 output['is_public'] = value == 'public' 

1021 elif name == 'size' and value is None: 1021 ↛ 1022line 1021 didn't jump to line 1022 because the condition on line 1021 was never true

1022 output['size'] = 0 

1023 elif schema.is_base_property(name): 

1024 output[name] = value 

1025 else: 

1026 output['properties'][name] = value 

1027 

1028 return output 

1029 

1030 

1031def _remove_read_only(image_meta): 

1032 IMAGE_ATTRIBUTES = ['status', 'updated_at', 'created_at', 'deleted_at'] 

1033 output = copy.deepcopy(image_meta) 

1034 for attr in IMAGE_ATTRIBUTES: 

1035 if attr in output: 

1036 del output[attr] 

1037 return output 

1038 

1039 

1040def _reraise_translated_image_exception(image_id): 

1041 """Transform the exception for the image but keep its traceback intact.""" 

1042 exc_type, exc_value, exc_trace = sys.exc_info() 

1043 new_exc = _translate_image_exception(image_id, exc_value) 

1044 raise new_exc.with_traceback(exc_trace) 

1045 

1046 

1047def _reraise_translated_exception(): 

1048 """Transform the exception but keep its traceback intact.""" 

1049 exc_type, exc_value, exc_trace = sys.exc_info() 

1050 new_exc = _translate_plain_exception(exc_value) 

1051 raise new_exc.with_traceback(exc_trace) 

1052 

1053 

1054def _translate_image_exception(image_id, exc_value): 

1055 if isinstance(exc_value, (glanceclient.exc.Forbidden, 

1056 glanceclient.exc.Unauthorized)): 

1057 return exception.ImageNotAuthorized(image_id=image_id) 

1058 if isinstance(exc_value, glanceclient.exc.NotFound): 

1059 return exception.ImageNotFound(image_id=image_id) 

1060 if isinstance(exc_value, glanceclient.exc.BadRequest): 1060 ↛ 1061line 1060 didn't jump to line 1061 because the condition on line 1060 was never true

1061 return exception.ImageBadRequest(image_id=image_id, 

1062 response=str(exc_value)) 

1063 if isinstance(exc_value, glanceclient.exc.HTTPOverLimit): 1063 ↛ 1065line 1063 didn't jump to line 1065 because the condition on line 1063 was always true

1064 return exception.ImageQuotaExceeded(image_id=image_id) 

1065 return exc_value 

1066 

1067 

1068def _translate_plain_exception(exc_value): 

1069 if isinstance(exc_value, (glanceclient.exc.Forbidden, 

1070 glanceclient.exc.Unauthorized)): 

1071 return exception.Forbidden(str(exc_value)) 

1072 if isinstance(exc_value, glanceclient.exc.NotFound): 

1073 return exception.NotFound(str(exc_value)) 

1074 if isinstance(exc_value, glanceclient.exc.BadRequest): 

1075 return exception.Invalid(str(exc_value)) 

1076 return exc_value 

1077 

1078 

1079def _verify_certs(context, img_sig_cert_uuid, trusted_certs): 

1080 try: 

1081 certificate_utils.verify_certificate( 

1082 context=context, 

1083 certificate_uuid=img_sig_cert_uuid, 

1084 trusted_certificate_uuids=trusted_certs.ids) 

1085 LOG.debug('Image signature certificate validation ' 

1086 'succeeded for certificate: %s', 

1087 img_sig_cert_uuid) 

1088 except cursive_exception.SignatureVerificationError as e: 

1089 LOG.warning('Image signature certificate validation ' 

1090 'failed for certificate: %s', 

1091 img_sig_cert_uuid) 

1092 raise exception.CertificateValidationFailed( 

1093 cert_uuid=img_sig_cert_uuid, reason=str(e)) 

1094 

1095 

1096def get_remote_image_service(context, image_href): 

1097 """Create an image_service and parse the id from the given image_href. 

1098 

1099 The image_href param can be an href of the form 

1100 'http://example.com:9292/v1/images/b8b2c6f7-7345-4e2f-afa2-eedaba9cbbe3', 

1101 or just an id such as 'b8b2c6f7-7345-4e2f-afa2-eedaba9cbbe3'. If the 

1102 image_href is a standalone id, then the default image service is returned. 

1103 

1104 :param image_href: href that describes the location of an image 

1105 :returns: a tuple of the form (image_service, image_id) 

1106 

1107 """ 

1108 # NOTE(bcwaldon): If image_href doesn't look like a URI, assume its a 

1109 # standalone image ID 

1110 if '/' not in str(image_href): 

1111 image_service = get_default_image_service() 

1112 return image_service, image_href 

1113 

1114 try: 

1115 (image_id, endpoint) = _endpoint_from_image_ref(image_href) 

1116 glance_client = GlanceClientWrapper(context=context, 

1117 endpoint=endpoint) 

1118 except ValueError: 

1119 raise exception.InvalidImageRef(image_href=image_href) 

1120 

1121 image_service = GlanceImageServiceV2(client=glance_client) 

1122 return image_service, image_id 

1123 

1124 

1125def get_default_image_service(): 

1126 return GlanceImageServiceV2() 

1127 

1128 

1129class UpdateGlanceImage(object): 

1130 def __init__(self, context, image_id, metadata, stream): 

1131 self.context = context 

1132 self.image_id = image_id 

1133 self.metadata = metadata 

1134 self.image_stream = stream 

1135 

1136 def start(self): 

1137 image_service, image_id = get_remote_image_service( 

1138 self.context, self.image_id) 

1139 image_service.update(self.context, image_id, self.metadata, 

1140 self.image_stream, purge_props=False) 

1141 

1142 

1143@profiler.trace_cls("nova_image") 

1144class API(object): 

1145 """API for interacting with the image service.""" 

1146 

1147 def _get_session_and_image_id(self, context, id_or_uri): 

1148 """Returns a tuple of (session, image_id). If the supplied `id_or_uri` 

1149 is an image ID, then the default client session will be returned 

1150 for the context's user, along with the image ID. If the supplied 

1151 `id_or_uri` parameter is a URI, then a client session connecting to 

1152 the URI's image service endpoint will be returned along with a 

1153 parsed image ID from that URI. 

1154 

1155 :param context: The `nova.context.Context` object for the request 

1156 :param id_or_uri: A UUID identifier or an image URI to look up image 

1157 information for. 

1158 """ 

1159 return get_remote_image_service(context, id_or_uri) 

1160 

1161 def _get_session(self, _context): 

1162 """Returns a client session that can be used to query for image 

1163 information. 

1164 

1165 :param _context: The `nova.context.Context` object for the request 

1166 """ 

1167 # TODO(jaypipes): Refactor get_remote_image_service and 

1168 # get_default_image_service into a single 

1169 # method that takes a context and actually respects 

1170 # it, returning a real session object that keeps 

1171 # the context alive... 

1172 return get_default_image_service() 

1173 

1174 @staticmethod 

1175 def generate_image_url(image_ref, context): 

1176 """Generate an image URL from an image_ref. 

1177 

1178 :param image_ref: The image ref to generate URL 

1179 :param context: The `nova.context.Context` object for the request 

1180 """ 

1181 return "%s/images/%s" % (next(get_api_servers(context)), image_ref) 

1182 

1183 def get_all(self, context, **kwargs): 

1184 """Retrieves all information records about all disk images available 

1185 to show to the requesting user. If the requesting user is an admin, 

1186 all images in an ACTIVE status are returned. If the requesting user 

1187 is not an admin, the all public images and all private images that 

1188 are owned by the requesting user in the ACTIVE status are returned. 

1189 

1190 :param context: The `nova.context.Context` object for the request 

1191 :param kwargs: A dictionary of filter and pagination values that 

1192 may be passed to the underlying image info driver. 

1193 """ 

1194 session = self._get_session(context) 

1195 return session.detail(context, **kwargs) 

1196 

1197 def get(self, context, id_or_uri, include_locations=False, 

1198 show_deleted=True): 

1199 """Retrieves the information record for a single disk image. If the 

1200 supplied identifier parameter is a UUID, the default driver will 

1201 be used to return information about the image. If the supplied 

1202 identifier is a URI, then the driver that matches that URI endpoint 

1203 will be used to query for image information. 

1204 

1205 :param context: The `nova.context.Context` object for the request 

1206 :param id_or_uri: A UUID identifier or an image URI to look up image 

1207 information for. 

1208 :param include_locations: (Optional) include locations in the returned 

1209 dict of information if the image service API 

1210 supports it. If the image service API does 

1211 not support the locations attribute, it will 

1212 still be included in the returned dict, as an 

1213 empty list. 

1214 :param show_deleted: (Optional) show the image even the status of 

1215 image is deleted. 

1216 """ 

1217 session, image_id = self._get_session_and_image_id(context, id_or_uri) 

1218 return session.show(context, image_id, 

1219 include_locations=include_locations, 

1220 show_deleted=show_deleted) 

1221 

1222 def create(self, context, image_info, data=None): 

1223 """Creates a new image record, optionally passing the image bits to 

1224 backend storage. 

1225 

1226 :param context: The `nova.context.Context` object for the request 

1227 :param image_info: A dict of information about the image that is 

1228 passed to the image registry. 

1229 :param data: Optional file handle or bytestream iterator that is 

1230 passed to backend storage. 

1231 """ 

1232 session = self._get_session(context) 

1233 return session.create(context, image_info, data=data) 

1234 

1235 def update(self, context, id_or_uri, image_info, 

1236 data=None, purge_props=False): 

1237 """Update the information about an image, optionally along with a file 

1238 handle or bytestream iterator for image bits. If the optional file 

1239 handle for updated image bits is supplied, the image may not have 

1240 already uploaded bits for the image. 

1241 

1242 :param context: The `nova.context.Context` object for the request 

1243 :param id_or_uri: A UUID identifier or an image URI to look up image 

1244 information for. 

1245 :param image_info: A dict of information about the image that is 

1246 passed to the image registry. 

1247 :param data: Optional file handle or bytestream iterator that is 

1248 passed to backend storage. 

1249 :param purge_props: Optional, defaults to False. If set, the backend 

1250 image registry will clear all image properties 

1251 and replace them the image properties supplied 

1252 in the image_info dictionary's 'properties' 

1253 collection. 

1254 """ 

1255 session, image_id = self._get_session_and_image_id(context, id_or_uri) 

1256 return session.update(context, image_id, image_info, data=data, 

1257 purge_props=purge_props) 

1258 

1259 def delete(self, context, id_or_uri): 

1260 """Delete the information about an image and mark the image bits for 

1261 deletion. 

1262 

1263 :param context: The `nova.context.Context` object for the request 

1264 :param id_or_uri: A UUID identifier or an image URI to look up image 

1265 information for. 

1266 """ 

1267 session, image_id = self._get_session_and_image_id(context, id_or_uri) 

1268 return session.delete(context, image_id) 

1269 

1270 def download(self, context, id_or_uri, data=None, dest_path=None, 

1271 trusted_certs=None): 

1272 """Transfer image bits from Glance or a known source location to the 

1273 supplied destination filepath. 

1274 

1275 :param context: The `nova.context.RequestContext` object for the 

1276 request 

1277 :param id_or_uri: A UUID identifier or an image URI to look up image 

1278 information for. 

1279 :param data: A file object to use in downloading image data. 

1280 :param dest_path: Filepath to transfer image bits to. 

1281 :param trusted_certs: A 'nova.objects.trusted_certs.TrustedCerts' 

1282 object with a list of trusted image certificate 

1283 IDs. 

1284 

1285 Note that because of the poor design of the 

1286 `glance.ImageService.download` method, the function returns different 

1287 things depending on what arguments are passed to it. If a data argument 

1288 is supplied but no dest_path is specified (not currently done by any 

1289 caller) then None is returned from the method. If the data argument is 

1290 not specified but a destination path *is* specified, then a writeable 

1291 file handle to the destination path is constructed in the method and 

1292 the image bits written to that file, and again, None is returned from 

1293 the method. If no data argument is supplied and no dest_path argument 

1294 is supplied (VMWare virt driver), then the method returns an iterator 

1295 to the image bits that the caller uses to write to wherever location it 

1296 wants. Finally, if the allow_direct_url_schemes CONF option is set to 

1297 something, then the nova.image.download modules are used to attempt to 

1298 do an SCP copy of the image bits from a file location to the dest_path 

1299 and None is returned after retrying one or more download locations 

1300 (libvirt and Hyper-V virt drivers through nova.virt.images.fetch). 

1301 

1302 I think the above points to just how hacky/wacky all of this code is, 

1303 and the reason it needs to be cleaned up and standardized across the 

1304 virt driver callers. 

1305 """ 

1306 # TODO(jaypipes): Deprecate and remove this method entirely when we 

1307 # move to a system that simply returns a file handle 

1308 # to a bytestream iterator and allows the caller to 

1309 # handle streaming/copying/zero-copy as they see fit. 

1310 session, image_id = self._get_session_and_image_id(context, id_or_uri) 

1311 return session.download(context, image_id, data=data, 

1312 dst_path=dest_path, 

1313 trusted_certs=trusted_certs) 

1314 

1315 def copy_image_to_store(self, context, image_id, store): 

1316 """Initiate a store-to-store copy in glance. 

1317 

1318 :param context: The RequestContext. 

1319 :param image_id: The image to copy. 

1320 :param store: The glance store to target the copy. 

1321 """ 

1322 session, image_id = self._get_session_and_image_id(context, image_id) 

1323 return session.image_import_copy(context, image_id, [store])