summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cinder/image/image_utils.py239
-rw-r--r--cinder/tests/unit/test_image_utils.py468
-rw-r--r--cinder/tests/unit/volume/drivers/test_nfs.py223
-rw-r--r--cinder/tests/unit/volume/drivers/test_quobyte.py127
-rw-r--r--cinder/volume/api.py4
-rw-r--r--cinder/volume/flows/manager/create_volume.py4
-rw-r--r--releasenotes/notes/bug-1996188-vmdk-subformat-allow-list-93e6943d9a486d11.yaml33
-rw-r--r--releasenotes/notes/fix-reserved-image-properties-9519ddc080e7ed1a.yaml28
8 files changed, 908 insertions, 218 deletions
diff --git a/cinder/image/image_utils.py b/cinder/image/image_utils.py
index 198499d8f..0c3c3ac95 100644
--- a/cinder/image/image_utils.py
+++ b/cinder/image/image_utils.py
@@ -80,6 +80,33 @@ image_opts = [
'conversion consumes a large amount of system resources and '
'can cause performance problems on the cinder-volume node. '
'When set True, this option disables image conversion.'),
+ cfg.ListOpt('vmdk_allowed_types',
+ default=['streamOptimized', 'monolithicSparse'],
+ help='A list of strings describing the VMDK createType '
+ 'subformats that are allowed. We recommend that you only '
+ 'include single-file-with-sparse-header variants to avoid '
+ 'potential host file exposure when processing named extents '
+ 'when an image is converted to raw format as it is written '
+ 'to a volume. If this list is empty, no VMDK images are '
+ 'allowed.'),
+ cfg.ListOpt('reserved_image_namespaces',
+ help='List of reserved image namespaces that should be '
+ 'filtered out when uploading a volume as an image back '
+ 'to Glance. When a volume is created from an image, '
+ 'Cinder stores the image properties as volume '
+ 'image metadata, and if the volume is later uploaded as '
+ 'an image, Cinder will add these properties when it '
+ 'creates the image in Glance. This can cause problems '
+ 'for image metadata that are in namespaces that glance '
+ 'reserves for itself, or when properties (such as an '
+ 'image signature) cannot apply to the new image, or when '
+ 'an operator has configured glance property protections '
+ 'to make some image properties read-only. Cinder will '
+ '*always* filter out image metadata in the namespaces '
+ '`os_glance` and `img_signature`; this configuration '
+ 'option allows operators to specify *additional* '
+ 'namespaces to be excluded.',
+ default=[]),
]
CONF = cfg.CONF
@@ -104,6 +131,8 @@ QEMU_IMG_MIN_CONVERT_LUKS_VERSION = '2.10'
COMPRESSIBLE_IMAGE_FORMATS = ('qcow2',)
+GLANCE_RESERVED_NAMESPACES = ["os_glance", "img_signature"]
+
def validate_stores_id(context: context.RequestContext,
image_service_store_id: str) -> None:
@@ -135,7 +164,7 @@ def qemu_img_info(path: str,
run_as_root: bool = True,
force_share: bool = False) -> imageutils.QemuImgInfo:
"""Return an object containing the parsed output from qemu-img info."""
- cmd = ['env', 'LC_ALL=C', 'qemu-img', 'info']
+ cmd = ['env', 'LC_ALL=C', 'qemu-img', 'info', '--output=json']
if force_share:
if qemu_img_supports_force_share():
cmd.append('--force-share')
@@ -149,7 +178,7 @@ def qemu_img_info(path: str,
cmd = cmd[2:]
out, _err = utils.execute(*cmd, run_as_root=run_as_root,
prlimit=QEMU_IMG_LIMITS)
- info = imageutils.QemuImgInfo(out)
+ info = imageutils.QemuImgInfo(out, format='json')
# From Cinder's point of view, any 'luks' formatted images
# should be treated as 'raw'.
@@ -435,7 +464,35 @@ def convert_image(source: str,
cipher_spec: Optional[dict] = None,
passphrase_file: Optional[str] = None,
compress: bool = False,
- src_passphrase_file: Optional[str] = None) -> None:
+ src_passphrase_file: Optional[str] = None,
+ image_id: Optional[str] = None,
+ data: Optional[imageutils.QemuImgInfo] = None) -> None:
+ """Convert image to other format.
+
+ NOTE: If the qemu-img convert command fails and this function raises an
+ exception, a non-empty dest file may be left in the filesystem.
+ It is the responsibility of the caller to decide what to do with this file.
+
+ :param source: source filename
+ :param dest: destination filename
+ :param out_format: output image format of qemu-img
+ :param out_subformat: output image subformat
+ :param src_format: source image format (use image_utils.fixup_disk_format()
+ to translate from a Glance format to one recognizable by qemu_img)
+ :param run_as_root: run qemu-img as root
+ :param throttle: a cinder.throttling.Throttle object, or None
+ :param cipher_spec: encryption details
+ :param passphrase_file: filename containing luks passphrase
+ :param compress: compress w/ qemu-img when possible (best effort)
+ :param src_passphrase_file: filename containing source volume's
+ luks passphrase
+ :param image_id: the image ID if this is a Glance image, or None
+ :param data: a imageutils.QemuImgInfo object from this image, or None
+ :raises ImageUnacceptable: when the image fails some format checks
+ :raises ProcessExecutionError: when something goes wrong during conversion
+ """
+ check_image_format(source, src_format, image_id, data, run_as_root)
+
if not throttle:
throttle = throttling.Throttle.get_default()
with throttle.subcommand(source, dest) as throttle_cmd:
@@ -630,6 +687,98 @@ def get_qemu_data(image_id: str,
return data
+def check_vmdk_image(image_id: str, data: imageutils.QemuImgInfo) -> None:
+ """Check some rules about VMDK images.
+
+ Make sure the VMDK subformat (the "createType" in vmware docs)
+ is one that we allow as determined by the 'vmdk_allowed_types'
+ configuration option. The default set includes only types that
+ do not reference files outside the VMDK file, which can otherwise
+ be used in exploits to expose host information.
+
+ :param image_id: the image id
+ :param data: an imageutils.QemuImgInfo object
+ :raises ImageUnacceptable: when the VMDK createType is not in the
+ allowed list
+ """
+ allowed_types = CONF.vmdk_allowed_types
+
+ if not len(allowed_types):
+ msg = _('Image is a VMDK, but no VMDK createType is allowed')
+ raise exception.ImageUnacceptable(image_id=image_id, reason=msg)
+
+ try:
+ create_type = data.format_specific['data']['create-type']
+ except KeyError:
+ msg = _('Unable to determine VMDK createType')
+ raise exception.ImageUnacceptable(image_id=image_id, reason=msg)
+ except TypeError:
+ msg = _('Unable to determine VMDK createType as no format-specific '
+ 'information is available')
+ raise exception.ImageUnacceptable(image_id=image_id, reason=msg)
+
+ if create_type not in allowed_types:
+ LOG.warning('Refusing to process VMDK file with createType of %r '
+ 'which is not in allowed set of: %s', create_type,
+ ','.join(allowed_types))
+ msg = _('Invalid VMDK create-type specified')
+ raise exception.ImageUnacceptable(image_id=image_id, reason=msg)
+
+
+def check_image_format(source: str,
+ src_format: Optional[str] = None,
+ image_id: Optional[str] = None,
+ data: Optional[imageutils.QemuImgInfo] = None,
+ run_as_root: bool = True) -> None:
+ """Do some image format checks.
+
+ Verifies that the src_format matches what qemu-img thinks the image
+ format is, and does some vmdk subformat checks. See Bug #1996188.
+
+ - Does not check for a qcow2 backing file.
+ - Will make a call out to qemu_img if data is None.
+
+ :param source: filename of the image to check
+ :param src_format: source image format recognized by qemu_img, or None
+ :param image_id: the image ID if this is a Glance image, or None
+ :param data: a imageutils.QemuImgInfo object from this image, or None
+ :param run_as_root: when 'data' is None, call 'qemu-img info' as root
+ :raises ImageUnacceptable: when the image fails some format checks
+ :raises ProcessExecutionError: if 'qemu-img info' fails
+ """
+ if image_id is None:
+ image_id = 'internal image'
+ if data is None:
+ data = qemu_img_info(source, run_as_root=run_as_root)
+
+ if data.file_format is None:
+ raise exception.ImageUnacceptable(
+ reason=_("'qemu-img info' parsing failed."),
+ image_id=image_id)
+
+ if src_format is not None:
+ if src_format.lower() == 'ami':
+ # qemu-img doesn't recognize AMI format; see change Icde4c0f936ce.
+ # We also use lower() here (though nowhere else) to be consistent
+ # with that change.
+ pass
+ elif data.file_format != src_format:
+ LOG.debug("Rejecting image %(image_id)s due to format mismatch. "
+ "src_format: '%(src)s', but qemu-img info reports: "
+ "'%(qemu)s'",
+ {'image_id': image_id,
+ 'src': src_format,
+ 'qemu': data.file_format})
+ msg = _("The image format was claimed to be '%(src)s' but the "
+ "image data appears to be in a different format.")
+ raise exception.ImageUnacceptable(
+ image_id=image_id,
+ reason=(msg % {'src': src_format}))
+
+ if data.file_format == 'vmdk':
+ check_vmdk_image(image_id, data)
+
+
def fetch_verify_image(context: context.RequestContext,
image_service: glance.GlanceImageService,
image_id: str,
@@ -647,7 +796,10 @@ def fetch_verify_image(context: context.RequestContext,
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
+ # qemu data to use.
+ # NOTE: We won't have data if qemu_img is not installed *and* the
+ # disk_format recorded in Glance is raw (otherwise an ImageUnacceptable
+ # would have been raised already). So this isn't as bad as it looks.
if data is not None:
fmt = data.file_format
if fmt is None:
@@ -662,6 +814,11 @@ def fetch_verify_image(context: context.RequestContext,
reason=(_("fmt=%(fmt)s backed by: %(backing_file)s") %
{'fmt': fmt, 'backing_file': backing_file}))
+ # a VMDK can have a backing file, but we have to check for
+ # it differently
+ if fmt == 'vmdk':
+ check_vmdk_image(image_id, data)
+
def fetch_to_vhd(context: context.RequestContext,
image_service: glance.GlanceImageService,
@@ -752,6 +909,10 @@ def fetch_to_volume_format(context: context.RequestContext,
format_raw = True if image_meta['disk_format'] == 'raw' else False
except TypeError:
format_raw = False
+
+ # Probe using the empty tmp file to see if qemu-img is available.
+ # If it's not, and the disk_format recorded in Glance is not 'raw',
+ # this will raise ImageUnacceptable
data = get_qemu_data(image_id, has_meta, format_raw,
tmp, run_as_root)
if data is None:
@@ -764,6 +925,21 @@ def fetch_to_volume_format(context: context.RequestContext,
else:
fetch(context, image_service, image_id, tmp, user_id, project_id)
+ # 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)
+
if is_xenserver_format(image_meta):
replace_xenserver_image_with_coalesced_vhd(tmp)
@@ -799,21 +975,6 @@ def fetch_to_volume_format(context: context.RequestContext,
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
@@ -821,13 +982,21 @@ def fetch_to_volume_format(context: context.RequestContext,
# 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.
+ # FIXME: revisit the above 2 comments. We already have an exception
+ # above for RAW format images when qemu-img is not available, and I'm
+ # pretty sure that the backing file exploit only happens when
+ # converting from some format that supports a backing file TO raw ...
+ # a bit-for-bit copy of a qcow2 with backing file will copy the backing
+ # file *reference* but not its *content*.
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)
+ run_as_root=run_as_root,
+ image_id=image_id,
+ data=data)
def upload_volume(context: context.RequestContext,
@@ -885,7 +1054,9 @@ def upload_volume(context: context.RequestContext,
out_format = fixup_disk_format(image_meta['disk_format'])
convert_image(volume_path, tmp, out_format,
run_as_root=run_as_root,
- compress=compress)
+ compress=compress,
+ image_id=image_id,
+ data=data)
data = qemu_img_info(tmp, run_as_root=run_as_root)
if data.file_format != out_format:
@@ -1133,3 +1304,29 @@ class TemporaryImages(object):
if not self.temporary_images.get(user):
return None
return self.temporary_images[user].get(image_id)
+
+
+def filter_out_reserved_namespaces_metadata(
+ metadata: Optional[dict[str, str]]) -> dict[str, str]:
+
+ reserved_name_spaces = GLANCE_RESERVED_NAMESPACES.copy()
+ if CONF.reserved_image_namespaces:
+ for image_namespace in CONF.reserved_image_namespaces:
+ if image_namespace not in reserved_name_spaces:
+ reserved_name_spaces.append(image_namespace)
+
+ if not metadata:
+ LOG.debug("No metadata to be filtered.")
+ return {}
+
+ new_metadata = {}
+ for k, v in metadata.items():
+ if any(k.startswith(reserved_name_space)
+ for reserved_name_space in reserved_name_spaces):
+ continue
+ new_metadata[k] = v
+
+ LOG.debug("The metadata set [%s] was filtered using the reserved name "
+ "spaces [%s], and the result is [%s].", metadata,
+ reserved_name_spaces, new_metadata)
+ return new_metadata
diff --git a/cinder/tests/unit/test_image_utils.py b/cinder/tests/unit/test_image_utils.py
index 452b644c4..d616b5b94 100644
--- a/cinder/tests/unit/test_image_utils.py
+++ b/cinder/tests/unit/test_image_utils.py
@@ -19,6 +19,7 @@ from unittest import mock
import cryptography
import ddt
from oslo_concurrency import processutils
+from oslo_utils import imageutils
from oslo_utils import units
from cinder import exception
@@ -40,7 +41,8 @@ class TestQemuImgInfo(test.TestCase):
output = image_utils.qemu_img_info(test_path)
mock_exec.assert_called_once_with('env', 'LC_ALL=C', 'qemu-img',
- 'info', test_path, run_as_root=True,
+ 'info', '--output=json', test_path,
+ run_as_root=True,
prlimit=image_utils.QEMU_IMG_LIMITS)
self.assertEqual(mock_info.return_value, output)
@@ -57,7 +59,8 @@ class TestQemuImgInfo(test.TestCase):
force_share=False,
run_as_root=False)
mock_exec.assert_called_once_with('env', 'LC_ALL=C', 'qemu-img',
- 'info', test_path, run_as_root=False,
+ 'info', '--output=json', test_path,
+ run_as_root=False,
prlimit=image_utils.QEMU_IMG_LIMITS)
self.assertEqual(mock_info.return_value, output)
@@ -72,8 +75,8 @@ class TestQemuImgInfo(test.TestCase):
mock_os.name = 'nt'
output = image_utils.qemu_img_info(test_path)
- mock_exec.assert_called_once_with('qemu-img', 'info', test_path,
- run_as_root=True,
+ mock_exec.assert_called_once_with('qemu-img', 'info', '--output=json',
+ test_path, run_as_root=True,
prlimit=image_utils.QEMU_IMG_LIMITS)
self.assertEqual(mock_info.return_value, output)
@@ -171,7 +174,8 @@ class TestConvertImage(test.TestCase):
source = mock.sentinel.source
dest = mock.sentinel.dest
out_format = mock.sentinel.out_format
- mock_info.side_effect = ValueError
+ mock_info.return_value.file_format = 'qcow2'
+ mock_info.return_value.virtual_size = 1048576
throttle = throttling.Throttle(prefix=['cgcmd'])
with mock.patch('cinder.volume.volume_utils.check_for_odirect_support',
@@ -179,7 +183,8 @@ class TestConvertImage(test.TestCase):
output = image_utils.convert_image(source, dest, out_format,
throttle=throttle)
- mock_info.assert_called_once_with(source, run_as_root=True)
+ my_call = mock.call(source, run_as_root=True)
+ mock_info.assert_has_calls([my_call, my_call])
self.assertIsNone(output)
mock_exec.assert_called_once_with('cgcmd', 'qemu-img', 'convert',
'-O', out_format, '-t', 'none',
@@ -235,7 +240,6 @@ class TestConvertImage(test.TestCase):
dest = mock.sentinel.dest
out_format = mock.sentinel.out_format
out_subformat = 'fake_subformat'
- mock_info.side_effect = ValueError
output = image_utils.convert_image(source, dest, out_format,
out_subformat=out_subformat)
@@ -334,7 +338,6 @@ class TestConvertImage(test.TestCase):
self.flags(image_conversion_dir='fakedir')
dest = ['fakedir']
out_format = mock.sentinel.out_format
- mock_info.side_effect = ValueError
mock_exec.side_effect = processutils.ProcessExecutionError(
stderr='No space left on device')
self.assertRaises(processutils.ProcessExecutionError,
@@ -740,7 +743,9 @@ class TestUploadVolume(test.TestCase):
temp_file,
output_format,
run_as_root=True,
- compress=do_compress)
+ compress=do_compress,
+ image_id=image_meta['id'],
+ data=data)
mock_info.assert_called_with(temp_file, run_as_root=True)
self.assertEqual(2, mock_info.call_count)
mock_open.assert_called_once_with(temp_file, 'rb')
@@ -827,7 +832,9 @@ class TestUploadVolume(test.TestCase):
temp_file,
'raw',
compress=True,
- run_as_root=True)
+ run_as_root=True,
+ image_id=image_meta['id'],
+ data=data)
mock_info.assert_called_with(temp_file, run_as_root=True)
self.assertEqual(2, mock_info.call_count)
mock_open.assert_called_once_with(temp_file, 'rb')
@@ -916,7 +923,9 @@ class TestUploadVolume(test.TestCase):
temp_file,
'raw',
compress=True,
- run_as_root=True)
+ run_as_root=True,
+ image_id=image_meta['id'],
+ data=data)
mock_info.assert_called_with(temp_file, run_as_root=True)
self.assertEqual(2, mock_info.call_count)
mock_open.assert_called_once_with(temp_file, 'rb')
@@ -951,7 +960,9 @@ class TestUploadVolume(test.TestCase):
temp_file,
mock.sentinel.disk_format,
run_as_root=True,
- compress=True)
+ compress=True,
+ image_id=image_meta['id'],
+ data=data)
mock_info.assert_called_with(temp_file, run_as_root=True)
self.assertEqual(2, mock_info.call_count)
self.assertFalse(image_service.update.called)
@@ -1109,8 +1120,10 @@ class TestFetchToVolumeFormat(test.TestCase):
out_subformat = None
blocksize = mock.sentinel.blocksize
+ disk_format = 'raw'
+
data = mock_info.return_value
- data.file_format = volume_format
+ data.file_format = disk_format
data.backing_file = None
data.virtual_size = 1234
tmp = mock_temp.return_value.__enter__.return_value
@@ -1132,7 +1145,9 @@ class TestFetchToVolumeFormat(test.TestCase):
mock_convert.assert_called_once_with(tmp, dest, volume_format,
out_subformat=out_subformat,
run_as_root=True,
- src_format='raw')
+ src_format=disk_format,
+ image_id=image_id,
+ data=data)
@mock.patch('cinder.image.image_utils.check_virtual_size')
@mock.patch('cinder.image.image_utils.check_available_space')
@@ -1149,7 +1164,9 @@ class TestFetchToVolumeFormat(test.TestCase):
mock_is_xen, mock_repl_xen, mock_copy, mock_convert,
mock_check_space, mock_check_size):
ctxt = mock.sentinel.context
- image_service = FakeImageService()
+ disk_format = 'ploop'
+ qemu_img_format = image_utils.QEMU_IMG_FORMAT_MAP[disk_format]
+ image_service = FakeImageService(disk_format=disk_format)
image_id = mock.sentinel.image_id
dest = mock.sentinel.dest
volume_format = mock.sentinel.volume_format
@@ -1161,7 +1178,7 @@ class TestFetchToVolumeFormat(test.TestCase):
run_as_root = mock.sentinel.run_as_root
data = mock_info.return_value
- data.file_format = volume_format
+ data.file_format = qemu_img_format
data.backing_file = None
data.virtual_size = 1234
tmp = mock_temp.return_value.__enter__.return_value
@@ -1184,7 +1201,9 @@ class TestFetchToVolumeFormat(test.TestCase):
mock_convert.assert_called_once_with(tmp, dest, volume_format,
out_subformat=out_subformat,
run_as_root=run_as_root,
- src_format='raw')
+ src_format=qemu_img_format,
+ image_id=image_id,
+ data=data)
mock_check_size.assert_called_once_with(data.virtual_size,
size, image_id)
@@ -1240,12 +1259,14 @@ class TestFetchToVolumeFormat(test.TestCase):
size = 4321
run_as_root = mock.sentinel.run_as_root
+ disk_format = 'vhd'
+
data = mock_info.return_value
- data.file_format = volume_format
+ data.file_format = image_utils.QEMU_IMG_FORMAT_MAP[disk_format]
data.backing_file = None
data.virtual_size = 1234
tmp = mock_temp.return_value.__enter__.return_value
- image_service = FakeImageService(disk_format='vhd')
+ image_service = FakeImageService(disk_format=disk_format)
expect_format = 'vpc'
output = image_utils.fetch_to_volume_format(
@@ -1266,7 +1287,9 @@ class TestFetchToVolumeFormat(test.TestCase):
mock_convert.assert_called_once_with(tmp, dest, volume_format,
out_subformat=out_subformat,
run_as_root=run_as_root,
- src_format=expect_format)
+ src_format=expect_format,
+ image_id=image_id,
+ data=data)
@mock.patch('cinder.image.image_utils.check_virtual_size')
@mock.patch('cinder.image.image_utils.check_available_space')
@@ -1292,12 +1315,14 @@ class TestFetchToVolumeFormat(test.TestCase):
size = 4321
run_as_root = mock.sentinel.run_as_root
+ disk_format = 'iso'
+
data = mock_info.return_value
- data.file_format = volume_format
+ data.file_format = image_utils.QEMU_IMG_FORMAT_MAP[disk_format]
data.backing_file = None
data.virtual_size = 1234
tmp = mock_temp.return_value.__enter__.return_value
- image_service = FakeImageService(disk_format='iso')
+ image_service = FakeImageService(disk_format=disk_format)
expect_format = 'raw'
output = image_utils.fetch_to_volume_format(
@@ -1317,7 +1342,9 @@ class TestFetchToVolumeFormat(test.TestCase):
mock_convert.assert_called_once_with(tmp, dest, volume_format,
out_subformat=out_subformat,
run_as_root=run_as_root,
- src_format=expect_format)
+ src_format=expect_format,
+ image_id=image_id,
+ data=data)
@mock.patch('cinder.image.image_utils.check_available_space',
new=mock.Mock())
@@ -1335,7 +1362,9 @@ class TestFetchToVolumeFormat(test.TestCase):
mock_copy, mock_convert):
ctxt = mock.sentinel.context
ctxt.user_id = mock.sentinel.user_id
- image_service = FakeImageService()
+ disk_format = 'ploop'
+ qemu_img_format = image_utils.QEMU_IMG_FORMAT_MAP[disk_format]
+ image_service = FakeImageService(disk_format=disk_format)
image_id = mock.sentinel.image_id
dest = mock.sentinel.dest
volume_format = mock.sentinel.volume_format
@@ -1343,7 +1372,7 @@ class TestFetchToVolumeFormat(test.TestCase):
blocksize = mock.sentinel.blocksize
data = mock_info.return_value
- data.file_format = volume_format
+ data.file_format = qemu_img_format
data.backing_file = None
data.virtual_size = 1234
tmp = mock.sentinel.tmp
@@ -1371,7 +1400,9 @@ class TestFetchToVolumeFormat(test.TestCase):
mock_convert.assert_called_once_with(tmp, dest, volume_format,
out_subformat=out_subformat,
run_as_root=True,
- src_format='raw')
+ src_format=qemu_img_format,
+ image_id=image_id,
+ data=data)
@mock.patch('cinder.image.image_utils.convert_image')
@mock.patch('cinder.image.image_utils.volume_utils.copy_volume')
@@ -1633,7 +1664,9 @@ class TestFetchToVolumeFormat(test.TestCase):
mock_copy, mock_convert, mock_check_space,
mock_check_size):
ctxt = mock.sentinel.context
- image_service = FakeImageService()
+ disk_format = 'vhd'
+ qemu_img_format = image_utils.QEMU_IMG_FORMAT_MAP[disk_format]
+ image_service = FakeImageService(disk_format=disk_format)
image_id = mock.sentinel.image_id
dest = mock.sentinel.dest
volume_format = mock.sentinel.volume_format
@@ -1644,7 +1677,7 @@ class TestFetchToVolumeFormat(test.TestCase):
run_as_root = mock.sentinel.run_as_root
data = mock_info.return_value
- data.file_format = volume_format
+ data.file_format = qemu_img_format
data.backing_file = None
data.virtual_size = 1234
tmp = mock_temp.return_value.__enter__.return_value
@@ -1667,7 +1700,9 @@ class TestFetchToVolumeFormat(test.TestCase):
mock_convert.assert_called_once_with(tmp, dest, volume_format,
out_subformat=None,
run_as_root=run_as_root,
- src_format='raw')
+ src_format=qemu_img_format,
+ image_id=image_id,
+ data=data)
@mock.patch('cinder.image.image_utils.fetch')
@mock.patch('cinder.image.image_utils.qemu_img_info',
@@ -1786,7 +1821,9 @@ class TestFetchToVolumeFormat(test.TestCase):
ctxt = mock.sentinel.context
ctxt.user_id = mock.sentinel.user_id
- image_service = FakeImageService()
+ disk_format = 'ploop'
+ qemu_img_format = image_utils.QEMU_IMG_FORMAT_MAP[disk_format]
+ image_service = FakeImageService(disk_format=disk_format)
image_id = mock.sentinel.image_id
dest = mock.sentinel.dest
volume_format = mock.sentinel.volume_format
@@ -1794,7 +1831,7 @@ class TestFetchToVolumeFormat(test.TestCase):
blocksize = mock.sentinel.blocksize
data = mock_info.return_value
- data.file_format = volume_format
+ data.file_format = qemu_img_format
data.backing_file = None
data.virtual_size = 1234
tmp = mock_temp.return_value.__enter__.return_value
@@ -1819,7 +1856,9 @@ class TestFetchToVolumeFormat(test.TestCase):
mock_convert.assert_called_once_with(tmp, dest, volume_format,
out_subformat=out_subformat,
run_as_root=True,
- src_format='raw')
+ src_format=qemu_img_format,
+ image_id=image_id,
+ data=data)
mock_engine.decompress_img.assert_called()
@@ -2117,3 +2156,368 @@ class TestImageUtils(test.TestCase):
image_utils.decode_cipher,
'aes',
256)
+
+
+@ddt.ddt(testNameFormat=ddt.TestNameFormat.INDEX_ONLY)
+class TestVmdkImageChecks(test.TestCase):
+ def setUp(self):
+ super(TestVmdkImageChecks, self).setUp()
+ # Test data from:
+ # $ qemu-img create -f vmdk fake.vmdk 1M -o subformat=monolithicSparse
+ # $ qemu-img info -f vmdk --output=json fake.vmdk
+ #
+ # What qemu-img calls the "subformat" is called the "createType" in
+ # vmware-speak and it's found at "/format-specific/data/create-type".
+ qemu_img_info = '''
+ {
+ "virtual-size": 1048576,
+ "filename": "fake.vmdk",
+ "cluster-size": 65536,
+ "format": "vmdk",
+ "actual-size": 12288,
+ "format-specific": {
+ "type": "vmdk",
+ "data": {
+ "cid": 1200165687,
+ "parent-cid": 4294967295,
+ "create-type": "monolithicSparse",
+ "extents": [
+ {
+ "virtual-size": 1048576,
+ "filename": "fake.vmdk",
+ "cluster-size": 65536,
+ "format": ""
+ }
+ ]
+ }
+ },
+ "dirty-flag": false
+ }'''
+ self.qdata = imageutils.QemuImgInfo(qemu_img_info, format='json')
+ self.qdata_data = self.qdata.format_specific['data']
+ # we will populate this in each test
+ self.qdata_data["create-type"] = None
+
+ @ddt.data('monolithicSparse', 'streamOptimized')
+ def test_check_vmdk_image_default_config(self, subformat):
+ # none of these should raise
+ self.qdata_data["create-type"] = subformat
+ image_utils.check_vmdk_image(fake.IMAGE_ID, self.qdata)
+
+ @ddt.data('monolithicFlat', 'twoGbMaxExtentFlat')
+ def test_check_vmdk_image_negative_default_config(self, subformat):
+ self.qdata_data["create-type"] = subformat
+ self.assertRaises(exception.ImageUnacceptable,
+ image_utils.check_vmdk_image,
+ fake.IMAGE_ID,
+ self.qdata)
+
+ def test_check_vmdk_image_handles_missing_info(self):
+ expected = 'Unable to determine VMDK createType'
+ # remove create-type
+ del(self.qdata_data['create-type'])
+ iue = self.assertRaises(exception.ImageUnacceptable,
+ image_utils.check_vmdk_image,
+ fake.IMAGE_ID,
+ self.qdata)
+ self.assertIn(expected, str(iue))
+
+ # remove entire data section
+ del(self.qdata_data)
+ iue = self.assertRaises(exception.ImageUnacceptable,
+ image_utils.check_vmdk_image,
+ fake.IMAGE_ID,
+ self.qdata)
+ self.assertIn(expected, str(iue))
+
+ # oslo.utils.imageutils guarantees that format_specific is
+ # defined, so let's see what happens when it's empty
+ self.qdata.format_specific = None
+ iue = self.assertRaises(exception.ImageUnacceptable,
+ image_utils.check_vmdk_image,
+ fake.IMAGE_ID,
+ self.qdata)
+ self.assertIn('no format-specific information is available', str(iue))
+
+ def test_check_vmdk_image_positive(self):
+ allowed = 'twoGbMaxExtentFlat'
+ self.flags(vmdk_allowed_types=['garbage', allowed])
+ self.qdata_data["create-type"] = allowed
+ image_utils.check_vmdk_image(fake.IMAGE_ID, self.qdata)
+
+ @ddt.data('monolithicSparse', 'streamOptimized')
+ def test_check_vmdk_image_negative(self, subformat):
+ allow_list = ['vmfs', 'filler']
+ self.assertNotIn(subformat, allow_list)
+ self.flags(vmdk_allowed_types=allow_list)
+ self.qdata_data["create-type"] = subformat
+ self.assertRaises(exception.ImageUnacceptable,
+ image_utils.check_vmdk_image,
+ fake.IMAGE_ID,
+ self.qdata)
+
+ @ddt.data('monolithicSparse', 'streamOptimized', 'twoGbMaxExtentFlat')
+ def test_check_vmdk_image_negative_empty_list(self, subformat):
+ # anything should raise
+ allow_list = []
+ self.flags(vmdk_allowed_types=allow_list)
+ self.qdata_data["create-type"] = subformat
+ self.assertRaises(exception.ImageUnacceptable,
+ image_utils.check_vmdk_image,
+ fake.IMAGE_ID,
+ self.qdata)
+
+ # OK, now that we know the function works properly, let's make sure
+ # it's called in all the situations where Bug #1996188 indicates that
+ # we need this check
+
+ @mock.patch('cinder.image.image_utils.check_vmdk_image')
+ @mock.patch('cinder.image.image_utils.qemu_img_info')
+ @mock.patch('cinder.image.image_utils.fileutils')
+ @mock.patch('cinder.image.image_utils.fetch')
+ def test_vmdk_subformat_checked_fetch_verify_image(
+ self, mock_fetch, mock_fileutils, mock_info, mock_check):
+ ctxt = mock.sentinel.context
+ image_service = mock.Mock()
+ image_id = mock.sentinel.image_id
+ dest = mock.sentinel.dest
+ mock_info.return_value = self.qdata
+ mock_check.side_effect = exception.ImageUnacceptable(
+ image_id=image_id, reason='mock check')
+
+ iue = self.assertRaises(exception.ImageUnacceptable,
+ image_utils.fetch_verify_image,
+ ctxt, image_service, image_id, dest)
+ self.assertIn('mock check', str(iue))
+ mock_check.assert_called_with(image_id, self.qdata)
+
+ @mock.patch('cinder.image.image_utils.check_vmdk_image')
+ @mock.patch('cinder.image.image_utils.qemu_img_info')
+ @mock.patch('cinder.image.image_utils.get_qemu_data')
+ @mock.patch('cinder.image.image_utils.check_image_conversion_disable')
+ def test_vmdk_subformat_checked_fetch_to_volume_format(
+ self, mock_convert, mock_qdata, mock_info, mock_check):
+ ctxt = mock.sentinel.context
+ image_service = mock.Mock()
+ image_meta = {'disk_format': 'vmdk'}
+ image_service.show.return_value = image_meta
+ image_id = mock.sentinel.image_id
+ dest = mock.sentinel.dest
+ volume_format = mock.sentinel.volume_format
+ blocksize = 1024
+ self.flags(allow_compression_on_image_upload=False)
+ mock_qdata.return_value = self.qdata
+ mock_info.return_value = self.qdata
+ mock_check.side_effect = exception.ImageUnacceptable(
+ image_id=image_id, reason='mock check')
+
+ iue = self.assertRaises(exception.ImageUnacceptable,
+ image_utils.fetch_to_volume_format,
+ ctxt,
+ image_service,
+ image_id,
+ dest,
+ volume_format,
+ blocksize)
+ self.assertIn('mock check', str(iue))
+ mock_check.assert_called_with(image_id, self.qdata)
+
+ @mock.patch('cinder.image.image_utils.check_vmdk_image')
+ @mock.patch('cinder.image.image_utils.qemu_img_info')
+ @mock.patch('cinder.image.image_utils.check_image_conversion_disable')
+ def test_vmdk_subformat_checked_upload_volume(
+ self, mock_convert, mock_info, mock_check):
+ ctxt = mock.sentinel.context
+ image_service = mock.Mock()
+ image_meta = {'disk_format': 'vmdk'}
+ image_id = mock.sentinel.image_id
+ image_meta['id'] = image_id
+ self.flags(allow_compression_on_image_upload=False)
+ mock_info.return_value = self.qdata
+ mock_check.side_effect = exception.ImageUnacceptable(
+ image_id=image_id, reason='mock check')
+
+ iue = self.assertRaises(exception.ImageUnacceptable,
+ image_utils.upload_volume,
+ ctxt,
+ image_service,
+ image_meta,
+ volume_path=mock.sentinel.volume_path,
+ volume_format=mock.sentinel.volume_format)
+ self.assertIn('mock check', str(iue))
+ mock_check.assert_called_with(image_id, self.qdata)
+
+ @mock.patch('cinder.image.image_utils.check_vmdk_image')
+ @mock.patch('cinder.image.image_utils.qemu_img_info')
+ def test_vmdk_checked_convert_image_no_src_format(
+ self, mock_info, mock_check):
+ source = mock.sentinel.source
+ dest = mock.sentinel.dest
+ out_format = mock.sentinel.out_format
+ mock_info.return_value = self.qdata
+ image_id = 'internal image'
+ mock_check.side_effect = exception.ImageUnacceptable(
+ image_id=image_id, reason='mock check')
+
+ self.assertRaises(exception.ImageUnacceptable,
+ image_utils.convert_image,
+ source, dest, out_format)
+ mock_check.assert_called_with(image_id, self.qdata)
+
+
+@ddt.ddt(testNameFormat=ddt.TestNameFormat.INDEX_ONLY)
+class TestImageFormatCheck(test.TestCase):
+ def setUp(self):
+ super(TestImageFormatCheck, self).setUp()
+ qemu_img_info = '''
+ {
+ "virtual-size": 1048576,
+ "filename": "whatever.img",
+ "cluster-size": 65536,
+ "format": "qcow2",
+ "actual-size": 200704,
+ "format-specific": {
+ "type": "qcow2",
+ "data": {
+ "compat": "1.1",
+ "compression-type": "zlib",
+ "lazy-refcounts": false,
+ "refcount-bits": 16,
+ "corrupt": false,
+ "extended-l2": false
+ }
+ },
+ "dirty-flag": false
+ }'''
+ self.qdata = imageutils.QemuImgInfo(qemu_img_info, format='json')
+
+ @mock.patch('cinder.image.image_utils.check_vmdk_image')
+ @mock.patch('cinder.image.image_utils.qemu_img_info')
+ def test_check_image_format_defaults(self, mock_info, mock_vmdk):
+ """Doesn't blow up when only the mandatory arg is passed."""
+ src = mock.sentinel.src
+ mock_info.return_value = self.qdata
+ expected_image_id = 'internal image'
+
+ # empty file_format should raise
+ self.qdata.file_format = None
+ iue = self.assertRaises(exception.ImageUnacceptable,
+ image_utils.check_image_format,
+ src)
+ self.assertIn(expected_image_id, str(iue))
+ mock_info.assert_called_with(src, run_as_root=True)
+
+ # a VMDK should trigger an additional check
+ mock_info.reset_mock()
+ self.qdata.file_format = 'vmdk'
+ image_utils.check_image_format(src)
+ mock_vmdk.assert_called_with(expected_image_id, self.qdata)
+
+ @mock.patch('cinder.image.image_utils.qemu_img_info')
+ def test_check_image_format_uses_passed_data(self, mock_info):
+ src = mock.sentinel.src
+ image_utils.check_image_format(src, data=self.qdata)
+ mock_info.assert_not_called()
+
+ @mock.patch('cinder.image.image_utils.qemu_img_info')
+ def test_check_image_format_mismatch(self, mock_info):
+ src = mock.sentinel.src
+ mock_info.return_value = self.qdata
+ self.qdata.file_format = 'fake_format'
+
+ src_format = 'qcow2'
+ iue = self.assertRaises(exception.ImageUnacceptable,
+ image_utils.check_image_format,
+ src,
+ src_format=src_format)
+ self.assertIn(src_format, str(iue))
+ self.assertIn('different format', str(iue))
+
+ @ddt.data('AMI', 'ami')
+ @mock.patch('cinder.image.image_utils.qemu_img_info')
+ def test_check_image_format_AMI(self, ami, mock_info):
+ """Mismatch OK in this case, see change Icde4c0f936ce."""
+ src = mock.sentinel.src
+ mock_info.return_value = self.qdata
+ self.qdata.file_format = 'raw'
+
+ src_format = ami
+ image_utils.check_image_format(src, src_format=src_format)
+
+ @mock.patch('cinder.image.image_utils._convert_image')
+ @mock.patch('cinder.image.image_utils.check_image_format')
+ def test_check_image_format_called_by_convert_image(
+ self, mock_check, mock__convert):
+ """Make sure the function we've been testing is actually called."""
+ src = mock.sentinel.src
+ dest = mock.sentinel.dest
+ out_fmt = mock.sentinel.out_fmt
+
+ image_utils.convert_image(src, dest, out_fmt)
+ mock_check.assert_called_once_with(src, None, None, None, True)
+
+
+@ddt.ddt
+class TestFilterReservedNamespaces(test.TestCase):
+
+ def setUp(self):
+ super(TestFilterReservedNamespaces, self).setUp()
+ self.mock_object(image_utils, 'LOG', side_effect=image_utils.LOG)
+
+ def test_filter_out_reserved_namespaces_metadata_with_empty_metadata(self):
+ metadata_for_test = None
+ method_return = image_utils.filter_out_reserved_namespaces_metadata(
+ metadata_for_test)
+
+ self.assertEqual({}, method_return)
+
+ image_utils.LOG.debug.assert_has_calls(
+ [mock.call("No metadata to be filtered.")]
+ )
+
+ @ddt.data( # remove default keys
+ ({"some_key": 13, "other_key": "test",
+ "os_glance_key": "this should be removed",
+ "os_glance_key2": "this should also be removed"},
+ None,
+ []),
+ # remove nothing
+ ({"some_key": 13, "other_key": "test"},
+ None,
+ []),
+ # custom config empty
+ ({"some_key": 13, "other_key": "test",
+ "os_glance_key": "this should be removed",
+ "os_glance_key2": "this should also be removed"},
+ [],
+ []),
+ # custom config
+ ({"some_key": 13, "other_key": "test",
+ "os_glance_key": "this should be removed",
+ "os_glance_key2": "this should also be removed",
+ "custom_key": "this should be removed",
+ "another_custom_key": "this should also be removed"},
+ ['custom_key', 'another_custom_key'],
+ ['custom_key', 'another_custom_key']))
+ @ddt.unpack
+ def test_filter_out_reserved_namespaces_metadata(
+ self, metadata_for_test, config, keys_to_pop):
+ hardcoded_keys = ['os_glance', "img_signature"]
+
+ keys_to_pop = hardcoded_keys + keys_to_pop
+
+ if config:
+ self.override_config('reserved_image_namespaces', config)
+
+ expected_result = {"some_key": 13, "other_key": "test"}
+
+ method_return = image_utils.filter_out_reserved_namespaces_metadata(
+ metadata_for_test)
+
+ self.assertEqual(expected_result, method_return)
+
+ image_utils.LOG.debug.assert_has_calls([
+ mock.call("The metadata set [%s] was filtered using the reserved "
+ "name spaces [%s], and the result is [%s].",
+ metadata_for_test, keys_to_pop, expected_result)
+ ])
diff --git a/cinder/tests/unit/volume/drivers/test_nfs.py b/cinder/tests/unit/volume/drivers/test_nfs.py
index 7aab7eff2..b1fa90cd1 100644
--- a/cinder/tests/unit/volume/drivers/test_nfs.py
+++ b/cinder/tests/unit/volume/drivers/test_nfs.py
@@ -336,102 +336,112 @@ NFS_CONFIG4 = {'max_over_subscription_ratio': 20.0,
'nas_secure_file_permissions': 'false',
'nas_secure_file_operations': 'true'}
-QEMU_IMG_INFO_OUT1 = """image: %(volid)s
- file format: raw
- virtual size: %(size_gb)sG (%(size_b)s bytes)
- disk size: 173K
- """
-
-QEMU_IMG_INFO_OUT2 = """image: %(volid)s
-file format: qcow2
-virtual size: %(size_gb)sG (%(size_b)s bytes)
-disk size: 196K
-cluster_size: 65536
-Format specific information:
- compat: 1.1
- lazy refcounts: false
- refcount bits: 16
- corrupt: false
- """
-
-QEMU_IMG_INFO_OUT3 = """image: volume-%(volid)s.%(snapid)s
-file format: qcow2
-virtual size: %(size_gb)sG (%(size_b)s bytes)
-disk size: 196K
-cluster_size: 65536
-backing file: volume-%(volid)s
-backing file format: qcow2
-Format specific information:
- compat: 1.1
- lazy refcounts: false
- refcount bits: 16
- corrupt: false
- """
-
-QEMU_IMG_INFO_OUT4 = """image: volume-%(volid)s.%(snapid)s
-file format: raw
-virtual size: %(size_gb)sG (%(size_b)s bytes)
-disk size: 196K
-cluster_size: 65536
-backing file: volume-%(volid)s
-backing file format: raw
-Format specific information:
- compat: 1.1
- lazy refcounts: false
- refcount bits: 16
- corrupt: false
- """
-
-QEMU_IMG_INFO_OUT5 = """image: volume-%(volid)s.%(snapid)s
-file format: qcow2
-virtual size: %(size_gb)sG (%(size_b)s bytes)
-disk size: 196K
-encrypted: yes
-cluster_size: 65536
-backing file: volume-%(volid)s
-backing file format: raw
-Format specific information:
- compat: 1.1
- lazy refcounts: false
- refcount bits: 16
- encrypt:
- ivgen alg: plain64
- hash alg: sha256
- cipher alg: aes-256
- uuid: 386f8626-33f0-4683-a517-78ddfe385e33
- format: luks
- cipher mode: xts
- slots:
- [0]:
- active: true
- iters: 1892498
- key offset: 4096
- stripes: 4000
- [1]:
- active: false
- key offset: 262144
- [2]:
- active: false
- key offset: 520192
- [3]:
- active: false
- key offset: 778240
- [4]:
- active: false
- key offset: 1036288
- [5]:
- active: false
- key offset: 1294336
- [6]:
- active: false
- key offset: 1552384
- [7]:
- active: false
- key offset: 1810432
- payload offset: 2068480
- master key iters: 459347
- corrupt: false
-"""
+QEMU_IMG_INFO_OUT1 = """{
+ "filename": "%(volid)s",
+ "format": "raw",
+ "virtual-size": %(size_b)s,
+ "actual-size": 173000
+}"""
+
+QEMU_IMG_INFO_OUT2 = """{
+ "filename": "%(volid)s",
+ "format": "qcow2",
+ "virtual-size": %(size_b)s,
+ "actual-size": 196000,
+ "cluster-size": 65536,
+ "format-specific": {
+ "compat": "1.1",
+ "lazy-refcounts": false,
+ "refcount-bits": 16,
+ "corrupt": false
+ }
+}"""
+
+QEMU_IMG_INFO_OUT3 = """{
+ "filename": "volume-%(volid)s.%(snapid)s",
+ "format": "qcow2",
+ "virtual-size": %(size_b)s,
+ "actual-size": 196000,
+ "cluster-size": 65536,
+ "backing-filename": "volume-%(volid)s",
+ "backing-filename-format": "qcow2",
+ "format-specific": {
+ "compat": "1.1",
+ "lazy-refcounts": false,
+ "refcount-bits": 16,
+ "corrupt": false
+ }
+}"""
+
+QEMU_IMG_INFO_OUT4 = """{
+ "filename": "volume-%(volid)s.%(snapid)s",
+ "format": "raw",
+ "virtual-size": %(size_b)s,
+ "actual-size": 196000,
+ "cluster-size": 65536,
+ "backing-filename": "volume-%(volid)s",
+ "backing-filename-format": "raw"
+}"""
+
+QEMU_IMG_INFO_OUT5 = """{
+ "filename": "volume-%(volid)s.%(snapid)s",
+ "format": "qcow2",
+ "virtual-size": %(size_b)s,
+ "actual-size": 196000,
+ "encrypted": true,
+ "cluster-size": 65536,
+ "backing-filename": "volume-%(volid)s",
+ "backing-filename-format": "raw",
+ "format-specific": {
+ "type": "luks",
+ "data": {
+ "ivgen-alg": "plain64",
+ "hash-alg": "sha256",
+ "cipher-alg": "aes-256",
+ "uuid": "386f8626-33f0-4683-a517-78ddfe385e33",
+ "cipher-mode": "xts",
+ "slots": [
+ {
+ "active": true,
+ "iters": 1892498,
+ "key offset": 4096,
+ "stripes": 4000
+ },
+ {
+ "active": false,
+ "key offset": 262144
+ },
+ {
+ "active": false,
+ "key offset": 520192
+ },
+ {
+ "active": false,
+ "key offset": 778240
+ },
+ {
+ "active": false,
+ "key offset": 1036288
+ },
+ {
+ "active": false,
+ "key offset": 1294336
+ },
+ {
+ "active": false,
+ "key offset": 1552384
+ },
+ {
+ "active": false,
+ "key offset": 1810432
+ }
+ ],
+ "payload-offset": 2068480,
+ "master-key-iters": 459347
+ },
+ "corrupt": false
+ }
+}"""
@ddt.ddt
@@ -1323,7 +1333,7 @@ class NfsDriverTestCase(test.TestCase):
'size_gb': src_volume.size,
'size_b': src_volume.size * units.Gi}
- img_info = imageutils.QemuImgInfo(img_out)
+ img_info = imageutils.QemuImgInfo(img_out, format='json')
mock_img_info = self.mock_object(image_utils, 'qemu_img_info')
mock_img_info.return_value = img_info
mock_convert_image = self.mock_object(image_utils, 'convert_image')
@@ -1406,7 +1416,7 @@ class NfsDriverTestCase(test.TestCase):
'snapid': fake_snap.id,
'size_gb': src_volume.size,
'size_b': src_volume.size * units.Gi}
- img_info = imageutils.QemuImgInfo(img_out)
+ img_info = imageutils.QemuImgInfo(img_out, format='json')
mock_img_info = self.mock_object(image_utils, 'qemu_img_info')
mock_img_info.return_value = img_info
@@ -1472,7 +1482,8 @@ class NfsDriverTestCase(test.TestCase):
mock_img_utils = self.mock_object(image_utils, 'qemu_img_info')
img_out = qemu_img_info % {'volid': volume.id, 'size_gb': volume.size,
'size_b': volume.size * units.Gi}
- mock_img_utils.return_value = imageutils.QemuImgInfo(img_out)
+ mock_img_utils.return_value = imageutils.QemuImgInfo(img_out,
+ format='json')
self.mock_object(drv, '_read_info_file',
return_value={'active': "volume-%s" % volume.id})
@@ -1492,12 +1503,14 @@ class NfsDriverTestCase(test.TestCase):
drv = self._driver
volume = self._simple_volume()
- qemu_img_output = """image: %s
- file format: iso
- virtual size: 1.0G (1073741824 bytes)
- disk size: 173K
- """ % volume['name']
- mock_img_info.return_value = imageutils.QemuImgInfo(qemu_img_output)
+ qemu_img_output = """{
+ "filename": "%s",
+ "format": "iso",
+ "virtual-size": 1073741824,
+ "actual-size": 173000
+}""" % volume['name']
+ mock_img_info.return_value = imageutils.QemuImgInfo(qemu_img_output,
+ format='json')
self.assertRaises(exception.InvalidVolume,
drv.initialize_connection,
diff --git a/cinder/tests/unit/volume/drivers/test_quobyte.py b/cinder/tests/unit/volume/drivers/test_quobyte.py
index 767afd97e..b4e41f04e 100644
--- a/cinder/tests/unit/volume/drivers/test_quobyte.py
+++ b/cinder/tests/unit/volume/drivers/test_quobyte.py
@@ -941,13 +941,14 @@ class QuobyteDriverTestCase(test.TestCase):
self.TEST_QUOBYTE_VOLUME),
self.VOLUME_UUID)
- qemu_img_info_output = """image: volume-%s
- file format: qcow2
- virtual size: 1.0G (1073741824 bytes)
- disk size: 473K
- """ % self.VOLUME_UUID
+ qemu_img_info_output = """{
+ "filename": "volume-%s",
+ "format": "qcow2",
+ "virtual-size": 1073741824,
+ "actual-size": 473000
+}""" % self.VOLUME_UUID
- img_info = imageutils.QemuImgInfo(qemu_img_info_output)
+ img_info = imageutils.QemuImgInfo(qemu_img_info_output, format='json')
self.mock_object(image_utils, 'qemu_img_info', return_value=img_info)
self.mock_object(image_utils, 'resize_image')
@@ -987,13 +988,14 @@ class QuobyteDriverTestCase(test.TestCase):
size = dest_volume['size']
- qemu_img_output = """image: %s
- file format: raw
- virtual size: 1.0G (1073741824 bytes)
- disk size: 173K
- backing file: %s
- """ % (snap_file, src_volume['name'])
- img_info = imageutils.QemuImgInfo(qemu_img_output)
+ qemu_img_output = """{
+ "filename": "%s",
+ "format": "raw",
+ "virtual-size": 1073741824,
+ "actual-size": 173000,
+ "backing-filename": "%s"
+}""" % (snap_file, src_volume['name'])
+ img_info = imageutils.QemuImgInfo(qemu_img_output, format='json')
# mocking and testing starts here
mock_convert = self.mock_object(image_utils, 'convert_image')
@@ -1043,13 +1045,14 @@ class QuobyteDriverTestCase(test.TestCase):
size = dest_volume['size']
- qemu_img_output = """image: %s
- file format: raw
- virtual size: 1.0G (1073741824 bytes)
- disk size: 173K
- backing file: %s
- """ % (snap_file, src_volume['name'])
- img_info = imageutils.QemuImgInfo(qemu_img_output)
+ qemu_img_output = """{
+ "filename": "%s",
+ "format": "raw",
+ "virtual-size": 1073741824,
+ "actual-size": 173000,
+ "backing-filename": "%s"
+}""" % (snap_file, src_volume['name'])
+ img_info = imageutils.QemuImgInfo(qemu_img_output, format='json')
# mocking and testing starts here
mock_convert = self.mock_object(image_utils, 'convert_image')
@@ -1104,13 +1107,14 @@ class QuobyteDriverTestCase(test.TestCase):
size = dest_volume['size']
- qemu_img_output = """image: %s
- file format: raw
- virtual size: 1.0G (1073741824 bytes)
- disk size: 173K
- backing file: %s
- """ % (snap_file, src_volume['name'])
- img_info = imageutils.QemuImgInfo(qemu_img_output)
+ qemu_img_output = """{
+ "filename": "%s",
+ "format": "raw",
+ "virtual-size": 1073741824,
+ "actual-size": 173000,
+ "backing-filename": "%s"
+}""" % (snap_file, src_volume['name'])
+ img_info = imageutils.QemuImgInfo(qemu_img_output, format='json')
# mocking and testing starts here
mock_convert = self.mock_object(image_utils, 'convert_image')
@@ -1168,13 +1172,14 @@ class QuobyteDriverTestCase(test.TestCase):
size = dest_volume['size']
- qemu_img_output = """image: %s
- file format: raw
- virtual size: 1.0G (1073741824 bytes)
- disk size: 173K
- backing file: %s
- """ % (snap_file, src_volume['name'])
- img_info = imageutils.QemuImgInfo(qemu_img_output)
+ qemu_img_output = """{
+ "filename": "%s",
+ "format": "raw",
+ "virtual-size": 1073741824,
+ "actual-size": 173000,
+ "backing-filename": "%s"
+}""" % (snap_file, src_volume['name'])
+ img_info = imageutils.QemuImgInfo(qemu_img_output, format='json')
# mocking and testing starts here
mock_convert = self.mock_object(image_utils, 'convert_image')
@@ -1245,12 +1250,13 @@ class QuobyteDriverTestCase(test.TestCase):
drv._get_hash_str(self.TEST_QUOBYTE_VOLUME))
vol_path = os.path.join(vol_dir, volume['name'])
- qemu_img_output = """image: %s
- file format: raw
- virtual size: 1.0G (1073741824 bytes)
- disk size: 173K
- """ % volume['name']
- img_info = imageutils.QemuImgInfo(qemu_img_output)
+ qemu_img_output = """{
+ "filename": "%s",
+ "format": "raw",
+ "virtual-size": 1073741824,
+ "actual-size": 173000
+}""" % volume['name']
+ img_info = imageutils.QemuImgInfo(qemu_img_output, format='json')
drv.get_active_image_from_info = mock.Mock(return_value=volume['name'])
self.mock_object(image_utils, 'qemu_img_info', return_value=img_info)
@@ -1294,12 +1300,13 @@ class QuobyteDriverTestCase(test.TestCase):
mock_create_temporary_file.return_value = self.TEST_TMP_FILE
- qemu_img_output = """image: %s
- file format: raw
- virtual size: 1.0G (1073741824 bytes)
- disk size: 173K
- """ % volume['name']
- img_info = imageutils.QemuImgInfo(qemu_img_output)
+ qemu_img_output = """{
+ "filename": "%s",
+ "format": "raw",
+ "virtual-size": 1073741824,
+ "actual-size": 173000
+}""" % volume['name']
+ img_info = imageutils.QemuImgInfo(qemu_img_output, format='json')
mock_qemu_img_info.return_value = img_info
upload_path = volume_path
@@ -1346,12 +1353,13 @@ class QuobyteDriverTestCase(test.TestCase):
mock_create_temporary_file.return_value = self.TEST_TMP_FILE
- qemu_img_output = """image: %s
- file format: qcow2
- virtual size: 1.0G (1073741824 bytes)
- disk size: 173K
- """ % volume['name']
- img_info = imageutils.QemuImgInfo(qemu_img_output)
+ qemu_img_output = """{
+ "filename": "%s",
+ "format": "qcow2",
+ "virtual-size": 1073741824,
+ "actual-size": 173000
+}""" % volume['name']
+ img_info = imageutils.QemuImgInfo(qemu_img_output, format='json')
mock_qemu_img_info.return_value = img_info
upload_path = self.TEST_TMP_FILE
@@ -1401,13 +1409,14 @@ class QuobyteDriverTestCase(test.TestCase):
mock_create_temporary_file.return_value = self.TEST_TMP_FILE
- qemu_img_output = """image: volume-%s.%s
- file format: qcow2
- virtual size: 1.0G (1073741824 bytes)
- disk size: 173K
- backing file: %s
- """ % (self.VOLUME_UUID, self.SNAP_UUID, volume_filename)
- img_info = imageutils.QemuImgInfo(qemu_img_output)
+ qemu_img_output = """{
+ "filename": "volume-%s.%s",
+ "format": "qcow2",
+ "virtual-size": 1073741824,
+ "actual-size": 173000,
+ "backing-filename": "%s"
+}""" % (self.VOLUME_UUID, self.SNAP_UUID, volume_filename)
+ img_info = imageutils.QemuImgInfo(qemu_img_output, format='json')
mock_qemu_img_info.return_value = img_info
upload_path = self.TEST_TMP_FILE
diff --git a/cinder/volume/api.py b/cinder/volume/api.py
index 87e64d41e..39e3efa5d 100644
--- a/cinder/volume/api.py
+++ b/cinder/volume/api.py
@@ -43,6 +43,7 @@ from cinder import flow_utils
from cinder.i18n import _
from cinder.image import cache as image_cache
from cinder.image import glance
+from cinder.image import image_utils
from cinder.message import api as message_api
from cinder.message import message_field
from cinder import objects
@@ -1483,6 +1484,9 @@ class API(base.Base):
try:
self._merge_volume_image_meta(context, volume, metadata)
+ metadata = image_utils.filter_out_reserved_namespaces_metadata(
+ metadata)
+
recv_metadata = self.image_service.create(context, metadata)
# NOTE(ZhengMa): Check if allow image compression before image
diff --git a/cinder/volume/flows/manager/create_volume.py b/cinder/volume/flows/manager/create_volume.py
index 782b991f6..0ae3cb59d 100644
--- a/cinder/volume/flows/manager/create_volume.py
+++ b/cinder/volume/flows/manager/create_volume.py
@@ -25,6 +25,7 @@ from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import fileutils
from oslo_utils import netutils
+from oslo_utils import strutils
from oslo_utils import timeutils
from oslo_utils import uuidutils
import taskflow.engines
@@ -572,7 +573,8 @@ class CreateVolumeFromSpecTask(flow_utils.CinderTask):
volume,
encryption)
- if image_info.encrypted == 'yes':
+ # see Bug #1942682 and Change I949f07582a708 for why we do this
+ if strutils.bool_from_string(image_info.encrypted):
key_str = source_pass + "\n" + new_pass + "\n"
del source_pass
diff --git a/releasenotes/notes/bug-1996188-vmdk-subformat-allow-list-93e6943d9a486d11.yaml b/releasenotes/notes/bug-1996188-vmdk-subformat-allow-list-93e6943d9a486d11.yaml
new file mode 100644
index 000000000..9b3292189
--- /dev/null
+++ b/releasenotes/notes/bug-1996188-vmdk-subformat-allow-list-93e6943d9a486d11.yaml
@@ -0,0 +1,33 @@
+---
+upgrade:
+ - |
+ This release introduces a new configuration option,
+ ``vmdk_allowed_types``, that specifies the list of VMDK image
+ subformats that Cinder will allow. The default setting allows
+ only the 'streamOptimized' and 'monolithicSparse' subformats,
+ which do not use named extents.
+security:
+ - |
+ This release introduces a new configuration option,
+ ``vmdk_allowed_types``, that specifies the list of VMDK image
+ subformats that Cinder will allow in order to prevent exposure
+ of host information by modifying the named extents in a VMDK
+ image. The default setting allows only the 'streamOptimized'
+ and 'monolithicSparse' subformats, which do not use named extents.
+ - |
+ As part of the fix for `Bug #1996188
+ <https://bugs.launchpad.net/cinder/+bug/1996188>`_, cinder is now more
+ strict in checking that the ``disk_format`` recorded for an image (as
+ revealed by the Image Service API image-show response) matches what
+ cinder detects when it downloads the image. Thus, some requests to
+ create a volume from a source image that had previously succeeded may
+ fail with an ``ImageUnacceptable`` error.
+fixes:
+ - |
+ `Bug #1996188 <https://bugs.launchpad.net/cinder/+bug/1996188>`_:
+ Fixed issue where a VMDK image file whose createType allowed named
+ extents could expose host information. This change introduces a new
+ configuration option, ``vmdk_allowed_types``, that specifies the list
+ of VMDK image subformats that Cinder will allow. The default
+ setting allows only the 'streamOptimized' and 'monolithicSparse'
+ subformats.
diff --git a/releasenotes/notes/fix-reserved-image-properties-9519ddc080e7ed1a.yaml b/releasenotes/notes/fix-reserved-image-properties-9519ddc080e7ed1a.yaml
new file mode 100644
index 000000000..8f3f92eee
--- /dev/null
+++ b/releasenotes/notes/fix-reserved-image-properties-9519ddc080e7ed1a.yaml
@@ -0,0 +1,28 @@
+---
+upgrade:
+ - |
+ We introduced a new config parameter, ``reserved_image_namespaces``,
+ that allows operators to set the image properties to filter out from
+ volume image metadata by namespace when uploading a volume to Glance.
+ These properties, if not filtered out, cause failures when uploading
+ images back to Glance. The error will happen on Glance side when the
+ reserved namespaces are used. This option is also useful when an operator
+ wants to use the Glance property protections feature to make some image
+ properties read-only.
+fixes:
+ - |
+ `Bug #1945500 <https://bugs.launchpad.net/cinder/+bug/1945500>`_: Fixed
+ an error when uploading to Glance a previously downloaded glance image
+ when glance multistore is enabled. Glance reserves image properties
+ in the namespace 'os_glance' for its own use and will not allow
+ images to be created with these properties. Additionally, there are image
+ properties, such as those associated with image signature verification,
+ that are stored in a volume's image metadata, which should not be added
+ to a new image when a volume is being uploaded as an image. Thus Cinder
+ will no longer include any volume image metadata in the namespaces
+ ``os_glance`` and ``img_signature`` when it creates an image in Glance.
+ Furthermore, because the Glance property protections feature allows an
+ operator to configure specific image properties as read-only, this fix
+ adds a configuration option, ``reserved_image_namespaces``, that allows an
+ operator to exclude additional image properties by namespace (the
+ ``os_glance`` and ``img_signature`` namespaces are *always* excluded).