scheduler: Request vTPM trait based on flavor or image

Add support for the 'hw:tpm_version' and 'hw:tpm_model' flavor extra
specs along with the equivalent image metadata properties. These are
picked up by the scheduler and transformed into trait requests. This is
effectively a no-op for now since we don't yet have a driver that
reports these traits.

Part of blueprint add-emulated-virtual-tpm

Change-Id: I8645c31b4ecb18afea592b2a5b360b0165626009
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane 2020-07-16 17:54:44 +01:00
parent e3b0412dda
commit 5550f86623
13 changed files with 282 additions and 10 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.4" "nova_object.version": "1.5"
} }

View File

@ -354,6 +354,35 @@ feature_flag_validators = [
'description': 'The number of serial ports to allocate', 'description': 'The number of serial ports to allocate',
}, },
), ),
base.ExtraSpecValidator(
name='hw:tpm_model',
description=(
'The model of the attached TPM device.'
),
value={
'type': str,
'description': 'A TPM model',
'enum': [
'tpm-tis',
'tpm-crb',
],
},
),
base.ExtraSpecValidator(
name='hw:tpm_version',
description=(
"The TPM version. Required if requesting a vTPM via the "
"'hw:tpm_model' extra spec or equivalent image metadata property."
),
value={
'type': str,
'description': 'A TPM version.',
'enum': [
'1.2',
'2.0',
],
},
),
base.ExtraSpecValidator( base.ExtraSpecValidator(
name='hw:watchdog_action', name='hw:watchdog_action',
description=( description=(

View File

@ -121,7 +121,8 @@ class ImageMetaPropsPayload(base.NotificationPayloadBase):
# Version 1.2: Added hw_pci_numa_affinity_policy field # Version 1.2: Added hw_pci_numa_affinity_policy field
# Version 1.3: Added hw_mem_encryption, hw_pmu and hw_time_hpet fields # Version 1.3: Added hw_mem_encryption, hw_pmu and hw_time_hpet fields
# Version 1.4: Added 'mixed' to hw_cpu_policy field # Version 1.4: Added 'mixed' to hw_cpu_policy field
VERSION = '1.4' # Version 1.5: Added 'hw_tpm_model' and 'hw_tpm_version' fields
VERSION = '1.5'
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

@ -510,6 +510,21 @@ class RNGModel(BaseNovaEnum):
ALL = (VIRTIO,) ALL = (VIRTIO,)
class TPMModel(BaseNovaEnum):
TIS = "tpm-tis"
CRB = "tpm-crb"
ALL = (TIS, CRB)
class TPMVersion(BaseNovaEnum):
v1_2 = "1.2"
v2_0 = "2.0"
ALL = (v1_2, v2_0)
class SCSIModel(BaseNovaEnum): class SCSIModel(BaseNovaEnum):
BUSLOGIC = "buslogic" BUSLOGIC = "buslogic"
@ -1230,6 +1245,14 @@ class RNGModelField(BaseEnumField):
AUTO_TYPE = RNGModel() AUTO_TYPE = RNGModel()
class TPMModelField(BaseEnumField):
AUTO_TYPE = TPMModel()
class TPMVersionField(BaseEnumField):
AUTO_TYPE = TPMVersion()
class SCSIModelField(BaseEnumField): class SCSIModelField(BaseEnumField):
AUTO_TYPE = SCSIModel() AUTO_TYPE = SCSIModel()

View File

@ -176,14 +176,18 @@ class ImageMetaProps(base.NovaObject):
# Version 1.24: Added 'hw_mem_encryption' field # Version 1.24: Added 'hw_mem_encryption' field
# Version 1.25: Added 'hw_pci_numa_affinity_policy' field # Version 1.25: Added 'hw_pci_numa_affinity_policy' field
# Version 1.26: Added 'mixed' to 'hw_cpu_policy' field # Version 1.26: Added 'mixed' to 'hw_cpu_policy' field
# Version 1.27: Added 'hw_tpm_model' and 'hw_tpm_version' fields
# 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.26' VERSION = '1.27'
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, 27):
primitive.pop('hw_tpm_model', None)
primitive.pop('hw_tpm_version', None)
if target_version < (1, 26): if target_version < (1, 26):
policy = primitive.get('hw_cpu_policy', None) policy = primitive.get('hw_cpu_policy', None)
if policy == fields.CPUAllocationPolicy.MIXED: if policy == fields.CPUAllocationPolicy.MIXED:
@ -403,6 +407,11 @@ class ImageMetaProps(base.NovaObject):
# boolean - If true, this will enable the virtio-multiqueue feature # boolean - If true, this will enable the virtio-multiqueue feature
'hw_vif_multiqueue_enabled': fields.FlexibleBooleanField(), 'hw_vif_multiqueue_enabled': fields.FlexibleBooleanField(),
# name of emulated TPM model to use.
'hw_tpm_model': fields.TPMModelField(),
# version of emulated TPM to use.
'hw_tpm_version': fields.TPMVersionField(),
# if true download using bittorrent # if true download using bittorrent
'img_bittorrent': fields.FlexibleBooleanField(), 'img_bittorrent': fields.FlexibleBooleanField(),

View File

@ -167,6 +167,8 @@ class ResourceRequest(object):
self._translate_vpmems_request(request_spec.flavor) self._translate_vpmems_request(request_spec.flavor)
self._translate_vtpm_request(request_spec.flavor, image)
self.strip_zeros() self.strip_zeros()
def _process_requested_resources(self, request_spec): def _process_requested_resources(self, request_spec):
@ -213,6 +215,20 @@ class ResourceRequest(object):
# supported in image traits # supported in image traits
self._add_trait(None, trait, "required") self._add_trait(None, trait, "required")
def _translate_vtpm_request(self, flavor, image):
vtpm_config = hardware.get_vtpm_constraint(flavor, image)
if not vtpm_config:
return
# Require the appropriate vTPM version support trait on a host.
if vtpm_config.version == obj_fields.TPMVersion.v1_2:
trait = os_traits.COMPUTE_SECURITY_TPM_1_2
else:
trait = os_traits.COMPUTE_SECURITY_TPM_2_0
self._add_trait(None, trait, "required")
LOG.debug("Requiring emulated TPM support via trait %s.", trait)
def _translate_memory_encryption(self, flavor, image): def _translate_memory_encryption(self, flavor, image):
"""When the hw:mem_encryption extra spec or the hw_mem_encryption """When the hw:mem_encryption extra spec or the hw_mem_encryption
image property are requested, translate into a request for image property are requested, translate into a request for

View File

@ -1249,7 +1249,8 @@ 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': u'1.4'}, 'nova_object.version': u'1.5',
},
'image.size': 58145823, 'image.size': 58145823,
'image.tags': [], 'image.tags': [],
'scheduler_hints': {'_nova_check_type': ['rebuild']}, 'scheduler_hints': {'_nova_check_type': ['rebuild']},
@ -1344,7 +1345,8 @@ 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': u'1.4'}, 'nova_object.version': u'1.5',
},
'image.size': 58145823, 'image.size': 58145823,
'image.tags': [], 'image.tags': [],
'scheduler_hints': {'_nova_check_type': ['rebuild']}, 'scheduler_hints': {'_nova_check_type': ['rebuild']},

View File

@ -387,7 +387,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.4-036c794843b95a3a39ee70830f5f6557', 'ImageMetaPropsPayload': '1.5-f4074a974d4a9f77e302a53ee9340287',
'InstanceActionNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'InstanceActionNotification': '1.0-a73147b93b520ff0061865849d3dfa56',
'InstanceActionPayload': '1.8-4fa3da9cbf0761f1f700ae578f36dc2f', 'InstanceActionPayload': '1.8-4fa3da9cbf0761f1f700ae578f36dc2f',
'InstanceActionRebuildNotification': 'InstanceActionRebuildNotification':

View File

@ -411,3 +411,16 @@ class TestImageMetaProps(test.NoDBTestCase):
old_primitive = obj.obj_to_primitive('1.22') old_primitive = obj.obj_to_primitive('1.22')
self.assertIn('hw_pmu', primitive['nova_object.data']) self.assertIn('hw_pmu', primitive['nova_object.data'])
self.assertNotIn('hw_pmu', old_primitive['nova_object.data']) self.assertNotIn('hw_pmu', old_primitive['nova_object.data'])
def test_obj_make_compatible_1_26(self):
"""Test that checks if we pop hw_tpm_model and hw_tpm_version."""
obj = objects.ImageMetaProps(
hw_tpm_model='tpm-tis', hw_tpm_version='1.2',
)
primitive = obj.obj_to_primitive()
self.assertIn('hw_tpm_model', primitive['nova_object.data'])
self.assertIn('hw_tpm_version', primitive['nova_object.data'])
primitive = obj.obj_to_primitive('1.25')
self.assertNotIn('hw_tpm_model', primitive['nova_object.data'])
self.assertNotIn('hw_tpm_version', primitive['nova_object.data'])

View File

@ -1077,7 +1077,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.26-b9f136cd10a2b5ffb3ae44332f2f687d', 'ImageMetaProps': '1.27-f3f17d5e35146a0dbb56420ffc4f3990',
'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

@ -1119,6 +1119,56 @@ class TestUtils(TestUtilsBase):
rr = utils.ResourceRequest(rs) rr = utils.ResourceRequest(rs)
self.assertResourceRequestsEqual(expected, rr) self.assertResourceRequestsEqual(expected, rr)
def test_resource_request_with_vtpm_1_2(self):
flavor = objects.Flavor(
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0,
extra_specs={'hw:tpm_version': '1.2', 'hw:tpm_model': 'tpm-tis'},
)
image = objects.ImageMeta(
properties=objects.ImageMetaProps(
hw_tpm_version='1.2',
hw_tpm_model='tpm-tis',
)
)
expected = FakeResourceRequest()
expected._rg_by_id[None] = objects.RequestGroup(
use_same_provider=False,
required_traits={'COMPUTE_SECURITY_TPM_1_2'},
resources={
'VCPU': 1,
'MEMORY_MB': 1024,
'DISK_GB': 15,
},
)
rs = objects.RequestSpec(flavor=flavor, image=image, is_bfv=False)
rr = utils.ResourceRequest(rs)
self.assertResourceRequestsEqual(expected, rr)
def test_resource_request_with_vtpm_2_0(self):
flavor = objects.Flavor(
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0,
extra_specs={'hw:tpm_version': '2.0', 'hw:tpm_model': 'tpm-crb'},
)
image = objects.ImageMeta(
properties=objects.ImageMetaProps(
hw_tpm_version='2.0',
hw_tpm_model='tpm-crb',
)
)
expected = FakeResourceRequest()
expected._rg_by_id[None] = objects.RequestGroup(
use_same_provider=False,
required_traits={'COMPUTE_SECURITY_TPM_2_0'},
resources={
'VCPU': 1,
'MEMORY_MB': 1024,
'DISK_GB': 15,
},
)
rs = objects.RequestSpec(flavor=flavor, image=image, is_bfv=False)
rr = utils.ResourceRequest(rs)
self.assertResourceRequestsEqual(expected, rr)
def test_resource_request_add_group_inserts_the_group(self): def test_resource_request_add_group_inserts_the_group(self):
flavor = objects.Flavor( flavor = objects.Flavor(
vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0) vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0)

View File

@ -4998,6 +4998,65 @@ class PCINUMAAffinityPolicyTest(test.NoDBTestCase):
image_meta.properties.hw_pci_numa_affinity_policy = "fake" image_meta.properties.hw_pci_numa_affinity_policy = "fake"
@ddt.ddt
class VTPMConfigTest(test.NoDBTestCase):
@ddt.unpack
@ddt.data(
# pass: no configuration
(None, None, None, None, None),
# pass: flavor-only (TIS) configuration
('1.2', 'tpm-tis', None, None, hw.VTPMConfig('1.2', 'tpm-tis')),
# pass: image-only (TIS) configuration
(None, None, '1.2', 'tpm-tis', hw.VTPMConfig('1.2', 'tpm-tis')),
# pass: identical image and flavor (TIS) configuration
('1.2', 'tpm-tis', '1.2', 'tpm-tis', hw.VTPMConfig('1.2', 'tpm-tis')),
# pass: identical image and flavor (CRB) configuration
('2.0', 'tpm-crb', '2.0', 'tpm-crb', hw.VTPMConfig('2.0', 'tpm-crb')),
# fail: mismatched image and flavor configuration
('1.2', 'tpm-tis', '2.0', 'tpm-crb', exception.FlavorImageConflict),
# fail: invalid version
('1.3', 'tpm-tis', None, None, exception.Invalid),
# fail: invalid model
('1.2', 'tpm-foo', None, None, exception.Invalid),
# fail: invalid version/model combination
('1.2', 'tpm-crb', None, None, exception.Invalid),
)
def test_get_vtpm_constraint(
self, flavor_version, flavor_model, image_version, image_model,
expected,
):
extra_specs = {}
if flavor_version:
extra_specs['hw:tpm_version'] = flavor_version
if flavor_model:
extra_specs['hw:tpm_model'] = flavor_model
image_meta_props = {}
if image_version:
image_meta_props['hw_tpm_version'] = image_version
if image_model:
image_meta_props['hw_tpm_model'] = image_model
flavor = objects.Flavor(
name='foo', vcpus=1, memory_mb=1024, extra_specs=extra_specs)
image_meta = objects.ImageMeta.from_dict(
{'name': 'bar', 'properties': image_meta_props})
if isinstance(expected, type) and issubclass(expected, Exception):
self.assertRaises(
expected, hw.get_vtpm_constraint, flavor, image_meta,
)
else:
self.assertEqual(
expected, hw.get_vtpm_constraint(flavor, image_meta),
)
@ddt.ddt @ddt.ddt
class RescuePropertyTestCase(test.NoDBTestCase): class RescuePropertyTestCase(test.NoDBTestCase):

View File

@ -1171,10 +1171,39 @@ def _get_flavor_image_meta(
flavor_key = ':'.join(['hw', key]) flavor_key = ':'.join(['hw', key])
image_key = '_'.join(['hw', key]) image_key = '_'.join(['hw', key])
flavor_policy = flavor.get('extra_specs', {}).get(flavor_key, default) flavor_value = flavor.get('extra_specs', {}).get(flavor_key, default)
image_policy = image_meta.properties.get(image_key, default) image_value = image_meta.properties.get(image_key, default)
return flavor_policy, image_policy return flavor_value, image_value
def _get_unique_flavor_image_meta(
key: str,
flavor: 'objects.Flavor',
image_meta: 'objects.ImageMeta',
default: ty.Any = None
) -> ty.Any:
"""A variant of '_get_flavor_image_meta' that errors out on conflicts."""
flavor_value, image_value = _get_flavor_image_meta(
key, flavor, image_meta, default,
)
if image_value and flavor_value and image_value != flavor_value:
msg = _(
"Flavor %(flavor_name)s has hw:%(key)s extra spec explicitly "
"set to %(flavor_val)s, conflicting with image %(image_name)s "
"which has hw_%(key)s explicitly set to %(image_val)s."
)
raise exception.FlavorImageConflict(
msg % {
'key': key,
'flavor_name': flavor.name,
'flavor_val': flavor_value,
'image_name': image_meta.name,
'image_val': image_value,
},
)
return flavor_value or image_value
def get_mem_encryption_constraint( def get_mem_encryption_constraint(
@ -1814,6 +1843,47 @@ def get_pci_numa_policy_constraint(flavor, image_meta):
return policy return policy
def get_vtpm_constraint(
flavor: 'objects.Flavor',
image_meta: 'objects.ImageMeta',
) -> ty.Optional[VTPMConfig]:
"""Validate and return the requested vTPM configuration.
:param flavor: ``nova.objects.Flavor`` instance
:param image_meta: ``nova.objects.ImageMeta`` instance
:raises: nova.exception.FlavorImageConflict if a value is specified in both
the flavor and the image, but the values do not match
:raises: nova.exception.Invalid if a value or combination of values is
invalid
:returns: A named tuple containing the vTPM version and model, else None.
"""
version = _get_unique_flavor_image_meta('tpm_version', flavor, image_meta)
if version is None:
return None
if version not in fields.TPMVersion.ALL:
raise exception.Invalid(
"Invalid TPM version %(version)r. Allowed values: %(valid)s." %
{'version': version, 'valid': ', '.join(fields.TPMVersion.ALL)}
)
model = _get_unique_flavor_image_meta('tpm_model', flavor, image_meta)
if model is None:
# model defaults to TIS
model = fields.TPMModel.TIS
elif model not in fields.TPMModel.ALL:
raise exception.Invalid(
"Invalid TPM model %(model)r. Allowed values: %(valid)s." %
{'model': model, 'valid': ', '.join(fields.TPMModel.ALL)}
)
elif model == fields.TPMModel.CRB and version != fields.TPMVersion.v2_0:
raise exception.Invalid(
"TPM model CRB is only valid with TPM version 2.0."
)
return VTPMConfig(version, model)
def numa_get_constraints(flavor, image_meta): def numa_get_constraints(flavor, image_meta):
"""Return topology related to input request. """Return topology related to input request.