Merge "Replace image_format_inspector with its oslo.utils version"
This commit is contained in:
commit
f087b3c8d6
File diff suppressed because it is too large
Load Diff
@ -26,13 +26,13 @@ import time
|
|||||||
from oslo_concurrency import processutils
|
from oslo_concurrency import processutils
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_utils import fileutils
|
from oslo_utils import fileutils
|
||||||
|
from oslo_utils.imageutils import format_inspector as image_format_inspector
|
||||||
import pycdlib
|
import pycdlib
|
||||||
|
|
||||||
from ironic.common import checksum_utils
|
from ironic.common import checksum_utils
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
from ironic.common.glance_service import service_utils as glance_utils
|
from ironic.common.glance_service import service_utils as glance_utils
|
||||||
from ironic.common.i18n import _
|
from ironic.common.i18n import _
|
||||||
from ironic.common import image_format_inspector
|
|
||||||
from ironic.common import image_service as service
|
from ironic.common import image_service as service
|
||||||
from ironic.common import qemu_img
|
from ironic.common import qemu_img
|
||||||
from ironic.common import utils
|
from ironic.common import utils
|
||||||
@ -401,7 +401,14 @@ def fetch(context, image_href, path, force_raw=False,
|
|||||||
def get_source_format(image_href, path):
|
def get_source_format(image_href, path):
|
||||||
try:
|
try:
|
||||||
img_format = image_format_inspector.detect_file_format(path)
|
img_format = image_format_inspector.detect_file_format(path)
|
||||||
except image_format_inspector.ImageFormatError:
|
except image_format_inspector.ImageFormatError as exc:
|
||||||
|
LOG.error("Parsing of the image %s failed: %s", image_href, exc)
|
||||||
|
raise exception.ImageUnacceptable(
|
||||||
|
reason=_("parsing of the image failed."),
|
||||||
|
image_id=image_href)
|
||||||
|
if img_format is None:
|
||||||
|
LOG.error("Parsing of the image %s failed: format not recognized",
|
||||||
|
image_href)
|
||||||
raise exception.ImageUnacceptable(
|
raise exception.ImageUnacceptable(
|
||||||
reason=_("parsing of the image failed."),
|
reason=_("parsing of the image failed."),
|
||||||
image_id=image_href)
|
image_id=image_href)
|
||||||
@ -411,9 +418,7 @@ def get_source_format(image_href, path):
|
|||||||
def force_raw_will_convert(image_href, path_tmp):
|
def force_raw_will_convert(image_href, path_tmp):
|
||||||
with fileutils.remove_path_on_error(path_tmp):
|
with fileutils.remove_path_on_error(path_tmp):
|
||||||
fmt = get_source_format(image_href, path_tmp)
|
fmt = get_source_format(image_href, path_tmp)
|
||||||
if fmt != "raw":
|
return fmt not in RAW_IMAGE_FORMATS
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def image_to_raw(image_href, path, path_tmp):
|
def image_to_raw(image_href, path, path_tmp):
|
||||||
@ -421,7 +426,7 @@ def image_to_raw(image_href, path, path_tmp):
|
|||||||
if not CONF.conductor.disable_deep_image_inspection:
|
if not CONF.conductor.disable_deep_image_inspection:
|
||||||
fmt = safety_check_image(path_tmp)
|
fmt = safety_check_image(path_tmp)
|
||||||
|
|
||||||
if fmt not in CONF.conductor.permitted_image_formats:
|
if not image_format_permitted(fmt):
|
||||||
LOG.error("Security: The requested image %(image_href)s "
|
LOG.error("Security: The requested image %(image_href)s "
|
||||||
"is of format image %(format)s and is not in "
|
"is of format image %(format)s and is not in "
|
||||||
"the [conductor]permitted_image_formats list.",
|
"the [conductor]permitted_image_formats list.",
|
||||||
@ -436,7 +441,7 @@ def image_to_raw(image_href, path, path_tmp):
|
|||||||
{'img_fmt': fmt,
|
{'img_fmt': fmt,
|
||||||
'path': path})
|
'path': path})
|
||||||
|
|
||||||
if fmt != "raw" and fmt != "iso":
|
if fmt not in RAW_IMAGE_FORMATS and fmt != "iso":
|
||||||
# When the target format is NOT raw, we need to convert it.
|
# When the target format is NOT raw, we need to convert it.
|
||||||
# however, we don't need nor want to do that when we have
|
# however, we don't need nor want to do that when we have
|
||||||
# an ISO image. If we have an ISO because it was requested,
|
# an ISO image. If we have an ISO because it was requested,
|
||||||
@ -453,7 +458,7 @@ def image_to_raw(image_href, path, path_tmp):
|
|||||||
source_format=fmt)
|
source_format=fmt)
|
||||||
os.unlink(path_tmp)
|
os.unlink(path_tmp)
|
||||||
new_fmt = get_source_format(image_href, staged)
|
new_fmt = get_source_format(image_href, staged)
|
||||||
if new_fmt != "raw":
|
if new_fmt not in RAW_IMAGE_FORMATS:
|
||||||
raise exception.ImageConvertFailed(
|
raise exception.ImageConvertFailed(
|
||||||
image_id=image_href,
|
image_id=image_href,
|
||||||
reason=_("Converted to raw, but format is "
|
reason=_("Converted to raw, but format is "
|
||||||
@ -836,22 +841,56 @@ def safety_check_image(image_path, node=None):
|
|||||||
id_string = __node_or_image_cache(node)
|
id_string = __node_or_image_cache(node)
|
||||||
try:
|
try:
|
||||||
img_class = image_format_inspector.detect_file_format(image_path)
|
img_class = image_format_inspector.detect_file_format(image_path)
|
||||||
if not img_class.safety_check():
|
if img_class is None:
|
||||||
LOG.error("Security: The requested image for "
|
LOG.error("Security: The requested user image for the "
|
||||||
"deployment of node %(node)s fails safety sanity "
|
"deployment node %(node)s does not match any known "
|
||||||
"checking.",
|
"format",
|
||||||
{'node': id_string})
|
{'node': id_string})
|
||||||
raise exception.InvalidImage()
|
raise exception.InvalidImage()
|
||||||
|
img_class.safety_check()
|
||||||
image_format_name = str(img_class)
|
image_format_name = str(img_class)
|
||||||
except image_format_inspector.ImageFormatError:
|
except image_format_inspector.ImageFormatError as exc:
|
||||||
LOG.error("Security: The requested user image for the "
|
LOG.error("Security: The requested user image for the "
|
||||||
"deployment node %(node)s failed to be able "
|
"deployment node %(node)s failed to be able "
|
||||||
"to be parsed by the image format checker.",
|
"to be parsed by the image format checker: %(exc)s",
|
||||||
{'node': id_string})
|
{'node': id_string, 'exc': exc})
|
||||||
|
raise exception.InvalidImage()
|
||||||
|
except image_format_inspector.SafetyCheckFailed as exc:
|
||||||
|
LOG.error("Security: The requested image for "
|
||||||
|
"deployment of node %(node)s fails safety sanity "
|
||||||
|
"checking: %(exc)s",
|
||||||
|
{'node': id_string, 'exc': exc})
|
||||||
raise exception.InvalidImage()
|
raise exception.InvalidImage()
|
||||||
return image_format_name
|
return image_format_name
|
||||||
|
|
||||||
|
|
||||||
|
RAW_IMAGE_FORMATS = {'raw', 'gpt'} # gpt is a whole-disk image
|
||||||
|
|
||||||
|
|
||||||
|
def image_format_permitted(img_format):
|
||||||
|
permitted = set(CONF.conductor.permitted_image_formats)
|
||||||
|
if 'raw' in permitted:
|
||||||
|
permitted.update(RAW_IMAGE_FORMATS)
|
||||||
|
return img_format in permitted
|
||||||
|
|
||||||
|
|
||||||
|
def image_format_matches(actual_format, expected_format):
|
||||||
|
if expected_format in ['ari', 'aki']:
|
||||||
|
# In this case, we have an ari or aki, meaning we're pulling
|
||||||
|
# down a kernel/ramdisk, and this is rooted in a misunderstanding.
|
||||||
|
# They should be raw. The detector should be detecting this *as*
|
||||||
|
# raw anyway, so the data just mismatches from a common
|
||||||
|
# misunderstanding, and that is okay in this case as they are not
|
||||||
|
# passed to qemu-img.
|
||||||
|
# TODO(TheJulia): Add a log entry to warn here at some point in
|
||||||
|
# the future as we begin to shift the perception around this.
|
||||||
|
# See: https://bugs.launchpad.net/ironic/+bug/2074090
|
||||||
|
return True
|
||||||
|
if expected_format == 'raw' and actual_format in RAW_IMAGE_FORMATS:
|
||||||
|
return True
|
||||||
|
return expected_format == actual_format
|
||||||
|
|
||||||
|
|
||||||
def check_if_image_format_is_permitted(img_format,
|
def check_if_image_format_is_permitted(img_format,
|
||||||
expected_format=None,
|
expected_format=None,
|
||||||
node=None):
|
node=None):
|
||||||
@ -867,25 +906,15 @@ def check_if_image_format_is_permitted(img_format,
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
id_string = __node_or_image_cache(node)
|
id_string = __node_or_image_cache(node)
|
||||||
if img_format not in CONF.conductor.permitted_image_formats:
|
if not image_format_permitted(img_format):
|
||||||
LOG.error("Security: The requested deploy image for node %(node)s "
|
LOG.error("Security: The requested deploy image for node %(node)s "
|
||||||
"is of format image %(format)s and is not in the "
|
"is of format image %(format)s and is not in the "
|
||||||
"[conductor]permitted_image_formats list.",
|
"[conductor]permitted_image_formats list.",
|
||||||
{'node': id_string,
|
{'node': id_string,
|
||||||
'format': img_format})
|
'format': img_format})
|
||||||
raise exception.InvalidImage()
|
raise exception.InvalidImage()
|
||||||
if expected_format is not None and img_format != expected_format:
|
if (expected_format is not None
|
||||||
if expected_format in ['ari', 'aki']:
|
and not image_format_matches(img_format, expected_format)):
|
||||||
# In this case, we have an ari or aki, meaning we're pulling
|
|
||||||
# down a kernel/ramdisk, and this is rooted in a misunderstanding.
|
|
||||||
# They should be raw. The detector should be detecting this *as*
|
|
||||||
# raw anyway, so the data just mismatches from a common
|
|
||||||
# misunderstanding, and that is okay in this case as they are not
|
|
||||||
# passed to qemu-img.
|
|
||||||
# TODO(TheJulia): Add a log entry to warn here at some point in
|
|
||||||
# the future as we begin to shift the perception around this.
|
|
||||||
# See: https://bugs.launchpad.net/ironic/+bug/2074090
|
|
||||||
return
|
|
||||||
LOG.error("Security: The requested deploy image for node %(node)s "
|
LOG.error("Security: The requested deploy image for node %(node)s "
|
||||||
"has a format (%(format)s) which does not match the "
|
"has a format (%(format)s) which does not match the "
|
||||||
"expected image format (%(expected)s) based upon "
|
"expected image format (%(expected)s) based upon "
|
||||||
|
@ -494,7 +494,7 @@ opts = [
|
|||||||
'[conductor]disable_deep_image_inspection to be set '
|
'[conductor]disable_deep_image_inspection to be set '
|
||||||
'to False.')),
|
'to False.')),
|
||||||
cfg.ListOpt('permitted_image_formats',
|
cfg.ListOpt('permitted_image_formats',
|
||||||
default=['raw', 'qcow2', 'iso'],
|
default=['raw', 'gpt', 'qcow2', 'iso'],
|
||||||
mutable=True,
|
mutable=True,
|
||||||
help=_('The supported list of image formats which are '
|
help=_('The supported list of image formats which are '
|
||||||
'permitted for deployment with Ironic. If an image '
|
'permitted for deployment with Ironic. If an image '
|
||||||
|
@ -406,7 +406,8 @@ def _fetch(context, image_href, path, force_raw=False, expected_format=None,
|
|||||||
if (force_raw
|
if (force_raw
|
||||||
and ((disable_dii
|
and ((disable_dii
|
||||||
and images.force_raw_will_convert(image_href, path_tmp))
|
and images.force_raw_will_convert(image_href, path_tmp))
|
||||||
or (not disable_dii and image_format != 'raw'))):
|
or (not disable_dii
|
||||||
|
and image_format not in images.RAW_IMAGE_FORMATS))):
|
||||||
# NOTE(TheJulia): What is happening here is the rest of the logic
|
# NOTE(TheJulia): What is happening here is the rest of the logic
|
||||||
# is hinged on force_raw, but also we don't need to take the entire
|
# is hinged on force_raw, but also we don't need to take the entire
|
||||||
# path *if* the image on disk is *already* raw. Depending on settings,
|
# path *if* the image on disk is *already* raw. Depending on settings,
|
||||||
|
@ -1,668 +0,0 @@
|
|||||||
# Copyright 2020 Red Hat, Inc
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import io
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import struct
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from oslo_utils import units
|
|
||||||
|
|
||||||
from ironic.common import image_format_inspector as format_inspector
|
|
||||||
from ironic.tests import base as test_base
|
|
||||||
|
|
||||||
|
|
||||||
TEST_IMAGE_PREFIX = 'ironic-unittest-formatinspector-'
|
|
||||||
|
|
||||||
|
|
||||||
def get_size_from_qemu_img(filename):
|
|
||||||
output = subprocess.check_output('qemu-img info "%s"' % filename,
|
|
||||||
shell=True)
|
|
||||||
for line in output.split(b'\n'):
|
|
||||||
m = re.search(b'^virtual size: .* .([0-9]+) bytes', line.strip())
|
|
||||||
if m:
|
|
||||||
return int(m.group(1))
|
|
||||||
|
|
||||||
raise Exception('Could not find virtual size with qemu-img')
|
|
||||||
|
|
||||||
|
|
||||||
class TestFormatInspectors(test_base.TestCase):
|
|
||||||
|
|
||||||
block_execute = False
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestFormatInspectors, self).setUp()
|
|
||||||
self._created_files = []
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
super(TestFormatInspectors, self).tearDown()
|
|
||||||
for fn in self._created_files:
|
|
||||||
try:
|
|
||||||
os.remove(fn)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _create_iso(self, image_size, subformat='9660'):
|
|
||||||
"""Create an ISO file of the given size.
|
|
||||||
|
|
||||||
:param image_size: The size of the image to create in bytes
|
|
||||||
:param subformat: The subformat to use, if any
|
|
||||||
"""
|
|
||||||
|
|
||||||
# these tests depend on mkisofs
|
|
||||||
# being installed and in the path,
|
|
||||||
# if it is not installed, skip
|
|
||||||
try:
|
|
||||||
subprocess.check_output('mkisofs --version', shell=True)
|
|
||||||
except Exception:
|
|
||||||
self.skipTest('mkisofs not installed')
|
|
||||||
|
|
||||||
size = image_size // units.Mi
|
|
||||||
base_cmd = "mkisofs"
|
|
||||||
if subformat == 'udf':
|
|
||||||
# depending on the distribution mkisofs may not support udf
|
|
||||||
# and may be provided by genisoimage instead. As a result we
|
|
||||||
# need to check if the command supports udf via help
|
|
||||||
# instead of checking the installed version.
|
|
||||||
# mkisofs --help outputs to stderr so we need to
|
|
||||||
# redirect it to stdout to use grep.
|
|
||||||
try:
|
|
||||||
subprocess.check_output(
|
|
||||||
'mkisofs --help 2>&1 | grep udf', shell=True)
|
|
||||||
except Exception:
|
|
||||||
self.skipTest('mkisofs does not support udf format')
|
|
||||||
base_cmd += " -udf"
|
|
||||||
prefix = TEST_IMAGE_PREFIX
|
|
||||||
prefix += '-%s-' % subformat
|
|
||||||
fn = tempfile.mktemp(prefix=prefix, suffix='.iso')
|
|
||||||
self._created_files.append(fn)
|
|
||||||
subprocess.check_output(
|
|
||||||
'dd if=/dev/zero of=%s bs=1M count=%i' % (fn, size),
|
|
||||||
shell=True)
|
|
||||||
# We need to use different file as input and output as the behavior
|
|
||||||
# of mkisofs is version dependent if both the input and the output
|
|
||||||
# are the same and can cause test failures
|
|
||||||
out_fn = "%s.iso" % fn
|
|
||||||
subprocess.check_output(
|
|
||||||
'%s -V "TEST" -o %s %s' % (base_cmd, out_fn, fn),
|
|
||||||
shell=True)
|
|
||||||
self._created_files.append(out_fn)
|
|
||||||
return out_fn
|
|
||||||
|
|
||||||
def _create_img(
|
|
||||||
self, fmt, size, subformat=None, options=None,
|
|
||||||
backing_file=None):
|
|
||||||
"""Create an image file of the given format and size.
|
|
||||||
|
|
||||||
:param fmt: The format to create
|
|
||||||
:param size: The size of the image to create in bytes
|
|
||||||
:param subformat: The subformat to use, if any
|
|
||||||
:param options: A dictionary of options to pass to the format
|
|
||||||
:param backing_file: The backing file to use, if any
|
|
||||||
"""
|
|
||||||
|
|
||||||
if fmt == 'iso':
|
|
||||||
return self._create_iso(size, subformat)
|
|
||||||
|
|
||||||
if fmt == 'vhd':
|
|
||||||
# QEMU calls the vhd format vpc
|
|
||||||
fmt = 'vpc'
|
|
||||||
|
|
||||||
# these tests depend on qemu-img being installed and in the path,
|
|
||||||
# if it is not installed, skip. we also need to ensure that the
|
|
||||||
# format is supported by qemu-img, this can vary depending on the
|
|
||||||
# distribution so we need to check if the format is supported via
|
|
||||||
# the help output.
|
|
||||||
try:
|
|
||||||
subprocess.check_output(
|
|
||||||
'qemu-img --help | grep %s' % fmt, shell=True)
|
|
||||||
except Exception:
|
|
||||||
self.skipTest(
|
|
||||||
'qemu-img not installed or does not support %s format' % fmt)
|
|
||||||
|
|
||||||
if options is None:
|
|
||||||
options = {}
|
|
||||||
opt = ''
|
|
||||||
prefix = TEST_IMAGE_PREFIX
|
|
||||||
|
|
||||||
if subformat:
|
|
||||||
options['subformat'] = subformat
|
|
||||||
prefix += subformat + '-'
|
|
||||||
|
|
||||||
if options:
|
|
||||||
opt += '-o ' + ','.join('%s=%s' % (k, v)
|
|
||||||
for k, v in options.items())
|
|
||||||
|
|
||||||
if backing_file is not None:
|
|
||||||
opt += ' -b %s -F raw' % backing_file
|
|
||||||
|
|
||||||
fn = tempfile.mktemp(prefix=prefix,
|
|
||||||
suffix='.%s' % fmt)
|
|
||||||
self._created_files.append(fn)
|
|
||||||
subprocess.check_output(
|
|
||||||
'qemu-img create -f %s %s %s %i' % (fmt, opt, fn, size),
|
|
||||||
shell=True)
|
|
||||||
return fn
|
|
||||||
|
|
||||||
def _create_allocated_vmdk(self, size_mb, subformat=None):
|
|
||||||
# We need a "big" VMDK file to exercise some parts of the code of the
|
|
||||||
# format_inspector. A way to create one is to first create an empty
|
|
||||||
# file, and then to convert it with the -S 0 option.
|
|
||||||
|
|
||||||
if subformat is None:
|
|
||||||
# Matches qemu-img default, see `qemu-img convert -O vmdk -o help`
|
|
||||||
subformat = 'monolithicSparse'
|
|
||||||
|
|
||||||
prefix = TEST_IMAGE_PREFIX
|
|
||||||
prefix += '-%s-' % subformat
|
|
||||||
fn = tempfile.mktemp(prefix=prefix, suffix='.vmdk')
|
|
||||||
self._created_files.append(fn)
|
|
||||||
raw = tempfile.mktemp(prefix=prefix, suffix='.raw')
|
|
||||||
self._created_files.append(raw)
|
|
||||||
|
|
||||||
# Create a file with pseudo-random data, otherwise it will get
|
|
||||||
# compressed in the streamOptimized format
|
|
||||||
subprocess.check_output(
|
|
||||||
'dd if=/dev/urandom of=%s bs=1M count=%i' % (raw, size_mb),
|
|
||||||
shell=True)
|
|
||||||
|
|
||||||
# Convert it to VMDK
|
|
||||||
subprocess.check_output(
|
|
||||||
'qemu-img convert -f raw -O vmdk -o subformat=%s -S 0 %s %s' % (
|
|
||||||
subformat, raw, fn),
|
|
||||||
shell=True)
|
|
||||||
return fn
|
|
||||||
|
|
||||||
def _test_format_at_block_size(self, format_name, img, block_size):
|
|
||||||
fmt = format_inspector.get_inspector(format_name)()
|
|
||||||
self.assertIsNotNone(fmt,
|
|
||||||
'Did not get format inspector for %s' % (
|
|
||||||
format_name))
|
|
||||||
wrapper = format_inspector.InfoWrapper(open(img, 'rb'), fmt)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
chunk = wrapper.read(block_size)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
|
|
||||||
wrapper.close()
|
|
||||||
return fmt
|
|
||||||
|
|
||||||
def _test_format_at_image_size(self, format_name, image_size,
|
|
||||||
subformat=None):
|
|
||||||
"""Test the format inspector for the given format at the given image size.
|
|
||||||
|
|
||||||
:param format_name: The format to test
|
|
||||||
:param image_size: The size of the image to create in bytes
|
|
||||||
:param subformat: The subformat to use, if any
|
|
||||||
""" # noqa
|
|
||||||
img = self._create_img(format_name, image_size, subformat=subformat)
|
|
||||||
|
|
||||||
# Some formats have internal alignment restrictions making this not
|
|
||||||
# always exactly like image_size, so get the real value for comparison
|
|
||||||
virtual_size = get_size_from_qemu_img(img)
|
|
||||||
|
|
||||||
# Read the format in various sizes, some of which will read whole
|
|
||||||
# sections in a single read, others will be completely unaligned, etc.
|
|
||||||
block_sizes = [64 * units.Ki, 1 * units.Mi]
|
|
||||||
# ISO images have a 32KB system area at the beginning of the image
|
|
||||||
# as a result reading that in 17 or 512 byte blocks takes too long,
|
|
||||||
# causing the test to fail. The 64KiB block size is enough to read
|
|
||||||
# the system area and header in a single read. the 1MiB block size
|
|
||||||
# adds very little time to the test so we include it.
|
|
||||||
if format_name != 'iso':
|
|
||||||
block_sizes.extend([17, 512])
|
|
||||||
for block_size in block_sizes:
|
|
||||||
fmt = self._test_format_at_block_size(format_name, img, block_size)
|
|
||||||
self.assertTrue(fmt.format_match,
|
|
||||||
'Failed to match %s at size %i block %i' % (
|
|
||||||
format_name, image_size, block_size))
|
|
||||||
self.assertEqual(virtual_size, fmt.virtual_size,
|
|
||||||
('Failed to calculate size for %s at size %i '
|
|
||||||
'block %i') % (format_name, image_size,
|
|
||||||
block_size))
|
|
||||||
memory = sum(fmt.context_info.values())
|
|
||||||
self.assertLess(memory, 512 * units.Ki,
|
|
||||||
'Format used more than 512KiB of memory: %s' % (
|
|
||||||
fmt.context_info))
|
|
||||||
|
|
||||||
def _test_format(self, format_name, subformat=None):
|
|
||||||
# Try a few different image sizes, including some odd and very small
|
|
||||||
# sizes
|
|
||||||
for image_size in (512, 513, 2057, 7):
|
|
||||||
self._test_format_at_image_size(format_name, image_size * units.Mi,
|
|
||||||
subformat=subformat)
|
|
||||||
|
|
||||||
def test_qcow2(self):
|
|
||||||
self._test_format('qcow2')
|
|
||||||
|
|
||||||
def test_iso_9660(self):
|
|
||||||
self._test_format('iso', subformat='9660')
|
|
||||||
|
|
||||||
def test_iso_udf(self):
|
|
||||||
self._test_format('iso', subformat='udf')
|
|
||||||
|
|
||||||
def _generate_bad_iso(self):
|
|
||||||
# we want to emulate a malicious user who uploads a an
|
|
||||||
# ISO file has a qcow2 header in the system area
|
|
||||||
# of the ISO file
|
|
||||||
# we will create a qcow2 image and an ISO file
|
|
||||||
# and then copy the qcow2 header to the ISO file
|
|
||||||
# e.g.
|
|
||||||
# mkisofs -o orig.iso /etc/resolv.conf
|
|
||||||
# qemu-img create orig.qcow2 -f qcow2 64M
|
|
||||||
# dd if=orig.qcow2 of=outcome bs=32K count=1
|
|
||||||
# dd if=orig.iso of=outcome bs=32K skip=1 seek=1
|
|
||||||
|
|
||||||
qcow = self._create_img('qcow2', 10 * units.Mi)
|
|
||||||
iso = self._create_iso(64 * units.Mi, subformat='9660')
|
|
||||||
# first ensure the files are valid
|
|
||||||
iso_fmt = self._test_format_at_block_size('iso', iso, 4 * units.Ki)
|
|
||||||
self.assertTrue(iso_fmt.format_match)
|
|
||||||
qcow_fmt = self._test_format_at_block_size('qcow2', qcow, 4 * units.Ki)
|
|
||||||
self.assertTrue(qcow_fmt.format_match)
|
|
||||||
# now copy the qcow2 header to an ISO file
|
|
||||||
prefix = TEST_IMAGE_PREFIX
|
|
||||||
prefix += '-bad-'
|
|
||||||
fn = tempfile.mktemp(prefix=prefix, suffix='.iso')
|
|
||||||
self._created_files.append(fn)
|
|
||||||
subprocess.check_output(
|
|
||||||
'dd if=%s of=%s bs=32K count=1' % (qcow, fn),
|
|
||||||
shell=True)
|
|
||||||
subprocess.check_output(
|
|
||||||
'dd if=%s of=%s bs=32K skip=1 seek=1' % (iso, fn),
|
|
||||||
shell=True)
|
|
||||||
return qcow, iso, fn
|
|
||||||
|
|
||||||
def test_bad_iso_qcow2(self):
|
|
||||||
|
|
||||||
_, _, fn = self._generate_bad_iso()
|
|
||||||
|
|
||||||
iso_check = self._test_format_at_block_size('iso', fn, 4 * units.Ki)
|
|
||||||
qcow_check = self._test_format_at_block_size('qcow2', fn, 4 * units.Ki)
|
|
||||||
# this system area of the ISO file is not considered part of the format
|
|
||||||
# the qcow2 header is in the system area of the ISO file
|
|
||||||
# so the ISO file is still valid
|
|
||||||
self.assertTrue(iso_check.format_match)
|
|
||||||
# the qcow2 header is in the system area of the ISO file
|
|
||||||
# but that will be parsed by the qcow2 format inspector
|
|
||||||
# and it will match
|
|
||||||
self.assertTrue(qcow_check.format_match)
|
|
||||||
# if we call format_inspector.detect_file_format it should detect
|
|
||||||
# and raise an exception because both match internally.
|
|
||||||
e = self.assertRaises(
|
|
||||||
format_inspector.ImageFormatError,
|
|
||||||
format_inspector.detect_file_format, fn)
|
|
||||||
self.assertIn('Multiple formats detected', str(e))
|
|
||||||
|
|
||||||
def test_vhd(self):
|
|
||||||
self._test_format('vhd')
|
|
||||||
|
|
||||||
# NOTE(TheJulia): This is not a supported format, and we know this
|
|
||||||
# test can timeout due to some of the inner workings. Overall the
|
|
||||||
# code voered by this is being moved to oslo in the future, so this
|
|
||||||
# test being in ironic is also not the needful.
|
|
||||||
# def test_vhdx(self):
|
|
||||||
# self._test_format('vhdx')
|
|
||||||
|
|
||||||
def test_vmdk(self):
|
|
||||||
self._test_format('vmdk')
|
|
||||||
|
|
||||||
def test_vmdk_stream_optimized(self):
|
|
||||||
self._test_format('vmdk', 'streamOptimized')
|
|
||||||
|
|
||||||
def test_from_file_reads_minimum(self):
|
|
||||||
img = self._create_img('qcow2', 10 * units.Mi)
|
|
||||||
file_size = os.stat(img).st_size
|
|
||||||
fmt = format_inspector.QcowInspector.from_file(img)
|
|
||||||
# We know everything we need from the first 512 bytes of a QCOW image,
|
|
||||||
# so make sure that we did not read the whole thing when we inspect
|
|
||||||
# a local file.
|
|
||||||
self.assertLess(fmt.actual_size, file_size)
|
|
||||||
|
|
||||||
def test_qed_always_unsafe(self):
|
|
||||||
img = self._create_img('qed', 10 * units.Mi)
|
|
||||||
fmt = format_inspector.get_inspector('qed').from_file(img)
|
|
||||||
self.assertTrue(fmt.format_match)
|
|
||||||
self.assertFalse(fmt.safety_check())
|
|
||||||
|
|
||||||
def _test_vmdk_bad_descriptor_offset(self, subformat=None):
|
|
||||||
format_name = 'vmdk'
|
|
||||||
image_size = 10 * units.Mi
|
|
||||||
descriptorOffsetAddr = 0x1c
|
|
||||||
BAD_ADDRESS = 0x400
|
|
||||||
img = self._create_img(format_name, image_size, subformat=subformat)
|
|
||||||
|
|
||||||
# Corrupt the header
|
|
||||||
fd = open(img, 'r+b')
|
|
||||||
fd.seek(descriptorOffsetAddr)
|
|
||||||
fd.write(struct.pack('<Q', BAD_ADDRESS // 512))
|
|
||||||
fd.close()
|
|
||||||
|
|
||||||
# Read the format in various sizes, some of which will read whole
|
|
||||||
# sections in a single read, others will be completely unaligned, etc.
|
|
||||||
for block_size in (64 * units.Ki, 512, 17, 1 * units.Mi):
|
|
||||||
fmt = self._test_format_at_block_size(format_name, img, block_size)
|
|
||||||
self.assertTrue(fmt.format_match,
|
|
||||||
'Failed to match %s at size %i block %i' % (
|
|
||||||
format_name, image_size, block_size))
|
|
||||||
self.assertEqual(0, fmt.virtual_size,
|
|
||||||
('Calculated a virtual size for a corrupt %s at '
|
|
||||||
'size %i block %i') % (format_name, image_size,
|
|
||||||
block_size))
|
|
||||||
|
|
||||||
def test_vmdk_bad_descriptor_offset(self):
|
|
||||||
self._test_vmdk_bad_descriptor_offset()
|
|
||||||
|
|
||||||
def test_vmdk_bad_descriptor_offset_stream_optimized(self):
|
|
||||||
self._test_vmdk_bad_descriptor_offset(subformat='streamOptimized')
|
|
||||||
|
|
||||||
def _test_vmdk_bad_descriptor_mem_limit(self, subformat=None):
|
|
||||||
format_name = 'vmdk'
|
|
||||||
image_size = 5 * units.Mi
|
|
||||||
virtual_size = 5 * units.Mi
|
|
||||||
descriptorOffsetAddr = 0x1c
|
|
||||||
descriptorSizeAddr = descriptorOffsetAddr + 8
|
|
||||||
twoMBInSectors = (2 << 20) // 512
|
|
||||||
# We need a big VMDK because otherwise we will not have enough data to
|
|
||||||
# fill-up the CaptureRegion.
|
|
||||||
img = self._create_allocated_vmdk(image_size // units.Mi,
|
|
||||||
subformat=subformat)
|
|
||||||
|
|
||||||
# Corrupt the end of descriptor address so it "ends" at 2MB
|
|
||||||
fd = open(img, 'r+b')
|
|
||||||
fd.seek(descriptorSizeAddr)
|
|
||||||
fd.write(struct.pack('<Q', twoMBInSectors))
|
|
||||||
fd.close()
|
|
||||||
|
|
||||||
# Read the format in various sizes, some of which will read whole
|
|
||||||
# sections in a single read, others will be completely unaligned, etc.
|
|
||||||
for block_size in (64 * units.Ki, 512, 17, 1 * units.Mi):
|
|
||||||
fmt = self._test_format_at_block_size(format_name, img, block_size)
|
|
||||||
self.assertTrue(fmt.format_match,
|
|
||||||
'Failed to match %s at size %i block %i' % (
|
|
||||||
format_name, image_size, block_size))
|
|
||||||
self.assertEqual(virtual_size, fmt.virtual_size,
|
|
||||||
('Failed to calculate size for %s at size %i '
|
|
||||||
'block %i') % (format_name, image_size,
|
|
||||||
block_size))
|
|
||||||
memory = sum(fmt.context_info.values())
|
|
||||||
self.assertLess(memory, 1.5 * units.Mi,
|
|
||||||
'Format used more than 1.5MiB of memory: %s' % (
|
|
||||||
fmt.context_info))
|
|
||||||
|
|
||||||
def test_vmdk_bad_descriptor_mem_limit(self):
|
|
||||||
self._test_vmdk_bad_descriptor_mem_limit()
|
|
||||||
|
|
||||||
def test_vmdk_bad_descriptor_mem_limit_stream_optimized(self):
|
|
||||||
self._test_vmdk_bad_descriptor_mem_limit(subformat='streamOptimized')
|
|
||||||
|
|
||||||
def test_qcow2_safety_checks(self):
|
|
||||||
# Create backing and data-file names (and initialize the backing file)
|
|
||||||
backing_fn = tempfile.mktemp(prefix='backing')
|
|
||||||
self._created_files.append(backing_fn)
|
|
||||||
with open(backing_fn, 'w') as f:
|
|
||||||
f.write('foobar')
|
|
||||||
data_fn = tempfile.mktemp(prefix='data')
|
|
||||||
self._created_files.append(data_fn)
|
|
||||||
|
|
||||||
# A qcow with no backing or data file is safe
|
|
||||||
fn = self._create_img('qcow2', 5 * units.Mi, None)
|
|
||||||
inspector = format_inspector.QcowInspector.from_file(fn)
|
|
||||||
self.assertTrue(inspector.safety_check())
|
|
||||||
|
|
||||||
# A backing file makes it unsafe
|
|
||||||
fn = self._create_img('qcow2', 5 * units.Mi, None,
|
|
||||||
backing_file=backing_fn)
|
|
||||||
inspector = format_inspector.QcowInspector.from_file(fn)
|
|
||||||
self.assertFalse(inspector.safety_check())
|
|
||||||
|
|
||||||
# A data-file makes it unsafe
|
|
||||||
fn = self._create_img('qcow2', 5 * units.Mi,
|
|
||||||
options={'data_file': data_fn,
|
|
||||||
'data_file_raw': 'on'})
|
|
||||||
inspector = format_inspector.QcowInspector.from_file(fn)
|
|
||||||
self.assertFalse(inspector.safety_check())
|
|
||||||
|
|
||||||
# Trying to load a non-QCOW file is an error
|
|
||||||
self.assertRaises(format_inspector.ImageFormatError,
|
|
||||||
format_inspector.QcowInspector.from_file,
|
|
||||||
backing_fn)
|
|
||||||
|
|
||||||
def test_qcow2_feature_flag_checks(self):
|
|
||||||
data = bytearray(512)
|
|
||||||
data[0:4] = b'QFI\xFB'
|
|
||||||
inspector = format_inspector.QcowInspector()
|
|
||||||
inspector.region('header').data = data
|
|
||||||
|
|
||||||
# All zeros, no feature flags - all good
|
|
||||||
self.assertFalse(inspector.has_unknown_features)
|
|
||||||
|
|
||||||
# A feature flag set in the first byte (highest-order) is not
|
|
||||||
# something we know about, so fail.
|
|
||||||
data[0x48] = 0x01
|
|
||||||
self.assertTrue(inspector.has_unknown_features)
|
|
||||||
|
|
||||||
# The first bit in the last byte (lowest-order) is known (the dirty
|
|
||||||
# bit) so that should pass
|
|
||||||
data[0x48] = 0x00
|
|
||||||
data[0x4F] = 0x01
|
|
||||||
self.assertFalse(inspector.has_unknown_features)
|
|
||||||
|
|
||||||
# Currently (as of 2024), the high-order feature flag bit in the low-
|
|
||||||
# order byte is not assigned, so make sure we reject it.
|
|
||||||
data[0x4F] = 0x80
|
|
||||||
self.assertTrue(inspector.has_unknown_features)
|
|
||||||
|
|
||||||
def test_vdi(self):
|
|
||||||
self._test_format('vdi')
|
|
||||||
|
|
||||||
def _test_format_with_invalid_data(self, format_name):
|
|
||||||
fmt = format_inspector.get_inspector(format_name)()
|
|
||||||
wrapper = format_inspector.InfoWrapper(open(__file__, 'rb'), fmt)
|
|
||||||
while True:
|
|
||||||
chunk = wrapper.read(32)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
|
|
||||||
wrapper.close()
|
|
||||||
self.assertFalse(fmt.format_match)
|
|
||||||
self.assertEqual(0, fmt.virtual_size)
|
|
||||||
memory = sum(fmt.context_info.values())
|
|
||||||
self.assertLess(memory, 512 * units.Ki,
|
|
||||||
'Format used more than 512KiB of memory: %s' % (
|
|
||||||
fmt.context_info))
|
|
||||||
|
|
||||||
def test_qcow2_invalid(self):
|
|
||||||
self._test_format_with_invalid_data('qcow2')
|
|
||||||
|
|
||||||
def test_vhd_invalid(self):
|
|
||||||
self._test_format_with_invalid_data('vhd')
|
|
||||||
|
|
||||||
def test_vhdx_invalid(self):
|
|
||||||
self._test_format_with_invalid_data('vhdx')
|
|
||||||
|
|
||||||
def test_vmdk_invalid(self):
|
|
||||||
self._test_format_with_invalid_data('vmdk')
|
|
||||||
|
|
||||||
def test_vdi_invalid(self):
|
|
||||||
self._test_format_with_invalid_data('vdi')
|
|
||||||
|
|
||||||
def test_vmdk_invalid_type(self):
|
|
||||||
fmt = format_inspector.get_inspector('vmdk')()
|
|
||||||
wrapper = format_inspector.InfoWrapper(open(__file__, 'rb'), fmt)
|
|
||||||
while True:
|
|
||||||
chunk = wrapper.read(32)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
|
|
||||||
wrapper.close()
|
|
||||||
|
|
||||||
fake_rgn = mock.MagicMock()
|
|
||||||
fake_rgn.complete = True
|
|
||||||
fake_rgn.data = b'foocreateType="someunknownformat"bar'
|
|
||||||
|
|
||||||
with mock.patch.object(fmt, 'has_region', return_value=True,
|
|
||||||
autospec=True):
|
|
||||||
with mock.patch.object(fmt, 'region', return_value=fake_rgn,
|
|
||||||
autospec=True):
|
|
||||||
self.assertEqual(0, fmt.virtual_size)
|
|
||||||
|
|
||||||
|
|
||||||
class TestFormatInspectorInfra(test_base.TestCase):
|
|
||||||
def _test_capture_region_bs(self, bs):
|
|
||||||
data = b''.join(chr(x).encode() for x in range(ord('A'), ord('z')))
|
|
||||||
|
|
||||||
regions = [
|
|
||||||
format_inspector.CaptureRegion(3, 9),
|
|
||||||
format_inspector.CaptureRegion(0, 256),
|
|
||||||
format_inspector.CaptureRegion(32, 8),
|
|
||||||
]
|
|
||||||
|
|
||||||
for region in regions:
|
|
||||||
# None of them should be complete yet
|
|
||||||
self.assertFalse(region.complete)
|
|
||||||
|
|
||||||
pos = 0
|
|
||||||
for i in range(0, len(data), bs):
|
|
||||||
chunk = data[i:i + bs]
|
|
||||||
pos += len(chunk)
|
|
||||||
for region in regions:
|
|
||||||
region.capture(chunk, pos)
|
|
||||||
|
|
||||||
self.assertEqual(data[3:12], regions[0].data)
|
|
||||||
self.assertEqual(data[0:256], regions[1].data)
|
|
||||||
self.assertEqual(data[32:40], regions[2].data)
|
|
||||||
|
|
||||||
# The small regions should be complete
|
|
||||||
self.assertTrue(regions[0].complete)
|
|
||||||
self.assertTrue(regions[2].complete)
|
|
||||||
|
|
||||||
# This region extended past the available data, so not complete
|
|
||||||
self.assertFalse(regions[1].complete)
|
|
||||||
|
|
||||||
def test_capture_region(self):
|
|
||||||
for block_size in (1, 3, 7, 13, 32, 64):
|
|
||||||
self._test_capture_region_bs(block_size)
|
|
||||||
|
|
||||||
def _get_wrapper(self, data):
|
|
||||||
source = io.BytesIO(data)
|
|
||||||
fake_fmt = mock.create_autospec(format_inspector.get_inspector('raw'))
|
|
||||||
return format_inspector.InfoWrapper(source, fake_fmt)
|
|
||||||
|
|
||||||
def test_info_wrapper_file_like(self):
|
|
||||||
data = b''.join(chr(x).encode() for x in range(ord('A'), ord('z')))
|
|
||||||
wrapper = self._get_wrapper(data)
|
|
||||||
|
|
||||||
read_data = b''
|
|
||||||
while True:
|
|
||||||
chunk = wrapper.read(8)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
read_data += chunk
|
|
||||||
|
|
||||||
self.assertEqual(data, read_data)
|
|
||||||
|
|
||||||
def test_info_wrapper_iter_like(self):
|
|
||||||
data = b''.join(chr(x).encode() for x in range(ord('A'), ord('z')))
|
|
||||||
wrapper = self._get_wrapper(data)
|
|
||||||
|
|
||||||
read_data = b''
|
|
||||||
for chunk in wrapper:
|
|
||||||
read_data += chunk
|
|
||||||
|
|
||||||
self.assertEqual(data, read_data)
|
|
||||||
|
|
||||||
def test_info_wrapper_file_like_eats_error(self):
|
|
||||||
wrapper = self._get_wrapper(b'123456')
|
|
||||||
wrapper._format.eat_chunk.side_effect = Exception('fail')
|
|
||||||
|
|
||||||
data = b''
|
|
||||||
while True:
|
|
||||||
chunk = wrapper.read(3)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
data += chunk
|
|
||||||
|
|
||||||
# Make sure we got all the data despite the error
|
|
||||||
self.assertEqual(b'123456', data)
|
|
||||||
|
|
||||||
# Make sure we only called this once and never again after
|
|
||||||
# the error was raised
|
|
||||||
wrapper._format.eat_chunk.assert_called_once_with(b'123')
|
|
||||||
|
|
||||||
def test_info_wrapper_iter_like_eats_error(self):
|
|
||||||
fake_fmt = mock.create_autospec(format_inspector.get_inspector('raw'))
|
|
||||||
wrapper = format_inspector.InfoWrapper(iter([b'123', b'456']),
|
|
||||||
fake_fmt)
|
|
||||||
fake_fmt.eat_chunk.side_effect = Exception('fail')
|
|
||||||
|
|
||||||
data = b''
|
|
||||||
for chunk in wrapper:
|
|
||||||
data += chunk
|
|
||||||
|
|
||||||
# Make sure we got all the data despite the error
|
|
||||||
self.assertEqual(b'123456', data)
|
|
||||||
|
|
||||||
# Make sure we only called this once and never again after
|
|
||||||
# the error was raised
|
|
||||||
fake_fmt.eat_chunk.assert_called_once_with(b'123')
|
|
||||||
|
|
||||||
def test_get_inspector(self):
|
|
||||||
self.assertEqual(format_inspector.QcowInspector,
|
|
||||||
format_inspector.get_inspector('qcow2'))
|
|
||||||
self.assertIsNone(format_inspector.get_inspector('foo'))
|
|
||||||
|
|
||||||
|
|
||||||
class TestFormatInspectorsTargeted(test_base.TestCase):
|
|
||||||
def _make_vhd_meta(self, guid_raw, item_length):
|
|
||||||
# Meta region header, padded to 32 bytes
|
|
||||||
data = struct.pack('<8sHH', b'metadata', 0, 1)
|
|
||||||
data += b'0' * 20
|
|
||||||
|
|
||||||
# Metadata table entry, 16-byte GUID, 12-byte information,
|
|
||||||
# padded to 32-bytes
|
|
||||||
data += guid_raw
|
|
||||||
data += struct.pack('<III', 256, item_length, 0)
|
|
||||||
data += b'0' * 6
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def test_vhd_table_over_limit(self):
|
|
||||||
ins = format_inspector.VHDXInspector()
|
|
||||||
meta = format_inspector.CaptureRegion(0, 0)
|
|
||||||
desired = b'012345678ABCDEF0'
|
|
||||||
# This is a poorly-crafted image that specifies a larger table size
|
|
||||||
# than is allowed
|
|
||||||
meta.data = self._make_vhd_meta(desired, 33 * 2048)
|
|
||||||
ins.new_region('metadata', meta)
|
|
||||||
new_region = ins._find_meta_entry(ins._guid(desired))
|
|
||||||
# Make sure we clamp to our limit of 32 * 2048
|
|
||||||
self.assertEqual(
|
|
||||||
format_inspector.VHDXInspector.VHDX_METADATA_TABLE_MAX_SIZE,
|
|
||||||
new_region.length)
|
|
||||||
|
|
||||||
def test_vhd_table_under_limit(self):
|
|
||||||
ins = format_inspector.VHDXInspector()
|
|
||||||
meta = format_inspector.CaptureRegion(0, 0)
|
|
||||||
desired = b'012345678ABCDEF0'
|
|
||||||
meta.data = self._make_vhd_meta(desired, 16 * 2048)
|
|
||||||
ins.new_region('metadata', meta)
|
|
||||||
new_region = ins._find_meta_entry(ins._guid(desired))
|
|
||||||
# Table size was under the limit, make sure we get it back
|
|
||||||
self.assertEqual(16 * 2048, new_region.length)
|
|
@ -24,10 +24,10 @@ from unittest import mock
|
|||||||
from oslo_concurrency import processutils
|
from oslo_concurrency import processutils
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_utils import fileutils
|
from oslo_utils import fileutils
|
||||||
|
from oslo_utils.imageutils import format_inspector as image_format_inspector
|
||||||
|
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
from ironic.common.glance_service import service_utils as glance_utils
|
from ironic.common.glance_service import service_utils as glance_utils
|
||||||
from ironic.common import image_format_inspector
|
|
||||||
from ironic.common import image_service
|
from ironic.common import image_service
|
||||||
from ironic.common import images
|
from ironic.common import images
|
||||||
from ironic.common import qemu_img
|
from ironic.common import qemu_img
|
||||||
@ -174,7 +174,6 @@ class IronicImagesTestCase(base.TestCase):
|
|||||||
info = mock.MagicMock()
|
info = mock.MagicMock()
|
||||||
# In the case the image looks okay, but it is not in our permitted
|
# In the case the image looks okay, but it is not in our permitted
|
||||||
# format list, we need to ensure we still fail appropriately.
|
# format list, we need to ensure we still fail appropriately.
|
||||||
info.safety_check.return_value = True
|
|
||||||
info.__str__.return_value = 'vhd'
|
info.__str__.return_value = 'vhd'
|
||||||
detect_format_mock.return_value = info
|
detect_format_mock.return_value = info
|
||||||
|
|
||||||
@ -189,7 +188,8 @@ class IronicImagesTestCase(base.TestCase):
|
|||||||
def test_image_to_raw_fails_safety_check(self, detect_format_mock):
|
def test_image_to_raw_fails_safety_check(self, detect_format_mock):
|
||||||
info = mock.MagicMock()
|
info = mock.MagicMock()
|
||||||
info.__str__.return_value = 'qcow2'
|
info.__str__.return_value = 'qcow2'
|
||||||
info.safety_check.return_value = False
|
info.safety_check.side_effect = \
|
||||||
|
image_format_inspector.SafetyCheckFailed({"I'm a teapot": True})
|
||||||
detect_format_mock.return_value = info
|
detect_format_mock.return_value = info
|
||||||
|
|
||||||
e = self.assertRaises(exception.ImageUnacceptable, images.image_to_raw,
|
e = self.assertRaises(exception.ImageUnacceptable, images.image_to_raw,
|
||||||
@ -197,6 +197,8 @@ class IronicImagesTestCase(base.TestCase):
|
|||||||
info.safety_check.assert_called_once()
|
info.safety_check.assert_called_once()
|
||||||
detect_format_mock.assert_called_once_with('path_tmp')
|
detect_format_mock.assert_called_once_with('path_tmp')
|
||||||
self.assertIn("The requested image is not valid for use.", str(e))
|
self.assertIn("The requested image is not valid for use.", str(e))
|
||||||
|
# Do not disclose the actual error message to evil hackers
|
||||||
|
self.assertNotIn("I'm a teapot", str(e))
|
||||||
|
|
||||||
@mock.patch.object(os, 'rename', autospec=True)
|
@mock.patch.object(os, 'rename', autospec=True)
|
||||||
@mock.patch.object(os, 'unlink', autospec=True)
|
@mock.patch.object(os, 'unlink', autospec=True)
|
||||||
@ -209,7 +211,6 @@ class IronicImagesTestCase(base.TestCase):
|
|||||||
info = mock.MagicMock()
|
info = mock.MagicMock()
|
||||||
info.__str__.side_effect = iter(['qcow2', 'raw'])
|
info.__str__.side_effect = iter(['qcow2', 'raw'])
|
||||||
info.backing_file = None
|
info.backing_file = None
|
||||||
info.saftey_check.return_value = True
|
|
||||||
detect_format_mock.return_value = info
|
detect_format_mock.return_value = info
|
||||||
|
|
||||||
def convert_side_effect(source, dest, out_format, source_format):
|
def convert_side_effect(source, dest, out_format, source_format):
|
||||||
@ -228,6 +229,67 @@ class IronicImagesTestCase(base.TestCase):
|
|||||||
unlink_mock.assert_called_once_with('path_tmp')
|
unlink_mock.assert_called_once_with('path_tmp')
|
||||||
rename_mock.assert_called_once_with('path.converted', 'path')
|
rename_mock.assert_called_once_with('path.converted', 'path')
|
||||||
|
|
||||||
|
@mock.patch.object(os, 'rename', autospec=True)
|
||||||
|
@mock.patch.object(os, 'unlink', autospec=True)
|
||||||
|
@mock.patch.object(qemu_img, 'convert_image', autospec=True)
|
||||||
|
@mock.patch.object(image_format_inspector, 'detect_file_format',
|
||||||
|
autospec=True)
|
||||||
|
def test_image_to_gpt(self, detect_format_mock, convert_image_mock,
|
||||||
|
unlink_mock, rename_mock):
|
||||||
|
CONF.set_override('force_raw_images', True)
|
||||||
|
info = mock.MagicMock()
|
||||||
|
info.__str__.side_effect = iter(['qcow2', 'gpt'])
|
||||||
|
info.backing_file = None
|
||||||
|
detect_format_mock.return_value = info
|
||||||
|
|
||||||
|
def convert_side_effect(source, dest, out_format, source_format):
|
||||||
|
info.file_format = 'gpt'
|
||||||
|
convert_image_mock.side_effect = convert_side_effect
|
||||||
|
|
||||||
|
images.image_to_raw('image_href', 'path', 'path_tmp')
|
||||||
|
info.safety_check.assert_called_once()
|
||||||
|
self.assertEqual(2, info.__str__.call_count)
|
||||||
|
detect_format_mock.assert_has_calls([
|
||||||
|
mock.call('path_tmp'),
|
||||||
|
mock.call('path.converted')])
|
||||||
|
convert_image_mock.assert_called_once_with('path_tmp',
|
||||||
|
'path.converted', 'raw',
|
||||||
|
source_format='qcow2')
|
||||||
|
unlink_mock.assert_called_once_with('path_tmp')
|
||||||
|
rename_mock.assert_called_once_with('path.converted', 'path')
|
||||||
|
|
||||||
|
@mock.patch.object(os, 'rename', autospec=True)
|
||||||
|
@mock.patch.object(os, 'unlink', autospec=True)
|
||||||
|
@mock.patch.object(qemu_img, 'convert_image', autospec=True)
|
||||||
|
@mock.patch.object(image_format_inspector, 'detect_file_format',
|
||||||
|
autospec=True)
|
||||||
|
def test_image_to_gpt_backward_compatibility(self, detect_format_mock,
|
||||||
|
convert_image_mock,
|
||||||
|
unlink_mock, rename_mock):
|
||||||
|
CONF.set_override('force_raw_images', True)
|
||||||
|
CONF.set_override('permitted_image_formats', 'raw,qcow2',
|
||||||
|
group='conductor')
|
||||||
|
info = mock.MagicMock()
|
||||||
|
info.__str__.side_effect = iter(['qcow2', 'gpt'])
|
||||||
|
info.backing_file = None
|
||||||
|
detect_format_mock.return_value = info
|
||||||
|
|
||||||
|
def convert_side_effect(source, dest, out_format, source_format):
|
||||||
|
info.file_format = 'gpt'
|
||||||
|
convert_image_mock.side_effect = convert_side_effect
|
||||||
|
|
||||||
|
images.image_to_raw('image_href', 'path', 'path_tmp')
|
||||||
|
info.safety_check.assert_called_once()
|
||||||
|
self.assertEqual(2, info.__str__.call_count)
|
||||||
|
detect_format_mock.assert_has_calls([
|
||||||
|
mock.call('path_tmp'),
|
||||||
|
mock.call('path.converted')])
|
||||||
|
convert_image_mock.assert_called_once_with('path_tmp',
|
||||||
|
'path.converted', 'raw',
|
||||||
|
source_format='qcow2')
|
||||||
|
unlink_mock.assert_called_once_with('path_tmp')
|
||||||
|
rename_mock.assert_called_once_with('path.converted', 'path')
|
||||||
|
|
||||||
@mock.patch.object(os, 'rename', autospec=True)
|
@mock.patch.object(os, 'rename', autospec=True)
|
||||||
@mock.patch.object(os, 'unlink', autospec=True)
|
@mock.patch.object(os, 'unlink', autospec=True)
|
||||||
@mock.patch.object(qemu_img, 'convert_image', autospec=True)
|
@mock.patch.object(qemu_img, 'convert_image', autospec=True)
|
||||||
@ -242,7 +304,6 @@ class IronicImagesTestCase(base.TestCase):
|
|||||||
info = mock.MagicMock()
|
info = mock.MagicMock()
|
||||||
info.__str__.side_effect = iter(['vmdk', 'raw'])
|
info.__str__.side_effect = iter(['vmdk', 'raw'])
|
||||||
info.backing_file = None
|
info.backing_file = None
|
||||||
info.saftey_check.return_value = None
|
|
||||||
detect_format_mock.return_value = info
|
detect_format_mock.return_value = info
|
||||||
|
|
||||||
def convert_side_effect(source, dest, out_format, source_format):
|
def convert_side_effect(source, dest, out_format, source_format):
|
||||||
@ -274,7 +335,6 @@ class IronicImagesTestCase(base.TestCase):
|
|||||||
info = mock.MagicMock()
|
info = mock.MagicMock()
|
||||||
info.__str__.return_value = 'vmdk'
|
info.__str__.return_value = 'vmdk'
|
||||||
info.backing_file = None
|
info.backing_file = None
|
||||||
info.saftey_check.return_value = None
|
|
||||||
detect_format_mock.return_value = info
|
detect_format_mock.return_value = info
|
||||||
|
|
||||||
self.assertRaises(exception.ImageConvertFailed,
|
self.assertRaises(exception.ImageConvertFailed,
|
||||||
@ -309,7 +369,6 @@ class IronicImagesTestCase(base.TestCase):
|
|||||||
source_format='qcow2')
|
source_format='qcow2')
|
||||||
unlink_mock.assert_called_once_with('path_tmp')
|
unlink_mock.assert_called_once_with('path_tmp')
|
||||||
info.safety_check.assert_called_once()
|
info.safety_check.assert_called_once()
|
||||||
info.safety_check.assert_called_once()
|
|
||||||
self.assertEqual(2, info.__str__.call_count)
|
self.assertEqual(2, info.__str__.call_count)
|
||||||
detect_format_mock.assert_has_calls([
|
detect_format_mock.assert_has_calls([
|
||||||
mock.call('path_tmp'),
|
mock.call('path_tmp'),
|
||||||
@ -328,6 +387,21 @@ class IronicImagesTestCase(base.TestCase):
|
|||||||
|
|
||||||
rename_mock.assert_called_once_with('path_tmp', 'path')
|
rename_mock.assert_called_once_with('path_tmp', 'path')
|
||||||
info.safety_check.assert_called_once()
|
info.safety_check.assert_called_once()
|
||||||
|
self.assertEqual(1, info.__str__.call_count)
|
||||||
|
detect_format_mock.assert_called_once_with('path_tmp')
|
||||||
|
|
||||||
|
@mock.patch.object(os, 'rename', autospec=True)
|
||||||
|
@mock.patch.object(image_format_inspector, 'detect_file_format',
|
||||||
|
autospec=True)
|
||||||
|
def test_image_to_raw_already_gpt_format(self, detect_format_mock,
|
||||||
|
rename_mock):
|
||||||
|
info = mock.MagicMock()
|
||||||
|
info.__str__.return_value = 'gpt'
|
||||||
|
detect_format_mock.return_value = info
|
||||||
|
|
||||||
|
images.image_to_raw('image_href', 'path', 'path_tmp')
|
||||||
|
|
||||||
|
rename_mock.assert_called_once_with('path_tmp', 'path')
|
||||||
info.safety_check.assert_called_once()
|
info.safety_check.assert_called_once()
|
||||||
self.assertEqual(1, info.__str__.call_count)
|
self.assertEqual(1, info.__str__.call_count)
|
||||||
detect_format_mock.assert_called_once_with('path_tmp')
|
detect_format_mock.assert_called_once_with('path_tmp')
|
||||||
|
@ -24,11 +24,11 @@ from unittest import mock
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
from oslo_utils.imageutils import format_inspector as image_format_inspector
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
from ironic.common import image_format_inspector
|
|
||||||
from ironic.common import image_service
|
from ironic.common import image_service
|
||||||
from ironic.common import images
|
from ironic.common import images
|
||||||
from ironic.common import utils
|
from ironic.common import utils
|
||||||
@ -833,6 +833,39 @@ class TestFetchCleanup(base.TestCase):
|
|||||||
image_check.safety_check.assert_called_once()
|
image_check.safety_check.assert_called_once()
|
||||||
self.assertEqual(1, image_check.__str__.call_count)
|
self.assertEqual(1, image_check.__str__.call_count)
|
||||||
|
|
||||||
|
@mock.patch.object(image_format_inspector, 'detect_file_format',
|
||||||
|
autospec=True)
|
||||||
|
@mock.patch.object(images, 'image_show', autospec=True)
|
||||||
|
@mock.patch.object(os, 'remove', autospec=True)
|
||||||
|
@mock.patch.object(images, 'converted_size', autospec=True)
|
||||||
|
@mock.patch.object(images, 'fetch', autospec=True)
|
||||||
|
@mock.patch.object(images, 'image_to_raw', autospec=True)
|
||||||
|
@mock.patch.object(image_cache, '_clean_up_caches', autospec=True)
|
||||||
|
def test__fetch_convert_to_gpt(
|
||||||
|
self, mock_clean, mock_raw, mock_fetch,
|
||||||
|
mock_size, mock_remove, mock_show, mock_format_inspector):
|
||||||
|
image_check = mock.MagicMock()
|
||||||
|
image_check.__str__.side_effect = iter(['qcow2', 'gpt'])
|
||||||
|
image_check.safety_check.return_value = True
|
||||||
|
mock_format_inspector.return_value = image_check
|
||||||
|
mock_show.return_value = {}
|
||||||
|
mock_size.return_value = 100
|
||||||
|
image_cache._fetch('fake', 'fake-uuid', '/foo/bar', force_raw=True,
|
||||||
|
expected_checksum='1234',
|
||||||
|
expected_checksum_algo='md5')
|
||||||
|
mock_fetch.assert_called_once_with('fake', 'fake-uuid',
|
||||||
|
'/foo/bar.part', force_raw=False,
|
||||||
|
checksum='1234',
|
||||||
|
checksum_algo='md5')
|
||||||
|
mock_clean.assert_called_once_with('/foo', 100)
|
||||||
|
mock_raw.assert_called_once_with('fake-uuid', '/foo/bar',
|
||||||
|
'/foo/bar.part')
|
||||||
|
mock_remove.assert_not_called()
|
||||||
|
mock_show.assert_called_once_with('fake', 'fake-uuid')
|
||||||
|
mock_format_inspector.assert_called_once_with('/foo/bar.part')
|
||||||
|
image_check.safety_check.assert_called_once()
|
||||||
|
self.assertEqual(1, image_check.__str__.call_count)
|
||||||
|
|
||||||
@mock.patch.object(image_format_inspector, 'detect_file_format',
|
@mock.patch.object(image_format_inspector, 'detect_file_format',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch.object(images, 'image_show', autospec=True)
|
@mock.patch.object(images, 'image_show', autospec=True)
|
||||||
@ -969,6 +1002,39 @@ class TestFetchCleanup(base.TestCase):
|
|||||||
self.assertEqual(1, image_check.__str__.call_count)
|
self.assertEqual(1, image_check.__str__.call_count)
|
||||||
mock_rename.assert_called_once_with('/foo/bar.part', '/foo/bar')
|
mock_rename.assert_called_once_with('/foo/bar.part', '/foo/bar')
|
||||||
|
|
||||||
|
@mock.patch.object(os, 'rename', autospec=True)
|
||||||
|
@mock.patch.object(image_format_inspector, 'detect_file_format',
|
||||||
|
autospec=True)
|
||||||
|
@mock.patch.object(images, 'image_show', autospec=True)
|
||||||
|
@mock.patch.object(images, 'converted_size', autospec=True)
|
||||||
|
@mock.patch.object(images, 'fetch', autospec=True)
|
||||||
|
@mock.patch.object(images, 'image_to_raw', autospec=True)
|
||||||
|
@mock.patch.object(image_cache, '_clean_up_caches', autospec=True)
|
||||||
|
def test__fetch_already_gpt(
|
||||||
|
self, mock_clean, mock_raw, mock_fetch,
|
||||||
|
mock_size, mock_show, mock_format_inspector,
|
||||||
|
mock_rename):
|
||||||
|
mock_show.return_value = {'disk_format': 'raw'}
|
||||||
|
image_check = mock.MagicMock()
|
||||||
|
image_check.__str__.return_value = 'gpt'
|
||||||
|
image_check.safety_check.return_value = True
|
||||||
|
mock_format_inspector.return_value = image_check
|
||||||
|
image_cache._fetch('fake', 'fake-uuid', '/foo/bar', force_raw=True,
|
||||||
|
expected_checksum='e00',
|
||||||
|
expected_checksum_algo='sha256')
|
||||||
|
mock_fetch.assert_called_once_with('fake', 'fake-uuid',
|
||||||
|
'/foo/bar.part', force_raw=False,
|
||||||
|
checksum='e00',
|
||||||
|
checksum_algo='sha256')
|
||||||
|
mock_clean.assert_not_called()
|
||||||
|
mock_size.assert_not_called()
|
||||||
|
mock_raw.assert_not_called()
|
||||||
|
mock_show.assert_called_once_with('fake', 'fake-uuid')
|
||||||
|
mock_format_inspector.assert_called_once_with('/foo/bar.part')
|
||||||
|
image_check.safety_check.assert_called_once()
|
||||||
|
self.assertEqual(1, image_check.__str__.call_count)
|
||||||
|
mock_rename.assert_called_once_with('/foo/bar.part', '/foo/bar')
|
||||||
|
|
||||||
@mock.patch.object(image_format_inspector, 'detect_file_format',
|
@mock.patch.object(image_format_inspector, 'detect_file_format',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch.object(images, 'image_show', autospec=True)
|
@mock.patch.object(images, 'image_show', autospec=True)
|
||||||
@ -1016,7 +1082,8 @@ class TestFetchCleanup(base.TestCase):
|
|||||||
mock_show.return_value = {'disk_format': 'qcow2'}
|
mock_show.return_value = {'disk_format': 'qcow2'}
|
||||||
image_check = mock.MagicMock()
|
image_check = mock.MagicMock()
|
||||||
image_check.__str__.return_value = 'qcow2'
|
image_check.__str__.return_value = 'qcow2'
|
||||||
image_check.safety_check.return_value = False
|
image_check.safety_check.side_effect = \
|
||||||
|
image_format_inspector.SafetyCheckFailed({"I'm a teapot": True})
|
||||||
mock_format_inspector.return_value = image_check
|
mock_format_inspector.return_value = image_check
|
||||||
self.assertRaises(exception.InvalidImage,
|
self.assertRaises(exception.InvalidImage,
|
||||||
image_cache._fetch,
|
image_cache._fetch,
|
||||||
|
@ -23,7 +23,7 @@ oslo.policy>=4.5.0 # Apache-2.0
|
|||||||
oslo.serialization>=2.25.0 # Apache-2.0
|
oslo.serialization>=2.25.0 # Apache-2.0
|
||||||
oslo.service>=1.24.0 # Apache-2.0
|
oslo.service>=1.24.0 # Apache-2.0
|
||||||
oslo.upgradecheck>=1.3.0 # Apache-2.0
|
oslo.upgradecheck>=1.3.0 # Apache-2.0
|
||||||
oslo.utils>=4.5.0 # Apache-2.0
|
oslo.utils>=7.3.0 # Apache-2.0
|
||||||
osprofiler>=1.5.0 # Apache-2.0
|
osprofiler>=1.5.0 # Apache-2.0
|
||||||
os-traits>=0.4.0 # Apache-2.0
|
os-traits>=0.4.0 # Apache-2.0
|
||||||
pecan>=1.0.0 # BSD
|
pecan>=1.0.0 # BSD
|
||||||
|
Loading…
Reference in New Issue
Block a user