Add locked_memory extra spec and image property

This change adds a new hw:locked_memory extra spec and hw_locked_memory
image property to contol preventing guest memory from swapping.

This change adds docs and extend the flavor
validators for the new extra spec.
Also add new image property.

Blueprint: libvirt-viommu-device
Change-Id: Id3779594f0078a5045031aded2ed68ee4301abbd
This commit is contained in:
Sean Mooney 2021-03-03 00:49:22 +00:00 committed by ricolin
parent 85c9544444
commit 572c2b18e2
15 changed files with 237 additions and 7 deletions

View File

@ -4,5 +4,5 @@
"hw_architecture": "x86_64" "hw_architecture": "x86_64"
}, },
"nova_object.name": "ImageMetaPropsPayload", "nova_object.name": "ImageMetaPropsPayload",
"nova_object.version": "1.10" "nova_object.version": "1.11"
} }

View File

@ -138,3 +138,33 @@ For example, to hide your signature from the guest OS, run:
.. code:: console .. code:: console
$ openstack flavor set $FLAVOR --property hw:hide_hypervisor_id=true $ openstack flavor set $FLAVOR --property hw:hide_hypervisor_id=true
.. _extra-spec-locked_memory:
Locked memory allocation
------------------------
.. versionadded:: 26.0.0 (Zed)
Locking memory marks the guest memory allocations as unmovable and
unswappable. It is implicitly enabled in a number of cases such as SEV or
realtime guests but can also be enabled explictly using the
``hw:locked_memory`` extra spec (or use ``hw_locked_memory`` image property).
``hw:locked_memory`` (also ``hw_locked_memory`` image property) accept
boolean values in string format like 'true' or 'false' value.
It will raise `FlavorImageLockedMemoryConflict` exception if both flavor and
image property are specified but with different boolean values.
This will only be allowed if you have also set ``hw:mem_page_size``,
so we can ensure that the scheduler can actually account for this correctly
and prevent out of memory events. Otherwise, will raise `LockMemoryForbidden`
exception.
.. code:: console
$ openstack flavor set FLAVOR-NAME \
--property hw:locked_memory=BOOLEAN_VALUE
.. note::
This is currently only supported by the libvirt driver.

View File

