From 2bdf12535c214a67381c890bdd415ba1779af231 Mon Sep 17 00:00:00 2001 From: melanie witt Date: Mon, 18 Aug 2025 21:03:14 +0000 Subject: [PATCH] TPM: prepare to bump service version for live migration This prepares for a service version bump and adds a minimum service version check in the API to reject live migration requests for vTPM instances until the entire cloud is upgraded to the new version. The actual service version bump will be included in a later patch that implements vTPM live migration. Related to blueprint vtpm-live-migration Change-Id: I7daef8037385a4077dc0a78f03ae4b34a57560b7 Signed-off-by: melanie witt --- nova/api/openstack/compute/migrate_server.py | 1 + nova/compute/api.py | 31 +++- nova/exception.py | 6 + nova/tests/functional/integrated_helpers.py | 5 +- nova/tests/functional/libvirt/test_vtpm.py | 150 ++++++++++++++++++- nova/tests/unit/compute/test_api.py | 55 +++++++ 6 files changed, 243 insertions(+), 5 deletions(-) diff --git a/nova/api/openstack/compute/migrate_server.py b/nova/api/openstack/compute/migrate_server.py index 8ab2c6c5dd..cd177ba2a6 100644 --- a/nova/api/openstack/compute/migrate_server.py +++ b/nova/api/openstack/compute/migrate_server.py @@ -163,6 +163,7 @@ class MigrateServerController(wsgi.Controller): except ( exception.ComputeHostNotFound, exception.ExtendedResourceRequestOldCompute, + exception.VTPMOldCompute, )as e: raise exc.HTTPBadRequest(explanation=e.format_message()) except exception.InstanceInvalidState as state_error: diff --git a/nova/compute/api.py b/nova/compute/api.py index 2cdaab6e96..b846ef7abc 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -129,6 +129,8 @@ SUPPORT_SHARES = 67 MIN_COMPUTE_SOUND_MODEL_TRAITS = 69 MIN_COMPUTE_USB_MODEL_TRAITS = 70 +MIN_COMPUTE_VTPM_LIVE_MIGRATION = None + # FIXME(danms): Keep a global cache of the cells we find the # first time we look. This needs to be refreshed on a timer or # trigger. @@ -268,6 +270,33 @@ def reject_sev_instances(operation): return outer +def reject_legacy_vtpm_live_migration(function): + + @functools.wraps(function) + def inner(self, context, instance, *args, **kwargs): + if hardware.get_vtpm_constraint( + instance.flavor, instance.image_meta): + # Only certain TPM secret security modes support live migration. + security = hardware.get_tpm_secret_security_constraint( + instance.flavor) or 'user' + if security != 'host': + raise exception.OperationNotSupportedForVTPM( + instance_uuid=instance.uuid, + operation=instance_actions.LIVE_MIGRATION) + # We need not check all cells because live migration only works + # within a single cell. + im = objects.InstanceMapping.get_by_instance_uuid(context, + instance.uuid) + with nova_context.target_cell(context, im.cell_mapping) as cctxt: + min_ver = objects.service.Service.get_minimum_version( + cctxt, 'nova-compute') + if (MIN_COMPUTE_VTPM_LIVE_MIGRATION is None or + min_ver < MIN_COMPUTE_VTPM_LIVE_MIGRATION): + raise exception.VTPMOldCompute() + return function(self, context, instance, *args, **kwargs) + return inner + + def reject_vtpm_instances(operation): """Reject requests to decorated function if instance has vTPM enabled. @@ -5612,7 +5641,7 @@ class API: until=MIN_COMPUTE_VDPA_HOTPLUG_LIVE_MIGRATION ) @block_accelerators() - @reject_vtpm_instances(instance_actions.LIVE_MIGRATION) + @reject_legacy_vtpm_live_migration @reject_sev_instances(instance_actions.LIVE_MIGRATION) @check_instance_lock @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.PAUSED]) diff --git a/nova/exception.py b/nova/exception.py index 22cd274c3b..2083758744 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -2670,3 +2670,9 @@ class InstanceEventTimeout(Exception): class VTPMSecretForbidden(Forbidden): pass + + +class VTPMOldCompute(Invalid): + msg_fmt = _('vTPM live migration is not supported by old nova-compute ' + 'services. Upgrade your nova-compute services to ' + 'Gazpacho (33.0.0) or later.') diff --git a/nova/tests/functional/integrated_helpers.py b/nova/tests/functional/integrated_helpers.py index 3898ea0afa..c7ec524fcc 100644 --- a/nova/tests/functional/integrated_helpers.py +++ b/nova/tests/functional/integrated_helpers.py @@ -642,9 +642,10 @@ class InstanceHelperMixin: def _live_migrate( self, server, migration_expected_state='completed', - server_expected_state='ACTIVE', + server_expected_state='ACTIVE', api=None, ): - self.api.post_server_action( + api = api or self.api + api.post_server_action( server['id'], {'os-migrateLive': {'host': None, 'block_migration': 'auto'}}) self._wait_for_migration_status(server, [migration_expected_state]) diff --git a/nova/tests/functional/libvirt/test_vtpm.py b/nova/tests/functional/libvirt/test_vtpm.py index c4b90b4939..9d8f7d5f48 100644 --- a/nova/tests/functional/libvirt/test_vtpm.py +++ b/nova/tests/functional/libvirt/test_vtpm.py @@ -26,6 +26,7 @@ from oslo_utils import uuidutils import nova.conf from nova import context as nova_context from nova import crypto +from nova.db.main import api as db_api from nova import exception from nova import objects from nova.tests.functional.api import client @@ -149,6 +150,8 @@ class VTPMServersTest(base.ServersTestBase): # Reflect reality more for async API requests like migration CAST_AS_CALL = False + # Enables block_migration='auto' required by the _live_migrate() helper. + microversion = '2.25' def setUp(self): # enable vTPM and use our own fake key service @@ -342,6 +345,146 @@ class VTPMServersTest(base.ServersTestBase): self.assertNotIn(instance.system_metadata['vtpm_secret_uuid'], conn._secrets) + def test_live_migrate_server_secret_security_user_too_old(self): + """Test behavior when a new server tries to migrate to an old compute + + We will simulate a migration attempt to an old host by setting the + service version of the destination to an old version and starting it + without any supported_tpm_secret_security. Then we will try to live + migrate to it. + + This should fail with BadRequest because the TPM secret security + policy 'user' is not allowed to live migrate. + """ + self.flags( + supported_tpm_secret_security=['user', 'host'], group='libvirt') + self.start_compute(hostname='src') + + server = self._create_server_with_vtpm(secret_security='user') + + # Set the destination compute to fake the old version. We need to use + # the DB API directly to get around the minimum service version check + # in the Service object save() method. + self.start_compute(hostname='dest') + ctx = nova_context.get_admin_context() + db_api.service_update( + ctx, self.computes['dest'].service_ref.id, {'version': 70}) + + ex = self.assertRaises( + client.OpenStackApiException, self._live_migrate, server, + api=self.admin_api) + self.assertEqual(400, ex.response.status_code) + msg = "'live-migration' not supported for vTPM-enabled instance" + self.assertIn(msg, str(ex)) + + def test_live_migrate_server_secret_security_host_too_old(self): + """Test behavior when a new server tries to migrate to an old compute + + We will simulate a migration attempt to an old host by setting the + service version of the destination to an old version and starting it + without any supported_tpm_secret_security. Then we will try to live + migrate to it. + + This should fail with BadRequest because of the service version check. + """ + self.flags(supported_tpm_secret_security=['host'], group='libvirt') + self.start_compute(hostname='src') + + server = self._create_server_with_vtpm(secret_security='host') + + # Set the destination compute to fake the old version. We need to use + # the DB API directly to get around the minimum service version check + # in the Service object save() method. + self.start_compute(hostname='dest') + ctx = nova_context.get_admin_context() + db_api.service_update( + ctx, self.computes['dest'].service_ref.id, {'version': 70}) + + ex = self.assertRaises( + client.OpenStackApiException, self._live_migrate, server, + api=self.admin_api) + self.assertEqual(400, ex.response.status_code) + self.assertIn( + 'vTPM live migration is not supported by old nova-compute ' + 'services. Upgrade your nova-compute services to ' + 'Gazpacho (33.0.0) or later.', str(ex)) + + def test_live_migrate_host_server_secret_security_host_too_old(self): + """Test behavior when a new server tries to migrate to an old compute + + This will request a destination host for live migration. + + We will simulate a migration attempt to an old host by setting the + service version of the destination to an old version and starting it + without any supported_tpm_secret_security. Then we will try to live + migrate to it. + + This should fail with BadRequest because of the service version check. + """ + self.flags(supported_tpm_secret_security=['host'], group='libvirt') + self.start_compute(hostname='src') + + server = self._create_server_with_vtpm(secret_security='host') + + # Set the destination compute to fake the old version. We need to use + # the DB API directly to get around the minimum service version check + # in the Service object save() method. + self.start_compute(hostname='dest') + ctx = nova_context.get_admin_context() + db_api.service_update( + ctx, self.computes['dest'].service_ref.id, {'version': 70}) + + ex = self.assertRaises( + client.OpenStackApiException, self._live_migrate, server, + api=self.admin_api) + self.assertEqual(400, ex.response.status_code) + self.assertIn( + 'vTPM live migration is not supported by old nova-compute ' + 'services. Upgrade your nova-compute services to ' + 'Gazpacho (33.0.0) or later.', str(ex)) + + def test_live_migrate_host_force_server_secret_security_host_too_old(self): + """Test behavior when a new server tries to migrate to an old compute + + This will request a destination host for live migration and force=True + by using an older microversion 2.30. + + We will simulate a migration attempt to an old host by setting the + service version of the destination to an old version and starting it + without any supported_tpm_secret_security. Then we will try to live + migrate to it. + + This should fail with BadRequest because of the service version check. + """ + self.flags(supported_tpm_secret_security=['host'], group='libvirt') + self.start_compute(hostname='src') + self.src = self.computes['src'] + + self.server = self._create_server_with_vtpm(secret_security='host') + + # Set the destination compute to fake the old version. We need to use + # the DB API directly to get around the minimum service version check + # in the Service object save() method. + self.start_compute(hostname='dest') + self.dest = self.computes['dest'] + ctx = nova_context.get_admin_context() + db_api.service_update(ctx, self.dest.service_ref.id, {'version': 70}) + + # The request should be rejected by the API with a 400 Bad Request due + # to the destination host service version being too old. + with utils.temporary_mutation(self.admin_api, microversion='2.30'): + ex = self.assertRaises( + client.OpenStackApiException, + self.admin_api.post_server_action, self.server['id'], + {'os-migrateLive': {'host': 'dest', + 'block_migration': 'auto', + 'force': 'True'}}) + self.assertEqual(400, ex.response.status_code) + self.assertIn( + 'vTPM live migration is not supported by old nova-compute ' + 'services. Upgrade your nova-compute services to ' + 'Gazpacho (33.0.0) or later.', str(ex)) + def test_suspend_resume_server(self): self.start_compute() @@ -787,9 +930,12 @@ class VTPMServersTest(base.ServersTestBase): self.assertInstanceHasSecret(server) # live migrate the server - self.assertRaises( + ex = self.assertRaises( client.OpenStackApiException, - self._live_migrate_server, server) + self._live_migrate_server, server, api=self.admin_api) + self.assertEqual(400, ex.response.status_code) + msg = "'live-migration' not supported for vTPM-enabled instance" + self.assertIn(msg, str(ex)) def test_shelve_server(self): for host in ('test_compute0', 'test_compute1'): diff --git a/nova/tests/unit/compute/test_api.py b/nova/tests/unit/compute/test_api.py index ce32cee1df..f2fed17b24 100644 --- a/nova/tests/unit/compute/test_api.py +++ b/nova/tests/unit/compute/test_api.py @@ -7504,6 +7504,7 @@ class _ComputeAPIUnitTestMixIn(object): # TODO(stephenfin): The separation of the mixin is a hangover from cells v1 # days and should be removed +@ddt.ddt class ComputeAPIUnitTestCase(_ComputeAPIUnitTestMixIn, test.NoDBTestCase): def setUp(self): super(ComputeAPIUnitTestCase, self).setUp() @@ -8749,3 +8750,57 @@ class ComputeAPIUnitTestCase(_ComputeAPIUnitTestMixIn, test.NoDBTestCase): mock_rec_action.assert_called_once_with( self.context, instance, instance_actions.DETACH_SHARE) + + @mock.patch.object(compute_api, 'MIN_COMPUTE_VTPM_LIVE_MIGRATION', 5) + @ddt.data(None, 'host') + def test_reject_legacy_vtpm_live_migration(self, secret_security): + """Test that live migration requests are rejected properly. + + Only certain TPM secret security modes are allowed to request live + migration. + """ + @compute_api.reject_legacy_vtpm_live_migration + def fake_compute_api_method(api_self, context, instance): + pass + + instance = self._create_instance_obj() + instance.flavor.extra_specs = { + 'hw:tpm_version': '1.2', + } + if secret_security: + instance.flavor.extra_specs[ + 'hw:tpm_secret_security'] = secret_security + + with mock.patch( + 'nova.objects.service.Service.get_minimum_version', + return_value=compute_api.MIN_COMPUTE_VTPM_LIVE_MIGRATION): + if secret_security == 'host': + fake_compute_api_method(self.compute_api, self.context, + instance) + else: + self.assertRaises(exception.OperationNotSupportedForVTPM, + fake_compute_api_method, self.compute_api, + self.context, instance) + + @mock.patch.object(compute_api, 'MIN_COMPUTE_VTPM_LIVE_MIGRATION', 5) + def test_reject_legacy_vtpm_live_migration_service_version(self): + """Test live migration request rejection based on service version. + + If a compute is not new enough, live migration will not be allowed. + """ + @compute_api.reject_legacy_vtpm_live_migration + def fake_compute_api_method(api_self, context, instance): + pass + + instance = self._create_instance_obj() + instance.flavor.extra_specs = { + 'hw:tpm_version': '1.2', + 'hw:tpm_secret_security': 'host', + } + + with mock.patch( + 'nova.objects.service.Service.get_minimum_version', + return_value=compute_api.MIN_COMPUTE_VTPM_LIVE_MIGRATION - 1): + self.assertRaises(exception.VTPMOldCompute, + fake_compute_api_method, self.compute_api, + self.context, instance)