diff --git a/doc/source/deploy/install-guide.rst b/doc/source/deploy/install-guide.rst index 44f8ba28f2..ebfa636706 100644 --- a/doc/source/deploy/install-guide.rst +++ b/doc/source/deploy/install-guide.rst @@ -675,6 +675,21 @@ steps on the Ironic conductor node to configure PXE UEFI environment. ironic node-update add properties/capabilities='boot_mode:uefi' +#. For deploying signed images, update the Ironic node with ``secure_boot`` + capability in node's properties. + field:: + + ironic node-update add properties/capabilities='secure_boot:true' + +#. Ensure the public key of the signed image is loaded into baremetal to deploy + signed images. + For HP Proliant Gen9 servers, one can enroll public key using iLO System + Utilities UI. Please refer to section ``Accessing Secure Boot options`` in + HP UEFI System Utilities User Guide http://www.hp.com/ctg/Manual/c04398276.pdf. + Also, one can refer to white paper on Secure Boot on Linux for HP Proliant + Servers at http://h20195.www2.hp.com/V2/getpdf.aspx/4AA5-4496ENW.pdf for + more details. + #. Make sure that bare metal node is configured to boot in UEFI boot mode and boot device is set to network/pxe. diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py index f9f2361d86..66c5db454a 100644 --- a/ironic/drivers/modules/deploy_utils.py +++ b/ironic/drivers/modules/deploy_utils.py @@ -1013,3 +1013,21 @@ def parse_root_device_hints(node): hints.append("%s=%s" % (key, value)) return ','.join(hints) + + +def is_secure_boot_requested(node): + """Returns True if secure_boot is requested for deploy. + + This method checks node property for secure_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 secure_boot is requested. + """ + + capabilities = parse_instance_info_capabilities(node) + sec_boot = capabilities.get('secure_boot', 'false').lower() + + return sec_boot == 'true' diff --git a/ironic/drivers/modules/ilo/deploy.py b/ironic/drivers/modules/ilo/deploy.py index 843f15b33b..09a99d19b2 100644 --- a/ironic/drivers/modules/ilo/deploy.py +++ b/ironic/drivers/modules/ilo/deploy.py @@ -274,6 +274,96 @@ def _prepare_agent_vmedia_boot(task): _reboot_into(task, deploy_iso, deploy_ramdisk_opts) +def _disable_secure_boot(task): + """Disables secure boot on node, if secure boot is enabled on node. + + This method checks if secure boot is enabled on node. If enabled, it + disables same and returns True. + + :param task: a TaskManager instance containing the node to act on. + :returns: It returns True, if secure boot was successfully disabled on + the node. + It returns False, if secure boot on node is in disabled state + or if secure boot feature is not supported by the node. + :raises: IloOperationError, if some operation on iLO failed. + """ + cur_sec_state = False + try: + cur_sec_state = ilo_common.get_secure_boot_mode(task) + except exception.IloOperationNotSupported: + LOG.debug('Secure boot mode is not supported for node %s', + task.node.uuid) + return False + + if cur_sec_state: + LOG.debug('Disabling secure boot for node %s', task.node.uuid) + ilo_common.set_secure_boot_mode(task, False) + return True + return False + + +def _prepare_node_for_deploy(task): + """Common preparatory steps for all iLO drivers. + + This method performs common preparatory steps required for all drivers. + 1. Power off node + 2. Disables secure boot, if it is in enabled state. + 3. Updates boot_mode capability to 'uefi' if secure boot is requested. + 4. Changes boot mode of the node if secure boot is disabled currently. + + :param task: a TaskManager instance containing the node to act on. + :raises: IloOperationError, if some operation on iLO failed. + """ + manager_utils.node_power_action(task, states.POWER_OFF) + + # Boot mode can be changed only if secure boot is in disabled state. + # secure boot and boot mode cannot be changed together. + change_boot_mode = True + + # Disable secure boot on the node if it is in enabled state. + if _disable_secure_boot(task): + change_boot_mode = False + + # Set boot_mode capability to uefi for secure boot + if deploy_utils.is_secure_boot_requested(task.node): + LOG.debug('Secure boot deploy requested for node %s', task.node.uuid) + _enable_uefi_capability(task) + + if change_boot_mode: + ilo_common.update_boot_mode(task) + + +def _update_secure_boot_mode(task, mode): + """Changes secure boot mode for next boot on the node. + + This method changes secure boot mode on the node for next boot. It changes + the secure boot mode setting on node only if the deploy has requested for + the secure boot. + During deploy, this method is used to enable secure boot on the node by + passing 'mode' as 'True'. + During teardown, this method is used to disable secure boot on the node by + passing 'mode' as 'False'. + + :param task: a TaskManager instance containing the node to act on. + :param mode: Boolean value requesting the next state for secure boot + :raises: IloOperationNotSupported, if operation is not supported on iLO + :raises: IloOperationError, if some operation on iLO failed. + """ + if deploy_utils.is_secure_boot_requested(task.node): + ilo_common.set_secure_boot_mode(task, mode) + LOG.info(_LI('Changed secure boot to %(mode)s for node %(node)s'), + {'mode': mode, 'node': task.node.uuid}) + + +def _enable_uefi_capability(task): + """Adds capability boot_mode='uefi' into Node property. + + :param task: a TaskManager instance containing the node to act on. + """ + driver_utils.rm_node_capability(task, 'boot_mode') + driver_utils.add_node_capability(task, 'boot_mode', 'uefi') + + class IloVirtualMediaIscsiDeploy(base.DeployInterface): def get_properties(self): diff --git a/ironic/tests/drivers/ilo/test_deploy.py b/ironic/tests/drivers/ilo/test_deploy.py index 5916e84a31..0f9f8ae58b 100644 --- a/ironic/tests/drivers/ilo/test_deploy.py +++ b/ironic/tests/drivers/ilo/test_deploy.py @@ -268,6 +268,143 @@ class IloDeployPrivateMethodsTestCase(db_base.DbTestCase): 'deploy-iso-uuid', deploy_opts) + @mock.patch.object(deploy_utils, 'is_secure_boot_requested') + @mock.patch.object(ilo_common, 'set_secure_boot_mode') + def test__update_secure_boot_passed_true(self, + func_set_secure_boot_mode, + func_is_secure_boot_requested): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + func_is_secure_boot_requested.return_value = True + ilo_deploy._update_secure_boot_mode(task, True) + func_set_secure_boot_mode.assert_called_once_with(task, True) + + @mock.patch.object(deploy_utils, 'is_secure_boot_requested') + @mock.patch.object(ilo_common, 'set_secure_boot_mode') + def test__update_secure_boot_passed_False(self, + func_set_secure_boot_mode, + func_is_secure_boot_requested): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + func_is_secure_boot_requested.return_value = False + ilo_deploy._update_secure_boot_mode(task, False) + self.assertFalse(func_set_secure_boot_mode.called) + + @mock.patch.object(driver_utils, 'add_node_capability') + @mock.patch.object(driver_utils, 'rm_node_capability') + def test__enable_uefi_capability(self, func_rm_node_capability, + func_add_node_capability): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ilo_deploy._enable_uefi_capability(task) + func_rm_node_capability.assert_called_once_with(task, + 'boot_mode') + func_add_node_capability.assert_called_once_with(task, + 'boot_mode', + 'uefi') + + @mock.patch.object(ilo_common, 'set_secure_boot_mode') + @mock.patch.object(ilo_common, 'get_secure_boot_mode') + def test__disable_secure_boot_false(self, + func_get_secure_boot_mode, + func_set_secure_boot_mode): + func_get_secure_boot_mode.return_value = False + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + returned_state = ilo_deploy._disable_secure_boot(task) + func_get_secure_boot_mode.assert_called_once_with(task) + self.assertFalse(func_set_secure_boot_mode.called) + self.assertFalse(returned_state) + + @mock.patch.object(ilo_common, 'set_secure_boot_mode') + @mock.patch.object(ilo_common, 'get_secure_boot_mode') + def test__disable_secure_boot_true(self, + func_get_secure_boot_mode, + func_set_secure_boot_mode): + func_get_secure_boot_mode.return_value = True + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + returned_state = ilo_deploy._disable_secure_boot(task) + func_get_secure_boot_mode.assert_called_once_with(task) + func_set_secure_boot_mode.assert_called_once_with(task, False) + self.assertTrue(returned_state) + + @mock.patch.object(ilo_deploy, 'exception') + @mock.patch.object(ilo_common, 'get_secure_boot_mode') + def test__disable_secure_boot_exception(self, + func_get_secure_boot_mode, + exception_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + exception_mock.IloOperationNotSupported = Exception + func_get_secure_boot_mode.side_effect = Exception + returned_state = ilo_deploy._disable_secure_boot(task) + func_get_secure_boot_mode.assert_called_once_with(task) + self.assertFalse(returned_state) + + @mock.patch.object(ilo_common, 'update_boot_mode') + @mock.patch.object(deploy_utils, 'is_secure_boot_requested') + @mock.patch.object(ilo_deploy, '_disable_secure_boot') + @mock.patch.object(manager_utils, 'node_power_action') + def test__prepare_node_for_deploy(self, + func_node_power_action, + func_disable_secure_boot, + func_is_secure_boot_requested, + func_update_boot_mode): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + func_disable_secure_boot.return_value = False + func_is_secure_boot_requested.return_value = False + ilo_deploy._prepare_node_for_deploy(task) + func_node_power_action.assert_called_once_with(task, + states.POWER_OFF) + func_disable_secure_boot.assert_called_once_with(task) + func_is_secure_boot_requested.assert_called_once_with(task.node) + func_update_boot_mode.assert_called_once_with(task) + + @mock.patch.object(ilo_common, 'update_boot_mode') + @mock.patch.object(deploy_utils, 'is_secure_boot_requested') + @mock.patch.object(ilo_deploy, '_disable_secure_boot') + @mock.patch.object(manager_utils, 'node_power_action') + def test__prepare_node_for_deploy_sec_boot_on(self, + func_node_power_action, + func_disable_secure_boot, + func_is_secure_boot_req, + func_update_boot_mode): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + func_disable_secure_boot.return_value = True + func_is_secure_boot_req.return_value = False + ilo_deploy._prepare_node_for_deploy(task) + func_node_power_action.assert_called_once_with(task, + states.POWER_OFF) + func_disable_secure_boot.assert_called_once_with(task) + func_is_secure_boot_req.assert_called_once_with(task.node) + self.assertFalse(func_update_boot_mode.called) + + @mock.patch.object(ilo_common, 'update_boot_mode') + @mock.patch.object(ilo_deploy, '_enable_uefi_capability') + @mock.patch.object(deploy_utils, 'is_secure_boot_requested') + @mock.patch.object(ilo_deploy, '_disable_secure_boot') + @mock.patch.object(manager_utils, 'node_power_action') + def test__prepare_node_for_deploy_sec_boot_req(self, + func_node_power_action, + func_disable_secure_boot, + func_is_secure_boot_req, + func_enable_uefi_cap, + func_update_boot_mode): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + func_disable_secure_boot.return_value = True + func_is_secure_boot_req.return_value = True + ilo_deploy._prepare_node_for_deploy(task) + func_node_power_action.assert_called_once_with(task, + states.POWER_OFF) + func_disable_secure_boot.assert_called_once_with(task) + func_is_secure_boot_req.assert_called_once_with(task.node) + func_enable_uefi_cap.assert_called_once_with(task) + self.assertFalse(func_update_boot_mode.called) + class IloVirtualMediaIscsiDeployTestCase(db_base.DbTestCase): diff --git a/ironic/tests/drivers/test_deploy_utils.py b/ironic/tests/drivers/test_deploy_utils.py index bdbec93196..dfbcc5532e 100644 --- a/ironic/tests/drivers/test_deploy_utils.py +++ b/ironic/tests/drivers/test_deploy_utils.py @@ -1452,6 +1452,18 @@ class ParseInstanceInfoCapabilitiesTestCase(tests_base.TestCase): self.assertRaises(exception.InvalidParameterValue, utils.parse_instance_info_capabilities, self.node) + def test_is_secure_boot_requested_true(self): + self.node.instance_info = {'capabilities': {"secure_boot": "true"}} + self.assertTrue(utils.is_secure_boot_requested(self.node)) + + def test_is_secure_boot_requested_false(self): + self.node.instance_info = {'capabilities': {"secure_boot": "false"}} + self.assertFalse(utils.is_secure_boot_requested(self.node)) + + def test_is_secure_boot_requested_invalid(self): + self.node.instance_info = {'capabilities': {"secure_boot": "invalid"}} + self.assertFalse(utils.is_secure_boot_requested(self.node)) + class TrySetBootDeviceTestCase(db_base.DbTestCase):