From 5550f866237157e82cad8c6146b486c62f35a0d8 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 16 Jul 2020 17:54:44 +0100 Subject: [PATCH] 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 --- .../ImageMetaPropsPayload.json | 2 +- nova/api/validation/extra_specs/hw.py | 29 +++++++ nova/notifications/objects/image.py | 3 +- nova/objects/fields.py | 23 ++++++ nova/objects/image_meta.py | 11 ++- nova/scheduler/utils.py | 16 ++++ .../test_instance.py | 6 +- .../objects/test_notification.py | 2 +- nova/tests/unit/objects/test_image_meta.py | 13 ++++ nova/tests/unit/objects/test_objects.py | 2 +- nova/tests/unit/scheduler/test_utils.py | 50 ++++++++++++ nova/tests/unit/virt/test_hardware.py | 59 ++++++++++++++ nova/virt/hardware.py | 76 ++++++++++++++++++- 13 files changed, 282 insertions(+), 10 deletions(-) diff --git a/doc/notification_samples/common_payloads/ImageMetaPropsPayload.json b/doc/notification_samples/common_payloads/ImageMetaPropsPayload.json index 6cd0c651ac22..a03b4ddd82c8 100644 --- a/doc/notification_samples/common_payloads/ImageMetaPropsPayload.json +++ b/doc/notification_samples/common_payloads/ImageMetaPropsPayload.json @@ -4,5 +4,5 @@ "hw_architecture": "x86_64" }, "nova_object.name": "ImageMetaPropsPayload", - "nova_object.version": "1.4" + "nova_object.version": "1.5" } diff --git a/nova/api/validation/extra_specs/hw.py b/nova/api/validation/extra_specs/hw.py index fde3675db412..85450db62379 100644 --- a/nova/api/validation/extra_specs/hw.py +++ b/nova/api/validation/extra_specs/hw.py @@ -354,6 +354,35 @@ feature_flag_validators = [ '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( name='hw:watchdog_action', description=( diff --git a/nova/notifications/objects/image.py b/nova/notifications/objects/image.py index bf8bd88326bb..613003f446a0 100644 --- a/nova/notifications/objects/image.py +++ b/nova/notifications/objects/image.py @@ -121,7 +121,8 @@ class ImageMetaPropsPayload(base.NotificationPayloadBase): # 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.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 = { k: ('image_meta_props', k) for k in image_meta.ImageMetaProps.fields} diff --git a/nova/objects/fields.py b/nova/objects/fields.py index bee8767fd0b9..8891afb15352 100644 --- a/nova/objects/fields.py +++ b/nova/objects/fields.py @@ -510,6 +510,21 @@ class RNGModel(BaseNovaEnum): 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): BUSLOGIC = "buslogic" @@ -1230,6 +1245,14 @@ class RNGModelField(BaseEnumField): AUTO_TYPE = RNGModel() +class TPMModelField(BaseEnumField): + AUTO_TYPE = TPMModel() + + +class TPMVersionField(BaseEnumField): + AUTO_TYPE = TPMVersion() + + class SCSIModelField(BaseEnumField): AUTO_TYPE = SCSIModel() diff --git a/nova/objects/image_meta.py b/nova/objects/image_meta.py index 4435e24bd760..5ecee52c6169 100644 --- a/nova/objects/image_meta.py +++ b/nova/objects/image_meta.py @@ -176,14 +176,18 @@ class ImageMetaProps(base.NovaObject): # Version 1.24: Added 'hw_mem_encryption' field # Version 1.25: Added 'hw_pci_numa_affinity_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 # ImageMetaPropsPayload must also be bumped. See its docstring for details. - VERSION = '1.26' + VERSION = '1.27' def obj_make_compatible(self, primitive, target_version): super(ImageMetaProps, self).obj_make_compatible(primitive, 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): policy = primitive.get('hw_cpu_policy', None) if policy == fields.CPUAllocationPolicy.MIXED: @@ -403,6 +407,11 @@ class ImageMetaProps(base.NovaObject): # boolean - If true, this will enable the virtio-multiqueue feature '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 'img_bittorrent': fields.FlexibleBooleanField(), diff --git a/nova/scheduler/utils.py b/nova/scheduler/utils.py index ef3a6cc87fc9..8923ea33f3d5 100644 --- a/nova/scheduler/utils.py +++ b/nova/scheduler/utils.py @@ -167,6 +167,8 @@ class ResourceRequest(object): self._translate_vpmems_request(request_spec.flavor) + self._translate_vtpm_request(request_spec.flavor, image) + self.strip_zeros() def _process_requested_resources(self, request_spec): @@ -213,6 +215,20 @@ class ResourceRequest(object): # supported in image traits 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): """When the hw:mem_encryption extra spec or the hw_mem_encryption image property are requested, translate into a request for diff --git a/nova/tests/functional/notification_sample_tests/test_instance.py b/nova/tests/functional/notification_sample_tests/test_instance.py index 580cdc88f31e..594bf4ad42e1 100644 --- a/nova/tests/functional/notification_sample_tests/test_instance.py +++ b/nova/tests/functional/notification_sample_tests/test_instance.py @@ -1249,7 +1249,8 @@ class TestInstanceNotificationSample( 'nova_object.data': {}, 'nova_object.name': 'ImageMetaPropsPayload', 'nova_object.namespace': 'nova', - 'nova_object.version': u'1.4'}, + 'nova_object.version': u'1.5', + }, 'image.size': 58145823, 'image.tags': [], 'scheduler_hints': {'_nova_check_type': ['rebuild']}, @@ -1344,7 +1345,8 @@ class TestInstanceNotificationSample( 'nova_object.data': {}, 'nova_object.name': 'ImageMetaPropsPayload', 'nova_object.namespace': 'nova', - 'nova_object.version': u'1.4'}, + 'nova_object.version': u'1.5', + }, 'image.size': 58145823, 'image.tags': [], 'scheduler_hints': {'_nova_check_type': ['rebuild']}, diff --git a/nova/tests/unit/notifications/objects/test_notification.py b/nova/tests/unit/notifications/objects/test_notification.py index 277d9ffb4900..ea643977238b 100644 --- a/nova/tests/unit/notifications/objects/test_notification.py +++ b/nova/tests/unit/notifications/objects/test_notification.py @@ -387,7 +387,7 @@ notification_object_data = { # ImageMetaProps, so when you see a fail here for that reason, you must # *also* bump the version of ImageMetaPropsPayload. See its docstring for # more information. - 'ImageMetaPropsPayload': '1.4-036c794843b95a3a39ee70830f5f6557', + 'ImageMetaPropsPayload': '1.5-f4074a974d4a9f77e302a53ee9340287', 'InstanceActionNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'InstanceActionPayload': '1.8-4fa3da9cbf0761f1f700ae578f36dc2f', 'InstanceActionRebuildNotification': diff --git a/nova/tests/unit/objects/test_image_meta.py b/nova/tests/unit/objects/test_image_meta.py index b457bddd5348..4d85b3630943 100644 --- a/nova/tests/unit/objects/test_image_meta.py +++ b/nova/tests/unit/objects/test_image_meta.py @@ -411,3 +411,16 @@ class TestImageMetaProps(test.NoDBTestCase): old_primitive = obj.obj_to_primitive('1.22') self.assertIn('hw_pmu', 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']) diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index 74fcd8d2afe3..60eb3fc08a2d 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1077,7 +1077,7 @@ object_data = { 'HyperVLiveMigrateData': '1.4-e265780e6acfa631476c8170e8d6fce0', 'IDEDeviceBus': '1.0-29d4c9f27ac44197f01b6ac1b7e16502', 'ImageMeta': '1.8-642d1b2eb3e880a367f37d72dd76162d', - 'ImageMetaProps': '1.26-b9f136cd10a2b5ffb3ae44332f2f687d', + 'ImageMetaProps': '1.27-f3f17d5e35146a0dbb56420ffc4f3990', 'Instance': '2.7-d187aec68cad2e4d8b8a03a68e4739ce', 'InstanceAction': '1.2-9a5abc87fdd3af46f45731960651efb5', 'InstanceActionEvent': '1.4-5b1f361bd81989f8bb2c20bb7e8a4cb4', diff --git a/nova/tests/unit/scheduler/test_utils.py b/nova/tests/unit/scheduler/test_utils.py index e3a999332c89..036bb734e87b 100644 --- a/nova/tests/unit/scheduler/test_utils.py +++ b/nova/tests/unit/scheduler/test_utils.py @@ -1119,6 +1119,56 @@ class TestUtils(TestUtilsBase): rr = utils.ResourceRequest(rs) 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): flavor = objects.Flavor( vcpus=1, memory_mb=1024, root_gb=10, ephemeral_gb=5, swap=0) diff --git a/nova/tests/unit/virt/test_hardware.py b/nova/tests/unit/virt/test_hardware.py index 45d5ed13b34f..375a3415b757 100644 --- a/nova/tests/unit/virt/test_hardware.py +++ b/nova/tests/unit/virt/test_hardware.py @@ -4998,6 +4998,65 @@ class PCINUMAAffinityPolicyTest(test.NoDBTestCase): 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 class RescuePropertyTestCase(test.NoDBTestCase): diff --git a/nova/virt/hardware.py b/nova/virt/hardware.py index f4af6faae6fb..258ae9c2f91f 100644 --- a/nova/virt/hardware.py +++ b/nova/virt/hardware.py @@ -1171,10 +1171,39 @@ def _get_flavor_image_meta( flavor_key = ':'.join(['hw', key]) image_key = '_'.join(['hw', key]) - flavor_policy = flavor.get('extra_specs', {}).get(flavor_key, default) - image_policy = image_meta.properties.get(image_key, default) + flavor_value = flavor.get('extra_specs', {}).get(flavor_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( @@ -1814,6 +1843,47 @@ def get_pci_numa_policy_constraint(flavor, image_meta): 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): """Return topology related to input request.