Add a new boot section 'trusted_boot' for PXE

Add a new boot section 'trusted_boot' for PXE to support
trusted boot. With this, Ironic can boot nodes with trusted
boot to measure BIOS, Option Rom and Kernel/Ramdisk.

Implements blueprint bare-metal-trust-using-intel-txt
Change-Id: I223569d59882c6629bfdd3a87baad6b6c853538e
This commit is contained in:
Lin Tan 2015-06-12 14:00:37 +08:00
parent 8f966109ab
commit 9d29d0bc94
6 changed files with 250 additions and 25 deletions

View File

@ -72,9 +72,12 @@ LOG = logging.getLogger(__name__)
VALID_ROOT_DEVICE_HINTS = set(('size', 'model', 'wwn', 'serial', 'vendor'))
SUPPORTED_CAPABILITIES = {'boot_option': ('local', 'netboot'),
'boot_mode': ('bios', 'uefi'),
'secure_boot': ('true', 'false')}
SUPPORTED_CAPABILITIES = {
'boot_option': ('local', 'netboot'),
'boot_mode': ('bios', 'uefi'),
'secure_boot': ('true', 'false'),
'trusted_boot': ('true', 'false'),
}
# All functions are called from deploy() directly or indirectly.
@ -357,9 +360,12 @@ def _replace_root_uuid(path, root_uuid):
_replace_lines_in_file(path, pattern, root)
def _replace_boot_line(path, boot_mode, is_whole_disk_image):
def _replace_boot_line(path, boot_mode, is_whole_disk_image,
trusted_boot=False):
if is_whole_disk_image:
boot_disk_type = 'boot_whole_disk'
elif trusted_boot:
boot_disk_type = 'trusted_boot'
else:
boot_disk_type = 'boot_partition'
@ -380,7 +386,7 @@ def _replace_disk_identifier(path, disk_identifier):
def switch_pxe_config(path, root_uuid_or_disk_id, boot_mode,
is_whole_disk_image):
is_whole_disk_image, trusted_boot=False):
"""Switch a pxe config from deployment mode to service mode.
:param path: path to the pxe config file in tftpboot.
@ -388,13 +394,16 @@ def switch_pxe_config(path, root_uuid_or_disk_id, boot_mode,
disk_id in case of whole disk image.
:param boot_mode: if boot mode is uefi or bios.
:param is_whole_disk_image: if the image is a whole disk image or not.
:param trusted_boot: if boot with trusted_boot or not. The usage of
is_whole_disk_image and trusted_boot are mutually exclusive. You can
have one or neither, but not both.
"""
if not is_whole_disk_image:
_replace_root_uuid(path, root_uuid_or_disk_id)
else:
_replace_disk_identifier(path, root_uuid_or_disk_id)
_replace_boot_line(path, boot_mode, is_whole_disk_image)
_replace_boot_line(path, boot_mode, is_whole_disk_image, trusted_boot)
def notify(address, port):
@ -1057,12 +1066,30 @@ def is_secure_boot_requested(node):
return sec_boot == 'true'
def is_trusted_boot_requested(node):
"""Returns True if trusted_boot is requested for deploy.
This method checks instance property for trusted_boot and returns True
if it is requested.
:param node: a single Node.
:raises: InvalidParameterValue if the capabilities string is not a
dictionary or is malformed.
:returns: True if trusted_boot is requested.
"""
capabilities = parse_instance_info_capabilities(node)
trusted_boot = capabilities.get('trusted_boot', 'false').lower()
return trusted_boot == 'true'
def get_boot_mode_for_deploy(node):
"""Returns the boot mode that would be used for deploy.
This method returns boot mode to be used for deploy.
It returns 'uefi' if 'secure_boot' is set to 'true' in
'instance_info/capabilities' of node.
It returns 'uefi' if 'secure_boot' is set to 'true' or returns 'bios' if
'trusted_boot' is set to 'true' in 'instance_info/capabilities' of node.
Otherwise it returns value of 'boot_mode' in 'properties/capabilities'
of node if set. If that is not set, it returns boot mode in
'instance_info/deploy_boot_mode' for the node.
@ -1077,6 +1104,12 @@ def get_boot_mode_for_deploy(node):
LOG.debug('Deploy boot mode is uefi for %s.', node.uuid)
return 'uefi'
if is_trusted_boot_requested(node):
# TODO(lintan) Trusted boot also supports uefi, but at the moment,
# it should only boot with bios.
LOG.debug('Deploy boot mode is bios for %s.', node.uuid)
return 'bios'
boot_mode = driver_utils.get_node_capability(node, 'boot_mode')
if boot_mode is None:
instance_info = node.instance_info

View File

@ -227,6 +227,29 @@ def validate_boot_option_for_uefi(node):
{'node_uuid': node.uuid})
def validate_boot_parameters_for_trusted_boot(node):
"""Check if boot parameters are valid for trusted boot."""
boot_mode = deploy_utils.get_boot_mode_for_deploy(node)
boot_option = iscsi_deploy.get_boot_option(node)
is_whole_disk_image = node.driver_internal_info.get('is_whole_disk_image')
# 'is_whole_disk_image' is not supported by trusted boot, because there is
# no Kernel/Ramdisk to measure at all.
if (boot_mode != 'bios' or
is_whole_disk_image or
boot_option != 'netboot'):
msg = (_("Trusted boot is only supported in BIOS boot mode with "
"netboot and without whole_disk_image, but Node "
"%(node_uuid)s was configured with boot_mode: %(boot_mode)s, "
"boot_option: %(boot_option)s, is_whole_disk_image: "
"%(is_whole_disk_image)s: at least one of them is wrong, and "
"this can be caused by enable secure boot.") %
{'node_uuid': node.uuid, 'boot_mode': boot_mode,
'boot_option': boot_option,
'is_whole_disk_image': is_whole_disk_image})
LOG.error(msg)
raise exception.InvalidParameterValue(msg)
@image_cache.cleanup(priority=25)
class TFTPImageCache(image_cache.ImageCache):
def __init__(self, image_service=None):
@ -325,6 +348,11 @@ class PXEDeploy(base.DeployInterface):
if boot_mode == 'uefi':
validate_boot_option_for_uefi(task.node)
if deploy_utils.is_trusted_boot_requested(task.node):
# Check if 'boot_option' and boot mode is compatible with
# trusted boot.
validate_boot_parameters_for_trusted_boot(task.node)
d_info = _parse_deploy_info(node)
iscsi_deploy.validate(task)
@ -438,7 +466,7 @@ class PXEDeploy(base.DeployInterface):
deploy_utils.switch_pxe_config(
pxe_config_path, root_uuid_or_disk_id,
deploy_utils.get_boot_mode_for_deploy(node),
iwdi)
iwdi, deploy_utils.is_trusted_boot_requested(node))
def clean_up(self, task):
"""Clean up the deployment environment for the task's node.
@ -583,9 +611,10 @@ class VendorPassthru(agent_base_vendor.BaseAgentVendor):
else:
pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid)
boot_mode = deploy_utils.get_boot_mode_for_deploy(node)
deploy_utils.switch_pxe_config(pxe_config_path,
root_uuid_or_disk_id,
boot_mode, is_whole_disk_image)
deploy_utils.switch_pxe_config(
pxe_config_path, root_uuid_or_disk_id,
boot_mode, is_whole_disk_image,
deploy_utils.is_trusted_boot_requested(node))
except Exception as e:
LOG.error(_LE('Deploy failed for instance %(instance)s. '
@ -633,8 +662,9 @@ class VendorPassthru(agent_base_vendor.BaseAgentVendor):
'root uuid', uuid_dict.get('disk identifier'))
pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid)
boot_mode = deploy_utils.get_boot_mode_for_deploy(node)
deploy_utils.switch_pxe_config(pxe_config_path,
root_uuid_or_disk_id,
boot_mode, is_whole_disk_image)
deploy_utils.switch_pxe_config(
pxe_config_path, root_uuid_or_disk_id,
boot_mode, is_whole_disk_image,
deploy_utils.is_trusted_boot_requested(node))
self.reboot_and_finish_deploy(task)

