Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Synchronise yoga with upstream #57

Open
wants to merge 7 commits into
base: stackhpc/yoga
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .zuul.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@
The regular tempest-integrated-storage job but with glance metadata injection
post-run: playbooks/post-check-metadata-injection.yaml
vars:
configure_swap_size: 8192
tempest_concurrency: 3
zuul_copy_output:
/etc/glance-remote: logs
devstack_localrc:
Expand Down Expand Up @@ -306,7 +308,6 @@
- release-notes-jobs-python3
check:
jobs:
- openstack-tox-functional-py36-fips
- openstack-tox-functional-py39
- glance-tox-functional-py39-rbac-defaults
- glance-ceph-thin-provisioning:
Expand Down
26 changes: 21 additions & 5 deletions glance/common/format_inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ class VHDXInspector(FileInspector):
"""
METAREGION = '8B7CA206-4790-4B9A-B8FE-575F050F886E'
VIRTUAL_DISK_SIZE = '2FA54224-CD1B-4876-B211-5DBED83BF4B8'
VHDX_METADATA_TABLE_MAX_SIZE = 32 * 2048 # From qemu

def __init__(self, *a, **k):
super(VHDXInspector, self).__init__(*a, **k)
Expand Down Expand Up @@ -459,6 +460,8 @@ def _find_meta_entry(self, desired_guid):
item_offset, item_length, _reserved = struct.unpack(
'<III',
meta_buffer[entry_offset + 16:entry_offset + 28])
item_length = min(item_length,
self.VHDX_METADATA_TABLE_MAX_SIZE)
self.region('metadata').length = len(meta_buffer)
self._log.debug('Found entry at offset %x', item_offset)
# Metadata item offset is from the beginning of the metadata
Expand Down Expand Up @@ -509,13 +512,19 @@ def __str__(self):
#
# https://www.vmware.com/app/vmdk/?src=vmdk
class VMDKInspector(FileInspector):
"""vmware VMDK format (monolithicSparse variant only)
"""vmware VMDK format (monolithicSparse and streamOptimized variants only)

This needs to store the 512 byte header and the descriptor region
which should be just after that. The descriptor region is some
variable number of 512 byte sectors, but is just text defining the
layout of the disk.
"""

# The beginning and max size of the descriptor is also hardcoded in Qemu
# at 0x200 and 1MB - 1
DESC_OFFSET = 0x200
DESC_MAX_SIZE = (1 << 20) - 1

def __init__(self, *a, **k):
super(VMDKInspector, self).__init__(*a, **k)
self.new_region('header', CaptureRegion(0, 512))
Expand All @@ -532,15 +541,22 @@ def post_process(self):

if sig != b'KDMV':
raise ImageFormatError('Signature KDMV not found: %r' % sig)
return

if ver not in (1, 2, 3):
raise ImageFormatError('Unsupported format version %i' % ver)
return

# Since we parse both desc_sec and desc_num (the location of the
# VMDK's descriptor, expressed in 512 bytes sectors) we enforce a
# check on the bounds to create a reasonable CaptureRegion. This
# is similar to how it's done in qemu.
desc_offset = desc_sec * 512
desc_size = min(desc_num * 512, self.DESC_MAX_SIZE)
if desc_offset != self.DESC_OFFSET:
raise ImageFormatError("Wrong descriptor location")

if not self.has_region('descriptor'):
self.new_region('descriptor', CaptureRegion(
desc_sec * 512, desc_num * 512))
desc_offset, desc_size))

@property
def format_match(self):
Expand All @@ -566,7 +582,7 @@ def virtual_size(self):
vmdktype = descriptor[type_idx:type_end]
else:
vmdktype = b'formatnotfound'
if vmdktype != b'monolithicSparse':
if vmdktype not in (b'monolithicSparse', b'streamOptimized'):
LOG.warning('Unsupported VMDK format %s', vmdktype)
return 0

