Merge "Hyper-V: Adds Hyper-V UEFI Secure Boot"
This commit is contained in:
commit
de82d98333
|
@ -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…
Reference in New Issue