Merge "Additional qemu safety checking on base images" into unmaintained/yoga
This commit is contained in:
commit
ef98d9d293
@ -368,6 +368,23 @@ class QcowInspector(FileInspector):
|
||||
not self.has_unknown_features)
|
||||
|
||||
|
||||
class QEDInspector(FileInspector):
|
||||
def __init__(self, tracing=False):
|
||||
super().__init__(tracing)
|
||||
self.new_region('header', CaptureRegion(0, 512))
|
||||
|
||||
@property
|
||||
def format_match(self):
|
||||
if not self.region('header').complete:
|
||||
return False
|
||||
return self.region('header').data.startswith(b'QED\x00')
|
||||
|
||||
def safety_check(self):
|
||||
# QED format is not supported by anyone, but we want to detect it
|
||||
# and mark it as just always unsafe.
|
||||
return False
|
||||
|
||||
|
||||
# The VHD (or VPC as QEMU calls it) format consists of a big-endian
|
||||
# 512-byte "footer" at the beginning of the file with various
|
||||
# information, most of which does not matter to us:
|
||||
@ -871,19 +888,52 @@ class InfoWrapper(object):
|
||||
self._source.close()
|
||||
|
||||
|
||||
ALL_FORMATS = {
|
||||
'raw': FileInspector,
|
||||
'qcow2': QcowInspector,
|
||||
'vhd': VHDInspector,
|
||||
'vhdx': VHDXInspector,
|
||||
'vmdk': VMDKInspector,
|
||||
'vdi': VDIInspector,
|
||||
'qed': QEDInspector,
|
||||
}
|
||||
|
||||
|
||||
def get_inspector(format_name):
|
||||
"""Returns a FormatInspector class based on the given name.
|
||||
|
||||
:param format_name: The name of the disk_format (raw, qcow2, etc).
|
||||
:returns: A FormatInspector or None if unsupported.
|
||||
"""
|
||||
formats = {
|
||||
'raw': FileInspector,
|
||||
'qcow2': QcowInspector,
|
||||
'vhd': VHDInspector,
|
||||
'vhdx': VHDXInspector,
|
||||
'vmdk': VMDKInspector,
|
||||
'vdi': VDIInspector,
|
||||
}
|
||||
|
||||
return formats.get(format_name)
|
||||
return ALL_FORMATS.get(format_name)
|
||||
|
||||
|
||||
def detect_file_format(filename):
|
||||
"""Attempts to detect the format of a file.
|
||||
|
||||
This runs through a file one time, running all the known inspectors in
|
||||
parallel. It stops reading the file once one of them matches or all of
|
||||
them are sure they don't match.
|
||||
|
||||
Returns the FileInspector that matched, if any. None if 'raw'.
|
||||
"""
|
||||
inspectors = {k: v() for k, v in ALL_FORMATS.items()}
|
||||
with open(filename, 'rb') as f:
|
||||
for chunk in chunked_reader(f):
|
||||
for format, inspector in list(inspectors.items()):
|
||||
try:
|
||||
inspector.eat_chunk(chunk)
|
||||
except ImageFormatError:
|
||||
# No match, so stop considering this format
|
||||
inspectors.pop(format)
|
||||
continue
|
||||
if (inspector.format_match and inspector.complete and
|
||||
format != 'raw'):
|
||||
# First complete match (other than raw) wins
|
||||
return inspector
|
||||
if all(i.complete for i in inspectors.values()):
|
||||
# If all the inspectors are sure they are not a match, avoid
|
||||
# reading to the end of the file to settle on 'raw'.
|
||||
break
|
||||
return inspectors['raw']
|
||||
|
@ -13793,10 +13793,11 @@ class LibvirtConnTestCase(test.NoDBTestCase,
|
||||
'/fake/instance/dir', disk_info)
|
||||
self.assertFalse(mock_fetch_image.called)
|
||||
|
||||
@mock.patch('nova.image.format_inspector.detect_file_format')
|
||||
@mock.patch('nova.privsep.path.utime')
|
||||
@mock.patch('nova.virt.libvirt.utils.create_cow_image')
|
||||
def test_create_images_and_backing_ephemeral_gets_created(
|
||||
self, mock_create_cow_image, mock_utime):
|
||||
self, mock_create_cow_image, mock_utime, mock_detect):
|
||||
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
|
||||
|
||||
base_dir = os.path.join(CONF.instances_path,
|
||||
@ -15532,11 +15533,13 @@ class LibvirtConnTestCase(test.NoDBTestCase,
|
||||
fake_mkfs.assert_has_calls([mock.call('ext4', '/dev/something',
|
||||
'myVol')])
|
||||
|
||||
@mock.patch('nova.image.format_inspector.detect_file_format')
|
||||
@mock.patch('nova.privsep.path.utime')
|
||||
@mock.patch('nova.virt.libvirt.utils.fetch_image')
|
||||
@mock.patch('nova.virt.libvirt.utils.create_cow_image')
|
||||
def test_create_ephemeral_specified_fs_not_valid(
|
||||
self, mock_create_cow_image, mock_fetch_image, mock_utime):
|
||||
self, mock_create_cow_image, mock_fetch_image, mock_utime,
|
||||
mock_detect):
|
||||
CONF.set_override('default_ephemeral_format', 'ext4')
|
||||
ephemerals = [{'device_type': 'disk',
|
||||
'disk_bus': 'virtio',
|
||||
|
@ -522,13 +522,15 @@ class Qcow2TestCase(_ImageTestCase, test.NoDBTestCase):
|
||||
|
||||
mock_exists.assert_has_calls(exist_calls)
|
||||
|
||||
@mock.patch('nova.image.format_inspector.detect_file_format')
|
||||
@mock.patch.object(imagebackend.utils, 'synchronized')
|
||||
@mock.patch('nova.virt.libvirt.utils.create_cow_image')
|
||||
@mock.patch.object(os.path, 'exists', side_effect=[])
|
||||
@mock.patch.object(imagebackend.Image, 'verify_base_size')
|
||||
@mock.patch('nova.privsep.path.utime')
|
||||
def test_create_image(
|
||||
self, mock_utime, mock_verify, mock_exist, mock_create, mock_sync
|
||||
self, mock_utime, mock_verify, mock_exist, mock_create, mock_sync,
|
||||
mock_detect_format
|
||||
):
|
||||
mock_sync.side_effect = lambda *a, **kw: self._fake_deco
|
||||
fn = mock.MagicMock()
|
||||
@ -549,7 +551,10 @@ class Qcow2TestCase(_ImageTestCase, test.NoDBTestCase):
|
||||
mock_exist.assert_has_calls(exist_calls)
|
||||
self.assertTrue(mock_sync.called)
|
||||
mock_utime.assert_called()
|
||||
mock_detect_format.assert_called_once()
|
||||
mock_detect_format.return_value.safety_check.assert_called_once_with()
|
||||
|
||||
@mock.patch('nova.image.format_inspector.detect_file_format')
|
||||
@mock.patch.object(imagebackend.utils, 'synchronized')
|
||||
@mock.patch('nova.virt.libvirt.utils.create_cow_image')
|
||||
@mock.patch.object(imagebackend.disk, 'extend')
|
||||
@ -557,7 +562,8 @@ class Qcow2TestCase(_ImageTestCase, test.NoDBTestCase):
|
||||
@mock.patch.object(imagebackend.Qcow2, 'get_disk_size')
|
||||
@mock.patch('nova.privsep.path.utime')
|
||||
def test_create_image_too_small(self, mock_utime, mock_get, mock_exist,
|
||||
mock_extend, mock_create, mock_sync):
|
||||
mock_extend, mock_create, mock_sync,
|
||||
mock_detect_format):
|
||||
mock_sync.side_effect = lambda *a, **kw: self._fake_deco
|
||||
mock_get.return_value = self.SIZE
|
||||
fn = mock.MagicMock()
|
||||
@ -574,7 +580,9 @@ class Qcow2TestCase(_ImageTestCase, test.NoDBTestCase):
|
||||
self.assertTrue(mock_sync.called)
|
||||
self.assertFalse(mock_create.called)
|
||||
self.assertFalse(mock_extend.called)
|
||||
mock_detect_format.assert_called_once()
|
||||
|
||||
@mock.patch('nova.image.format_inspector.detect_file_format')
|
||||
@mock.patch.object(imagebackend.utils, 'synchronized')
|
||||
@mock.patch('nova.virt.libvirt.utils.create_cow_image')
|
||||
@mock.patch('nova.virt.libvirt.utils.get_disk_backing_file')
|
||||
@ -586,7 +594,8 @@ class Qcow2TestCase(_ImageTestCase, test.NoDBTestCase):
|
||||
def test_generate_resized_backing_files(self, mock_utime, mock_copy,
|
||||
mock_verify, mock_exist,
|
||||
mock_extend, mock_get,
|
||||
mock_create, mock_sync):
|
||||
mock_create, mock_sync,
|
||||
mock_detect_format):
|
||||
mock_sync.side_effect = lambda *a, **kw: self._fake_deco
|
||||
mock_get.return_value = self.QCOW2_BASE
|
||||
fn = mock.MagicMock()
|
||||
@ -613,7 +622,9 @@ class Qcow2TestCase(_ImageTestCase, test.NoDBTestCase):
|
||||
self.assertTrue(mock_sync.called)
|
||||
self.assertFalse(mock_create.called)
|
||||
mock_utime.assert_called()
|
||||
mock_detect_format.assert_called_once()
|
||||
|
||||
@mock.patch('nova.image.format_inspector.detect_file_format')
|
||||
@mock.patch.object(imagebackend.utils, 'synchronized')
|
||||
@mock.patch('nova.virt.libvirt.utils.create_cow_image')
|
||||
@mock.patch('nova.virt.libvirt.utils.get_disk_backing_file')
|
||||
@ -624,7 +635,8 @@ class Qcow2TestCase(_ImageTestCase, test.NoDBTestCase):
|
||||
def test_qcow2_exists_and_has_no_backing_file(self, mock_utime,
|
||||
mock_verify, mock_exist,
|
||||
mock_extend, mock_get,
|
||||
mock_create, mock_sync):
|
||||
mock_create, mock_sync,
|
||||
mock_detect_format):
|
||||
mock_sync.side_effect = lambda *a, **kw: self._fake_deco
|
||||
mock_get.return_value = None
|
||||
fn = mock.MagicMock()
|
||||
@ -645,6 +657,31 @@ class Qcow2TestCase(_ImageTestCase, test.NoDBTestCase):
|
||||
self.assertTrue(mock_sync.called)
|
||||
self.assertFalse(mock_create.called)
|
||||
self.assertFalse(mock_extend.called)
|
||||
mock_detect_format.assert_called_once()
|
||||
|
||||
@mock.patch('nova.image.format_inspector.detect_file_format')
|
||||
@mock.patch.object(imagebackend.utils, 'synchronized')
|
||||
@mock.patch('nova.virt.libvirt.utils.create_image')
|
||||
@mock.patch('nova.virt.libvirt.utils.get_disk_backing_file')
|
||||
@mock.patch.object(imagebackend.disk, 'extend')
|
||||
@mock.patch.object(os.path, 'exists', side_effect=[])
|
||||
@mock.patch.object(imagebackend.Image, 'verify_base_size')
|
||||
def test_qcow2_exists_and_fails_safety_check(self,
|
||||
mock_verify, mock_exist,
|
||||
mock_extend, mock_get,
|
||||
mock_create, mock_sync,
|
||||
mock_detect_format):
|
||||
mock_detect_format.return_value.safety_check.return_value = False
|
||||
mock_sync.side_effect = lambda *a, **kw: self._fake_deco
|
||||
mock_get.return_value = None
|
||||
fn = mock.MagicMock()
|
||||
mock_exist.side_effect = [False, True, False, True, True]
|
||||
image = self.image_class(self.INSTANCE, self.NAME)
|
||||
|
||||
self.assertRaises(exception.InvalidDiskInfo,
|
||||
image.create_image, fn, self.TEMPLATE_PATH,
|
||||
self.SIZE)
|
||||
mock_verify.assert_not_called()
|
||||
|
||||
def test_resolve_driver_format(self):
|
||||
image = self.image_class(self.INSTANCE, self.NAME)
|
||||
|
@ -117,11 +117,27 @@ class LibvirtUtilsTestCase(test.NoDBTestCase):
|
||||
@mock.patch('os.path.exists', return_value=True)
|
||||
@mock.patch('oslo_concurrency.processutils.execute')
|
||||
@mock.patch('nova.virt.images.qemu_img_info')
|
||||
def test_create_cow_image(self, mock_info, mock_execute, mock_exists):
|
||||
@mock.patch('nova.image.format_inspector.detect_file_format')
|
||||
def _test_create_cow_image(
|
||||
self, mock_detect, mock_info, mock_execute,
|
||||
mock_exists, backing_file=None, safety_check=True
|
||||
):
|
||||
if isinstance(backing_file, dict):
|
||||
backing_info = backing_file
|
||||
backing_file = backing_info.pop('file', None)
|
||||
else:
|
||||
backing_info = {}
|
||||
backing_backing_file = backing_info.pop('backing_file', None)
|
||||
|
||||
mock_execute.return_value = ('stdout', None)
|
||||
mock_info.return_value = mock.Mock(
|
||||
file_format=mock.sentinel.backing_fmt,
|
||||
cluster_size=mock.sentinel.cluster_size)
|
||||
cluster_size=mock.sentinel.cluster_size,
|
||||
backing_file=backing_backing_file,
|
||||
format_specific=backing_info)
|
||||
|
||||
mock_detect.return_value.safety_check.return_value = safety_check
|
||||
|
||||
libvirt_utils.create_cow_image(mock.sentinel.backing_path,
|
||||
mock.sentinel.new_path)
|
||||
mock_info.assert_called_once_with(mock.sentinel.backing_path)
|
||||
@ -131,6 +147,33 @@ class LibvirtUtilsTestCase(test.NoDBTestCase):
|
||||
mock.sentinel.backing_path, mock.sentinel.backing_fmt,
|
||||
mock.sentinel.cluster_size),
|
||||
mock.sentinel.new_path)])
|
||||
if backing_file:
|
||||
mock_detect.return_value.safety_check.assert_called_once_with()
|
||||
|
||||
def test_create_image_qcow2(self):
|
||||
self._test_create_cow_image()
|
||||
|
||||
def test_create_image_backing_file(self):
|
||||
self._test_create_cow_image(
|
||||
backing_file=mock.sentinel.backing_file
|
||||
)
|
||||
|
||||
def test_create_image_base_has_backing_file(self):
|
||||
self.assertRaises(
|
||||
exception.InvalidDiskInfo,
|
||||
self._test_create_cow_image,
|
||||
backing_file={'file': mock.sentinel.backing_file,
|
||||
'backing_file': mock.sentinel.backing_backing_file},
|
||||
)
|
||||
|
||||
def test_create_image_base_has_data_file(self):
|
||||
self.assertRaises(
|
||||
exception.InvalidDiskInfo,
|
||||
self._test_create_cow_image,
|
||||
backing_file={'file': mock.sentinel.backing_file,
|
||||
'backing_file': mock.sentinel.backing_backing_file,
|
||||
'data': {'data-file': mock.sentinel.data_file}},
|
||||
)
|
||||
|
||||
@ddt.unpack
|
||||
@ddt.data({'fs_type': 'some_fs_type',
|
||||
|
@ -34,6 +34,7 @@ from oslo_utils import units
|
||||
import nova.conf
|
||||
from nova import exception
|
||||
from nova.i18n import _
|
||||
from nova.image import format_inspector
|
||||
from nova.image import glance
|
||||
import nova.privsep.libvirt
|
||||
import nova.privsep.path
|
||||
@ -637,6 +638,20 @@ class Qcow2(Image):
|
||||
if not os.path.exists(base):
|
||||
prepare_template(target=base, *args, **kwargs)
|
||||
|
||||
# NOTE(danms): We need to perform safety checks on the base image
|
||||
# before we inspect it for other attributes. We do this each time
|
||||
# because additional safety checks could have been added since we
|
||||
# downloaded the image.
|
||||
if not CONF.workarounds.disable_deep_image_inspection:
|
||||
inspector = format_inspector.detect_file_format(base)
|
||||
if not inspector.safety_check():
|
||||
LOG.warning('Base image %s failed safety check', base)
|
||||
# NOTE(danms): This is the same exception as would be raised
|
||||
# by qemu_img_info() if the disk format was unreadable or
|
||||
# otherwise unsuitable.
|
||||
raise exception.InvalidDiskInfo(
|
||||
reason=_('Base image failed safety check'))
|
||||
|
||||
# NOTE(ankit): Update the mtime of the base file so the image
|
||||
# cache manager knows it is in use.
|
||||
_update_utime_ignore_eacces(base)
|
||||
|
@ -34,6 +34,7 @@ import nova.conf
|
||||
from nova import context as nova_context
|
||||
from nova import exception
|
||||
from nova.i18n import _
|
||||
from nova.image import format_inspector
|
||||
from nova import objects
|
||||
from nova.objects import fields as obj_fields
|
||||
import nova.privsep.fs
|
||||
@ -139,7 +140,35 @@ def create_cow_image(
|
||||
base_cmd = ['qemu-img', 'create', '-f', 'qcow2']
|
||||
cow_opts = []
|
||||
if backing_file:
|
||||
# NOTE(danms): We need to perform safety checks on the base image
|
||||
# before we inspect it for other attributes. We do this each time
|
||||
# because additional safety checks could have been added since we
|
||||
# downloaded the image.
|
||||
if not CONF.workarounds.disable_deep_image_inspection:
|
||||
inspector = format_inspector.detect_file_format(backing_file)
|
||||
if not inspector.safety_check():
|
||||
LOG.warning('Base image %s failed safety check', backing_file)
|
||||
# NOTE(danms): This is the same exception as would be raised
|
||||
# by qemu_img_info() if the disk format was unreadable or
|
||||
# otherwise unsuitable.
|
||||
raise exception.InvalidDiskInfo(
|
||||
reason=_('Base image failed safety check'))
|
||||
|
||||
base_details = images.qemu_img_info(backing_file)
|
||||
|
||||
if base_details.backing_file is not None:
|
||||
LOG.warning('Base image %s failed safety check', backing_file)
|
||||
raise exception.InvalidDiskInfo(
|
||||
reason=_('Base image failed safety check'))
|
||||
try:
|
||||
data_file = base_details.format_specific['data']['data-file']
|
||||
except (KeyError, TypeError, AttributeError):
|
||||
data_file = None
|
||||
if data_file is not None:
|
||||
LOG.warning('Base image %s failed safety check', backing_file)
|
||||
raise exception.InvalidDiskInfo(
|
||||
reason=_('Base image failed safety check'))
|
||||
|
||||
cow_opts += ['backing_file=%s' % backing_file]
|
||||
cow_opts += ['backing_fmt=%s' % base_details.file_format]
|
||||
else:
|
||||
|
Loading…
x
Reference in New Issue
Block a user