Merge "Migrate MEM_ENCRYPTION_CONTEXT from root provider"

This commit is contained in:
Zuul
2025-08-28 20:36:20 +00:00
committed by Gerrit Code Review
4 changed files with 624 additions and 68 deletions

View File

@@ -30,15 +30,13 @@ CONF = conf.CONF
class LibvirtReportTraitsTestBase( class LibvirtReportTraitsTestBase(
integrated_helpers.LibvirtProviderUsageBaseTestCase): integrated_helpers.LibvirtProviderUsageBaseTestCase):
pass
def assertMemEncryptionSlotsEqual(self, slots): def assertMemEncryptionSlotsEqual(self, rp_uuid, slots):
inventory = self._get_provider_inventory(self.host_uuid) inventory = self._get_provider_inventory(rp_uuid)
if slots == 0: if slots == 0:
self.assertNotIn(orc.MEM_ENCRYPTION_CONTEXT, inventory) self.assertNotIn(orc.MEM_ENCRYPTION_CONTEXT, inventory)
else: else:
self.assertEqual( self.assertEqual(
inventory[orc.MEM_ENCRYPTION_CONTEXT],
{ {
'total': slots, 'total': slots,
'min_unit': 1, 'min_unit': 1,
@@ -46,9 +44,16 @@ class LibvirtReportTraitsTestBase(
'step_size': 1, 'step_size': 1,
'allocation_ratio': 1.0, 'allocation_ratio': 1.0,
'reserved': 0, 'reserved': 0,
} },
inventory[orc.MEM_ENCRYPTION_CONTEXT]
) )
def _get_amd_sev_rps(self):
root_rp = self._get_resource_provider_by_uuid(self.host_uuid)
rps = self._get_all_rps_in_a_tree(self.host_uuid)
return [rp for rp in rps
if rp['name'] == '%s_amd_sev' % root_rp['name']]
class LibvirtReportTraitsTests(LibvirtReportTraitsTestBase): class LibvirtReportTraitsTests(LibvirtReportTraitsTestBase):
# These must match the capabilities in # These must match the capabilities in
@@ -143,8 +148,10 @@ class LibvirtReportNoSevTraitsTests(LibvirtReportTraitsTestBase):
traits = self._get_provider_traits(self.host_uuid) traits = self._get_provider_traits(self.host_uuid)
self.assertNotIn(sev_trait, traits) self.assertNotIn(sev_trait, traits)
self.assertMemEncryptionSlotsEqual(self.host_uuid, 0)
self.assertMemEncryptionSlotsEqual(0) sev_rps = self._get_amd_sev_rps()
self.assertEqual(0, len(sev_rps))
# Now simulate the host gaining SEV functionality. Here we # Now simulate the host gaining SEV functionality. Here we
# simulate a kernel update or reconfiguration which causes the # simulate a kernel update or reconfiguration which causes the
@@ -178,13 +185,21 @@ class LibvirtReportNoSevTraitsTests(LibvirtReportTraitsTestBase):
self.compute.driver._static_traits = None self.compute.driver._static_traits = None
self._run_periodics() self._run_periodics()
traits = self._get_provider_traits(self.host_uuid)
self.assertIn(sev_trait, traits)
# Sanity check that we've still got the trait globally. # Sanity check that we've still got the trait globally.
self.assertIn(sev_trait, self._get_all_traits()) self.assertIn(sev_trait, self._get_all_traits())
self.assertMemEncryptionSlotsEqual(db_const.MAX_INT) # sev capabilities are managed by sub rp and are not present in
# root rp
traits = self._get_provider_traits(self.host_uuid)
self.assertNotIn(sev_trait, traits)
self.assertMemEncryptionSlotsEqual(self.host_uuid, 0)
sev_rps = self._get_amd_sev_rps()
self.assertEqual(1, len(sev_rps))
sev_rp_uuid = sev_rps[0]['uuid']
sev_rp_traits = self._get_provider_traits(sev_rp_uuid)
self.assertIn(sev_trait, sev_rp_traits)
self.assertMemEncryptionSlotsEqual(sev_rp_uuid, db_const.MAX_INT)
class LibvirtReportSevTraitsTests(LibvirtReportTraitsTestBase): class LibvirtReportSevTraitsTests(LibvirtReportTraitsTestBase):
@@ -221,10 +236,17 @@ class LibvirtReportSevTraitsTests(LibvirtReportTraitsTestBase):
global_traits = self._get_all_traits() global_traits = self._get_all_traits()
self.assertIn(sev_trait, global_traits) self.assertIn(sev_trait, global_traits)
# sev capabilities are managed by sub rp and are not present in root rp
traits = self._get_provider_traits(self.host_uuid) traits = self._get_provider_traits(self.host_uuid)
self.assertIn(sev_trait, traits) self.assertNotIn(sev_trait, traits)
self.assertMemEncryptionSlotsEqual(self.host_uuid, 0)
self.assertMemEncryptionSlotsEqual(16) sev_rps = self._get_amd_sev_rps()
self.assertEqual(1, len(sev_rps))
sev_rp_uuid = sev_rps[0]['uuid']
sev_rp_traits = self._get_provider_traits(sev_rp_uuid)
self.assertIn(sev_trait, sev_rp_traits)
self.assertMemEncryptionSlotsEqual(sev_rp_uuid, 16)
# Now simulate the host losing SEV functionality. Here we # Now simulate the host losing SEV functionality. Here we
# simulate a kernel downgrade or reconfiguration which causes # simulate a kernel downgrade or reconfiguration which causes
@@ -247,10 +269,14 @@ class LibvirtReportSevTraitsTests(LibvirtReportTraitsTestBase):
self.compute.driver._static_traits = None self.compute.driver._static_traits = None
self._run_periodics() self._run_periodics()
traits = self._get_provider_traits(self.host_uuid)
self.assertNotIn(sev_trait, traits)
# Sanity check that we've still got the trait globally. # Sanity check that we've still got the trait globally.
self.assertIn(sev_trait, self._get_all_traits()) self.assertIn(sev_trait, self._get_all_traits())
self.assertMemEncryptionSlotsEqual(0) traits = self._get_provider_traits(self.host_uuid)
self.assertNotIn(sev_trait, traits)
# NOTE(tkajinam): Currently the sev rp is not deleted after sev
# support is turned off. This follows the existing behavior for
# other resources such as vGPU.
# sev_rps = self._get_amd_sev_rps()
# self.assertEqual(0, len(sev_rps))

View File

@@ -11,11 +11,13 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import copy
import io import io
from unittest import mock from unittest import mock
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils.fixture import uuidsentinel
from nova import context from nova import context
from nova import objects from nova import objects
@@ -236,3 +238,100 @@ class VGPUReshapeTests(base.ServersTestBase):
self.assertEqual( self.assertEqual(
{'VGPU': 1}, {'VGPU': 1},
allocations[gpu_rp_uuid]['resources']) allocations[gpu_rp_uuid]['resources'])
class SevResphapeTests(base.ServersTestBase):
def setUp(self):
super().setUp()
admin_context = context.get_admin_context()
hw_mem_enc_image = copy.deepcopy(self.glance.image1)
hw_mem_enc_image['id'] = uuidsentinel.mem_enc_image_id
hw_mem_enc_image['properties']['hw_machine_type'] = 'q35'
hw_mem_enc_image['properties']['hw_firmware_type'] = 'uefi'
hw_mem_enc_image['properties']['hw_mem_encryption'] = True
self.glance.create(admin_context, hw_mem_enc_image)
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._guest_configure_sev')
def test_create_servers_with_amd_sev(self, mock_configure_sev):
self.hostname = self.start_compute(
hostname='compute1',
)
self.compute = self.computes[self.hostname]
self.flags(num_memory_encrypted_guests=16, group='libvirt')
# create the MEM_ENCRYPTION_CONTEXT resource in placement manually,
# to simulate the old layout.
compute_rp_uuid = self.placement.get(
'/resource_providers?name=compute1').body[
'resource_providers'][0]['uuid']
inventories = self.placement.get(
'/resource_providers/%s/inventories' % compute_rp_uuid).body
inventories['inventories']['MEM_ENCRYPTION_CONTEXT'] = {
'allocation_ratio': 1.0,
'max_unit': 1,
'min_unit': 1,
'reserved': 0,
'step_size': 1,
'total': 16}
self.placement.put(
'/resource_providers/%s/inventories' % compute_rp_uuid,
inventories)
# create a server before reshape
with mock.patch('nova.virt.libvirt.driver.LibvirtDriver.'
'update_provider_tree'):
pre_server = self._create_server(
image_uuid=uuidsentinel.mem_enc_image_id)
self.addCleanup(self._delete_server, pre_server)
# verify that the inventory, usages and allocation are correct before
# the reshape
compute_inventory = self.placement.get(
'/resource_providers/%s/inventories' % compute_rp_uuid).body[
'inventories']
self.assertEqual(
16, compute_inventory['MEM_ENCRYPTION_CONTEXT']['total'])
compute_usages = self.placement.get(
'/resource_providers/%s/usages' % compute_rp_uuid).body[
'usages']
self.assertEqual(1, compute_usages['MEM_ENCRYPTION_CONTEXT'])
# restart the compute service to trigger reshape
with mock.patch('nova.virt.libvirt.host.Host.supports_amd_sev',
return_value=True):
self.compute = self.restart_compute_service(self.hostname)
# verify that the inventory, usages and allocation are correct after
# the reshape
compute_inventory = self.placement.get(
'/resource_providers/%s/inventories' % compute_rp_uuid).body[
'inventories']
self.assertNotIn('MEM_ENCRYPTION_CONTEXT', compute_inventory)
compute_usages = self.placement.get(
'/resource_providers/%s/usages' % compute_rp_uuid).body[
'usages']
self.assertNotIn('MEM_ENCRYPTION_CONTEXT', compute_usages)
sev_rp_uuid = self.placement.get(
'/resource_providers?name=compute1_amd_sev').body[
'resource_providers'][0]['uuid']
sev_inventory = self.placement.get(
'/resource_providers/%s/inventories' % sev_rp_uuid).body[
'inventories']
self.assertEqual(
16, sev_inventory['MEM_ENCRYPTION_CONTEXT']['total'])
sev_usages = self.placement.get(
'/resource_providers/%s/usages' % sev_rp_uuid).body[
'usages']
self.assertEqual(1, sev_usages['MEM_ENCRYPTION_CONTEXT'])
# create a new server after reshape
post_server = self._create_server(
image_uuid=uuidsentinel.mem_enc_image_id)
self.addCleanup(self._delete_server, post_server)
compute_usages = self.placement.get(
'/resource_providers/%s/usages' % sev_rp_uuid).body[
'usages']
self.assertEqual(2, compute_usages['MEM_ENCRYPTION_CONTEXT'])

View File

@@ -23130,6 +23130,9 @@ class TestUpdateProviderTree(test.NoDBTestCase):
mock_gpu_invs.return_value = gpu_inventory_dicts mock_gpu_invs.return_value = gpu_inventory_dicts
# Use an empty list for vpmems. # Use an empty list for vpmems.
self.driver._vpmems_by_rc = {'CUSTOM_PMEM_NAMESPACE_4GB': []} self.driver._vpmems_by_rc = {'CUSTOM_PMEM_NAMESPACE_4GB': []}
# Use total=0 for MEM_ENCRYPTION_CONTEXT
self.driver._host._supports_amd_sev = True
self.driver._host._max_sev_guests = 0
# Before we update_provider_tree, we have 2 providers from setUp(): # Before we update_provider_tree, we have 2 providers from setUp():
# self.cn_rp and self.shared_rp and they are both empty {}. # self.cn_rp and self.shared_rp and they are both empty {}.
self.assertEqual(2, len(self.pt.get_provider_uuids())) self.assertEqual(2, len(self.pt.get_provider_uuids()))
@@ -23251,6 +23254,35 @@ class TestUpdateProviderTree(test.NoDBTestCase):
self.assertEqual(expected_resources, self.assertEqual(expected_resources,
self.pt.data(self.cn_rp['uuid']).resources) self.pt.data(self.cn_rp['uuid']).resources)
def test_update_provider_tree_with_memory_encryption(self):
self.driver._host._supports_amd_sev = True
self.driver._host._max_sev_guests = 16
self._test_update_provider_tree()
inventory = self._get_inventory()
# root compute node provider inventory is unchanged
self.assertEqual(inventory,
(self.pt.data(self.cn_rp['uuid'])).inventory)
# We should have new sev child providers in the tree under the
# compute node root provider.
compute_node_tree_uuids = self.pt.get_provider_uuids(
self.cn_rp['name'])
self.assertEqual(2, len(compute_node_tree_uuids))
sev_rp_uuid = compute_node_tree_uuids[1]
sev_provider_data = self.pt.data(sev_rp_uuid)
self.assertEqual('%s_amd_sev' % self.cn_rp['name'],
sev_provider_data.name)
self.assertEqual({
orc.MEM_ENCRYPTION_CONTEXT: {
'total': 16,
'step_size': 1,
'max_unit': 1,
'min_unit': 1,
'reserved': 0,
'allocation_ratio': 1.0
}
}, sev_provider_data.inventory)
self.assertEqual({ot.HW_CPU_X86_AMD_SEV}, sev_provider_data.traits)
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_local_gb_info', @mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_local_gb_info',
new=mock.Mock(return_value={'total': disk_gb})) new=mock.Mock(return_value={'total': disk_gb}))
@mock.patch('nova.virt.libvirt.host.Host.get_memory_mb_total', @mock.patch('nova.virt.libvirt.host.Host.get_memory_mb_total',
@@ -23551,6 +23583,170 @@ class TestUpdateProviderTree(test.NoDBTestCase):
self.assertIn('Unexpected VGPU resource allocation on provider %s' self.assertIn('Unexpected VGPU resource allocation on provider %s'
% uuids.other_rp, str(ex)) % uuids.other_rp, str(ex))
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver.'
'_get_cpu_feature_traits',
new=mock.Mock(return_value=cpu_traits))
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_local_gb_info',
new=mock.Mock(return_value={'total': disk_gb}))
@mock.patch('nova.virt.libvirt.host.Host.get_memory_mb_total',
new=mock.Mock(return_value=memory_mb))
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_pcpu_available',
new=mock.Mock(return_value=range(pcpus)))
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_vcpu_available',
new=mock.Mock(return_value=range(vcpus)))
def test_update_provider_tree_for_memory_encryption_reshape(self):
self.driver._host._supports_amd_sev = True
self.driver._host._max_sev_guests = 16
# First create a provider tree with MEM_ENCRYPTION_CONTEXT inventory on
# the root node provider.
inventory = self._get_inventory()
sev_inventory = {
orc.MEM_ENCRYPTION_CONTEXT: {
'total': 16,
'step_size': 1,
'max_unit': 1,
'min_unit': 1,
'reserved': 0,
'allocation_ratio': 1.0
}
}
inventory.update(sev_inventory)
self.pt.update_inventory(self.cn_rp['uuid'], inventory)
# Call update_provider_tree which will raise ReshapeNeeded because
# there is MEM_ENCRYPTION_CONTEXT on the root node provider
self.assertRaises(exception.ReshapeNeeded,
self.driver.update_provider_tree,
self.pt, self.cn_rp['name'])
# Now make up some fake allocations to pass back to the upt method
# for the reshape
allocations = {
uuids.consumer1: {
'allocations': {
# This consumer has MEM_ENCRYPTION_CONTEXT allocations on
# the root node provider and *should* be changed.
self.cn_rp['uuid']: {
'resources': {
orc.MEMORY_MB: 512,
orc.VCPU: 2,
orc.MEM_ENCRYPTION_CONTEXT: 1
}
}
}
},
uuids.consumer2: {
'allocations': {
# This consumer has no MEM_ENCRYPTION_CONTEXT allocations
# on the root provider *should not* be changed.
self.cn_rp['uuid']: {
'resources': {
orc.MEMORY_MB: 512,
orc.VCPU: 2
}
}
}
}
}
original_allocations = copy.deepcopy(allocations)
# Initiate the reshape
self.driver.update_provider_tree(
self.pt, self.cn_rp['name'], allocations=allocations)
# We should have one SEV child provider in the tree under the compute
# node root provider.
compute_node_tree_uuids = self.pt.get_provider_uuids(
self.cn_rp['name'])
self.assertEqual(2, len(compute_node_tree_uuids))
# The SEV provider should be the 2nd UUID in the list
sev_rp_uuid = compute_node_tree_uuids[1]
# The MEM_ENCRYPTION_CONTEXT inventory should be on the SEV child
# provider
sev_provider_data = self.pt.data(sev_rp_uuid)
self.assertEqual('%s_amd_sev' % self.cn_rp['name'],
sev_provider_data.name)
self.assertEqual({
orc.MEM_ENCRYPTION_CONTEXT: {
'total': 16,
'step_size': 1,
'max_unit': 1,
'min_unit': 1,
'reserved': 0,
'allocation_ratio': 1.0
}
}, sev_provider_data.inventory)
# Make sure the child provider has the SEV trait
self.assertEqual({ot.HW_CPU_X86_AMD_SEV}, sev_provider_data.traits)
# The compute node root provider should not have MEM_ENCRYPTION_CONTEXT
# inventory.
del inventory[orc.MEM_ENCRYPTION_CONTEXT]
self.assertEqual(inventory, self.pt.data(self.cn_rp['uuid']).inventory)
# consumer1 should now have allocations against two providers,
# MEMORY_MB on the root compute node provider and
# MEM_ENCRYPTION_CONTEXT on the child provider.
consumer1_allocs = allocations[uuids.consumer1]['allocations']
self.assertEqual(2, len(consumer1_allocs))
self.assertEqual({orc.MEMORY_MB: 512, orc.VCPU: 2},
consumer1_allocs[self.cn_rp['uuid']]['resources'])
# Make sure the MEM_ENCRYPTION_CONTEXT allocation moved to
# the corresponding child RP
self.assertEqual({orc.MEM_ENCRYPTION_CONTEXT: 1},
consumer1_allocs[sev_rp_uuid]['resources'])
# The allocations on consumer2 should be unchanged.
self.assertEqual(original_allocations[uuids.consumer2],
allocations[uuids.consumer2])
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver.'
'_get_cpu_feature_traits',
new=mock.Mock(return_value=cpu_traits))
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_local_gb_info',
new=mock.Mock(return_value={'total': disk_gb}))
@mock.patch('nova.virt.libvirt.host.Host.get_memory_mb_total',
new=mock.Mock(return_value=memory_mb))
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_pcpu_available',
new=mock.Mock(return_value=range(pcpus)))
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._get_vcpu_available',
new=mock.Mock(return_value=range(vcpus)))
def test_update_provider_tree_for_memory_encryption_reshape_fails(self):
self.driver._host._supports_amd_sev = True
self.driver._host._max_sev_guests = 16
# First create a provider tree with MEM_ENCRYPTION_CONTEXT inventory on
# the root node provider.
inventory = self._get_inventory()
sev_inventory = {
orc.MEM_ENCRYPTION_CONTEXT: {
'total': 16,
'step_size': 1,
'max_unit': 1,
'min_unit': 1,
'reserved': 0,
'allocation_ratio': 1.0
}
}
inventory.update(sev_inventory)
self.pt.update_inventory(self.cn_rp['uuid'], inventory)
# Now make up some fake allocations to pass back to the upt method
# for the reshape
allocations = {
uuids.consumer1: {
'allocations': {
# This consumer has invalid MEM_ENCRYPTION_CONTEXT on
# a non-root compute node provider.
uuids.other_rp: {
'resources': {
orc.MEMORY_MB: 512,
orc.MEM_ENCRYPTION_CONTEXT: 1
}
}
}
}
}
# Initiate the reshape.
ex = self.assertRaises(exception.ReshapeFailed,
self.driver.update_provider_tree,
self.pt, self.cn_rp['name'],
allocations=allocations)
self.assertIn('Unexpected MEM_ENCRYPTION_CONTEXT resource allocation '
'on provider %s' % uuids.other_rp, str(ex))
@mock.patch('nova.objects.instance.Instance.get_by_uuid') @mock.patch('nova.objects.instance.Instance.get_by_uuid')
@mock.patch('nova.objects.migration.MigrationList' @mock.patch('nova.objects.migration.MigrationList'
'.get_in_progress_by_host_and_node') '.get_in_progress_by_host_and_node')
@@ -28798,15 +28994,6 @@ class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin):
trait = f'COMPUTE_GRAPHICS_MODEL_{model.upper()}' trait = f'COMPUTE_GRAPHICS_MODEL_{model.upper()}'
self.assertIn(trait, model_traits) self.assertIn(trait, model_traits)
@mock.patch.object(libvirt_driver.LibvirtDriver, '_get_cpu_feature_traits',
new=mock.Mock(return_value={}))
def test_cpu_traits__sev_support(self):
for support in (False, True):
self.drvr._host._supports_amd_sev = support
traits = self.drvr._get_cpu_traits()
self.assertIn(ot.HW_CPU_X86_AMD_SEV, traits)
self.assertEqual(support, traits[ot.HW_CPU_X86_AMD_SEV])
@mock.patch.object(libvirt_driver.LibvirtDriver, '_get_cpu_feature_traits', @mock.patch.object(libvirt_driver.LibvirtDriver, '_get_cpu_feature_traits',
new=mock.Mock(return_value={})) new=mock.Mock(return_value={}))
def test_cpu_traits__hyperthreading_support(self): def test_cpu_traits__hyperthreading_support(self):
@@ -30981,25 +31168,25 @@ class TestLibvirtSEV(test.NoDBTestCase):
@mock.patch.object(os.path, 'exists', new=mock.Mock(return_value=False)) @mock.patch.object(os.path, 'exists', new=mock.Mock(return_value=False))
class TestLibvirtSEVUnsupported(TestLibvirtSEV): class TestLibvirtSEVUnsupported(TestLibvirtSEV):
def test_get_mem_encrypted_slots_no_config(self): def test_get_memory_encryption_inventories_no_config(self):
self.assertEqual(0, self.driver._get_memory_encrypted_slots()) self.assertEqual({}, self.driver._get_memory_encryption_inventories())
def test_get_mem_encrypted_slots_config_zero(self): def test_get_memory_encryption_inventories_config_zero(self):
self.flags(num_memory_encrypted_guests=0, group='libvirt') self.flags(num_memory_encrypted_guests=0, group='libvirt')
self.assertEqual(0, self.driver._get_memory_encrypted_slots()) self.assertEqual({}, self.driver._get_memory_encryption_inventories())
@mock.patch.object(libvirt_driver.LOG, 'warning') @mock.patch.object(libvirt_driver.LOG, 'warning')
def test_get_mem_encrypted_slots_config_non_zero_unsupported( def test_get_memory_encryption_inventories_config_non_zero_unsupported(
self, mock_log): self, mock_log):
self.flags(num_memory_encrypted_guests=16, group='libvirt') self.flags(num_memory_encrypted_guests=16, group='libvirt')
# Still zero without mocked SEV support # Still zero without mocked SEV support
self.assertEqual(0, self.driver._get_memory_encrypted_slots()) self.assertEqual({}, self.driver._get_memory_encryption_inventories())
mock_log.assert_called_with( mock_log.assert_called_with(
'Host is configured with libvirt.num_memory_encrypted_guests ' 'Host is configured with libvirt.num_memory_encrypted_guests '
'set to %d, but is not SEV-capable.', 16) 'set to %d, but is not SEV-capable.', 16)
def test_get_mem_encrypted_slots_unsupported(self): def test_get_memory_encryption_inventories_unsupported(self):
self.assertEqual(0, self.driver._get_memory_encrypted_slots()) self.assertEqual({}, self.driver._get_memory_encryption_inventories())
@mock.patch.object(vc, '_domain_capability_features', @mock.patch.object(vc, '_domain_capability_features',
@@ -31008,21 +31195,50 @@ class TestLibvirtSEVSupportedNoMaxGuests(TestLibvirtSEV):
"""Libvirt driver tests for when AMD SEV support is present.""" """Libvirt driver tests for when AMD SEV support is present."""
@test.patch_exists(SEV_KERNEL_PARAM_FILE, True) @test.patch_exists(SEV_KERNEL_PARAM_FILE, True)
@test.patch_open(SEV_KERNEL_PARAM_FILE, "1\n") @test.patch_open(SEV_KERNEL_PARAM_FILE, "1\n")
def test_get_mem_encrypted_slots_unlimited(self): def test_get_memory_encryption_inventories_unlimited(self):
self.assertEqual(db_const.MAX_INT, self.assertEqual({
self.driver._get_memory_encrypted_slots()) 'amd_sev': {
'total': db_const.MAX_INT,
'step_size': 1,
'max_unit': 1,
'min_unit': 1,
'reserved': 0,
'allocation_ratio': 1.0,
'traits': [ot.HW_CPU_X86_AMD_SEV]
}
}, self.driver._get_memory_encryption_inventories())
@test.patch_exists(SEV_KERNEL_PARAM_FILE, True) @test.patch_exists(SEV_KERNEL_PARAM_FILE, True)
@test.patch_open(SEV_KERNEL_PARAM_FILE, "1\n") @test.patch_open(SEV_KERNEL_PARAM_FILE, "1\n")
def test_get_mem_encrypted_slots_config_non_zero_supported(self): def test_get_memory_encryption_inventories_config_non_zero_supported(self):
self.flags(num_memory_encrypted_guests=16, group='libvirt') self.flags(num_memory_encrypted_guests=16, group='libvirt')
self.assertEqual(16, self.driver._get_memory_encrypted_slots()) self.assertEqual({
'amd_sev': {
'total': 16,
'step_size': 1,
'max_unit': 1,
'min_unit': 1,
'reserved': 0,
'allocation_ratio': 1.0,
'traits': [ot.HW_CPU_X86_AMD_SEV]
}
}, self.driver._get_memory_encryption_inventories())
@test.patch_exists(SEV_KERNEL_PARAM_FILE, True) @test.patch_exists(SEV_KERNEL_PARAM_FILE, True)
@test.patch_open(SEV_KERNEL_PARAM_FILE, "1\n") @test.patch_open(SEV_KERNEL_PARAM_FILE, "1\n")
def test_get_mem_encrypted_slots_config_zero_supported(self): def test_get_memory_encryption_inventories_config_zero_supported(self):
self.flags(num_memory_encrypted_guests=0, group='libvirt') self.flags(num_memory_encrypted_guests=0, group='libvirt')
self.assertEqual(0, self.driver._get_memory_encrypted_slots()) self.assertEqual({
'amd_sev': {
'total': 0,
'step_size': 1,
'max_unit': 1,
'min_unit': 1,
'reserved': 0,
'allocation_ratio': 1.0,
'traits': [ot.HW_CPU_X86_AMD_SEV]
},
}, self.driver._get_memory_encryption_inventories())
@mock.patch.object(vc, '_domain_capability_features', @mock.patch.object(vc, '_domain_capability_features',
@@ -31032,16 +31248,36 @@ class TestLibvirtSEVSupportedMaxGuests(TestLibvirtSEV):
@test.patch_exists(SEV_KERNEL_PARAM_FILE, True) @test.patch_exists(SEV_KERNEL_PARAM_FILE, True)
@test.patch_open(SEV_KERNEL_PARAM_FILE, "1\n") @test.patch_open(SEV_KERNEL_PARAM_FILE, "1\n")
@mock.patch.object(libvirt_driver.LOG, 'warning') @mock.patch.object(libvirt_driver.LOG, 'warning')
def test_get_mem_encrypted_slots_no_override(self, mock_log): def test_get_memory_encryption_inventories_no_override(self, mock_log):
self.assertEqual(100, self.driver._get_memory_encrypted_slots()) self.assertEqual({
'amd_sev': {
'total': 100,
'step_size': 1,
'max_unit': 1,
'min_unit': 1,
'reserved': 0,
'allocation_ratio': 1.0,
'traits': [ot.HW_CPU_X86_AMD_SEV]
},
}, self.driver._get_memory_encryption_inventories())
mock_log.assert_not_called() mock_log.assert_not_called()
@test.patch_exists(SEV_KERNEL_PARAM_FILE, True) @test.patch_exists(SEV_KERNEL_PARAM_FILE, True)
@test.patch_open(SEV_KERNEL_PARAM_FILE, "1\n") @test.patch_open(SEV_KERNEL_PARAM_FILE, "1\n")
@mock.patch.object(libvirt_driver.LOG, 'warning') @mock.patch.object(libvirt_driver.LOG, 'warning')
def test_get_mem_encrypted_slots_overlide_more(self, mock_log): def test_get_memory_encryption_inventories_override_more(self, mock_log):
self.flags(num_memory_encrypted_guests=120, group='libvirt') self.flags(num_memory_encrypted_guests=120, group='libvirt')
self.assertEqual(100, self.driver._get_memory_encrypted_slots()) self.assertEqual({
'amd_sev': {
'total': 100,
'step_size': 1,
'max_unit': 1,
'min_unit': 1,
'reserved': 0,
'allocation_ratio': 1.0,
'traits': [ot.HW_CPU_X86_AMD_SEV]
}
}, self.driver._get_memory_encryption_inventories())
mock_log.assert_called_with( mock_log.assert_called_with(
'Host is configured with libvirt.num_memory_encrypted_guests ' 'Host is configured with libvirt.num_memory_encrypted_guests '
'set to %d, but supports only %d.', 120, 100) 'set to %d, but supports only %d.', 120, 100)
@@ -31049,9 +31285,19 @@ class TestLibvirtSEVSupportedMaxGuests(TestLibvirtSEV):
@test.patch_exists(SEV_KERNEL_PARAM_FILE, True) @test.patch_exists(SEV_KERNEL_PARAM_FILE, True)
@test.patch_open(SEV_KERNEL_PARAM_FILE, "1\n") @test.patch_open(SEV_KERNEL_PARAM_FILE, "1\n")
@mock.patch.object(libvirt_driver.LOG, 'warning') @mock.patch.object(libvirt_driver.LOG, 'warning')
def test_get_mem_encrypted_slots_override_less(self, mock_log): def test_get_memory_encryption_inventories_override_less(self, mock_log):
self.flags(num_memory_encrypted_guests=80, group='libvirt') self.flags(num_memory_encrypted_guests=80, group='libvirt')
self.assertEqual(80, self.driver._get_memory_encrypted_slots()) self.assertEqual({
'amd_sev': {
'total': 80,
'step_size': 1,
'max_unit': 1,
'min_unit': 1,
'reserved': 0,
'allocation_ratio': 1.0,
'traits': [ot.HW_CPU_X86_AMD_SEV]
}
}, self.driver._get_memory_encryption_inventories())
mock_log.assert_not_called() mock_log.assert_not_called()

View File

@@ -9437,7 +9437,6 @@ class LibvirtDriver(driver.ComputeDriver):
memory_mb = int(self._host.get_memory_mb_total()) memory_mb = int(self._host.get_memory_mb_total())
vcpus = len(self._get_vcpu_available()) vcpus = len(self._get_vcpu_available())
pcpus = len(self._get_pcpu_available()) pcpus = len(self._get_pcpu_available())
memory_enc_slots = self._get_memory_encrypted_slots()
# NOTE(yikun): If the inv record does not exists, the allocation_ratio # NOTE(yikun): If the inv record does not exists, the allocation_ratio
# will use the CONF.xxx_allocation_ratio value if xxx_allocation_ratio # will use the CONF.xxx_allocation_ratio value if xxx_allocation_ratio
@@ -9482,16 +9481,6 @@ class LibvirtDriver(driver.ComputeDriver):
'reserved': 0, 'reserved': 0,
} }
if memory_enc_slots:
result[orc.MEM_ENCRYPTION_CONTEXT] = {
'total': memory_enc_slots,
'min_unit': 1,
'max_unit': 1,
'step_size': 1,
'allocation_ratio': 1.0,
'reserved': 0,
}
# If a sharing DISK_GB provider exists in the provider tree, then our # If a sharing DISK_GB provider exists in the provider tree, then our
# storage is shared, and we should not report the DISK_GB inventory in # storage is shared, and we should not report the DISK_GB inventory in
# the compute node provider. # the compute node provider.
@@ -9523,6 +9512,9 @@ class LibvirtDriver(driver.ComputeDriver):
self._update_provider_tree_for_vpmems( self._update_provider_tree_for_vpmems(
provider_tree, nodename, result, resources) provider_tree, nodename, result, resources)
self._update_provider_tree_for_memory_encryption(
provider_tree, nodename, allocations=allocations)
provider_tree.update_inventory(nodename, result) provider_tree.update_inventory(nodename, result)
provider_tree.update_resources(nodename, resources) provider_tree.update_resources(nodename, resources)
@@ -9564,7 +9556,189 @@ class LibvirtDriver(driver.ComputeDriver):
metadata=vpmem) metadata=vpmem)
resources[rc].add(resource_obj) resources[rc].add(resource_obj)
def _get_memory_encrypted_slots(self): def _update_provider_tree_for_memory_encryption(self, provider_tree,
nodename, allocations):
"""Updates the provider tree for MEM_ENCRYPTION_CONTEXT inventory.
Before 2025.2, MEM_ENCRYPTION_CONTEXT inventory and allocations were on
the root compute node provider in the tree. Starting in 2025.2,
the MEM_ENCRYPTION_CONTEXT inventory is on a child provider in
the tree. As a result, this method will "reshape" the tree if necessary
on first start of this compute service in 2025.2.
:param provider_tree: The ProviderTree to update.
:param nodename: The ComputeNode.hypervisor_hostname, also known as
the name of the root node provider in the tree for this host.
:param allocations: If not None, indicates a reshape was requested and
should be performed.
:raises: nova.exception.ReshapeNeeded if ``allocations`` is None and
the method determines a reshape of the tree is needed, i.e.
MEM_ENCRYPTION_CONTEXT inventory and allocations must be migrated
from the root node provider to a child provider of
MEM_ENCRYPTION_CONTEXT resources in the tree.
:raises: nova.exception.ReshapeFailed if the requested tree reshape
fails for whatever reason.
"""
inventories_dict = self._get_memory_encryption_inventories()
if not inventories_dict:
return
me_rps = self._ensure_memory_encryption_providers(
inventories_dict, provider_tree, nodename)
if self._is_reshape_needed_memory_encryption_on_root(provider_tree,
nodename):
if allocations is None:
LOG.info('Requesting provider tree reshape in order to move '
'memory encryption context inventory from the root '
'compute node provider %s to a child provider.',
nodename)
raise exception.ReshapeNeeded()
root_node = provider_tree.data(nodename)
self._reshape_memory_encryption_resources(allocations, root_node,
me_rps)
if provider_tree.has_traits(nodename, [ot.HW_CPU_X86_AMD_SEV]):
provider_tree.remove_traits(nodename, ot.HW_CPU_X86_AMD_SEV)
if orc.MEM_ENCRYPTION_CONTEXT in root_node.inventory:
del root_node.inventory[orc.MEM_ENCRYPTION_CONTEXT]
provider_tree.update_inventory(nodename, root_node.inventory)
@staticmethod
def _is_reshape_needed_memory_encryption_on_root(provider_tree, nodename):
"""Determine if root RP has MEM_ENCRYPTION_CONTEXT inventories.
Check to see if the root compute node provider in the tree for
this host already has MEM_ENCRYPTION_CONTEXT inventory because if it
does, we either need to signal for a reshape (if
_update_provider_tree_for_memory_encryption () has no allocations) or
move the allocations within the ProviderTree if passed.
:param provider_tree: The ProviderTree object for this host.
:param nodename: The ComputeNode.hypervisor_hostname, also known as
the name of the root node provider in the tree for this host.
:returns: boolean, whether we have MEM_ENCRYPTION_CONTEXT root
inventory.
"""
root_node = provider_tree.data(nodename)
return orc.MEM_ENCRYPTION_CONTEXT in root_node.inventory
def _ensure_memory_encryption_providers(self, inventories_dict,
provider_tree, nodename):
"""Ensures MEM_ENCRYPTION_CONTEXT inventory providers exist in the tree
for $nodename.
MEM_ENCRYPTION_CONTEXT providers are named $nodename_$model, e.g.
``somehost.foo.bar.com_amd_sev``.
:param inventories_dict: Dictionary of inventories for
MEM_ENCRYPTION_CONTEXT class
directly provided by _get_memory_encryption_inventories() and which
looks like:
{'amd_sev':
{'total': $TOTAL,
'min_unit': 1,
'max_unit': 1,
'step_size': 1,
'reserved': 0,
'allocation_ratio': 1.0,
'traits': [ot.HW_CPU_X86_AMD_SEV],
}
}
:param provider_tree: The ProviderTree to update.
:param nodename: The ComputeNode.hypervisor_hostname, also known as
the name of the root node provider in the tree for this host.
:returns: dict, keyed by memory encryption model, to ProviderData
object representing that resource provider in the tree
"""
me_rps = {}
for me_id, inventory in inventories_dict.items():
me_rp_name = '%s_%s' % (nodename, me_id)
if not inventory['total']:
if provider_tree.exists(me_rp_name):
provider_tree.remove(me_rp_name)
break
if not provider_tree.exists(me_rp_name):
provider_tree.new_child(me_rp_name, nodename)
me_rp = provider_tree.data(me_rp_name)
me_rps[me_id] = me_rp
me_traits = inventory.pop('traits', [])
me_inventory = {orc.MEM_ENCRYPTION_CONTEXT: inventory}
provider_tree.update_inventory(me_rp_name, me_inventory)
provider_tree.add_traits(me_rp_name, *me_traits)
return me_rps
def _reshape_memory_encryption_resources(
self, allocations, root_node, me_rps):
for consumer_uuid, alloc_data in allocations.items():
allocs = alloc_data['allocations']
for rp_uuid in list(allocs):
resources = allocs[rp_uuid]['resources']
if orc.MEM_ENCRYPTION_CONTEXT in resources:
self._reshape_memory_encryption_allocations(
rp_uuid, root_node, consumer_uuid, alloc_data,
resources, me_rps)
def _reshape_memory_encryption_allocations(
self, rp_uuid, root_node, consumer_uuid, alloc_data, resources,
me_rps):
"""Update existing MEM_ENCRYPTION_CONTEXT allocations by moving them
from the root node provider to the child provider for AMD SEV
:param rp_uuid: UUID of the MEM_ENCRYPTION_CONTEXT resource provider
with allocations from consumer_uuid (should be the root node
provider before reshaping occurs)
:param root_node: ProviderData object for the root compute node
resource provider in the provider tree
:param consumer_uuid: UUID of the consumer (instance) with
MEM_ENCRYPTION_CONTEXT allocations against the resource provider
represented by rp_uuid
:param alloc_data: dict of allocation information for consumer_uuid
:param resources: dict, keyed by resource class, of resources allocated
to consumer_uuid from rp_uuid
:param me_rps: dict, keyed by memory encryption model, to ProviderData
object representing that resource provider in the tree
:raises: ReshapeFailed if the reshape fails for whatever reason
"""
self._assert_is_root_provider(
orc.MEM_ENCRYPTION_CONTEXT, rp_uuid, root_node, consumer_uuid,
alloc_data)
sev_rp = None
for me_rp_name in me_rps:
if ot.HW_CPU_X86_AMD_SEV in me_rps[me_rp_name].traits:
sev_rp = me_rps[me_rp_name]
break
if sev_rp is None:
msg = (_('MEM_ENCRYPTION_CONTEXT resources in the root provider '
'%(rp_uuid)s are allocated by %(consumer_uuid)s but '
'the child resource provider for AMD SEV is not found.')
% {'rp_uuid': rp_uuid, 'consumer_uuid': consumer_uuid})
raise exception.ReshapeFailed(error=msg)
allocs = alloc_data['allocations']
allocs[sev_rp.uuid] = {
'resources': {
orc.MEM_ENCRYPTION_CONTEXT: 1
}
}
del resources[orc.MEM_ENCRYPTION_CONTEXT]
def _get_memory_encryption_inventories(self):
"""Returns the inventories for MEM_ENCRYPTION_CONTEXT.
:returns: dict, keyed by memory encryption model, of dicts like:
{'amd_sev':
{'total': $TOTAL,
'min_unit': 1,
'max_unit': 1,
'step_size': 1,
'reserved': 0,
'allocation_ratio': 1.0,
'traits': [ot.HW_CPU_X86_AMD_SEV]
}
}
"""
conf_slots = CONF.libvirt.num_memory_encrypted_guests conf_slots = CONF.libvirt.num_memory_encrypted_guests
if not self._host.supports_amd_sev: if not self._host.supports_amd_sev:
@@ -9572,7 +9746,7 @@ class LibvirtDriver(driver.ComputeDriver):
LOG.warning("Host is configured with " LOG.warning("Host is configured with "
"libvirt.num_memory_encrypted_guests set to " "libvirt.num_memory_encrypted_guests set to "
"%d, but is not SEV-capable.", conf_slots) "%d, but is not SEV-capable.", conf_slots)
return 0 return {}
slots = db_const.MAX_INT slots = db_const.MAX_INT
@@ -9587,8 +9761,18 @@ class LibvirtDriver(driver.ComputeDriver):
"but supports only %d.", conf_slots, slots) "but supports only %d.", conf_slots, slots)
slots = min(slots, conf_slots) slots = min(slots, conf_slots)
LOG.debug("Available memory encrypted slots: %d", slots) LOG.debug("Available memory encrypted slots: AMD SEV=%d", slots)
return slots return {
'amd_sev': {
'total': slots,
'step_size': 1,
'max_unit': 1,
'min_unit': 1,
'allocation_ratio': 1.0,
'reserved': 0,
'traits': [ot.HW_CPU_X86_AMD_SEV]
}
}
@property @property
def static_traits(self) -> ty.Dict[str, bool]: def static_traits(self) -> ty.Dict[str, bool]:
@@ -9688,12 +9872,13 @@ class LibvirtDriver(driver.ComputeDriver):
@staticmethod @staticmethod
def _assert_is_root_provider( def _assert_is_root_provider(
rp_uuid, root_node, consumer_uuid, alloc_data): rc_name, rp_uuid, root_node, consumer_uuid, alloc_data):
"""Asserts during a reshape that rp_uuid is for the root node provider. """Asserts during a reshape that rp_uuid is for the root node provider.
When reshaping, inventory and allocations should be on the root node When reshaping, inventory and allocations should be on the root node
provider and then moved to child providers. provider and then moved to child providers.
:param rc_name: Resource class name
:param rp_uuid: UUID of the provider that holds inventory/allocations. :param rp_uuid: UUID of the provider that holds inventory/allocations.
:param root_node: ProviderData object representing the root node in a :param root_node: ProviderData object representing the root node in a
provider tree. provider tree.
@@ -9705,15 +9890,16 @@ class LibvirtDriver(driver.ComputeDriver):
expected. expected.
""" """
if rp_uuid != root_node.uuid: if rp_uuid != root_node.uuid:
# Something is wrong - VGPU inventory should # Something is wrong - the inventory should
# only be on the root node provider if we are # only be on the root node provider if we are
# reshaping the tree. # reshaping the tree.
msg = (_('Unexpected VGPU resource allocation ' msg = (_('Unexpected %(rc_name)s resource allocation '
'on provider %(rp_uuid)s for consumer ' 'on provider %(rp_uuid)s for consumer '
'%(consumer_uuid)s: %(alloc_data)s. ' '%(consumer_uuid)s: %(alloc_data)s. '
'Expected VGPU allocation to be on root ' 'Expected %(rc_name)s allocation to be on root '
'compute node provider %(root_uuid)s.') 'compute node provider %(root_uuid)s.')
% {'rp_uuid': rp_uuid, % {'rc_name': rc_name,
'rp_uuid': rp_uuid,
'consumer_uuid': consumer_uuid, 'consumer_uuid': consumer_uuid,
'alloc_data': alloc_data, 'alloc_data': alloc_data,
'root_uuid': root_node.uuid}) 'root_uuid': root_node.uuid})
@@ -9827,7 +10013,7 @@ class LibvirtDriver(driver.ComputeDriver):
# We've found VGPU allocations on a provider. It should be the root # We've found VGPU allocations on a provider. It should be the root
# node provider. # node provider.
self._assert_is_root_provider( self._assert_is_root_provider(
rp_uuid, root_node, consumer_uuid, alloc_data) orc.VGPU, rp_uuid, root_node, consumer_uuid, alloc_data)
# Find which physical GPU corresponds to this allocation. # Find which physical GPU corresponds to this allocation.
mdev_uuids = self._get_assigned_mdevs_for_reshape( mdev_uuids = self._get_assigned_mdevs_for_reshape(
@@ -13220,7 +13406,6 @@ class LibvirtDriver(driver.ComputeDriver):
:return: A dict of trait names mapped to boolean values. :return: A dict of trait names mapped to boolean values.
""" """
traits = self._get_cpu_feature_traits() traits = self._get_cpu_feature_traits()
traits[ot.HW_CPU_X86_AMD_SEV] = self._host.supports_amd_sev
traits[ot.HW_CPU_HYPERTHREADING] = self._host.has_hyperthreading traits[ot.HW_CPU_HYPERTHREADING] = self._host.has_hyperthreading
traits.update(self._get_cpu_arch_traits()) traits.update(self._get_cpu_arch_traits())
traits.update(self._get_cpu_emulation_arch_traits()) traits.update(self._get_cpu_emulation_arch_traits())