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. Change-Id: I1ea96930018d997820df2b7b4640fe1f241ee8d6 Implements: blueprint hyper-v-uefi-secureboot
This commit is contained in:
parent
68222bcc21
commit
b39231574a
@ -132,3 +132,9 @@ JOB_STATE_COMPLETED = 7
|
||||
JOB_STATE_TERMINATED = 8
|
||||
JOB_STATE_KILLED = 9
|
||||
JOB_STATE_COMPLETED_WITH_WARNINGS = 32768
|
||||
|
||||
IMAGE_PROP_SECURE_BOOT = "os_secure_boot"
|
||||
FLAVOR_SPEC_SECURE_BOOT = "os:secure_boot"
|
||||
REQUIRED = "required"
|
||||
DISABLED = "disabled"
|
||||
OPTIONAL = "optional"
|
||||
|
@ -40,7 +40,8 @@ class_utils = {
|
||||
'max_version': None}},
|
||||
'pathutils': {'PathUtils': {'min_version': 6.0, 'max_version': None}},
|
||||
'vmutils': {'VMUtils': {'min_version': 6.0, 'max_version': 6.2},
|
||||
'VMUtilsV2': {'min_version': 6.2, 'max_version': 10}},
|
||||
'VMUtilsV2': {'min_version': 6.2, 'max_version': 10},
|
||||
'VMUtils10': {'min_version': 10, 'max_version': None}},
|
||||
'vhdutils': {'VHDUtils': {'min_version': 6.0, 'max_version': 6.2},
|
||||
'VHDUtilsV2': {'min_version': 6.2, 'max_version': None}},
|
||||
'volumeutils': {'VolumeUtils': {'min_version': 6.0,
|
||||
|
@ -292,6 +292,36 @@ class VMOps(object):
|
||||
with excutils.save_and_reraise_exception():
|
||||
self.destroy(instance)
|
||||
|
||||
def _requires_certificate(self, image_meta):
|
||||
os_type = image_meta.get('properties', {}).get('os_type', None)
|
||||
if not os_type:
|
||||
raise vmutils.HyperVException(
|
||||
_('For secure boot, os_type must be specified in image '
|
||||
'properties.'))
|
||||
elif os_type == 'windows':
|
||||
return False
|
||||
return True
|
||||
|
||||
# Secure Boot feature will be enabled by setting the "os_secure_boot"
|
||||
# image property or the "os:secure_boot" flavor extra spec to required.
|
||||
# The flavor extra spec value overrides the image property value.
|
||||
def _requires_secure_boot(self, instance, image_meta, vm_gen):
|
||||
flavor = instance.flavor
|
||||
flavor_secure_boot = flavor.extra_specs.get(
|
||||
constants.FLAVOR_SPEC_SECURE_BOOT, None)
|
||||
|
||||
image_props = image_meta['properties']
|
||||
image_prop_secure_boot = image_props.get(
|
||||
constants.IMAGE_PROP_SECURE_BOOT, None)
|
||||
|
||||
if flavor_secure_boot in (constants.REQUIRED, constants.DISABLED):
|
||||
requires_secure_boot = constants.REQUIRED == flavor_secure_boot
|
||||
else:
|
||||
requires_secure_boot = image_prop_secure_boot == constants.REQUIRED
|
||||
if vm_gen != constants.VM_GEN_2 and requires_secure_boot:
|
||||
raise vmutils.HyperVException(_('Secure boot requires gen 2 VM.'))
|
||||
return requires_secure_boot
|
||||
|
||||
def create_instance(self, instance, network_info, block_device_info,
|
||||
root_vhd_path, eph_vhd_path, vm_gen, image_meta):
|
||||
instance_name = instance.name
|
||||
@ -351,6 +381,12 @@ class VMOps(object):
|
||||
|
||||
if CONF.hyperv.enable_instance_metrics_collection:
|
||||
self._vmutils.enable_vm_metrics_collection(instance_name)
|
||||
secure_boot_enabled = self._requires_secure_boot(
|
||||
instance, image_meta, vm_gen)
|
||||
if secure_boot_enabled:
|
||||
certificate_required = self._requires_certificate(image_meta)
|
||||
self._vmutils.enable_secure_boot(instance.name,
|
||||
certificate_required)
|
||||
|
||||
def _attach_drive(self, instance_name, path, drive_addr, ctrl_disk_addr,
|
||||
controller_type, drive_type=constants.DISK):
|
||||
|
28
hyperv/nova/vmutils10.py
Normal file
28
hyperv/nova/vmutils10.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Copyright 2015 Cloudbase Solutions Srl
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from hyperv.nova import vmutilsv2
|
||||
|
||||
|
||||
class VMUtils10(vmutilsv2.VMUtilsV2):
|
||||
|
||||
_UEFI_CERTIFICATE_AUTH = 'MicrosoftUEFICertificateAuthority'
|
||||
|
||||
def _set_secure_boot(self, vs_data, certificate_required):
|
||||
vs_data.SecureBootEnabled = True
|
||||
if certificate_required:
|
||||
uefi_data = self._conn.Msvm_VirtualSystemSettingData(
|
||||
ElementName=self._UEFI_CERTIFICATE_AUTH)[0]
|
||||
vs_data.SecureBootTemplateId = uefi_data.SecureBootTemplateId
|
@ -426,3 +426,22 @@ class VMUtilsV2(vmutils.VMUtils):
|
||||
return
|
||||
# VMUtilsV2._modify_virt_resource does not require the vm path.
|
||||
self._modify_virt_resource(disk_resource, None)
|
||||
|
||||
def enable_secure_boot(self, vm_name, certificate_required):
|
||||
vm = self._lookup_vm_check(vm_name)
|
||||
vs_data = self._get_vm_setting_data(vm)
|
||||
self._set_secure_boot(vs_data, certificate_required)
|
||||
vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0]
|
||||
|
||||
self._modify_virtual_system(vs_man_svc, vm.path_(), vs_data)
|
||||
|
||||
def _set_secure_boot(self, vs_data, certificate_required):
|
||||
vs_data.SecureBootEnabled = True
|
||||
if certificate_required:
|
||||
raise vmutils.HyperVException(
|
||||
_('UEFI SecureBoot is supported only on Windows instances.'))
|
||||
|
||||
def _modify_virtual_system(self, vs_man_svc, vm_path, vmsetting):
|
||||
(job_path, ret_val) = vs_man_svc.ModifySystemSettings(
|
||||
SystemSettings=vmsetting.GetText_(1))
|
||||
self.check_ret_val(ret_val, job_path)
|
||||
|
@ -392,6 +392,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('hyperv.nova.vif.get_vif_driver')
|
||||
@mock.patch.object(vmops.VMOps, '_set_instance_disk_qos_specs')
|
||||
@mock.patch.object(volumeops.VolumeOps, 'attach_volumes')
|
||||
@ -402,10 +404,11 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
||||
def _test_create_instance(self, mock_configure_remotefx, mock_create_pipes,
|
||||
mock_get_port_settings, mock_attach_drive,
|
||||
mock_attach_volumes, mock_set_qos_specs,
|
||||
mock_get_vif_driver,
|
||||
fake_root_path, fake_ephemeral_path,
|
||||
enable_instance_metrics,
|
||||
vm_gen=constants.VM_GEN_1, remotefx=False):
|
||||
mock_get_vif_driver, mock_requires_certificate,
|
||||
mock_requires_secure_boot, fake_root_path,
|
||||
fake_ephemeral_path, enable_instance_metrics,
|
||||
vm_gen=constants.VM_GEN_2,
|
||||
requires_sec_boot=True, remotefx=False):
|
||||
mock_vif_driver = mock_get_vif_driver()
|
||||
self.flags(enable_instance_metrics_collection=enable_instance_metrics,
|
||||
group='hyperv')
|
||||
@ -413,6 +416,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 = requires_sec_boot
|
||||
flavor = flavor_obj.Flavor(**test_flavor.fake_flavor)
|
||||
if remotefx is True:
|
||||
flavor.extra_specs['hyperv:remotefx'] = "1920x1200,2"
|
||||
@ -482,6 +486,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)
|
||||
if requires_sec_boot:
|
||||
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, mock_requires_certificate.return_value)
|
||||
|
||||
def test_create_instance(self):
|
||||
fake_ephemeral_path = mock.sentinel.FAKE_EPHEMERAL_PATH
|
||||
@ -489,6 +501,16 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
||||
fake_ephemeral_path=fake_ephemeral_path,
|
||||
enable_instance_metrics=True)
|
||||
|
||||
def test_create_instance_exception(self):
|
||||
# Secure Boot requires Generation 2 VMs. If boot is required while the
|
||||
# vm_gen is 1, exception is raised.
|
||||
|
||||
fake_ephemeral_path = mock.sentinel.FAKE_EPHEMERAL_PATH
|
||||
self._test_create_instance(fake_root_path=mock.sentinel.FAKE_ROOT_PATH,
|
||||
fake_ephemeral_path=fake_ephemeral_path,
|
||||
enable_instance_metrics=True,
|
||||
vm_gen=constants.VM_GEN_1)
|
||||
|
||||
def test_create_instance_no_root_path(self):
|
||||
fake_ephemeral_path = mock.sentinel.FAKE_EPHEMERAL_PATH
|
||||
self._test_create_instance(fake_root_path=None,
|
||||
@ -514,21 +536,18 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
||||
def test_create_instance_gen2(self):
|
||||
self._test_create_instance(fake_root_path=None,
|
||||
fake_ephemeral_path=None,
|
||||
enable_instance_metrics=False,
|
||||
vm_gen=constants.VM_GEN_2)
|
||||
enable_instance_metrics=False)
|
||||
|
||||
def test_create_instance_with_remote_fx(self):
|
||||
self._test_create_instance(fake_root_path=None,
|
||||
fake_ephemeral_path=None,
|
||||
enable_instance_metrics=False,
|
||||
vm_gen=constants.VM_GEN_1,
|
||||
remotefx=True)
|
||||
|
||||
def test_create_instance_with_remote_fx_gen2(self):
|
||||
self._test_create_instance(fake_root_path=None,
|
||||
fake_ephemeral_path=None,
|
||||
enable_instance_metrics=False,
|
||||
vm_gen=constants.VM_GEN_2,
|
||||
remotefx=True)
|
||||
|
||||
def test_attach_drive_vm_to_scsi(self):
|
||||
@ -1455,3 +1474,66 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
|
||||
|
||||
self.assertEqual(mock_parse_specs.return_value, ret_val)
|
||||
mock_parse_specs.assert_called_once_with(expected_qos_specs_dict)
|
||||
|
||||
def _test_requires_secure_boot(self, flavor_secure_boot,
|
||||
image_prop_secure_boot,
|
||||
fake_vm_gen=constants.VM_GEN_2):
|
||||
mock_instance = mock.MagicMock()
|
||||
flavor_secure_boot = {
|
||||
'extra_specs': {'os:secure_boot': flavor_secure_boot}}
|
||||
mock_image_meta = {'properties':
|
||||
{'os_secure_boot': image_prop_secure_boot}}
|
||||
|
||||
if flavor_secure_boot in ('required', 'disabled'):
|
||||
expected_result = constants.REQUIRED == flavor_secure_boot
|
||||
else:
|
||||
expected_result = image_prop_secure_boot == 'required'
|
||||
if fake_vm_gen != constants.VM_GEN_2 and expected_result:
|
||||
self.assertRaises(vmutils.HyperVException,
|
||||
self._vmops._requires_secure_boot,
|
||||
mock_instance, mock_image_meta)
|
||||
else:
|
||||
result = self._vmops._requires_secure_boot(mock_instance,
|
||||
mock_image_meta,
|
||||
fake_vm_gen)
|
||||
self.assertEqual(expected_result, result)
|
||||
|
||||
def test_requires_secure_boot_disabled(self):
|
||||
self._test_requires_secure_boot(
|
||||
flavor_secure_boot=constants.DISABLED,
|
||||
image_prop_secure_boot=constants.REQUIRED)
|
||||
|
||||
def test_requires_secure_boot_optional(self):
|
||||
self._test_requires_secure_boot(
|
||||
flavor_secure_boot=constants.OPTIONAL,
|
||||
image_prop_secure_boot=constants.OPTIONAL)
|
||||
|
||||
def test_requires_secure_boot_required(self):
|
||||
self._test_requires_secure_boot(
|
||||
flavor_secure_boot=constants.REQUIRED,
|
||||
image_prop_secure_boot=constants.OPTIONAL)
|
||||
|
||||
def test_requires_secure_boot_bad_vm_gen(self):
|
||||
self._test_requires_secure_boot(
|
||||
flavor_secure_boot=constants.REQUIRED,
|
||||
image_prop_secure_boot=constants.OPTIONAL,
|
||||
fake_vm_gen=constants.VM_GEN_1)
|
||||
|
||||
def _test_requires_certificate(self, os_type):
|
||||
image_meta = {'properties': {'os_type': os_type}}
|
||||
if not os_type:
|
||||
self.assertRaises(vmutils.HyperVException,
|
||||
self._vmops._requires_certificate, image_meta)
|
||||
else:
|
||||
expected_result = os_type == 'linux'
|
||||
result = self._vmops._requires_certificate(image_meta)
|
||||
self.assertEqual(expected_result, result)
|
||||
|
||||
def test_requires_certificate_windows(self):
|
||||
self._test_requires_certificate(os_type='windows')
|
||||
|
||||
def test_requires_certificate_linux(self):
|
||||
self._test_requires_certificate(os_type='linux')
|
||||
|
||||
def test_requires_certificate_os_type_none(self):
|
||||
self._test_requires_certificate(os_type=None)
|
||||
|
41
hyperv/tests/unit/test_vmutils10.py
Normal file
41
hyperv/tests/unit/test_vmutils10.py
Normal file
@ -0,0 +1,41 @@
|
||||
# Copyright 2015 Cloudbase Solutions Srl
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
|
||||
from hyperv.nova import vmutils10
|
||||
from hyperv.tests.unit import test_vmutilsv2
|
||||
|
||||
|
||||
class VMUtils10TestCase(test_vmutilsv2.VMUtilsV2TestCase):
|
||||
"""Unit tests for the Hyper-V VMUtils10 class."""
|
||||
|
||||
def setUp(self):
|
||||
super(VMUtils10TestCase, self).setUp()
|
||||
self._vmutils = vmutils10.VMUtils10()
|
||||
self._vmutils._conn = mock.MagicMock()
|
||||
|
||||
def test_set_secure_boot_certificate_required(self):
|
||||
vs_data = mock.MagicMock()
|
||||
mock_vssd = self._vmutils._conn.Msvm_VirtualSystemSettingData
|
||||
mock_vssd.return_value = [
|
||||
mock.MagicMock(SecureBootTemplateId=mock.sentinel.template_id)]
|
||||
|
||||
self._vmutils._set_secure_boot(vs_data, certificate_required=True)
|
||||
|
||||
self.assertTrue(vs_data.SecureBootEnabled)
|
||||
self.assertEqual(mock.sentinel.template_id,
|
||||
vs_data.SecureBootTemplateId)
|
||||
mock_vssd.assert_called_once_with(
|
||||
ElementName=self._vmutils._UEFI_CERTIFICATE_AUTH)
|
@ -393,3 +393,50 @@ class VMUtilsV2TestCase(test_vmutils.VMUtilsTestCase):
|
||||
|
||||
def test_set_disk_qos_specs_unsupported_feature(self):
|
||||
self._test_set_disk_qos_specs(qos_available=False)
|
||||
|
||||
@mock.patch.object(vmutils.VMUtils, 'check_ret_val')
|
||||
def test_modify_virtual_system(self, mock_check_ret_val):
|
||||
mock_vs_man_svc = mock.MagicMock()
|
||||
mock_vmsettings = mock.MagicMock()
|
||||
mock_vs_man_svc.ModifySystemSettings.return_value = (
|
||||
mock.sentinel.fake_job_path, mock.sentinel.fake_ret_val)
|
||||
self._vmutils._modify_virtual_system(vs_man_svc=mock_vs_man_svc,
|
||||
vm_path=None,
|
||||
vmsetting=mock_vmsettings)
|
||||
mock_vs_man_svc.ModifySystemSettings.assert_called_once_with(
|
||||
SystemSettings=mock_vmsettings.GetText_.return_value)
|
||||
mock_check_ret_val.assert_called_once_with(mock.sentinel.fake_ret_val,
|
||||
mock.sentinel.fake_job_path)
|
||||
|
||||
def test_set_secure_boot(self):
|
||||
vs_data = mock.MagicMock()
|
||||
self._vmutils._set_secure_boot(vs_data, certificate_required=False)
|
||||
|
||||
self.assertTrue(vs_data.SecureBootEnabled)
|
||||
|
||||
def test_set_secure_boot_certificate_required(self):
|
||||
self.assertRaises(vmutils.HyperVException,
|
||||
self._vmutils._set_secure_boot,
|
||||
mock.MagicMock(), True)
|
||||
|
||||
@mock.patch.object(vmutilsv2.VMUtilsV2, '_modify_virtual_system')
|
||||
@mock.patch.object(vmutilsv2.VMUtilsV2, '_get_vm_setting_data')
|
||||
@mock.patch.object(vmutils.VMUtils, '_lookup_vm_check')
|
||||
def test_enable_secure_boot(self, mock_lookup_vm_check,
|
||||
mock_get_vm_setting_data,
|
||||
mock_modify_virtual_system):
|
||||
vm = mock_lookup_vm_check.return_value
|
||||
vs_data = mock_get_vm_setting_data.return_value
|
||||
vs_svc = self._vmutils._conn.Msvm_VirtualSystemManagementService()[0]
|
||||
|
||||
with mock.patch.object(self._vmutils,
|
||||
'_set_secure_boot') as mock_set_secure_boot:
|
||||
self._vmutils.enable_secure_boot(
|
||||
mock.sentinel.VM_NAME, mock.sentinel.certificate_required)
|
||||
|
||||
mock_lookup_vm_check.assert_called_with(mock.sentinel.VM_NAME)
|
||||
mock_get_vm_setting_data.assert_called_once_with(vm)
|
||||
mock_set_secure_boot.assert_called_once_with(
|
||||
vs_data, mock.sentinel.certificate_required)
|
||||
mock_modify_virtual_system.assert_called_once_with(
|
||||
vs_svc, vm.path_(), vs_data)
|
||||
|
Loading…
Reference in New Issue
Block a user