Files
nova/nova/tests/unit/virt/test_images.py
T
Dan Smith d854e7cc16 Use format_inspector from oslo
This removes the in-tree format_inspector and makes us use the one in
oslo.utils. Minor changes are required around the behavior of the
safety_check() (raising instead of returning). This also allows images
detected as 'gpt' but registered as 'raw', since that's how everything
currently works. Hopefully later we can start registering in glance as
gpt, and rejecting boots from raw images.

Change-Id: I5b89e17438157690495961c306861a06532c8b4e
Depends-On: https://review.opendev.org/c/openstack/tempest/+/927146
2024-08-26 07:21:48 -07:00

387 lines
19 KiB
Python

# Copyright 2013 IBM Corp.
#
# 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 os
from unittest import mock
from oslo_concurrency import processutils
from oslo_serialization import jsonutils
from oslo_utils import imageutils
from oslo_utils.imageutils import format_inspector
from nova.compute import utils as compute_utils
from nova import exception
from nova import test
from nova.virt import images
class QemuTestCase(test.NoDBTestCase):
def test_qemu_info_with_bad_path(self):
self.assertRaises(exception.DiskNotFound,
images.qemu_img_info,
'/path/that/does/not/exist')
@mock.patch('oslo_concurrency.processutils.execute')
@mock.patch.object(os.path, 'exists', return_value=True)
def test_qemu_info_with_errors(self, path_exists, mock_exec):
err = processutils.ProcessExecutionError(
exit_code=1, stderr='No such file or directory')
mock_exec.side_effect = err
self.assertRaises(exception.DiskNotFound,
images.qemu_img_info,
'/fake/path')
@mock.patch.object(os.path, 'exists', return_value=True)
@mock.patch('nova.privsep.qemu.unprivileged_qemu_img_info',
return_value={})
def test_qemu_info_with_no_errors(self, path_exists,
utils_execute):
image_info = images.qemu_img_info('/fake/path')
self.assertTrue(image_info)
@mock.patch('nova.privsep.qemu.unprivileged_qemu_img_info',
return_value={})
def test_qemu_info_with_rbd_path(self, utils_execute):
# Assert that the use of a RBD URI as the path doesn't raise
# exception.DiskNotFound
image_info = images.qemu_img_info('rbd:volume/pool')
self.assertTrue(image_info)
@mock.patch.object(compute_utils, 'disk_ops_semaphore')
@mock.patch('nova.privsep.utils.supports_direct_io', return_value=True)
@mock.patch.object(processutils, 'execute',
side_effect=processutils.ProcessExecutionError)
def test_convert_image_with_errors(self, mocked_execute, mock_direct_io,
mock_disk_op_sema):
self.assertRaises(exception.ImageUnacceptable,
images.convert_image,
'/path/that/does/not/exist',
'/other/path/that/does/not/exist',
'qcow2',
'raw')
mock_disk_op_sema.__enter__.assert_called_once()
@mock.patch('oslo_concurrency.processutils.execute')
@mock.patch.object(os.path, 'exists', return_value=True)
def test_convert_image_with_prlimit_fail(self, path, mocked_execute):
mocked_execute.side_effect = \
processutils.ProcessExecutionError(exit_code=-9)
exc = self.assertRaises(exception.InvalidDiskInfo,
images.qemu_img_info,
'/fake/path')
self.assertIn('qemu-img aborted by prlimits', str(exc))
@mock.patch('oslo_concurrency.processutils.execute')
@mock.patch.object(os.path, 'exists', return_value=True)
def test_qemu_img_info_with_disk_not_found(self, exists, mocked_execute):
"""Tests that the initial os.path.exists check passes but the qemu-img
command fails because the path is gone by the time the command runs.
"""
path = '/opt/stack/data/nova/instances/some-uuid/disk'
stderr = (u"qemu-img: Could not open "
"'/opt/stack/data/nova/instances/some-uuid/disk': "
"Could not open '/opt/stack/data/nova/instances/some-uuid/"
"disk': No such file or directory\n")
mocked_execute.side_effect = (
processutils.ProcessExecutionError(
exit_code=1, stderr=stderr))
self.assertRaises(exception.DiskNotFound, images.qemu_img_info, path)
exists.assert_called_once_with(path)
mocked_execute.assert_called_once()
@mock.patch.object(images, 'IMAGE_API')
@mock.patch('oslo_utils.imageutils.format_inspector.detect_file_format')
@mock.patch.object(images, 'convert_image',
side_effect=exception.ImageUnacceptable)
@mock.patch.object(images, 'qemu_img_info')
@mock.patch.object(images, 'fetch')
def test_fetch_to_raw_errors(self, convert_image, qemu_img_info, fetch,
mock_detect, glance):
inspector = mock_detect.return_value
inspector.safety_check.return_value = True
inspector.__str__.return_value = 'qcow2'
glance.get.return_value = {'disk_format': 'qcow2'}
qemu_img_info.backing_file = None
qemu_img_info.file_format = 'qcow2'
qemu_img_info.virtual_size = 20
self.assertRaisesRegex(exception.ImageUnacceptable,
'Image href123 is unacceptable.*',
images.fetch_to_raw,
None, 'href123', '/no/path')
@mock.patch.object(images, 'IMAGE_API')
@mock.patch('oslo_utils.imageutils.format_inspector.detect_file_format')
@mock.patch.object(images, 'convert_image',
side_effect=exception.ImageUnacceptable)
@mock.patch.object(images, 'qemu_img_info')
@mock.patch.object(images, 'fetch')
def test_fetch_to_raw_data_file(self, convert_image, qemu_img_info_fn,
fetch, mock_detect, mock_glance):
mock_glance.get.return_value = {'disk_format': 'qcow2'}
inspector = mock_detect.return_value
inspector.safety_check.return_value = True
inspector.__str__.return_value = 'qcow2'
# NOTE(danms): the above test needs the following line as well, as it
# is broken without it.
qemu_img_info = qemu_img_info_fn.return_value
qemu_img_info.backing_file = None
qemu_img_info.file_format = 'qcow2'
qemu_img_info.virtual_size = 20
qemu_img_info.format_specific = {'data': {'data-file': 'somefile'}}
self.assertRaisesRegex(exception.ImageUnacceptable,
'Image href123 is unacceptable.*somefile',
images.fetch_to_raw,
None, 'href123', '/no/path')
@mock.patch('oslo_utils.imageutils.format_inspector.detect_file_format')
@mock.patch.object(images, 'IMAGE_API')
@mock.patch('os.rename')
@mock.patch.object(images, 'qemu_img_info')
@mock.patch.object(images, 'fetch')
def test_fetch_to_raw_from_raw(self, fetch, qemu_img_info_fn, mock_rename,
mock_glance, mock_detect):
# Make sure we support a case where we fetch an already-raw image and
# qemu-img returns None for "format_specific".
mock_glance.get.return_value = {'disk_format': 'raw'}
mock_detect.return_value.__str__.return_value = 'raw'
qemu_img_info = qemu_img_info_fn.return_value
qemu_img_info.file_format = 'raw'
qemu_img_info.backing_file = None
qemu_img_info.format_specific = None
images.fetch_to_raw(None, 'href123', '/no/path')
mock_rename.assert_called_once_with('/no/path.part', '/no/path')
@mock.patch.object(compute_utils, 'disk_ops_semaphore')
@mock.patch('nova.privsep.utils.supports_direct_io', return_value=True)
@mock.patch('oslo_concurrency.processutils.execute')
def test_convert_image_with_direct_io_support(self, mock_execute,
mock_direct_io,
mock_disk_op_sema):
images._convert_image('source', 'dest', 'in_format', 'out_format',
run_as_root=False)
expected = ('qemu-img', 'convert', '-t', 'none', '-O', 'out_format',
'-f', 'in_format', 'source', 'dest')
mock_disk_op_sema.__enter__.assert_called_once()
self.assertTupleEqual(expected, mock_execute.call_args[0])
@mock.patch.object(compute_utils, 'disk_ops_semaphore')
@mock.patch('nova.privsep.utils.supports_direct_io', return_value=False)
@mock.patch('oslo_concurrency.processutils.execute')
def test_convert_image_without_direct_io_support(self, mock_execute,
mock_direct_io,
mock_disk_op_sema):
images._convert_image('source', 'dest', 'in_format', 'out_format',
run_as_root=False)
expected = ('qemu-img', 'convert', '-t', 'writeback',
'-O', 'out_format', '-f', 'in_format', 'source', 'dest')
mock_disk_op_sema.__enter__.assert_called_once()
self.assertTupleEqual(expected, mock_execute.call_args[0])
def test_convert_image_vmdk_allowed_list_checking(self):
info = {'format': 'vmdk',
'format-specific': {
'type': 'vmdk',
'data': {
'create-type': 'monolithicFlat',
}}}
# If the format is not in the allowed list, we should get an error
self.assertRaises(exception.ImageUnacceptable,
images.check_vmdk_image, 'foo',
imageutils.QemuImgInfo(jsonutils.dumps(info),
format='json'))
# With the format in the allowed list, no error
self.flags(vmdk_allowed_types=['streamOptimized', 'monolithicFlat',
'monolithicSparse'],
group='compute')
images.check_vmdk_image('foo',
imageutils.QemuImgInfo(jsonutils.dumps(info),
format='json'))
# With an empty list, allow nothing
self.flags(vmdk_allowed_types=[], group='compute')
self.assertRaises(exception.ImageUnacceptable,
images.check_vmdk_image, 'foo',
imageutils.QemuImgInfo(jsonutils.dumps(info),
format='json'))
@mock.patch.object(images, 'IMAGE_API')
@mock.patch('oslo_utils.imageutils.format_inspector.detect_file_format')
@mock.patch.object(images, 'fetch')
@mock.patch('nova.privsep.qemu.unprivileged_qemu_img_info')
def test_fetch_checks_vmdk_rules(self, mock_info, mock_fetch, mock_detect,
mock_glance):
mock_glance.get.return_value = {'disk_format': 'vmdk'}
inspector = mock_detect.return_value
inspector.safety_check.return_value = True
inspector.__str__.return_value = 'vmdk'
info = {'format': 'vmdk',
'format-specific': {
'type': 'vmdk',
'data': {
'create-type': 'monolithicFlat',
}}}
mock_info.return_value = jsonutils.dumps(info)
with mock.patch('os.path.exists', return_value=True):
e = self.assertRaises(exception.ImageUnacceptable,
images.fetch_to_raw, None, 'foo', 'anypath')
self.assertIn('Invalid VMDK create-type specified', str(e))
@mock.patch('os.rename')
@mock.patch.object(images, 'IMAGE_API')
@mock.patch('oslo_utils.imageutils.format_inspector.get_inspector')
@mock.patch('oslo_utils.imageutils.format_inspector.detect_file_format')
@mock.patch.object(images, 'fetch')
@mock.patch('nova.privsep.qemu.unprivileged_qemu_img_info')
def test_fetch_iso_is_raw(
self, mock_info, mock_fetch, mock_detect_file_format, mock_gi,
mock_glance, mock_rename):
mock_glance.get.return_value = {'disk_format': 'iso'}
inspector = mock_gi.return_value.from_file.return_value
inspector.safety_check.return_value = True
inspector.__str__.return_value = 'iso'
mock_detect_file_format.return_value = inspector
# qemu-img does not have a parser for iso so it is treated as raw
info = {
"virtual-size": 356352,
"filename": "foo.iso",
"format": "raw",
"actual-size": 356352,
"dirty-flag": False
}
mock_info.return_value = jsonutils.dumps(info)
with mock.patch('os.path.exists', return_value=True):
images.fetch_to_raw(None, 'foo', 'anypath')
# Make sure we called info with -f raw for an iso, since qemu-img does
# not support iso
mock_info.assert_called_once_with('anypath.part', format=None)
# Make sure that since we considered this to be a raw file, we did the
# just-rename-don't-convert path
mock_rename.assert_called_once_with('anypath.part', 'anypath')
@mock.patch.object(images, 'IMAGE_API')
@mock.patch('oslo_utils.imageutils.format_inspector.detect_file_format')
@mock.patch.object(images, 'qemu_img_info')
@mock.patch.object(images, 'fetch')
def test_fetch_to_raw_inspector(self, fetch, qemu_img_info, mock_detect,
mock_glance):
# Image claims to be qcow2, is qcow2, but fails safety check, so we
# abort before qemu-img-info
mock_glance.get.return_value = {'disk_format': 'qcow2'}
inspector = mock_detect.return_value
inspector.safety_check.side_effect = (
format_inspector.SafetyCheckFailed({}))
inspector.__str__.return_value = 'qcow2'
self.assertRaises(exception.ImageUnacceptable,
images.fetch_to_raw, None, 'href123', '/no.path')
qemu_img_info.assert_not_called()
mock_detect.assert_called_once_with('/no.path.part')
inspector.safety_check.assert_called_once_with()
mock_glance.get.assert_called_once_with(None, 'href123')
# Image claims to be qcow2, is qcow2, passes safety check, so we make
# it all the way to qemu-img-info
inspector.safety_check.side_effect = None
qemu_img_info.side_effect = test.TestingException
self.assertRaises(test.TestingException,
images.fetch_to_raw, None, 'href123', '/no.path')
# Image claims to be qcow2 in glance, but the image is something else,
# so we abort before qemu-img-info
qemu_img_info.reset_mock()
mock_detect.reset_mock()
inspector.safety_check.reset_mock()
mock_detect.return_value.__str__.return_value = 'vmdk'
self.assertRaises(exception.ImageUnacceptable,
images.fetch_to_raw, None, 'href123', '/no.path')
mock_detect.assert_called_once_with('/no.path.part')
inspector.safety_check.assert_called_once_with()
qemu_img_info.assert_not_called()
@mock.patch.object(images, 'IMAGE_API')
@mock.patch('oslo_utils.imageutils.format_inspector.detect_file_format')
@mock.patch.object(images, 'qemu_img_info')
@mock.patch.object(images, 'fetch')
def test_fetch_to_raw_inspector_disabled(self, fetch, qemu_img_info,
mock_gi, mock_glance):
self.flags(disable_deep_image_inspection=True,
group='workarounds')
qemu_img_info.side_effect = test.TestingException
self.assertRaises(test.TestingException,
images.fetch_to_raw, None, 'href123', '/no.path')
# If deep inspection is disabled, we should never call the inspector
mock_gi.assert_not_called()
# ... and we let qemu-img detect the format itself.
qemu_img_info.assert_called_once_with('/no.path.part')
mock_glance.get.assert_not_called()
@mock.patch.object(images, 'IMAGE_API')
@mock.patch.object(images, 'qemu_img_info')
@mock.patch('oslo_utils.imageutils.format_inspector.detect_file_format')
def test_fetch_inspect_ami(self, detect, imginfo, glance):
glance.get.return_value = {'disk_format': 'ami'}
detect.return_value.__str__.return_value = 'raw'
self.assertRaises(exception.ImageUnacceptable,
images.fetch_to_raw, None, 'href123', '/no.path')
# Make sure 'ami was translated into 'raw' before we call qemu-img
imginfo.assert_called_once_with('/no.path.part')
@mock.patch.object(images, 'IMAGE_API')
@mock.patch.object(images, 'qemu_img_info')
@mock.patch('oslo_utils.imageutils.format_inspector.detect_file_format')
def test_fetch_inspect_aki(self, detect, imginfo, glance):
glance.get.return_value = {'disk_format': 'aki'}
detect.return_value.__str__.return_value = 'raw'
self.assertRaises(exception.ImageUnacceptable,
images.fetch_to_raw, None, 'href123', '/no.path')
# Make sure 'aki was translated into 'raw' before we call qemu-img
imginfo.assert_called_once_with('/no.path.part')
@mock.patch.object(images, 'IMAGE_API')
@mock.patch.object(images, 'qemu_img_info')
@mock.patch('oslo_utils.imageutils.format_inspector.detect_file_format')
def test_fetch_inspect_ari(self, detect, imginfo, glance):
glance.get.return_value = {'disk_format': 'ari'}
detect.return_value.__str__.return_value = 'raw'
self.assertRaises(exception.ImageUnacceptable,
images.fetch_to_raw, None, 'href123', '/no.path')
# Make sure 'aki was translated into 'raw' before we call qemu-img
imginfo.assert_called_once_with('/no.path.part')
@mock.patch.object(images, 'IMAGE_API')
@mock.patch.object(images, 'qemu_img_info')
def test_fetch_inspect_unknown_format(self, imginfo, glance):
glance.get.return_value = {'disk_format': 'commodore-64-disk'}
self.assertRaises(exception.ImageUnacceptable,
images.fetch_to_raw, None, 'href123', '/no.path')
# Unsupported formats do not make it past deep inspection
imginfo.assert_not_called()
@mock.patch.object(images, 'IMAGE_API')
@mock.patch.object(images, 'qemu_img_info')
@mock.patch('oslo_utils.imageutils.format_inspector.detect_file_format')
def test_fetch_inspect_disagrees_qemu(self, mock_detect, imginfo, glance):
glance.get.return_value = {'disk_format': 'qcow2'}
mock_detect.return_value.__str__.return_value = 'qcow2'
# Glance and inspector think it is a qcow2 file, but qemu-img does not
# agree.
imginfo.return_value.data_file = None
imginfo.return_value.file_format = 'vmdk'
ex = self.assertRaises(exception.ImageUnacceptable,
images.fetch_to_raw,
None, 'href123', '/no.path')
self.assertIn('content does not match disk_format', str(ex))
imginfo.assert_called_once_with('/no.path.part')