@ -163,6 +163,18 @@ hugepage_validators = [
'pattern': r'(large|small|any|\d+([kKMGT]i?)?(b|bit|B)?)', 'pattern': r'(large|small|any|\d+([kKMGT]i?)?(b|bit|B)?)',
}, },
), ),
base.ExtraSpecValidator(
name='hw:locked_memory',
description=(
'Determine if **guest** (instance) memory should be locked '
'preventing swaping. This is required in rare cases for device '
'DMA transfers. Only supported by the libvirt virt driver.'
),
value={
'type': bool,
'description': 'Whether to lock **guest** (instance) memory.',
},
),
] ]
numa_validators = [ numa_validators = [

View File

@ -1861,6 +1861,17 @@ class MemoryPageSizeNotSupported(Invalid):
msg_fmt = _("Page size %(pagesize)s is not supported by the host.") msg_fmt = _("Page size %(pagesize)s is not supported by the host.")
class LockMemoryForbidden(Forbidden):
msg_fmt = _("locked_memory value in image or flavor is forbidden when "
"mem_page_size is not set.")
class FlavorImageLockedMemoryConflict(NovaException):
msg_fmt = _("locked_memory value in image (%(image)s) and flavor "
"(%(flavor)s) conflict. A consistent value is expected if "
"both specified.")
class CPUPinningInvalid(Invalid): class CPUPinningInvalid(Invalid):
msg_fmt = _("CPU set to pin %(requested)s must be a subset of " msg_fmt = _("CPU set to pin %(requested)s must be a subset of "
"free CPU set %(available)s") "free CPU set %(available)s")

View File

@ -128,7 +128,8 @@ class ImageMetaPropsPayload(base.NotificationPayloadBase):
# Version 1.9: Added 'hw_emulation_architecture' field # Version 1.9: Added 'hw_emulation_architecture' field
# Version 1.10: Added 'hw_ephemeral_encryption' and # Version 1.10: Added 'hw_ephemeral_encryption' and
# 'hw_ephemeral_encryption_format' fields # 'hw_ephemeral_encryption_format' fields
VERSION = '1.10' # Version 1.11: Added 'hw_locked_memory' field
VERSION = '1.11'
SCHEMA = { SCHEMA = {
k: ('image_meta_props', k) for k in image_meta.ImageMetaProps.fields} k: ('image_meta_props', k) for k in image_meta.ImageMetaProps.fields}

View File

@ -190,14 +190,17 @@ class ImageMetaProps(base.NovaObject):
# Version 1.31: Added 'hw_emulation_architecture' field # Version 1.31: Added 'hw_emulation_architecture' field
# Version 1.32: Added 'hw_ephemeral_encryption' and # Version 1.32: Added 'hw_ephemeral_encryption' and
# 'hw_ephemeral_encryption_format' fields # 'hw_ephemeral_encryption_format' fields
# Version 1.33: Added 'hw_locked_memory' field
# NOTE(efried): When bumping this version, the version of # NOTE(efried): When bumping this version, the version of
# ImageMetaPropsPayload must also be bumped. See its docstring for details. # ImageMetaPropsPayload must also be bumped. See its docstring for details.
VERSION = '1.32' VERSION = '1.33'
def obj_make_compatible(self, primitive, target_version): def obj_make_compatible(self, primitive, target_version):
super(ImageMetaProps, self).obj_make_compatible(primitive, super(ImageMetaProps, self).obj_make_compatible(primitive,
target_version) target_version)
target_version = versionutils.convert_version_to_tuple(target_version) target_version = versionutils.convert_version_to_tuple(target_version)
if target_version < (1, 33):
primitive.pop('hw_locked_memory', None)
if target_version < (1, 32): if target_version < (1, 32):
primitive.pop('hw_ephemeral_encryption', None) primitive.pop('hw_ephemeral_encryption', None)
primitive.pop('hw_ephemeral_encryption_format', None) primitive.pop('hw_ephemeral_encryption_format', None)
@ -368,6 +371,10 @@ class ImageMetaProps(base.NovaObject):
# image with a network boot image # image with a network boot image
'hw_ipxe_boot': fields.FlexibleBooleanField(), 'hw_ipxe_boot': fields.FlexibleBooleanField(),
# string - make sure ``locked`` element is present in the
# ``memoryBacking``.
'hw_locked_memory': fields.FlexibleBooleanField(),
# There are sooooooooooo many possible machine types in # There are sooooooooooo many possible machine types in
# QEMU - several new ones with each new release - that it # QEMU - several new ones with each new release - that it
# is not practical to enumerate them all. So we use a free # is not practical to enumerate them all. So we use a free

View File

@ -1231,7 +1231,7 @@ class TestInstanceNotificationSample(
'nova_object.data': {}, 'nova_object.data': {},
'nova_object.name': 'ImageMetaPropsPayload', 'nova_object.name': 'ImageMetaPropsPayload',
'nova_object.namespace': 'nova', 'nova_object.namespace': 'nova',
'nova_object.version': '1.10', 'nova_object.version': '1.11',
}, },
'image.size': 58145823, 'image.size': 58145823,
'image.tags': [], 'image.tags': [],
@ -1327,7 +1327,7 @@ class TestInstanceNotificationSample(
'nova_object.data': {}, 'nova_object.data': {},
'nova_object.name': 'ImageMetaPropsPayload', 'nova_object.name': 'ImageMetaPropsPayload',
'nova_object.namespace': 'nova', 'nova_object.namespace': 'nova',
'nova_object.version': '1.10', 'nova_object.version': '1.11',
}, },
'image.size': 58145823, 'image.size': 58145823,
'image.tags': [], 'image.tags': [],

View File