View File

@ -14,3 +14,7 @@ append initrd={{ pxe_options.ari_path }} root={{ ROOT }} ro text {{ pxe_options.
label boot_whole_disk
COM32 chain.c32
append mbr:{{ DISK_IDENTIFIER }}
label trusted_boot
kernel mboot
append tboot.gz --- {{pxe_options.aki_path}} root={{ ROOT }} ro text {{ pxe_options.pxe_append_params|default("", true) }} intel_iommu=on --- {{pxe_options.ari_path}}

View File

@ -14,3 +14,7 @@ append initrd=/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/ramdisk root={{ ROO
label boot_whole_disk
COM32 chain.c32
append mbr:{{ DISK_IDENTIFIER }}
label trusted_boot
kernel mboot
append tboot.gz --- /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/kernel root={{ ROOT }} ro text test_param intel_iommu=on --- /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/ramdisk

View File

@ -62,6 +62,10 @@ append initrd=ramdisk root={{ ROOT }}
label boot_whole_disk
COM32 chain.c32
append mbr:{{ DISK_IDENTIFIER }}
label trusted_boot
kernel mboot
append tboot.gz --- kernel root={{ ROOT }} --- ramdisk
"""
_PXECONF_BOOT_PARTITION = """
@ -79,6 +83,11 @@ append initrd=ramdisk root=UUID=12345678-1234-1234-1234-1234567890abcdef
label boot_whole_disk
COM32 chain.c32
append mbr:{{ DISK_IDENTIFIER }}
label trusted_boot
kernel mboot
append tboot.gz --- kernel root=UUID=12345678-1234-1234-1234-1234567890abcdef \
--- ramdisk
"""
_PXECONF_BOOT_WHOLE_DISK = """
@ -96,6 +105,32 @@ append initrd=ramdisk root={{ ROOT }}
label boot_whole_disk
COM32 chain.c32
append mbr:0x12345678
label trusted_boot
kernel mboot
append tboot.gz --- kernel root={{ ROOT }} --- ramdisk
"""
_PXECONF_TRUSTED_BOOT = """
default trusted_boot
label deploy
kernel deploy_kernel
append initrd=deploy_ramdisk
ipappend 3
label boot_partition
kernel kernel
append initrd=ramdisk root=UUID=12345678-1234-1234-1234-1234567890abcdef
label boot_whole_disk
COM32 chain.c32
append mbr:{{ DISK_IDENTIFIER }}
label trusted_boot
kernel mboot
append tboot.gz --- kernel root=UUID=12345678-1234-1234-1234-1234567890abcdef \
--- ramdisk
"""
_IPXECONF_DEPLOY = b"""
@ -907,6 +942,17 @@ class SwitchPxeConfigTestCase(tests_base.TestCase):
pxeconf = f.read()
self.assertEqual(_PXECONF_BOOT_WHOLE_DISK, pxeconf)
def test_switch_pxe_config_trusted_boot(self):
boot_mode = 'bios'
fname = self._create_config()
utils.switch_pxe_config(fname,
'12345678-1234-1234-1234-1234567890abcdef',
boot_mode,
False, True)
with open(fname, 'r') as f:
pxeconf = f.read()
self.assertEqual(_PXECONF_TRUSTED_BOOT, pxeconf)
def test_switch_ipxe_config_partition_image(self):
boot_mode = 'bios'
cfg.CONF.set_override('ipxe_enabled', True, 'pxe')
@ -1538,6 +1584,18 @@ class ParseInstanceInfoCapabilitiesTestCase(tests_base.TestCase):
self.node.instance_info = {'capabilities': {"secure_boot": "invalid"}}
self.assertFalse(utils.is_secure_boot_requested(self.node))
def test_is_trusted_boot_requested_true(self):
self.node.instance_info = {'capabilities': {"trusted_boot": "true"}}
self.assertTrue(utils.is_trusted_boot_requested(self.node))
def test_is_trusted_boot_requested_false(self):
self.node.instance_info = {'capabilities': {"trusted_boot": "false"}}
self.assertFalse(utils.is_trusted_boot_requested(self.node))
def test_is_trusted_boot_requested_invalid(self):
self.node.instance_info = {'capabilities': {"trusted_boot": "invalid"}}
self.assertFalse(utils.is_trusted_boot_requested(self.node))
def test_get_boot_mode_for_deploy_using_capabilities(self):
properties = {'capabilities': 'boot_mode:uefi,cap2:value2'}
self.node.properties = properties
@ -1552,6 +1610,19 @@ class ParseInstanceInfoCapabilitiesTestCase(tests_base.TestCase):
result = utils.get_boot_mode_for_deploy(self.node)
self.assertEqual('uefi', result)
instance_info = {'capabilities': {'trusted_boot': 'True'}}
self.node.instance_info = instance_info
result = utils.get_boot_mode_for_deploy(self.node)
self.assertEqual('bios', result)
instance_info = {'capabilities': {'trusted_boot': 'True'},
'capabilities': {'secure_boot': 'True'}}
self.node.instance_info = instance_info
result = utils.get_boot_mode_for_deploy(self.node)
self.assertEqual('uefi', result)
def test_get_boot_mode_for_deploy_using_instance_info(self):
instance_info = {'deploy_boot_mode': 'bios'}
self.node.instance_info = instance_info
@ -1594,6 +1665,8 @@ class ParseInstanceInfoCapabilitiesTestCase(tests_base.TestCase):
utils.SUPPORTED_CAPABILITIES['boot_mode'])
self.assertEqual(('true', 'false'),
utils.SUPPORTED_CAPABILITIES['secure_boot'])
self.assertEqual(('true', 'false'),
utils.SUPPORTED_CAPABILITIES['trusted_boot'])
class TrySetBootDeviceTestCase(db_base.DbTestCase):

View File

@ -449,6 +449,52 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
pxe.validate_boot_option_for_uefi(self.node)
self.assertFalse(mock_log.called)
@mock.patch.object(pxe.LOG, 'error', autospec=True)
def test_validate_boot_parameters_for_trusted_boot_one(self, mock_log):
properties = {'capabilities': 'boot_mode:uefi'}
instance_info = {"boot_option": "netboot"}
self.node.properties = properties
self.node.instance_info['capabilities'] = instance_info
self.node.driver_internal_info['is_whole_disk_image'] = False
self.assertRaises(exception.InvalidParameterValue,
pxe.validate_boot_parameters_for_trusted_boot,
self.node)
self.assertTrue(mock_log.called)
@mock.patch.object(pxe.LOG, 'error', autospec=True)
def test_validate_boot_parameters_for_trusted_boot_two(self, mock_log):
properties = {'capabilities': 'boot_mode:bios'}
instance_info = {"boot_option": "local"}
self.node.properties = properties
self.node.instance_info['capabilities'] = instance_info
self.node.driver_internal_info['is_whole_disk_image'] = False
self.assertRaises(exception.InvalidParameterValue,
pxe.validate_boot_parameters_for_trusted_boot,
self.node)
self.assertTrue(mock_log.called)
@mock.patch.object(pxe.LOG, 'error', autospec=True)
def test_validate_boot_parameters_for_trusted_boot_three(self, mock_log):
properties = {'capabilities': 'boot_mode:bios'}
instance_info = {"boot_option": "netboot"}
self.node.properties = properties
self.node.instance_info['capabilities'] = instance_info
self.node.driver_internal_info['is_whole_disk_image'] = True
self.assertRaises(exception.InvalidParameterValue,
pxe.validate_boot_parameters_for_trusted_boot,
self.node)
self.assertTrue(mock_log.called)
@mock.patch.object(pxe.LOG, 'error', autospec=True)
def test_validate_boot_parameters_for_trusted_boot_pass(self, mock_log):
properties = {'capabilities': 'boot_mode:bios'}
instance_info = {"boot_option": "netboot"}
self.node.properties = properties
self.node.instance_info['capabilities'] = instance_info
self.node.driver_internal_info['is_whole_disk_image'] = False
pxe.validate_boot_parameters_for_trusted_boot(self.node)
self.assertFalse(mock_log.called)
class PXEDriverTestCase(db_base.DbTestCase):
@ -565,6 +611,25 @@ class PXEDriverTestCase(db_base.DbTestCase):
self.assertRaises(exception.MissingParameterValue,
task.driver.deploy.validate, task)
def test_validate_fail_trusted_boot(self):
properties = {'capabilities': 'boot_mode:uefi'}
instance_info = {"boot_option": "netboot", 'trusted_boot': 'true'}
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
task.node.properties = properties
task.node.instance_info['capabilities'] = instance_info
task.node.driver_internal_info['is_whole_disk_image'] = False
self.assertRaises(exception.InvalidParameterValue,
task.driver.deploy.validate, task)
def test_validate_fail_invalid_trusted_boot_value(self):
properties = {'capabilities': 'trusted_boot:value'}
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
task.node.properties = properties
self.assertRaises(exception.InvalidParameterValue,
task.driver.deploy.validate, task)
@mock.patch.object(base_image_service.BaseImageService, '_show',
autospec=True)
@mock.patch.object(keystone, 'get_service_url', autospec=True)
@ -786,7 +851,8 @@ class PXEDriverTestCase(db_base.DbTestCase):
mock_pxe_get_cfg.assert_called_once_with(task.node.uuid)
iwdi = task.node.driver_internal_info.get('is_whole_disk_image')
mock_switch.assert_called_once_with('/path', 'abcd', None, iwdi)
mock_switch.assert_called_once_with('/path', 'abcd', None,
iwdi, False)
def test_prepare_node_active(self):
self.node.driver_internal_info = {'root_uuid_or_disk_id': 'abcd',
@ -894,23 +960,30 @@ class PXEDriverTestCase(db_base.DbTestCase):
def _test_pass_deploy_info_deploy(self, is_localboot, mock_deploy,
mock_image_cache, mock_switch_config,
notify_mock, mock_node_boot_dev,
mock_clean_pxe):
mock_clean_pxe, trusted_boot=False):
root_uuid = "12345678-1234-1234-1234-1234567890abcxyz"
mock_deploy.return_value = {'root uuid': root_uuid}
boot_mode = None
is_whole_disk_image = False
# set local boot
if is_localboot:
i_info = self.node.instance_info
i_info['capabilities'] = '{"boot_option": "local"}'
self.node.instance_info = i_info
if trusted_boot:
i_info = self.node.instance_info
i_info['capabilities'] = '{"trusted_boot": "true"}'
self.node.instance_info = i_info
boot_mode = 'bios'
self.node.power_state = states.POWER_ON
self.node.provision_state = states.DEPLOYWAIT
self.node.target_provision_state = states.ACTIVE
self.node.save()
root_uuid = "12345678-1234-1234-1234-1234567890abcxyz"
mock_deploy.return_value = {'root uuid': root_uuid}
boot_mode = None
is_whole_disk_image = False
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.vendor.pass_deploy_info(
task, address='123456', iqn='aaa-bbb', key='fake-56789')
@ -932,7 +1005,8 @@ class PXEDriverTestCase(db_base.DbTestCase):
mock_switch_config.assert_called_once_with(pxe_config_path,
root_uuid,
boot_mode,
is_whole_disk_image)
is_whole_disk_image,
trusted_boot)
self.assertFalse(mock_node_boot_dev.called)
self.assertFalse(mock_clean_pxe.called)
@ -965,6 +1039,7 @@ class PXEDriverTestCase(db_base.DbTestCase):
is_whole_disk_image = True
disk_id = '0x12345678'
mock_deploy.return_value = {'disk identifier': disk_id}
trusted_boot = False
with task_manager.acquire(self.context, self.node.uuid) as task:
task.node.driver_internal_info['is_whole_disk_image'] = True
@ -988,7 +1063,8 @@ class PXEDriverTestCase(db_base.DbTestCase):
mock_switch_config.assert_called_once_with(pxe_config_path,
disk_id,
boot_mode,
is_whole_disk_image)
is_whole_disk_image,
trusted_boot)
self.assertFalse(mock_node_boot_dev.called)
self.assertFalse(mock_clean_pxe.called)
@ -1012,6 +1088,11 @@ class PXEDriverTestCase(db_base.DbTestCase):
self.assertEqual(states.ACTIVE, self.node.provision_state)
self.assertEqual(states.NOSTATE, self.node.target_provision_state)
def test_pass_deploy_info_deploy_trusted_boot(self):
self._test_pass_deploy_info_deploy(False, trusted_boot=True)
self.assertEqual(states.ACTIVE, self.node.provision_state)
self.assertEqual(states.NOSTATE, self.node.target_provision_state)
def test_pass_deploy_info_invalid(self):
self.node.power_state = states.POWER_ON
self.node.provision_state = states.AVAILABLE
@ -1221,7 +1302,7 @@ class TestAgentVendorPassthru(db_base.DbTestCase):
tftp_config = '/tftpboot/%s/config' % self.node.uuid
switch_pxe_config_mock.assert_called_once_with(tftp_config,
'some-root-uuid',
None, False)
None, False, False)
reboot_and_finish_deploy_mock.assert_called_once_with(
mock.ANY, self.task)