From 29dab997b4e7039cbf036edb5db35b6d18e6b6ca Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Mon, 26 Sep 2016 20:14:43 -0400 Subject: [PATCH] Hyper-V: Adds Hyper-V UEFI Secure Boot Hyper-V supports UEFI SecureBoot since the 2012 R2 version for Windows guests and this has been extended to Linux guests as well with the upcoming release. This blueprint implements UEFI SecureBoot for Linux guests. DocImpact: The nova flavor extra specs docs needs to be updated to include 'os:secure_boot' and its possible values. The image metadata property docs needs to be updated to include "os_secure_boot" property and its possible values. Co-Authored-By: Claudiu Belu Implements: blueprint hyper-v-uefi-secureboot Change-Id: I1ea96930018d997820df2b7b4640fe1f241ee8d6 --- .../unit/virt/hyperv/test_migrationops.py | 6 +- nova/tests/unit/virt/hyperv/test_vmops.py | 95 ++++++++++++++++++- nova/virt/hyperv/constants.py | 1 + nova/virt/hyperv/migrationops.py | 4 +- nova/virt/hyperv/vmops.py | 68 ++++++++++++- ...erv-uefi-secure-boot-a2a617ac2c313afd.yaml | 20 ++++ 6 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/hyperv-uefi-secure-boot-a2a617ac2c313afd.yaml diff --git a/nova/tests/unit/virt/hyperv/test_migrationops.py b/nova/tests/unit/virt/hyperv/test_migrationops.py index ca90879b9f75..0973e509c990 100644 --- a/nova/tests/unit/virt/hyperv/test_migrationops.py +++ b/nova/tests/unit/virt/hyperv/test_migrationops.py @@ -272,7 +272,8 @@ class MigrationOpsTestCase(test_base.HyperVBaseTestCase): mock_instance.uuid, test.MatchType(objects.ImageMeta)) self._migrationops._vmops.create_instance.assert_called_once_with( mock_instance, mock.sentinel.network_info, root_device, - block_device_info, get_image_vm_gen.return_value) + block_device_info, get_image_vm_gen.return_value, + mock_image.return_value) mock_check_attach_config_drive.assert_called_once_with( mock_instance, get_image_vm_gen.return_value) self._migrationops._vmops.power_on.assert_called_once_with( @@ -433,7 +434,8 @@ class MigrationOpsTestCase(test_base.HyperVBaseTestCase): mock.sentinel.image_meta) self._migrationops._vmops.create_instance.assert_called_once_with( mock_instance, mock.sentinel.network_info, root_device, - block_device_info, get_image_vm_gen.return_value) + block_device_info, get_image_vm_gen.return_value, + mock.sentinel.image_meta) mock_check_attach_config_drive.assert_called_once_with( mock_instance, get_image_vm_gen.return_value) self._migrationops._vmops.power_on.assert_called_once_with( diff --git a/nova/tests/unit/virt/hyperv/test_vmops.py b/nova/tests/unit/virt/hyperv/test_vmops.py index cb9bec037053..893c847fc6d0 100644 --- a/nova/tests/unit/virt/hyperv/test_vmops.py +++ b/nova/tests/unit/virt/hyperv/test_vmops.py @@ -26,6 +26,7 @@ from oslo_utils import units from nova.compute import vm_states from nova import exception from nova import objects +from nova.objects import fields from nova.objects import flavor as flavor_obj from nova.tests.unit import fake_instance from nova.tests.unit.objects import test_flavor @@ -395,11 +396,12 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): mock_configdrive_required, mock_create_config_drive, mock_attach_config_drive, mock_power_on, mock_destroy, exists, - configdrive_required, fail): + configdrive_required, fail, + fake_vm_gen=constants.VM_GEN_2): mock_instance = fake_instance.fake_instance_obj(self.context) mock_image_meta = mock.MagicMock() root_device_info = mock.sentinel.ROOT_DEV_INFO - fake_vm_gen = mock_get_image_vm_gen.return_value + mock_get_image_vm_gen.return_value = fake_vm_gen fake_config_drive_path = mock_create_config_drive.return_value block_device_info = {'ephemerals': [], 'root_disk': root_device_info} @@ -439,7 +441,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): mock_image_meta) mock_create_instance.assert_called_once_with( mock_instance, mock.sentinel.INFO, root_device_info, - block_device_info, fake_vm_gen) + block_device_info, fake_vm_gen, mock_image_meta) mock_save_device_metadata.assert_called_once_with( self.context, mock_instance, block_device_info) mock_configdrive_required.assert_called_once_with(mock_instance) @@ -474,6 +476,8 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): [mock.sentinel.FILE], mock.sentinel.PASSWORD, mock.sentinel.INFO, mock.sentinel.DEV_INFO) + @mock.patch.object(vmops.VMOps, '_requires_secure_boot') + @mock.patch.object(vmops.VMOps, '_requires_certificate') @mock.patch('nova.virt.hyperv.volumeops.VolumeOps' '.attach_volumes') @mock.patch.object(vmops.VMOps, '_set_instance_disk_qos_specs') @@ -487,6 +491,8 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): mock_create_pipes, mock_set_qos_specs, mock_attach_volumes, + mock_requires_certificate, + mock_requires_secure_boot, enable_instance_metrics, vm_gen=constants.VM_GEN_1): mock_vif_driver = mock.MagicMock() @@ -499,6 +505,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): 'address': mock.sentinel.ADDRESS} mock_instance = fake_instance.fake_instance_obj(self.context) instance_path = os.path.join(CONF.instances_path, mock_instance.name) + mock_requires_secure_boot.return_value = True flavor = flavor_obj.Flavor(**test_flavor.fake_flavor) mock_instance.flavor = flavor @@ -507,7 +514,8 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): network_info=[fake_network_info], root_device=root_device_info, block_device_info=block_device_info, - vm_gen=vm_gen) + vm_gen=vm_gen, + image_meta=mock.sentinel.image_meta) self._vmops._vmutils.create_vm.assert_called_once_with( mock_instance.name, mock_instance.flavor.memory_mb, mock_instance.flavor.vcpus, CONF.hyperv.limit_cpu_features, @@ -533,6 +541,14 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): if enable_instance_metrics: mock_enable.assert_called_once_with(mock_instance.name) mock_set_qos_specs.assert_called_once_with(mock_instance) + mock_requires_secure_boot.assert_called_once_with( + mock_instance, mock.sentinel.image_meta, vm_gen) + mock_requires_certificate.assert_called_once_with( + mock.sentinel.image_meta) + enable_secure_boot = self._vmops._vmutils.enable_secure_boot + enable_secure_boot.assert_called_once_with( + mock_instance.name, + msft_ca_required=mock_requires_certificate.return_value) def test_create_instance(self): self._test_create_instance(enable_instance_metrics=True) @@ -655,6 +671,77 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): mock.sentinel.instance_id, constants.VM_GEN_2, mock.sentinel.FAKE_PATH) + def _check_requires_certificate(self, os_type): + mock_image_meta = mock.MagicMock() + mock_image_meta.properties = {'os_type': os_type} + + expected_result = os_type == fields.OSType.LINUX + result = self._vmops._requires_certificate(mock_image_meta) + self.assertEqual(expected_result, result) + + def test_requires_certificate_windows(self): + self._check_requires_certificate(os_type=fields.OSType.WINDOWS) + + def test_requires_certificate_linux(self): + self._check_requires_certificate(os_type=fields.OSType.LINUX) + + def _check_requires_secure_boot( + self, image_prop_os_type=fields.OSType.LINUX, + image_prop_secure_boot=fields.SecureBoot.REQUIRED, + flavor_secure_boot=fields.SecureBoot.REQUIRED, + vm_gen=constants.VM_GEN_2, expected_exception=True): + mock_instance = fake_instance.fake_instance_obj(self.context) + if flavor_secure_boot: + mock_instance.flavor.extra_specs = { + constants.FLAVOR_SPEC_SECURE_BOOT: flavor_secure_boot} + mock_image_meta = mock.MagicMock() + mock_image_meta.properties = {'os_type': image_prop_os_type} + if image_prop_secure_boot: + mock_image_meta.properties['os_secure_boot'] = ( + image_prop_secure_boot) + + if expected_exception: + self.assertRaises(exception.InstanceUnacceptable, + self._vmops._requires_secure_boot, + mock_instance, mock_image_meta, vm_gen) + else: + result = self._vmops._requires_secure_boot(mock_instance, + mock_image_meta, + vm_gen) + + requires_sb = fields.SecureBoot.REQUIRED in [ + flavor_secure_boot, image_prop_secure_boot] + self.assertEqual(requires_sb, result) + + def test_requires_secure_boot_ok(self): + self._check_requires_secure_boot( + expected_exception=False) + + def test_requires_secure_boot_image_img_prop_none(self): + self._check_requires_secure_boot( + image_prop_secure_boot=None, + expected_exception=False) + + def test_requires_secure_boot_image_extra_spec_none(self): + self._check_requires_secure_boot( + flavor_secure_boot=None, + expected_exception=False) + + def test_requires_secure_boot_flavor_no_os_type(self): + self._check_requires_secure_boot( + image_prop_os_type=None) + + def test_requires_secure_boot_flavor_disabled(self): + self._check_requires_secure_boot( + flavor_secure_boot=fields.SecureBoot.DISABLED) + + def test_requires_secure_boot_image_disabled(self): + self._check_requires_secure_boot( + image_prop_secure_boot=fields.SecureBoot.DISABLED) + + def test_requires_secure_boot_generation_1(self): + self._check_requires_secure_boot(vm_gen=constants.VM_GEN_1) + @mock.patch('nova.api.metadata.base.InstanceMetadata') @mock.patch('nova.virt.configdrive.ConfigDriveBuilder') @mock.patch('nova.utils.execute') diff --git a/nova/virt/hyperv/constants.py b/nova/virt/hyperv/constants.py index c636fafe37a6..e97167945073 100644 --- a/nova/virt/hyperv/constants.py +++ b/nova/virt/hyperv/constants.py @@ -65,6 +65,7 @@ HOST_POWER_ACTION_SHUTDOWN = "shutdown" HOST_POWER_ACTION_REBOOT = "reboot" HOST_POWER_ACTION_STARTUP = "startup" +FLAVOR_SPEC_SECURE_BOOT = "os:secure_boot" IMAGE_PROP_VM_GEN_1 = "hyperv-gen1" IMAGE_PROP_VM_GEN_2 = "hyperv-gen2" diff --git a/nova/virt/hyperv/migrationops.py b/nova/virt/hyperv/migrationops.py index 50fcca828b0a..3445b712e0be 100644 --- a/nova/virt/hyperv/migrationops.py +++ b/nova/virt/hyperv/migrationops.py @@ -184,7 +184,7 @@ class MigrationOps(object): self._check_ephemeral_disks(instance, ephemerals) self._vmops.create_instance(instance, network_info, root_device, - block_device_info, vm_gen) + block_device_info, vm_gen, image_meta) self._check_and_attach_config_drive(instance, vm_gen) @@ -293,7 +293,7 @@ class MigrationOps(object): self._check_ephemeral_disks(instance, ephemerals, resize_instance) self._vmops.create_instance(instance, network_info, root_device, - block_device_info, vm_gen) + block_device_info, vm_gen, image_meta) self._check_and_attach_config_drive(instance, vm_gen) diff --git a/nova/virt/hyperv/vmops.py b/nova/virt/hyperv/vmops.py index f1fc289b70ce..71f31e70a72e 100644 --- a/nova/virt/hyperv/vmops.py +++ b/nova/virt/hyperv/vmops.py @@ -40,6 +40,7 @@ import nova.conf from nova import exception from nova.i18n import _, _LI, _LE, _LW from nova import objects +from nova.objects import fields from nova import utils from nova.virt import configdrive from nova.virt import hardware @@ -291,7 +292,7 @@ class VMOps(object): try: self.create_instance(instance, network_info, root_device, - block_device_info, vm_gen) + block_device_info, vm_gen, image_meta) self._save_device_metadata(context, instance, block_device_info) if configdrive.required_by(instance): @@ -309,9 +310,11 @@ class VMOps(object): self.destroy(instance) def create_instance(self, instance, network_info, root_device, - block_device_info, vm_gen): + block_device_info, vm_gen, image_meta): instance_name = instance.name instance_path = os.path.join(CONF.instances_path, instance_name) + secure_boot_enabled = self._requires_secure_boot(instance, image_meta, + vm_gen) self._vmutils.create_vm(instance_name, instance.flavor.memory_mb, @@ -352,6 +355,11 @@ class VMOps(object): self._set_instance_disk_qos_specs(instance) + if secure_boot_enabled: + certificate_required = self._requires_certificate(image_meta) + self._vmutils.enable_secure_boot( + instance.name, msft_ca_required=certificate_required) + def _configure_remotefx(self, instance, vm_gen): extra_specs = instance.flavor.extra_specs remotefx_max_resolution = extra_specs.get( @@ -443,6 +451,62 @@ class VMOps(object): raise exception.InstanceUnacceptable(instance_id=instance_id, reason=reason) + def _requires_certificate(self, image_meta): + os_type = image_meta.properties.get('os_type') + if os_type == fields.OSType.WINDOWS: + return False + return True + + def _requires_secure_boot(self, instance, image_meta, vm_gen): + """Checks whether the given instance requires Secure Boot. + + Secure Boot feature will be enabled by setting the "os_secure_boot" + image property or the "os:secure_boot" flavor extra spec to required. + + :raises exception.InstanceUnacceptable: if the given image_meta has + no os_type property set, or if the image property value and the + flavor extra spec value are conflicting, or if Secure Boot is + required, but the instance's VM generation is 1. + """ + os_type = image_meta.properties.get('os_type') + if not os_type: + reason = _('For secure boot, os_type must be specified in image ' + 'properties.') + raise exception.InstanceUnacceptable(instance_id=instance.uuid, + reason=reason) + + img_secure_boot = image_meta.properties.get('os_secure_boot') + flavor_secure_boot = instance.flavor.extra_specs.get( + constants.FLAVOR_SPEC_SECURE_BOOT) + + requires_sb = False + conflicting_values = False + + if flavor_secure_boot == fields.SecureBoot.REQUIRED: + requires_sb = True + if img_secure_boot == fields.SecureBoot.DISABLED: + conflicting_values = True + elif img_secure_boot == fields.SecureBoot.REQUIRED: + requires_sb = True + if flavor_secure_boot == fields.SecureBoot.DISABLED: + conflicting_values = True + + if conflicting_values: + reason = _( + "Conflicting image metadata property and flavor extra_specs " + "values: os_secure_boot (%(image_secure_boot)s) / " + "os:secure_boot (%(flavor_secure_boot)s)") % { + 'image_secure_boot': img_secure_boot, + 'flavor_secure_boot': flavor_secure_boot} + raise exception.InstanceUnacceptable(instance_id=instance.uuid, + reason=reason) + + if vm_gen != constants.VM_GEN_2 and requires_sb: + reason = _('Secure boot requires generation 2 VM.') + raise exception.InstanceUnacceptable(instance_id=instance.uuid, + reason=reason) + return requires_sb + def _create_config_drive(self, context, instance, injected_files, admin_password, network_info, rescue=False): if CONF.config_drive_format != 'iso9660': diff --git a/releasenotes/notes/hyperv-uefi-secure-boot-a2a617ac2c313afd.yaml b/releasenotes/notes/hyperv-uefi-secure-boot-a2a617ac2c313afd.yaml new file mode 100644 index 000000000000..72f738080989 --- /dev/null +++ b/releasenotes/notes/hyperv-uefi-secure-boot-a2a617ac2c313afd.yaml @@ -0,0 +1,20 @@ +--- +features: + - | + Added support for Hyper-V VMs with UEFI Secure Boot enabled. + In order to create such VMs, there are a couple of things to consider: + + * Images should be prepared for Generation 2 VMs. The image property + "hw_machine_type=hyperv-gen2" is mandatory. + * The guest OS type must be specified in order to properly spawn the VMs. + It can be specifed through the image property "os_type", and the + acceptable values are "windows" or "linux". + * The UEFI Secure Boot feature can be requested through the image property + "os_secure_boot" (acceptable values: "disabled", "optional", "required") + or flavor extra spec "os:secure_boot" (acceptable values: "disabled", + "required"). The flavor extra spec will take precedence. If the image + property and the flavor extra spec values are conflicting, then an + exception is raised. + * This feature is supported on Windows / Hyper-V Server 2012 R2 for + Windows guests, and Windows / Hyper-V Server 2016 for both + Windows and Linux guests.