def fetch(context: context.RequestContext, image_service: glance.GlanceImageService, image_id: str, path: str, _user_id, _project_id) -> None: # TODO(vish): Improve context handling and add owner and auth data # when it is added to glance. Right now there is no # auth checking in glance, so we assume that access was # checked before we got here. start_time = timeutils.utcnow() with fileutils.remove_path_on_error(path): with open(path, "wb") as image_file: try: image_service.download(context, image_id, tpool.Proxy(image_file)) except IOError as e: if e.errno == errno.ENOSPC: params = {'path': os.path.dirname(path), 'image': image_id} reason = _("No space left in image_conversion_dir " "path (%(path)s) while fetching " "image %(image)s.") % params LOG.exception(reason) raise exception.ImageTooBig(image_id=image_id, reason=reason) reason = ("IOError: %(errno)s %(strerror)s" % { 'errno': e.errno, 'strerror': e.strerror }) LOG.error(reason) raise exception.ImageDownloadFailed(image_href=image_id, reason=reason) duration = timeutils.delta_seconds(start_time, timeutils.utcnow()) # NOTE(jdg): use a default of 1, mostly for unit test, but in # some incredible event this is 0 (cirros image?) don't barf if duration < 1: duration = 1 fsz_mb = os.stat(image_file.name).st_size / units.Mi mbps = (fsz_mb / duration) msg = ("Image fetch details: dest %(dest)s, size %(sz).2f MB, " "duration %(duration).2f sec") LOG.debug(msg, { "dest": image_file.name, "sz": fsz_mb, "duration": duration }) msg = "Image download %(sz).2f MB at %(mbps).2f MB/s" LOG.info(msg, {"sz": fsz_mb, "mbps": mbps})
def fetch_verify_image(context: context.RequestContext, image_service: glance.GlanceImageService, image_id: str, dest: str) -> None: fetch(context, image_service, image_id, dest, None, None) image_meta = image_service.show(context, image_id) with fileutils.remove_path_on_error(dest): has_meta = False if not image_meta else True try: format_raw = True if image_meta['disk_format'] == 'raw' else False except TypeError: format_raw = False data = get_qemu_data(image_id, has_meta, format_raw, dest, True) # We can only really do verification of the image if we have # qemu data to use if data is not None: fmt = data.file_format if fmt is None: raise exception.ImageUnacceptable( reason=_("'qemu-img info' parsing failed."), image_id=image_id) backing_file = data.backing_file if backing_file is not None: raise exception.ImageUnacceptable( image_id=image_id, reason=(_("fmt=%(fmt)s backed by: %(backing_file)s") % { 'fmt': fmt, 'backing_file': backing_file }))
def upload_volume(context: context.RequestContext, image_service: glance.GlanceImageService, image_meta: dict, volume_path: str, volume_format: str = 'raw', run_as_root: bool = True, compress: bool = True, store_id: Optional[str] = None, base_image_ref: Optional[str] = None) -> None: # NOTE: You probably want to use volume_utils.upload_volume(), # not this function. image_id = image_meta['id'] check_image_conversion_disable(image_meta['disk_format'], volume_format, image_id, upload=True) if image_meta.get('container_format') != 'compressed': if (image_meta['disk_format'] == volume_format): LOG.debug("%s was %s, no need to convert to %s", image_id, volume_format, image_meta['disk_format']) if os.name == 'nt' or os.access(volume_path, os.R_OK): with open(volume_path, 'rb') as image_file: image_service.update(context, image_id, {}, tpool.Proxy(image_file), store_id=store_id, base_image_ref=base_image_ref) else: with utils.temporary_chown(volume_path): with open(volume_path, 'rb') as image_file: image_service.update(context, image_id, {}, tpool.Proxy(image_file), store_id=store_id, base_image_ref=base_image_ref) return with temporary_file(prefix='vol_upload_') as tmp: LOG.debug("%s was %s, converting to %s", image_id, volume_format, image_meta['disk_format']) data = qemu_img_info(volume_path, run_as_root=run_as_root) backing_file = data.backing_file fmt = data.file_format if backing_file is not None: # Disallow backing files as a security measure. # This prevents a user from writing an image header into a raw # volume with a backing file pointing to data they wish to # access. raise exception.ImageUnacceptable( image_id=image_id, reason=_("fmt=%(fmt)s backed by:%(backing_file)s") % { 'fmt': fmt, 'backing_file': backing_file }) out_format = fixup_disk_format(image_meta['disk_format']) convert_image(volume_path, tmp, out_format, run_as_root=run_as_root, compress=compress) data = qemu_img_info(tmp, run_as_root=run_as_root) if data.file_format != out_format: raise exception.ImageUnacceptable( image_id=image_id, reason=_("Converted to %(f1)s, but format is now %(f2)s") % { 'f1': out_format, 'f2': data.file_format }) # NOTE(ZhengMa): This is used to do image compression on image # uploading with 'compressed' container_format. # Compress file 'tmp' in-place if image_meta.get('container_format') == 'compressed': LOG.debug("Container_format set to 'compressed', compressing " "image before uploading.") accel = accelerator.ImageAccel(tmp, tmp) accel.compress_img(run_as_root=run_as_root) with open(tmp, 'rb') as image_file: image_service.update(context, image_id, {}, tpool.Proxy(image_file), store_id=store_id, base_image_ref=base_image_ref)
def fetch_to_volume_format(context: context.RequestContext, image_service: glance.GlanceImageService, image_id: str, dest: str, volume_format: str, blocksize: int, volume_subformat: Optional[str] = None, user_id: Optional[str] = None, project_id: Optional[str] = None, size: Optional[int] = None, run_as_root: bool = True) -> None: qemu_img = True image_meta = image_service.show(context, image_id) check_image_conversion_disable(image_meta['disk_format'], volume_format, image_id, upload=False) allow_image_compression = CONF.allow_compression_on_image_upload if image_meta and (image_meta.get('container_format') == 'compressed'): if allow_image_compression is False: compression_param = { 'container_format': image_meta.get('container_format') } raise exception.ImageUnacceptable( image_id=image_id, reason=_("Image compression disallowed, " "but container_format is " "%(container_format)s.") % compression_param) # NOTE(avishay): I'm not crazy about creating temp files which may be # large and cause disk full errors which would confuse users. # Unfortunately it seems that you can't pipe to 'qemu-img convert' because # it seeks. Maybe we can think of something for a future version. with temporary_file(prefix='image_download_%s_' % image_id) as tmp: has_meta = False if not image_meta else True try: format_raw = True if image_meta['disk_format'] == 'raw' else False except TypeError: format_raw = False data = get_qemu_data(image_id, has_meta, format_raw, tmp, run_as_root) if data is None: qemu_img = False tmp_images = TemporaryImages.for_image_service(image_service) tmp_image = tmp_images.get(context, image_id) if tmp_image: tmp = tmp_image else: fetch(context, image_service, image_id, tmp, user_id, project_id) if is_xenserver_format(image_meta): replace_xenserver_image_with_coalesced_vhd(tmp) if not qemu_img: # qemu-img is not installed but we do have a RAW image. As a # result we only need to copy the image to the destination and then # return. LOG.debug( 'Copying image from %(tmp)s to volume %(dest)s - ' 'size: %(size)s', { 'tmp': tmp, 'dest': dest, 'size': image_meta['size'] }) image_size_m = math.ceil(float(image_meta['size']) / units.Mi) volume_utils.copy_volume(tmp, dest, image_size_m, blocksize) return data = qemu_img_info(tmp, run_as_root=run_as_root) # NOTE(xqueralt): If the image virtual size doesn't fit in the # requested volume there is no point on resizing it because it will # generate an unusable image. if size is not None: check_virtual_size(data.virtual_size, size, image_id) fmt = data.file_format if fmt is None: raise exception.ImageUnacceptable( reason=_("'qemu-img info' parsing failed."), image_id=image_id) backing_file = data.backing_file if backing_file is not None: raise exception.ImageUnacceptable( image_id=image_id, reason=_("fmt=%(fmt)s backed by:%(backing_file)s") % { 'fmt': fmt, 'backing_file': backing_file, }) # NOTE(ZhengMa): This is used to do image decompression on image # downloading with 'compressed' container_format. It is a # transparent level between original image downloaded from # Glance and Cinder image service. So the source file path is # the same with destination file path. if image_meta.get('container_format') == 'compressed': LOG.debug("Found image with compressed container format") if not accelerator.is_gzip_compressed(tmp): raise exception.ImageUnacceptable( image_id=image_id, reason=_("Unsupported compressed image format found. " "Only gzip is supported currently")) accel = accelerator.ImageAccel(tmp, tmp) accel.decompress_img(run_as_root=run_as_root) # NOTE(jdg): I'm using qemu-img convert to write # to the volume regardless if it *needs* conversion or not # TODO(avishay): We can speed this up by checking if the image is raw # and if so, writing directly to the device. However, we need to keep # check via 'qemu-img info' that what we copied was in fact a raw # image and not a different format with a backing file, which may be # malicious. disk_format = fixup_disk_format(image_meta['disk_format']) LOG.debug("%s was %s, converting to %s", image_id, fmt, volume_format) convert_image(tmp, dest, volume_format, out_subformat=volume_subformat, src_format=disk_format, run_as_root=run_as_root)
def verify_glance_image_signature(context: context.RequestContext, image_service: glance.GlanceImageService, image_id: str, path: str) -> bool: verifier = None image_meta = image_service.show(context, image_id) image_properties = image_meta.get('properties', {}) img_signature = image_properties.get('img_signature') img_sig_hash_method = image_properties.get('img_signature_hash_method') img_sig_cert_uuid = image_properties.get('img_signature_certificate_uuid') img_sig_key_type = image_properties.get('img_signature_key_type') if all(m is None for m in [ img_signature, img_sig_cert_uuid, img_sig_hash_method, img_sig_key_type ]): # NOTE(tommylikehu): We won't verify the image signature # if none of the signature metadata presents. return False if any(m is None for m in [ img_signature, img_sig_cert_uuid, img_sig_hash_method, img_sig_key_type ]): LOG.error('Image signature metadata for image %s is ' 'incomplete.', image_id) raise exception.InvalidSignatureImage(image_id=image_id) try: verifier = signature_utils.get_verifier( context=context, img_signature_certificate_uuid=img_sig_cert_uuid, img_signature_hash_method=img_sig_hash_method, img_signature=img_signature, img_signature_key_type=img_sig_key_type, ) except cursive_exception.SignatureVerificationError: message = _('Failed to get verifier for image: %s') % image_id LOG.error(message) raise exception.ImageSignatureVerificationException(reason=message) if verifier: with fileutils.remove_path_on_error(path): with open(path, "rb") as tem_file: try: tpool.execute(_verify_image, tem_file, verifier) LOG.info( 'Image signature verification succeeded ' 'for image: %s', image_id) return True except cryptography.exceptions.InvalidSignature: message = _('Image signature verification ' 'failed for image: %s') % image_id LOG.error(message) raise exception.ImageSignatureVerificationException( reason=message) except Exception as ex: message = _('Failed to verify signature for ' 'image: %(image)s due to ' 'error: %(error)s ') % { 'image': image_id, 'error': ex } LOG.error(message) raise exception.ImageSignatureVerificationException( reason=message) return False
def __init__(self, image_service: glance.GlanceImageService): self.temporary_images: dict[str, dict] = {} self.image_service = image_service image_service.temp_images = self