@ -386,7 +386,7 @@ notification_object_data = {
# ImageMetaProps, so when you see a fail here for that reason, you must # ImageMetaProps, so when you see a fail here for that reason, you must
# *also* bump the version of ImageMetaPropsPayload. See its docstring for # *also* bump the version of ImageMetaPropsPayload. See its docstring for
# more information. # more information.
'ImageMetaPropsPayload': '1.10-44cf0030dc94a1a60ba7a0e222e854d6', 'ImageMetaPropsPayload': '1.11-938809cd33367c52cbc814fb9b6783dc',
'InstanceActionNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'InstanceActionNotification': '1.0-a73147b93b520ff0061865849d3dfa56',
'InstanceActionPayload': '1.8-4fa3da9cbf0761f1f700ae578f36dc2f', 'InstanceActionPayload': '1.8-4fa3da9cbf0761f1f700ae578f36dc2f',
'InstanceActionRebuildNotification': 'InstanceActionRebuildNotification':

View File

@ -108,6 +108,7 @@ class TestImageMetaProps(test.NoDBTestCase):
'hw_video_model': 'vga', 'hw_video_model': 'vga',
'hw_video_ram': '512', 'hw_video_ram': '512',
'hw_qemu_guest_agent': 'yes', 'hw_qemu_guest_agent': 'yes',
'hw_locked_memory': 'true',
'trait:CUSTOM_TRUSTED': 'required', 'trait:CUSTOM_TRUSTED': 'required',
# Fill sane values for the rest here # Fill sane values for the rest here
} }
@ -116,6 +117,7 @@ class TestImageMetaProps(test.NoDBTestCase):
self.assertEqual('vga', virtprops.hw_video_model) self.assertEqual('vga', virtprops.hw_video_model)
self.assertEqual(512, virtprops.hw_video_ram) self.assertEqual(512, virtprops.hw_video_ram)
self.assertTrue(virtprops.hw_qemu_guest_agent) self.assertTrue(virtprops.hw_qemu_guest_agent)
self.assertTrue(virtprops.hw_locked_memory)
self.assertIsNotNone(virtprops.traits_required) self.assertIsNotNone(virtprops.traits_required)
self.assertIn('CUSTOM_TRUSTED', virtprops.traits_required) self.assertIn('CUSTOM_TRUSTED', virtprops.traits_required)
@ -285,6 +287,28 @@ class TestImageMetaProps(test.NoDBTestCase):
self.assertEqual([set([0, 1, 2, 3])], self.assertEqual([set([0, 1, 2, 3])],
virtprops.hw_numa_cpus) virtprops.hw_numa_cpus)
def test_locked_memory_prop(self):
props = {'hw_locked_memory': 'true'}
virtprops = objects.ImageMetaProps.from_dict(props)
self.assertTrue(virtprops.hw_locked_memory)
def test_obj_make_compatible_hw_locked_memory(self):
"""Check 'hw_locked_memory' compatibility."""
# assert that 'hw_locked_memory' is supported
# on a suitably new version
obj = objects.ImageMetaProps(
hw_locked_memory='true',
)
primitive = obj.obj_to_primitive('1.33')
self.assertIn('hw_locked_memory',
primitive['nova_object.data'])
self.assertTrue(primitive['nova_object.data']['hw_locked_memory'])
# and is absent on older versions
primitive = obj.obj_to_primitive('1.32')
self.assertNotIn('hw_locked_memory',
primitive['nova_object.data'])
def test_get_unnumbered_trait_fields(self): def test_get_unnumbered_trait_fields(self):
"""Tests that only valid un-numbered required traits are parsed from """Tests that only valid un-numbered required traits are parsed from
the properties. the properties.

View File