Expand Down
165 changes: 158 additions & 7 deletions glance/tests/unit/common/test_format_inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import io
import os
import re
import struct
import subprocess
import tempfile
from unittest import mock
Expand Down Expand Up @@ -50,16 +51,51 @@ def tearDown(self):
except Exception:
pass

def _create_img(self, fmt, size):
def _create_img(self, fmt, size, subformat=None):
if fmt == 'vhd':
# QEMU calls the vhd format vpc
fmt = 'vpc'

fn = tempfile.mktemp(prefix='glance-unittest-formatinspector-',
opt = ''
prefix = 'glance-unittest-formatinspector-'

if subformat:
opt = ' -o subformat=%s' % subformat
prefix += subformat + '-'

fn = tempfile.mktemp(prefix=prefix,
suffix='.%s' % fmt)
self._created_files.append(fn)
subprocess.check_output(
'qemu-img create -f %s %s %i' % (fmt, fn, size),
'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 = 'glance-unittest-formatinspector-%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

Expand All @@ -78,8 +114,9 @@ def _test_format_at_block_size(self, format_name, img, block_size):
wrapper.close()
return fmt

def _test_format_at_image_size(self, format_name, image_size):
img = self._create_img(format_name, image_size)
def _test_format_at_image_size(self, format_name, image_size,
subformat=None):
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
Expand All @@ -101,11 +138,12 @@ def _test_format_at_image_size(self, format_name, image_size):
'Format used more than 512KiB of memory: %s' % (
fmt.context_info))

def _test_format(self, format_name):
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)
self._test_format_at_image_size(format_name, image_size * units.Mi,
subformat=subformat)

def test_qcow2(self):
self._test_format('qcow2')
Expand All @@ -119,6 +157,80 @@ def test_vhdx(self):
def test_vmdk(self):
self._test_format('vmdk')

def test_vmdk_stream_optimized(self):
self._test_format('vmdk', 'streamOptimized')

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_vdi(self):
self._test_format('vdi')

Expand Down Expand Up @@ -275,3 +387,42 @@ 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_utils.BaseTestCase):
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)
16 changes: 10 additions & 6 deletions playbooks/post-check-metadata-injection.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@
set -xe
cirrosimg=$(glance image-list | grep cirros | cut -d" " -f 2)

echo "Dumping the cirros image for debugging..."
glance image-show $cirrosimg
# There could be more than one cirros image so traverse through the list
for image in $cirrosimg
do
echo "Dumping the cirros image for debugging..."
glance image-show $image

echo "Checking that the cirros image was decorated with metdata on import..."
glance image-list --property-filter 'glance_devstack_test=doyouseeme?' | grep cirros
echo "Checking that the cirros image was decorated with metdata on import..."
glance image-list --property-filter 'glance_devstack_test=doyouseeme?' | grep $image

echo "Checking that the cirros image was converted to raw on import..."
glance image-show $cirrosimg | egrep -e 'disk_format.*raw'
echo "Checking that the cirros image was converted to raw on import..."
glance image-show $image | egrep -e 'disk_format.*raw'
done
environment: '{{ zuul | zuul_legacy_vars }}'
7 changes: 7 additions & 0 deletions tools/test_format_inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ def main():
else:
print('Confirmed size with qemu-img')

print('Image safety check: %s' % (
fmt.safety_check() and 'passed' or 'FAILED'))
if args.input:
detected_fmt = format_inspector.detect_file_format(args.input)
print('Detected inspector for image as: %s' % (
detected_fmt.__class__.__name__))


if __name__ == '__main__':
sys.exit(main())
6 changes: 6 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ skip_missing_interpreters = true
# this allows tox to infer the base python from the environment name
# and override any basepython configured in this file
ignore_basepython_conflict=true
# Cap setuptools via virtualenv to prevent compatibility issue with yoga
# branch's upper constraint of 'packaging' package (21.3).
requires =
virtualenv<20.26.4
tox<4
setuptools<71.0.0

[testenv]
# Set default python version
Expand Down