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 <melwittt@gmail.com>
This commit is contained in:
melanie witt
2025-08-18 21:03:14 +00:00
parent 8b3701490e
commit 2bdf12535c
6 changed files with 243 additions and 5 deletions
@@ -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:
+30 -1
View File
@@ -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])
+6
View File
@@ -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.')
+3 -2
View File
@@ -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])
+148 -2
View File
@@ -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'):
+55
View File
@@ -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)