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 <cbelu@cloudbasesolutions.com>

Implements: blueprint hyper-v-uefi-secureboot

Change-Id: I1ea96930018d997820df2b7b4640fe1f241ee8d6
This commit is contained in:
Matt Riedemann 2016-09-26 20:14:43 -04:00
parent 5158ca7dcf
commit 29dab997b4
6 changed files with 184 additions and 10 deletions

View File

@ -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(

View File

@ -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')

View File

@ -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"

View File

@ -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)

View File

@ -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':

View File

@ -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.