Merge "TPM: prepare to bump service version for live migration"

This commit is contained in:
Zuul
2026-02-16 18:30:42 +00:00
committed by Gerrit Code Review
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)