@ -1072,7 +1072,7 @@ object_data = {
'HyperVLiveMigrateData': '1.4-e265780e6acfa631476c8170e8d6fce0', 'HyperVLiveMigrateData': '1.4-e265780e6acfa631476c8170e8d6fce0',
'IDEDeviceBus': '1.0-29d4c9f27ac44197f01b6ac1b7e16502', 'IDEDeviceBus': '1.0-29d4c9f27ac44197f01b6ac1b7e16502',
'ImageMeta': '1.8-642d1b2eb3e880a367f37d72dd76162d', 'ImageMeta': '1.8-642d1b2eb3e880a367f37d72dd76162d',
'ImageMetaProps': '1.32-4967d35948af08b710b8b861f3fff0f9', 'ImageMetaProps': '1.33-6b7a29f769e6b8eee3f05832d78c85a2',
'Instance': '2.7-d187aec68cad2e4d8b8a03a68e4739ce', 'Instance': '2.7-d187aec68cad2e4d8b8a03a68e4739ce',
'InstanceAction': '1.2-9a5abc87fdd3af46f45731960651efb5', 'InstanceAction': '1.2-9a5abc87fdd3af46f45731960651efb5',
'InstanceActionEvent': '1.4-5b1f361bd81989f8bb2c20bb7e8a4cb4', 'InstanceActionEvent': '1.4-5b1f361bd81989f8bb2c20bb7e8a4cb4',

View File

@ -3131,6 +3131,41 @@ class LibvirtConnTestCase(test.NoDBTestCase,
self.assertTrue(membacking.locked) self.assertTrue(membacking.locked)
self.assertFalse(membacking.sharedpages) self.assertFalse(membacking.sharedpages)
def test_get_guest_memory_backing_config_locked_flavor(self):
extra_specs = {
"hw:locked_memory": "True",
"hw:mem_page_size": 1000,
}
flavor = objects.Flavor(
name='m1.small', memory_mb=6, vcpus=28, root_gb=496,
ephemeral_gb=8128, swap=33550336, extra_specs=extra_specs)
image_meta = objects.ImageMeta.from_dict(self.test_image_meta)
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
membacking = drvr._get_guest_memory_backing_config(
None, None, flavor, image_meta)
self.assertTrue(membacking.locked)
def test_get_guest_memory_backing_config_locked_image_meta(self):
extra_specs = {}
flavor = objects.Flavor(
name='m1.small',
memory_mb=6,
vcpus=28,
root_gb=496,
ephemeral_gb=8128,
swap=33550336,
extra_specs=extra_specs)
image_meta = objects.ImageMeta.from_dict({
"disk_format": "raw",
"properties": {
"hw_locked_memory": "True",
"hw_mem_page_size": 1000,
}})
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
membacking = drvr._get_guest_memory_backing_config(
None, None, flavor, image_meta)
self.assertTrue(membacking.locked)
def test_get_guest_memory_backing_config_realtime_invalid_share(self): def test_get_guest_memory_backing_config_realtime_invalid_share(self):
"""Test behavior when there is no pool of shared CPUS on which to place """Test behavior when there is no pool of shared CPUS on which to place
the emulator threads, isolating them from the instance CPU processes. the emulator threads, isolating them from the instance CPU processes.

View File

@ -2814,6 +2814,54 @@ class NumberOfSerialPortsTest(test.NoDBTestCase):
flavor, image_meta) flavor, image_meta)
class VirtLockMemoryTestCase(test.NoDBTestCase):
def _test_get_locked_memory_constraint(self, spec=None, props=None):
flavor = objects.Flavor(vcpus=16, memory_mb=2048,
extra_specs=spec or {})
image_meta = objects.ImageMeta.from_dict({"properties": props or {}})
return hw.get_locked_memory_constraint(flavor, image_meta)
def test_get_locked_memory_constraint_image(self):
self.assertTrue(
self._test_get_locked_memory_constraint(
spec={"hw:mem_page_size": "small"},
props={"hw_locked_memory": "True"}))
def test_get_locked_memory_conflict(self):
ex = self.assertRaises(
exception.FlavorImageLockedMemoryConflict,
self._test_get_locked_memory_constraint,
spec={
"hw:locked_memory": "False",
"hw:mem_page_size": "small"
},
props={"hw_locked_memory": "True"}
)
ex_msg = ("locked_memory value in image (True) and flavor (False) "
"conflict. A consistent value is expected if both "
"specified.")
self.assertEqual(ex_msg, str(ex))
def test_get_locked_memory_constraint_forbidden(self):
self.assertRaises(
exception.LockMemoryForbidden,
self._test_get_locked_memory_constraint,
{"hw:locked_memory": "True"})
self.assertRaises(
exception.LockMemoryForbidden,
self._test_get_locked_memory_constraint,
{},
{"hw_locked_memory": "True"})
def test_get_locked_memory_constraint_image_false(self):
# False value of locked_memory will not raise LockMemoryForbidden
self.assertFalse(
self._test_get_locked_memory_constraint(
spec=None,
props={"hw_locked_memory": "False"}))
class VirtMemoryPagesTestCase(test.NoDBTestCase): class VirtMemoryPagesTestCase(test.NoDBTestCase):
def test_cell_instance_pagesize(self): def test_cell_instance_pagesize(self):
cell = objects.InstanceNUMACell( cell = objects.InstanceNUMACell(

View File

@ -1337,6 +1337,48 @@ def _get_constraint_mappings_from_flavor(flavor, key, func):
return hw_numa_map or None return hw_numa_map or None
def get_locked_memory_constraint(
flavor: 'objects.Flavor',
image_meta: 'objects.ImageMeta',
) -> ty.Optional[bool]:
"""Validate and return the requested locked memory.
:param flavor: ``nova.objects.Flavor`` instance
:param image_meta: ``nova.objects.ImageMeta`` instance
:raises: exception.LockMemoryForbidden if mem_page_size is not set
while provide locked_memory value in image or flavor.
:returns: The locked memory flag requested.
"""
mem_page_size_flavor, mem_page_size_image = _get_flavor_image_meta(
'mem_page_size', flavor, image_meta)
locked_memory_flavor, locked_memory_image = _get_flavor_image_meta(
'locked_memory', flavor, image_meta)
if locked_memory_flavor is not None:
# locked_memory_image is boolean type already
locked_memory_flavor = strutils.bool_from_string(locked_memory_flavor)
if locked_memory_image is not None and (
locked_memory_flavor != locked_memory_image
):
# We don't allow provide different value to flavor and image
raise exception.FlavorImageLockedMemoryConflict(
image=locked_memory_image, flavor=locked_memory_flavor)
locked_memory = locked_memory_flavor
else:
locked_memory = locked_memory_image
if locked_memory and not (
mem_page_size_flavor or mem_page_size_image
):
raise exception.LockMemoryForbidden()
return locked_memory
def _get_numa_cpu_constraint( def _get_numa_cpu_constraint(
flavor: 'objects.Flavor', flavor: 'objects.Flavor',
image_meta: 'objects.ImageMeta', image_meta: 'objects.ImageMeta',
@ -2107,6 +2149,8 @@ def numa_get_constraints(flavor, image_meta):
pagesize = _get_numa_pagesize_constraint(flavor, image_meta) pagesize = _get_numa_pagesize_constraint(flavor, image_meta)
vpmems = get_vpmems(flavor) vpmems = get_vpmems(flavor)
get_locked_memory_constraint(flavor, image_meta)
# If 'hw:cpu_dedicated_mask' is not found in flavor extra specs, the # If 'hw:cpu_dedicated_mask' is not found in flavor extra specs, the
# 'dedicated_cpus' variable is None, while we hope it being an empty set. # 'dedicated_cpus' variable is None, while we hope it being an empty set.
dedicated_cpus = dedicated_cpus or set() dedicated_cpus = dedicated_cpus or set()

View File

@ -6342,6 +6342,11 @@ class LibvirtDriver(driver.ComputeDriver):
membacking = vconfig.LibvirtConfigGuestMemoryBacking() membacking = vconfig.LibvirtConfigGuestMemoryBacking()
membacking.locked = True membacking.locked = True
if hardware.get_locked_memory_constraint(flavor, image_meta):
if not membacking:
membacking = vconfig.LibvirtConfigGuestMemoryBacking()
membacking.locked = True
return membacking return membacking
def _get_memory_backing_hugepages_support(self, inst_topology, numatune): def _get_memory_backing_hugepages_support(self, inst_topology, numatune):

View File

@ -0,0 +1,13 @@
---
features:
- |
Add new ``hw:locked_memory`` extra spec and ``hw_locked_memory`` image
property to lock memory on libvirt guest. Locking memory marks the guest
memory allocations as unmovable and unswappable.
``hw:locked_memory`` extra spec and ``hw_locked_memory`` image property
accept boolean values in string format like 'Yes' or 'false' value.
Exception `LockMemoryForbidden` will raise, if you set lock memory value
but not set either flavor extra spec
``hw:mem_page_size`` or image property ``hw_mem_page_size``,
so we can ensure that the scheduler can actually account for this correctly
and prevent out of memory events.