Ensure non-q35 machine type is not used when booting with SEV
As explained in the SEV spec[0], SEV needs a q35 machine type in order to bind all the virtio devices to the PCIe bridge so that they use virtio 1.0 and not virtio 0.9, since QEMU's iommu_platform feature was added in virtio 1.0 only. So add an extra check to be run in the API layer whenever SEV is requested, to ensure that a machine type *outside* the q35 family (e.g. 'pc', or something like 'pc-i440fx-2.11') is not explicitly selected in the image via the hw_machine_type property. Since code in the API layer doesn't run on the compute host, at this stage we can't check CONF.libvirt.hw_machine_type via libvirt.utils. A subsequent commit will change the libvirt driver to trigger an extra, later check on the compute node, although if that late check fails, the best we can do is to fail the operation on that compute host, in which case it will potentially be retried on another compute host. nova's hardcoded default for x86_64 is 'pc' (which in fact matches QEMU's current default). This means that it will be recommended that SEV-capable compute hosts have CONF.libvirt.hw_machine_type configured to include 'x86_64=q35', otherwise attempts to boot SEV guests without the image property 'hw_machine_type=q35' will fail unpleasantly. In the future it is expected that both of these defaults will change to 'q35'[1]. Once that happens, x86_64 images will be bootable without needing to set either the hw_machine_type image property or CONF.libvirt.hw_machine_type. While extending the unit tests for invalid combinations of image properties, add tests for the case where the 'hw_firmware_type' property is not specified at all; previously the tests only covered the case where it was set to 'bios'. [0] http://specs.openstack.org/openstack/nova-specs/specs/train/approved/amd-sev-libvirt-support.html [1] https://bugs.launchpad.net/nova/+bug/1780138 blueprint: amd-sev-libvirt-support Change-Id: Ibf66a0b371685c673644493bf12663dbf71fab6c
This commit is contained in:
parent
f7f5e1846c
commit
c1179d1ff2
|
@ -75,6 +75,7 @@ INVALID_FLAVOR_IMAGE_EXCEPTIONS = (
|
|||
exception.InvalidCPUAllocationPolicy,
|
||||
exception.InvalidCPUThreadAllocationPolicy,
|
||||
exception.InvalidEmulatorThreadsPolicy,
|
||||
exception.InvalidMachineType,
|
||||
exception.InvalidNUMANodesNumber,
|
||||
exception.InvalidRequest,
|
||||
exception.MemoryPageSizeForbidden,
|
||||
|
|
|
@ -1977,6 +1977,11 @@ class InvalidHypervisorVirtType(Invalid):
|
|||
"recognised")
|
||||
|
||||
|
||||
class InvalidMachineType(Invalid):
|
||||
msg_fmt = _("Machine type '%(mtype)s' is not compatible with image "
|
||||
"%(image_name)s (%(image_id)s): %(reason)s")
|
||||
|
||||
|
||||
class InvalidVirtualMachineMode(Invalid):
|
||||
msg_fmt = _("Virtual machine mode '%(vmmode)s' is not recognised")
|
||||
|
||||
|
|
|
@ -6140,6 +6140,15 @@ class ServersControllerCreateTest(test.TestCase):
|
|||
self.controller.create,
|
||||
self.req, body=self.body)
|
||||
|
||||
@mock.patch('nova.virt.hardware.get_mem_encryption_constraint',
|
||||
side_effect=exception.InvalidMachineType(
|
||||
message="fake conflict reason"))
|
||||
def test_create_instance_raise_invalid_machine_type(
|
||||
self, mock_conflict):
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller.create,
|
||||
self.req, body=self.body)
|
||||
|
||||
@mock.patch('nova.virt.hardware.numa_get_constraints',
|
||||
side_effect=exception.ImageCPUPinningForbidden())
|
||||
def test_create_instance_raise_image_cpu_pinning_forbidden(
|
||||
|
|
|
@ -1317,7 +1317,9 @@ class TestEncryptedMemoryTranslation(TestUtilsBase):
|
|||
'hw:mem_encryption extra spec',
|
||||
{'hw:mem_encryption': extra_spec},
|
||||
image=objects.ImageMeta(
|
||||
id='005249be-3c2f-4351-9df7-29bb13c21b14',
|
||||
properties=objects.ImageMetaProps(
|
||||
hw_machine_type='q35',
|
||||
hw_firmware_type='uefi'))
|
||||
)
|
||||
|
||||
|
@ -1327,8 +1329,10 @@ class TestEncryptedMemoryTranslation(TestUtilsBase):
|
|||
'hw_mem_encryption image property',
|
||||
{},
|
||||
image=objects.ImageMeta(
|
||||
id='005249be-3c2f-4351-9df7-29bb13c21b14',
|
||||
name=self.image_name,
|
||||
properties=objects.ImageMetaProps(
|
||||
hw_machine_type='q35',
|
||||
hw_firmware_type='uefi',
|
||||
hw_mem_encryption=image_prop))
|
||||
)
|
||||
|
@ -1341,8 +1345,10 @@ class TestEncryptedMemoryTranslation(TestUtilsBase):
|
|||
'hw_mem_encryption image property',
|
||||
{'hw:mem_encryption': extra_spec},
|
||||
image=objects.ImageMeta(
|
||||
id='005249be-3c2f-4351-9df7-29bb13c21b14',
|
||||
name=self.image_name,
|
||||
properties=objects.ImageMetaProps(
|
||||
hw_machine_type='q35',
|
||||
hw_firmware_type='uefi',
|
||||
hw_mem_encryption=image_prop))
|
||||
)
|
||||
|
|
|
@ -3798,32 +3798,50 @@ class MemEncryptionFlavorImageConflictTestCase(test.NoDBTestCase):
|
|||
)
|
||||
|
||||
|
||||
class MemEncryptionRequestedWithoutUEFITestCase(test.NoDBTestCase):
|
||||
class MemEncryptionRequestedInvalidImagePropsTestCase(test.NoDBTestCase):
|
||||
flavor_name = 'm1.faketiny'
|
||||
image_name = 'fakecirros'
|
||||
image_id = '7ec4448e-f3fd-44b1-b172-9a7980f0f29f'
|
||||
|
||||
def _test_encrypted_memory_support_no_uefi(self, extra_spec, image_prop,
|
||||
requesters):
|
||||
def _test_encrypted_memory_support_raises(self, enc_extra_spec,
|
||||
enc_image_prop, image_props,
|
||||
error_data):
|
||||
extra_specs = {}
|
||||
if extra_spec:
|
||||
extra_specs['hw:mem_encryption'] = extra_spec
|
||||
if enc_extra_spec:
|
||||
extra_specs['hw:mem_encryption'] = enc_extra_spec
|
||||
flavor = objects.Flavor(name=self.flavor_name, extra_specs=extra_specs)
|
||||
if enc_image_prop:
|
||||
image_props['hw_mem_encryption'] = enc_image_prop
|
||||
image_meta = fake_image_obj(
|
||||
{'name': self.image_name}, {'hw_firmware_type': 'bios'},
|
||||
{'hw_mem_encryption': True} if image_prop else {})
|
||||
error = (
|
||||
"Memory encryption requested by %(requesters)s but image "
|
||||
"%(image_name)s doesn't have 'hw_firmware_type' property "
|
||||
"set to 'uefi'"
|
||||
)
|
||||
exc = self.assertRaises(
|
||||
exception.FlavorImageConflict,
|
||||
hw.get_mem_encryption_constraint,
|
||||
flavor, image_meta
|
||||
)
|
||||
{'id': self.image_id, 'name': self.image_name},
|
||||
{}, image_props)
|
||||
exc = self.assertRaises(self.expected_exception,
|
||||
hw.get_mem_encryption_constraint,
|
||||
flavor, image_meta)
|
||||
self.assertEqual(self.expected_error % error_data, str(exc))
|
||||
|
||||
|
||||
class MemEncryptionRequestedWithoutUEFITestCase(
|
||||
MemEncryptionRequestedInvalidImagePropsTestCase):
|
||||
expected_exception = exception.FlavorImageConflict
|
||||
expected_error = (
|
||||
"Memory encryption requested by %(requesters)s but image "
|
||||
"%(image_name)s doesn't have 'hw_firmware_type' property "
|
||||
"set to 'uefi'"
|
||||
)
|
||||
|
||||
def _test_encrypted_memory_support_no_uefi(self, enc_extra_spec,
|
||||
enc_image_prop, requesters):
|
||||
error_data = {'requesters': requesters,
|
||||
'image_name': self.image_name}
|
||||
self.assertEqual(error % error_data, str(exc))
|
||||
for image_props in ({},
|
||||
{'hw_machine_type': 'q35'},
|
||||
{'hw_firmware_type': 'bios'},
|
||||
{'hw_machine_type': 'q35',
|
||||
'hw_firmware_type': 'bios'}):
|
||||
self._test_encrypted_memory_support_raises(enc_extra_spec,
|
||||
enc_image_prop,
|
||||
image_props, error_data)
|
||||
|
||||
def test_flavor_requires_encrypted_memory_support_no_uefi(self):
|
||||
for extra_spec in ('1', 'true', 'True'):
|
||||
|
@ -3847,28 +3865,73 @@ class MemEncryptionRequestedWithoutUEFITestCase(test.NoDBTestCase):
|
|||
% (self.flavor_name, self.image_name))
|
||||
|
||||
|
||||
class MemEncryptionRequestedWithInvalidMachineTypeTestCase(
|
||||
MemEncryptionRequestedInvalidImagePropsTestCase):
|
||||
expected_exception = exception.InvalidMachineType
|
||||
expected_error = (
|
||||
"Machine type '%(mtype)s' is not compatible with image %(image_name)s "
|
||||
"(%(image_id)s): q35 type is required for SEV to work")
|
||||
|
||||
def _test_encrypted_memory_support_pc(self, enc_extra_spec,
|
||||
enc_image_prop):
|
||||
error_data = {'image_id': self.image_id,
|
||||
'image_name': self.image_name,
|
||||
'mtype': 'pc'}
|
||||
image_props = {'hw_firmware_type': 'uefi',
|
||||
'hw_machine_type': 'pc'}
|
||||
self._test_encrypted_memory_support_raises(enc_extra_spec,
|
||||
enc_image_prop,
|
||||
image_props, error_data)
|
||||
|
||||
def test_flavor_requires_encrypted_memory_support_pc(self):
|
||||
for extra_spec in ('1', 'true', 'True'):
|
||||
self._test_encrypted_memory_support_pc(extra_spec, None)
|
||||
|
||||
def test_image_requires_encrypted_memory_support_pc(self):
|
||||
for image_prop in ('1', 'true', 'True'):
|
||||
self._test_encrypted_memory_support_pc(None, image_prop)
|
||||
|
||||
def test_flavor_image_require_encrypted_memory_support_pc(self):
|
||||
for extra_spec in ('1', 'true', 'True'):
|
||||
for image_prop in ('1', 'true', 'True'):
|
||||
self._test_encrypted_memory_support_pc(
|
||||
extra_spec, image_prop)
|
||||
|
||||
|
||||
class MemEncryptionRequiredTestCase(test.NoDBTestCase):
|
||||
flavor_name = "m1.faketiny"
|
||||
image_name = 'fakecirros'
|
||||
image_id = '8a71a380-be23-47eb-9e72-9998586a0268'
|
||||
|
||||
@mock.patch.object(hw, 'LOG')
|
||||
def _test_encrypted_memory_support_required(self, extra_specs,
|
||||
image_props,
|
||||
requesters, mock_log):
|
||||
flavor = objects.Flavor(name=self.flavor_name, extra_specs=extra_specs)
|
||||
image_meta = objects.ImageMeta(name=self.image_name,
|
||||
properties=image_props)
|
||||
image_props['hw_firmware_type'] = 'uefi'
|
||||
|
||||
self.assertTrue(hw.get_mem_encryption_constraint(flavor, image_meta))
|
||||
mock_log.debug.assert_has_calls([
|
||||
mock.call("Memory encryption requested by %s", requesters)
|
||||
])
|
||||
def _test_get_mem_encryption_constraint():
|
||||
flavor = objects.Flavor(name=self.flavor_name,
|
||||
extra_specs=extra_specs)
|
||||
image_meta = objects.ImageMeta.from_dict({
|
||||
'id': self.image_id,
|
||||
'name': self.image_name,
|
||||
'properties': image_props})
|
||||
self.assertTrue(hw.get_mem_encryption_constraint(flavor,
|
||||
image_meta))
|
||||
mock_log.debug.assert_has_calls([
|
||||
mock.call("Memory encryption requested by %s", requesters)
|
||||
])
|
||||
|
||||
_test_get_mem_encryption_constraint()
|
||||
for mtype in ('q35', 'pc-q35-2.1'):
|
||||
image_props['hw_machine_type'] = mtype
|
||||
_test_get_mem_encryption_constraint()
|
||||
|
||||
def test_require_encrypted_memory_support_extra_spec(self):
|
||||
for extra_spec in ('1', 'true', 'True'):
|
||||
self._test_encrypted_memory_support_required(
|
||||
{'hw:mem_encryption': extra_spec},
|
||||
objects.ImageMetaProps(hw_firmware_type='uefi'),
|
||||
{},
|
||||
"hw:mem_encryption extra spec in %s flavor" % self.flavor_name
|
||||
)
|
||||
|
||||
|
@ -3876,9 +3939,7 @@ class MemEncryptionRequiredTestCase(test.NoDBTestCase):
|
|||
for image_prop in ('1', 'true', 'True'):
|
||||
self._test_encrypted_memory_support_required(
|
||||
{},
|
||||
objects.ImageMetaProps(
|
||||
hw_mem_encryption=image_prop,
|
||||
hw_firmware_type='uefi'),
|
||||
{'hw_mem_encryption': image_prop},
|
||||
"hw_mem_encryption property of image %s" % self.image_name
|
||||
)
|
||||
|
||||
|
@ -3887,9 +3948,7 @@ class MemEncryptionRequiredTestCase(test.NoDBTestCase):
|
|||
for image_prop in ('1', 'true', 'True'):
|
||||
self._test_encrypted_memory_support_required(
|
||||
{'hw:mem_encryption': extra_spec},
|
||||
objects.ImageMetaProps(
|
||||
hw_mem_encryption=image_prop,
|
||||
hw_firmware_type='uefi'),
|
||||
{'hw_mem_encryption': image_prop},
|
||||
"hw:mem_encryption extra spec in %s flavor and "
|
||||
"hw_mem_encryption property of image %s" %
|
||||
(self.flavor_name, self.image_name)
|
||||
|
|
|
@ -1153,9 +1153,17 @@ def get_mem_encryption_constraint(flavor, image_meta):
|
|||
2) the flavor and/or image request memory encryption, but the
|
||||
image is missing hw_firmware_type=uefi
|
||||
|
||||
3) the flavor and/or image request memory encryption, but the
|
||||
machine type is set to a value which does not contain 'q35'
|
||||
|
||||
This is called from the API layer, so get_machine_type() cannot be
|
||||
called since it relies on being run from the compute node in order
|
||||
to retrieve CONF.libvirt.hw_machine_type.
|
||||
|
||||
:param instance_type: Flavor object
|
||||
:param image: an ImageMeta object
|
||||
:raises: nova.exception.FlavorImageConflict
|
||||
:raises: nova.exception.InvalidMachineType
|
||||
:returns: boolean indicating whether encryption of guest memory
|
||||
was requested
|
||||
"""
|
||||
|
@ -1188,6 +1196,7 @@ def get_mem_encryption_constraint(flavor, image_meta):
|
|||
image_meta.name)
|
||||
|
||||
_check_mem_encryption_uses_uefi_image(requesters, image_meta)
|
||||
_check_mem_encryption_machine_type(image_meta)
|
||||
|
||||
LOG.debug("Memory encryption requested by %s", " and ".join(requesters))
|
||||
return True
|
||||
|
@ -1215,7 +1224,7 @@ def _check_for_mem_encryption_requirement_conflicts(
|
|||
|
||||
|
||||
def _check_mem_encryption_uses_uefi_image(requesters, image_meta):
|
||||
if image_meta.properties.hw_firmware_type == 'uefi':
|
||||
if image_meta.properties.get('hw_firmware_type') == 'uefi':
|
||||
return
|
||||
|
||||
emsg = _(
|
||||
|
@ -1227,6 +1236,38 @@ def _check_mem_encryption_uses_uefi_image(requesters, image_meta):
|
|||
raise exception.FlavorImageConflict(emsg % data)
|
||||
|
||||
|
||||
def _check_mem_encryption_machine_type(image_meta):
|
||||
# NOTE(aspiers): As explained in the SEV spec, SEV needs a q35
|
||||
# machine type in order to bind all the virtio devices to the PCIe
|
||||
# bridge so that they use virtio 1.0 and not virtio 0.9, since
|
||||
# QEMU's iommu_platform feature was added in virtio 1.0 only:
|
||||
#
|
||||
# http://specs.openstack.org/openstack/nova-specs/specs/train/approved/amd-sev-libvirt-support.html
|
||||
#
|
||||
# So if the image explicitly requests a machine type which is not
|
||||
# in the q35 family, raise an exception.
|
||||
#
|
||||
# Note that this check occurs at API-level, therefore we can't
|
||||
# check here what value of CONF.libvirt.hw_machine_type may have
|
||||
# been configured on the compute node.
|
||||
mach_type = image_meta.properties.get('hw_machine_type')
|
||||
|
||||
# If hw_machine_type is not specified on the image and is not
|
||||
# configured correctly on SEV compute nodes, then a separate check
|
||||
# in the driver will catch that and potentially retry on other
|
||||
# compute nodes.
|
||||
if mach_type is None:
|
||||
return
|
||||
|
||||
# Could be something like pc-q35-2.11 if a specific version of the
|
||||
# machine type is required, so do substring matching.
|
||||
if 'q35' not in mach_type:
|
||||
raise exception.InvalidMachineType(
|
||||
mtype=mach_type,
|
||||
image_id=image_meta.id, image_name=image_meta.name,
|
||||
reason=_("q35 type is required for SEV to work"))
|
||||
|
||||
|
||||
def _get_numa_pagesize_constraint(flavor, image_meta):
|
||||
"""Return the requested memory page size
|
||||
|
||||
|
|
Loading…
Reference in New Issue