Exemplo n.º 1
0
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})
Exemplo n.º 2
0
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
                    }))
Exemplo n.º 3
0
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)
Exemplo n.º 4
0
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)
Exemplo n.º 5
0
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
Exemplo n.º 6
0
 def __init__(self, image_service: glance.GlanceImageService):
     self.temporary_images: dict[str, dict] = {}
     self.image_service = image_service
     image_service.temp_images = self