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:
Simona Iuliana Toader 2015-08-25 18:25:53 +03:00 committed by Claudiu Belu
parent 68222bcc21
commit b39231574a
8 changed files with 269 additions and 9 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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