diff --git a/ironic/drivers/ilo.py b/ironic/drivers/ilo.py index 3540c6944c..36f094ddca 100644 --- a/ironic/drivers/ilo.py +++ b/ironic/drivers/ilo.py @@ -38,7 +38,7 @@ class IloHardware(generic.GenericHardware): @property def supported_boot_interfaces(self): """List of supported boot interfaces.""" - return [boot.IloVirtualMediaBoot, boot.IloPXEBoot] + return [boot.IloVirtualMediaBoot, boot.IloPXEBoot, boot.IloiPXEBoot] @property def supported_bios_interfaces(self): diff --git a/ironic/drivers/modules/ilo/boot.py b/ironic/drivers/modules/ilo/boot.py index 557a8d257d..a15a516c41 100644 --- a/ironic/drivers/modules/ilo/boot.py +++ b/ironic/drivers/modules/ilo/boot.py @@ -38,6 +38,7 @@ from ironic.drivers import base from ironic.drivers.modules import boot_mode_utils from ironic.drivers.modules import deploy_utils from ironic.drivers.modules.ilo import common as ilo_common +from ironic.drivers.modules import ipxe from ironic.drivers.modules import pxe LOG = logging.getLogger(__name__) @@ -749,3 +750,101 @@ class IloPXEBoot(pxe.PXEBoot): # Volume boot in BIOS boot mode is handled using # PXE boot interface super(IloPXEBoot, self).clean_up_instance(task) + + +class IloiPXEBoot(ipxe.iPXEBoot): + + @METRICS.timer('IloiPXEBoot.prepare_ramdisk') + def prepare_ramdisk(self, task, ramdisk_params): + """Prepares the boot of Ironic ramdisk using PXE. + + This method prepares the boot of the deploy or rescue ramdisk after + reading relevant information from the node's driver_info and + instance_info. + + :param task: a task from TaskManager. + :param ramdisk_params: the parameters to be passed to the ramdisk. + :returns: None + :raises: MissingParameterValue, if some information is missing in + node's driver_info or instance_info. + :raises: InvalidParameterValue, if some information provided is + invalid. + :raises: IronicException, if some power or set boot boot device + operation failed on the node. + :raises: IloOperationError, if some operation on iLO failed. + """ + + if task.node.provision_state in (states.DEPLOYING, states.RESCUING, + states.CLEANING): + prepare_node_for_deploy(task) + + super(IloiPXEBoot, self).prepare_ramdisk(task, ramdisk_params) + + @METRICS.timer('IloiPXEBoot.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. + In case of 'boot from volume', it updates the iSCSI info onto iLO and + sets the node to boot from 'UefiTarget' boot device. + + :param task: a task from TaskManager. + :returns: None + :raises: IloOperationError, if some operation on iLO failed. + """ + + # Set boot mode + ilo_common.update_boot_mode(task) + # Need to enable secure boot, if being requested + ilo_common.update_secure_boot_mode(task, True) + + boot_mode = boot_mode_utils.get_boot_mode(task.node) + + if deploy_utils.is_iscsi_boot(task) and boot_mode == 'uefi': + # Need to set 'ilo_uefi_iscsi_boot' param for clean up + driver_internal_info = task.node.driver_internal_info + driver_internal_info['ilo_uefi_iscsi_boot'] = True + task.node.driver_internal_info = driver_internal_info + task.node.save() + # It will set iSCSI info onto iLO + task.driver.management.set_iscsi_boot_target(task) + manager_utils.node_set_boot_device(task, boot_devices.ISCSIBOOT, + persistent=True) + else: + # Volume boot in BIOS boot mode is handled using + # PXE boot interface + super(IloiPXEBoot, self).prepare_instance(task) + + @METRICS.timer('IloiPXEBoot.clean_up_instance') + def clean_up_instance(self, task): + """Cleans up the boot of instance. + + This method cleans up the PXE environment that was setup for booting + the instance. It unlinks the instance kernel/ramdisk in the node's + directory in tftproot and removes it's PXE config. + In case of UEFI iSCSI booting, it cleans up iSCSI target information + from the node. + + :param task: a task from TaskManager. + :returns: None + :raises: IloOperationError, if some operation on iLO failed. + """ + manager_utils.node_power_action(task, states.POWER_OFF) + disable_secure_boot_if_supported(task) + driver_internal_info = task.node.driver_internal_info + + if (deploy_utils.is_iscsi_boot(task) + and task.node.driver_internal_info.get('ilo_uefi_iscsi_boot')): + # It will clear iSCSI info from iLO in case of booting from + # volume in UEFI boot mode + task.driver.management.clear_iscsi_boot_target(task) + driver_internal_info.pop('ilo_uefi_iscsi_boot', None) + task.node.driver_internal_info = driver_internal_info + task.node.save() + else: + # Volume boot in BIOS boot mode is handled using + # PXE boot interface + super(IloiPXEBoot, self).clean_up_instance(task) diff --git a/ironic/tests/unit/drivers/modules/ilo/test_boot.py b/ironic/tests/unit/drivers/modules/ilo/test_boot.py index f85f79a08b..384f5f43f2 100644 --- a/ironic/tests/unit/drivers/modules/ilo/test_boot.py +++ b/ironic/tests/unit/drivers/modules/ilo/test_boot.py @@ -36,6 +36,7 @@ from ironic.drivers.modules import deploy_utils from ironic.drivers.modules.ilo import boot as ilo_boot from ironic.drivers.modules.ilo import common as ilo_common from ironic.drivers.modules.ilo import management as ilo_management +from ironic.drivers.modules import ipxe from ironic.drivers.modules import pxe from ironic.drivers.modules.storage import noop as noop_storage from ironic.drivers import utils as driver_utils @@ -1392,3 +1393,186 @@ class IloPXEBootTestCase(test_common.BaseIloTest): update_secure_boot_mode_mock.assert_called_once_with(task, True) self.assertTrue(task.node.driver_internal_info.get( 'ilo_uefi_iscsi_boot')) + + +@mock.patch.object(ipxe.iPXEBoot, '__init__', lambda self: None) +class IloiPXEBootTestCase(test_common.BaseIloTest): + + boot_interface = 'ilo-ipxe' + + def setUp(self): + super(IloiPXEBootTestCase, self).setUp() + self.config(enabled_boot_interfaces=['ilo-ipxe']) + + @mock.patch.object(ilo_boot, 'prepare_node_for_deploy', spec_set=True, + autospec=True) + @mock.patch.object(ipxe.iPXEBoot, 'prepare_ramdisk', spec_set=True, + autospec=True) + def _test_prepare_ramdisk_needs_node_prep(self, pxe_prepare_ramdisk_mock, + prepare_node_mock, prov_state): + self.node.provision_state = prov_state + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertIsNone( + task.driver.boot.prepare_ramdisk(task, None)) + + prepare_node_mock.assert_called_once_with(task) + pxe_prepare_ramdisk_mock.assert_called_once_with( + mock.ANY, task, None) + + def test_prepare_ramdisk_in_deploying(self): + self._test_prepare_ramdisk_needs_node_prep(prov_state=states.DEPLOYING) + + def test_prepare_ramdisk_in_rescuing(self): + self._test_prepare_ramdisk_needs_node_prep(prov_state=states.RESCUING) + + def test_prepare_ramdisk_in_cleaning(self): + self._test_prepare_ramdisk_needs_node_prep(prov_state=states.CLEANING) + + @mock.patch.object(deploy_utils, 'is_iscsi_boot', + spec_set=True, autospec=True) + @mock.patch.object(ilo_common, 'update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + @mock.patch.object(ipxe.iPXEBoot, 'clean_up_instance', spec_set=True, + autospec=True) + def test_clean_up_instance(self, pxe_cleanup_mock, node_power_mock, + update_secure_boot_mode_mock, + is_iscsi_boot_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot.clean_up_instance(task) + is_iscsi_boot_mock.return_value = False + node_power_mock.assert_called_once_with(task, states.POWER_OFF) + update_secure_boot_mode_mock.assert_called_once_with(task, False) + pxe_cleanup_mock.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(deploy_utils, 'is_iscsi_boot', + spec_set=True, autospec=True) + @mock.patch.object(ilo_common, 'update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + @mock.patch.object(ipxe.iPXEBoot, 'clean_up_instance', spec_set=True, + autospec=True) + def test_clean_up_instance_boot_from_volume_bios( + self, pxe_cleanup_mock, node_power_mock, + update_secure_boot_mode_mock, is_iscsi_boot_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot.clean_up_instance(task) + is_iscsi_boot_mock.return_value = True + node_power_mock.assert_called_once_with(task, states.POWER_OFF) + update_secure_boot_mode_mock.assert_called_once_with(task, False) + pxe_cleanup_mock.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(deploy_utils, 'is_iscsi_boot', + spec_set=True, autospec=True) + @mock.patch.object(ilo_management.IloManagement, 'clear_iscsi_boot_target', + spec_set=True, autospec=True) + @mock.patch.object(ilo_common, 'update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', spec_set=True, + autospec=True) + def test_clean_up_instance_boot_from_volume(self, node_power_mock, + update_secure_boot_mode_mock, + clear_iscsi_boot_target_mock, + is_iscsi_boot_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + driver_internal_info = task.node.driver_internal_info + driver_internal_info['ilo_uefi_iscsi_boot'] = True + task.node.driver_internal_info = driver_internal_info + task.node.save() + is_iscsi_boot_mock.return_value = True + task.driver.boot.clean_up_instance(task) + clear_iscsi_boot_target_mock.assert_called_once_with(mock.ANY, + task) + node_power_mock.assert_called_once_with(task, states.POWER_OFF) + update_secure_boot_mode_mock.assert_called_once_with(task, False) + self.assertIsNone(task.node.driver_internal_info.get( + 'ilo_uefi_iscsi_boot')) + + @mock.patch.object(deploy_utils, 'is_iscsi_boot', + spec_set=True, autospec=True) + @mock.patch.object(boot_mode_utils, 'get_boot_mode_for_deploy', + spec_set=True, autospec=True) + @mock.patch.object(ilo_common, 'update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ipxe.iPXEBoot, 'prepare_instance', spec_set=True, + autospec=True) + def test_prepare_instance(self, pxe_prepare_instance_mock, + update_boot_mode_mock, + update_secure_boot_mode_mock, + get_boot_mode_mock, + is_iscsi_boot_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot.prepare_instance(task) + is_iscsi_boot_mock.return_value = False + get_boot_mode_mock.return_value = 'uefi' + update_boot_mode_mock.assert_called_once_with(task) + update_secure_boot_mode_mock.assert_called_once_with(task, True) + pxe_prepare_instance_mock.assert_called_once_with(mock.ANY, task) + self.assertIsNone(task.node.driver_internal_info.get( + 'ilo_uefi_iscsi_boot')) + + @mock.patch.object(deploy_utils, 'is_iscsi_boot', + spec_set=True, autospec=True) + @mock.patch.object(boot_mode_utils, 'get_boot_mode_for_deploy', + spec_set=True, autospec=True) + @mock.patch.object(ilo_common, 'update_secure_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ipxe.iPXEBoot, 'prepare_instance', spec_set=True, + autospec=True) + def test_prepare_instance_bios(self, pxe_prepare_instance_mock, + update_boot_mode_mock, + update_secure_boot_mode_mock, + get_boot_mode_mock, + is_iscsi_boot_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot.prepare_instance(task) + is_iscsi_boot_mock.return_value = False + get_boot_mode_mock.return_value = 'bios' + update_boot_mode_mock.assert_called_once_with(task) + update_secure_boot_mode_mock.assert_called_once_with(task, True) + pxe_prepare_instance_mock.assert_called_once_with(mock.ANY, task) + self.assertIsNone(task.node.driver_internal_info.get( + 'ilo_uefi_iscsi_boot')) + + @mock.patch.object(deploy_utils, 'is_iscsi_boot', + spec_set=True, autospec=True) + @mock.patch.object(boot_mode_utils, 'get_boot_mode_for_deploy', + spec_set=True, autospec=True) + @mock.patch.object(ilo_management.IloManagement, 'set_iscsi_boot_target', + spec_set=True, autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'update_boot_mode', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'update_secure_boot_mode', spec_set=True, + autospec=True) + def test_prepare_instance_boot_from_volume( + self, update_secure_boot_mode_mock, + update_boot_mode_mock, set_boot_device_mock, + set_iscsi_boot_target_mock, get_boot_mode_mock, + is_iscsi_boot_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + is_iscsi_boot_mock.return_value = True + get_boot_mode_mock.return_value = 'uefi' + task.driver.boot.prepare_instance(task) + set_iscsi_boot_target_mock.assert_called_once_with(mock.ANY, task) + set_boot_device_mock.assert_called_once_with( + task, boot_devices.ISCSIBOOT, persistent=True) + update_boot_mode_mock.assert_called_once_with(task) + update_secure_boot_mode_mock.assert_called_once_with(task, True) + self.assertTrue(task.node.driver_internal_info.get( + 'ilo_uefi_iscsi_boot')) diff --git a/releasenotes/notes/adds-ilo-ipxe-boot-interface-4fc75292122db80d.yaml b/releasenotes/notes/adds-ilo-ipxe-boot-interface-4fc75292122db80d.yaml new file mode 100644 index 0000000000..d24bbec5d1 --- /dev/null +++ b/releasenotes/notes/adds-ilo-ipxe-boot-interface-4fc75292122db80d.yaml @@ -0,0 +1,19 @@ +--- +features: + - | + Adds an ``ilo-ipxe`` boot interface to ``ilo`` hardware type which + allows for instance level iPXE enablement as opposed to + conductor-wide enablement of iPXE. + To perform iPXE boot with ``ilo-ipxe`` boot interface: + + * Add ``ilo-ipxe`` to ``enabled_boot_interfaces`` in ``ironic.conf`` + * Set up TFTP & HTTP server using `Ironic document on iPXE boot + configuration + `_ + * Create/Set baremetal node with ``--boot-interface ilo-ipxe`` +fixes: + - | + From Stein release, ``[pxe]ipxe_enabled`` option has been deprecated. + The ``ilo`` hardware type supports iPXE boot through + ``[pxe]ipxe_enabled`` option. To cope with this incompatibility, + ``ilo`` hardware type has added new ``ilo-ipxe`` boot interface. diff --git a/setup.cfg b/setup.cfg index dfae78963b..eb12c825f7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -64,6 +64,7 @@ ironic.hardware.interfaces.bios = ironic.hardware.interfaces.boot = fake = ironic.drivers.modules.fake:FakeBoot ilo-pxe = ironic.drivers.modules.ilo.boot:IloPXEBoot + ilo-ipxe = ironic.drivers.modules.ilo.boot:IloiPXEBoot ilo-virtual-media = ironic.drivers.modules.ilo.boot:IloVirtualMediaBoot ipxe = ironic.drivers.modules.ipxe:iPXEBoot irmc-pxe = ironic.drivers.modules.irmc.boot:IRMCPXEBoot