From 802c86ef045a007adad7f087082928550268b623 Mon Sep 17 00:00:00 2001 From: Luong Anh Tuan Date: Wed, 5 Jul 2017 15:21:09 +0700 Subject: [PATCH] Secure boot support for irmc-pxe driver This patch adds secure boot support for irmc-pxe boot interface as follows: - Implement secure boot support for irmc-pxe boot interface - Update version of python-scciclient supporting secure boot - Update irmc-pxe driver documentation Change-Id: Ie82ff07421d23b5c0d26e2d2fbde33fc9f8e3c42 Partial-Bug: #1694649 --- doc/source/admin/drivers/irmc.rst | 4 +- driver-requirements.txt | 2 +- ironic/drivers/modules/irmc/boot.py | 36 ++++++ ironic/drivers/modules/irmc/common.py | 26 ++++ .../unit/drivers/modules/irmc/test_boot.py | 118 ++++++++++++++++++ .../unit/drivers/modules/irmc/test_common.py | 34 +++++ .../drivers/third_party_driver_mock_specs.py | 1 + ...ure-boot-suport-irmc-2c1f09271f96424d.yaml | 5 + 8 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-secure-boot-suport-irmc-2c1f09271f96424d.yaml diff --git a/doc/source/admin/drivers/irmc.rst b/doc/source/admin/drivers/irmc.rst index bf6e219c2b..0f64ab938f 100644 --- a/doc/source/admin/drivers/irmc.rst +++ b/doc/source/admin/drivers/irmc.rst @@ -22,7 +22,7 @@ Prerequisites * Install `python-scciclient `_ and `pysnmp `_ packages:: - $ pip install "python-scciclient>=0.4.0" pysnmp + $ pip install "python-scciclient>=0.5.0" pysnmp Drivers ======= @@ -55,6 +55,8 @@ Node configuration irmc_username. - ``properties/capabilities`` property to be ``boot_mode:uefi`` if UEFI boot is required. + - ``properties/capabilities`` property to be ``boot_mode:uefi,secure_boot:true`` if + UEFI Secure Boot is required. * All of nodes are configured by setting the following configuration options in ``[irmc]`` section of ``/etc/ironic/ironic.conf``: diff --git a/driver-requirements.txt b/driver-requirements.txt index a986ada105..2ca2f6597d 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -8,7 +8,7 @@ proliantutils>=2.2.1 pysnmp python-ironic-inspector-client>=1.5.0 python-oneviewclient<3.0.0,>=2.5.2 -python-scciclient>=0.4.0 +python-scciclient>=0.5.0 UcsSdk==0.8.2.2 python-dracclient>=1.3.0 diff --git a/ironic/drivers/modules/irmc/boot.py b/ironic/drivers/modules/irmc/boot.py index 78f9a3253e..e8a93aba78 100644 --- a/ironic/drivers/modules/irmc/boot.py +++ b/ironic/drivers/modules/irmc/boot.py @@ -692,3 +692,39 @@ class IRMCPXEBoot(pxe.PXEBoot): irmc_management.backup_bios_config(task) super(IRMCPXEBoot, self).prepare_ramdisk(task, ramdisk_params) + + @METRICS.timer('IRMCPXEBoot.prepare_instance') + def prepare_instance(self, task): + """Prepares the boot of instance. + + This method prepares the boot of the instance after reading + relevant information from the node's instance_info. In case of netboot, + it updates the dhcp entries and switches the PXE config. In case of + localboot, it cleans up the PXE config. + + :param task: a task from TaskManager. + :returns: None + :raises: IRMCOperationError, if some operation on iRMC failed. + """ + + super(IRMCPXEBoot, self).prepare_instance(task) + node = task.node + if deploy_utils.is_secure_boot_requested(node): + irmc_common.set_secure_boot_mode(node, enable=True) + + @METRICS.timer('IRMCPXEBoot.clean_up_instance') + def clean_up_instance(self, task): + """Cleans up the boot of instance. + + This method cleans up the environment that was setup for booting + the instance. It unlinks the instance kernel/ramdisk in node's + directory in tftproot and removes the PXE config. + + :param task: a task from TaskManager. + :raises: IRMCOperationError, if some operation on iRMC failed. + :returns: None + """ + node = task.node + if deploy_utils.is_secure_boot_requested(node): + irmc_common.set_secure_boot_mode(node, enable=False) + super(IRMCPXEBoot, self).clean_up_instance(task) diff --git a/ironic/drivers/modules/irmc/common.py b/ironic/drivers/modules/irmc/common.py index ba735299c1..c8ce45b906 100644 --- a/ironic/drivers/modules/irmc/common.py +++ b/ironic/drivers/modules/irmc/common.py @@ -17,6 +17,7 @@ Common functionalities shared between different iRMC modules. """ import six +from oslo_log import log as logging from oslo_utils import importutils from ironic.common import exception @@ -24,7 +25,9 @@ from ironic.common.i18n import _ from ironic.conf import CONF scci = importutils.try_import('scciclient.irmc.scci') +elcm = importutils.try_import('scciclient.irmc.elcm') +LOG = logging.getLogger(__name__) REQUIRED_PROPERTIES = { 'irmc_address': _("IP address or hostname of the iRMC. Required."), 'irmc_username': _("Username for the iRMC with administrator privileges. " @@ -195,3 +198,26 @@ def get_irmc_report(node): port=driver_info['irmc_port'], auth_method=driver_info['irmc_auth_method'], client_timeout=driver_info['irmc_client_timeout']) + + +def set_secure_boot_mode(node, enable): + """Enable or disable UEFI Secure Boot + + Enable or disable UEFI Secure Boot + + :param node: An ironic node object. + :param enable: Boolean value. True if the secure boot to be + enabled. + :raises: IRMCOperationError if the operation fails. + """ + driver_info = parse_driver_info(node) + + try: + elcm.set_secure_boot_mode(driver_info, enable) + LOG.info("Set secure boot to %(flag)s for node %(node)s", + {'flag': enable, 'node': node.uuid}) + except scci.SCCIError as irmc_exception: + LOG.error("Failed to set secure boot to %(flag)s for node %(node)s", + {'flag': enable, 'node': node.uuid}) + raise exception.IRMCOperationError(operation=_("set_secure_boot_mode"), + error=irmc_exception) diff --git a/ironic/tests/unit/drivers/modules/irmc/test_boot.py b/ironic/tests/unit/drivers/modules/irmc/test_boot.py index 1cbbf1d892..0e03218481 100644 --- a/ironic/tests/unit/drivers/modules/irmc/test_boot.py +++ b/ironic/tests/unit/drivers/modules/irmc/test_boot.py @@ -1097,3 +1097,121 @@ class IRMCPXEBootTestCase(db_base.DbTestCase): self.assertFalse(mock_backup_bios.called) mock_parent_prepare.assert_called_once_with( task.driver.boot, task, {}) + + @mock.patch.object(irmc_common, 'set_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(pxe.PXEBoot, 'prepare_instance', spec_set=True, + autospec=True) + def test_prepare_instance_with_secure_boot(self, mock_prepare_instance, + mock_set_secure_boot_mode): + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.instance_info = { + 'capabilities': { + "secure_boot": "true" + } + } + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot.prepare_instance(task) + mock_set_secure_boot_mode.assert_called_once_with(task.node, + enable=True) + mock_prepare_instance.assert_called_once_with( + task.driver.boot, task) + + @mock.patch.object(irmc_common, 'set_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(pxe.PXEBoot, 'prepare_instance', spec_set=True, + autospec=True) + def test_prepare_instance_with_secure_boot_false( + self, mock_prepare_instance, mock_set_secure_boot_mode): + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.instance_info = { + 'capabilities': { + "secure_boot": "false" + } + } + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot.prepare_instance(task) + self.assertFalse(mock_set_secure_boot_mode.called) + mock_prepare_instance.assert_called_once_with( + task.driver.boot, task) + + @mock.patch.object(irmc_common, 'set_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(pxe.PXEBoot, 'prepare_instance', spec_set=True, + autospec=True) + def test_prepare_instance_without_secure_boot(self, mock_prepare_instance, + mock_set_secure_boot_mode): + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot.prepare_instance(task) + self.assertFalse(mock_set_secure_boot_mode.called) + mock_prepare_instance.assert_called_once_with( + task.driver.boot, task) + + @mock.patch.object(irmc_common, 'set_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(pxe.PXEBoot, 'clean_up_instance', spec_set=True, + autospec=True) + def test_clean_up_instance_with_secure_boot(self, mock_clean_up_instance, + mock_set_secure_boot_mode): + self.node.provision_state = states.CLEANING + self.node.target_provision_state = states.AVAILABLE + self.node.instance_info = { + 'capabilities': { + "secure_boot": "true" + } + } + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot.clean_up_instance(task) + mock_set_secure_boot_mode.assert_called_once_with(task.node, + enable=False) + mock_clean_up_instance.assert_called_once_with( + task.driver.boot, task) + + @mock.patch.object(irmc_common, 'set_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(pxe.PXEBoot, 'clean_up_instance', spec_set=True, + autospec=True) + def test_clean_up_instance_secure_boot_false(self, mock_clean_up_instance, + mock_set_secure_boot_mode): + self.node.provision_state = states.CLEANING + self.node.target_provision_state = states.AVAILABLE + self.node.instance_info = { + 'capabilities': { + "secure_boot": "false" + } + } + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot.clean_up_instance(task) + self.assertFalse(mock_set_secure_boot_mode.called) + mock_clean_up_instance.assert_called_once_with( + task.driver.boot, task) + + @mock.patch.object(irmc_common, 'set_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(pxe.PXEBoot, 'clean_up_instance', spec_set=True, + autospec=True) + def test_clean_up_instance_without_secure_boot( + self, mock_clean_up_instance, mock_set_secure_boot_mode): + self.node.provision_state = states.CLEANING + self.node.target_provision_state = states.AVAILABLE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot.clean_up_instance(task) + self.assertFalse(mock_set_secure_boot_mode.called) + mock_clean_up_instance.assert_called_once_with( + task.driver.boot, task) diff --git a/ironic/tests/unit/drivers/modules/irmc/test_common.py b/ironic/tests/unit/drivers/modules/irmc/test_common.py index 197efaf9b3..f42d5b0599 100644 --- a/ironic/tests/unit/drivers/modules/irmc/test_common.py +++ b/ironic/tests/unit/drivers/modules/irmc/test_common.py @@ -210,3 +210,37 @@ class IRMCCommonMethodsTestCase(db_base.DbTestCase): def test_out_range_sensor_method(self): self.assertRaises(ValueError, cfg.CONF.set_override, 'sensor_method', 'fake', 'irmc') + + @mock.patch.object(irmc_common, 'elcm', + spec_set=mock_specs.SCCICLIENT_IRMC_ELCM_SPEC) + def test_set_secure_boot_mode_enable(self, mock_elcm): + mock_elcm.set_secure_boot_mode.return_value = 'set_secure_boot_mode' + info = irmc_common.parse_driver_info(self.node) + irmc_common.set_secure_boot_mode(self.node, True) + mock_elcm.set_secure_boot_mode.assert_called_once_with( + info, True) + + @mock.patch.object(irmc_common, 'elcm', + spec_set=mock_specs.SCCICLIENT_IRMC_ELCM_SPEC) + def test_set_secure_boot_mode_disable(self, mock_elcm): + mock_elcm.set_secure_boot_mode.return_value = 'set_secure_boot_mode' + info = irmc_common.parse_driver_info(self.node) + irmc_common.set_secure_boot_mode(self.node, False) + mock_elcm.set_secure_boot_mode.assert_called_once_with( + info, False) + + @mock.patch.object(irmc_common, 'elcm', + spec_set=mock_specs.SCCICLIENT_IRMC_ELCM_SPEC) + @mock.patch.object(irmc_common, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + def test_set_secure_boot_mode_fail(self, mock_scci, mock_elcm): + irmc_common.scci.SCCIError = Exception + mock_elcm.set_secure_boot_mode.side_effect = Exception + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IRMCOperationError, + irmc_common.set_secure_boot_mode, + task.node, True) + info = irmc_common.parse_driver_info(task.node) + mock_elcm.set_secure_boot_mode.assert_called_once_with( + info, True) diff --git a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py index a3e9b0183b..f07f20f096 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py +++ b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py @@ -105,6 +105,7 @@ SCCICLIENT_IRMC_SCCI_SPEC = ( SCCICLIENT_IRMC_ELCM_SPEC = ( 'backup_bios_config', 'restore_bios_config', + 'set_secure_boot_mode', ) ONEVIEWCLIENT_SPEC = ( diff --git a/releasenotes/notes/add-secure-boot-suport-irmc-2c1f09271f96424d.yaml b/releasenotes/notes/add-secure-boot-suport-irmc-2c1f09271f96424d.yaml new file mode 100644 index 0000000000..ebae7b1294 --- /dev/null +++ b/releasenotes/notes/add-secure-boot-suport-irmc-2c1f09271f96424d.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds support to provision an instance in UEFI secure boot for + ``irmc-pxe`` boot interface.