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:
parent
5158ca7dcf
commit
29dab997b4
@ -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(
|
||||
|
@ -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')
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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':
|
||||
|
@ -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.
|
Loading…
x
Reference in New Issue
Block a user