diff --git a/doc/source/admin/drivers/ilo.rst b/doc/source/admin/drivers/ilo.rst index 5f08359652..ef32f74a79 100644 --- a/doc/source/admin/drivers/ilo.rst +++ b/doc/source/admin/drivers/ilo.rst @@ -235,9 +235,9 @@ Prerequisites which contains a set of modules for managing HPE ProLiant hardware. Install ``proliantutils`` module on the ironic conductor node. Minimum - version required is 2.4.1:: + version required is 2.5.0:: - $ pip install "proliantutils>=2.4.1" + $ pip install "proliantutils>=2.5.0" * ``ipmitool`` command must be present on the service node(s) where ``ironic-conductor`` is running. On most distros, this is provided as part @@ -1812,7 +1812,7 @@ firmware components on the node. Refer to `SUM User Guide`_ to get more information on SUM based firmware update. ``update_firmware_sum`` clean step requires the agent ramdisk with -``Proliant Hardware Manager`` from the proliantutils version 2.4.0 or higher. +``Proliant Hardware Manager`` from the proliantutils version 2.5.0 or higher. See `DIB support for Proliant Hardware Manager`_ to create the agent ramdisk with ``Proliant Hardware Manager``. diff --git a/driver-requirements.txt b/driver-requirements.txt index 255a36e529..6435463334 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -4,7 +4,7 @@ # python projects they should package as optional dependencies for Ironic. # These are available on pypi -proliantutils>=2.4.1 +proliantutils>=2.5.0 pysnmp python-ironic-inspector-client>=1.5.0 python-oneviewclient<3.0.0,>=2.5.2 diff --git a/ironic/common/boot_devices.py b/ironic/common/boot_devices.py index 4fcda5ffdc..e834f2d24f 100644 --- a/ironic/common/boot_devices.py +++ b/ironic/common/boot_devices.py @@ -43,3 +43,6 @@ SAFE = 'safe' WANBOOT = 'wanboot' "Boot from Wide Area Network" + +ISCSIBOOT = 'iscsiboot' +"Boot from iSCSI volume" diff --git a/ironic/drivers/modules/ilo/boot.py b/ironic/drivers/modules/ilo/boot.py index ff31544bfa..b879b4a0bd 100644 --- a/ironic/drivers/modules/ilo/boot.py +++ b/ironic/drivers/modules/ilo/boot.py @@ -383,6 +383,8 @@ def disable_secure_boot_if_supported(task): class IloVirtualMediaBoot(base.BootInterface): + capabilities = ['iscsi_volume_boot'] + def get_properties(self): return COMMON_PROPERTIES @@ -397,9 +399,13 @@ class IloVirtualMediaBoot(base.BootInterface): in instance_info for non-Glance image. """ - _validate_instance_image_info(task) _validate_driver_info(task) + if not task.driver.storage.should_write_image(task): + return + else: + _validate_instance_image_info(task) + @METRICS.timer('IloVirtualMediaBoot.prepare_ramdisk') def prepare_ramdisk(self, task, ramdisk_params): """Prepares the boot of deploy ramdisk using virtual media. @@ -464,8 +470,12 @@ class IloVirtualMediaBoot(base.BootInterface): relevant information from the node's instance_info. It does the following depending on boot_option for deploy: - - If the boot_option requested for this deploy is 'local' or image - is a whole disk image, then it sets the node to boot from disk. + - If the boot mode is 'uefi' and its booting from volume, then it + sets the iSCSI target info and node to boot from 'UefiTarget' + boot device. + - If not 'boot from volume' and the boot_option requested for + this deploy is 'local' or image is a whole disk image, then + it sets the node to boot from disk. - Otherwise it finds/creates the boot ISO to boot the instance image, attaches the boot ISO to the bare metal and then sets the node to boot from CDROM. @@ -473,25 +483,44 @@ class IloVirtualMediaBoot(base.BootInterface): :param task: a task from TaskManager. :returns: None :raises: IloOperationError, if some operation on iLO failed. + :raises: InstanceDeployFailure, if its try to boot iSCSI volume in + 'BIOS' boot mode. """ ilo_common.cleanup_vmedia_boot(task) - # For iscsi_ilo driver, we boot from disk every time if the image - # deployed is a whole disk image. - node = task.node - iwdi = node.driver_internal_info.get('is_whole_disk_image') - if deploy_utils.get_boot_option(node) == "local" or iwdi: - manager_utils.node_set_boot_device(task, boot_devices.DISK, - persistent=True) - else: - drv_int_info = node.driver_internal_info - root_uuid_or_disk_id = drv_int_info.get('root_uuid_or_disk_id') - if root_uuid_or_disk_id: - self._configure_vmedia_boot(task, root_uuid_or_disk_id) + boot_mode = deploy_utils.get_boot_mode_for_deploy(task.node) + + if deploy_utils.is_iscsi_boot(task): + # It will set iSCSI info onto iLO + if 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() + task.driver.management.set_iscsi_boot_target(task) + manager_utils.node_set_boot_device( + task, boot_devices.ISCSIBOOT, persistent=True) else: - LOG.warning("The UUID for the root partition could not " - "be found for node %s", node.uuid) + msg = 'Virtual media can not boot volume in BIOS boot mode.' + raise exception.InstanceDeployFailure(msg) + else: + # For iscsi_ilo driver, we boot from disk every time if the image + # deployed is a whole disk image. + node = task.node + iwdi = node.driver_internal_info.get('is_whole_disk_image') + if deploy_utils.get_boot_option(node) == "local" or iwdi: + manager_utils.node_set_boot_device(task, boot_devices.DISK, + persistent=True) + else: + drv_int_info = node.driver_internal_info + root_uuid_or_disk_id = drv_int_info.get('root_uuid_or_disk_id') + if root_uuid_or_disk_id: + self._configure_vmedia_boot(task, root_uuid_or_disk_id) + else: + LOG.warning("The UUID for the root partition could not " + "be found for node %s", node.uuid) # Set boot mode ilo_common.update_boot_mode(task) # Need to enable secure boot, if being requested @@ -502,7 +531,9 @@ class IloVirtualMediaBoot(base.BootInterface): """Cleans up the boot of instance. This method cleans up the environment that was setup for booting - the instance. It ejects virtual media + the instance. It ejects virtual media. + In case of UEFI iSCSI booting, it cleans up iSCSI target information + from the node. :param task: a task from TaskManager. :returns: None @@ -512,16 +543,23 @@ class IloVirtualMediaBoot(base.BootInterface): LOG.debug("Cleaning up the instance.") manager_utils.node_power_action(task, states.POWER_OFF) disable_secure_boot_if_supported(task) - - _clean_up_boot_iso_for_instance(task.node) - driver_internal_info = task.node.driver_internal_info - driver_internal_info.pop('boot_iso_created_in_web_server', None) - driver_internal_info.pop('root_uuid_or_disk_id', None) - task.node.driver_internal_info = driver_internal_info - task.node.save() - ilo_common.cleanup_vmedia_boot(task) + 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 + 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: + _clean_up_boot_iso_for_instance(task.node) + driver_internal_info = task.node.driver_internal_info + driver_internal_info.pop('boot_iso_created_in_web_server', None) + driver_internal_info.pop('root_uuid_or_disk_id', None) + task.node.driver_internal_info = driver_internal_info + task.node.save() + ilo_common.cleanup_vmedia_boot(task) @METRICS.timer('IloVirtualMediaBoot.clean_up_ramdisk') def clean_up_ramdisk(self, task): @@ -601,6 +639,8 @@ class IloPXEBoot(pxe.PXEBoot): 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 @@ -612,7 +652,22 @@ class IloPXEBoot(pxe.PXEBoot): # Need to enable secure boot, if being requested ilo_common.update_secure_boot_mode(task, True) - super(IloPXEBoot, self).prepare_instance(task) + boot_mode = deploy_utils.get_boot_mode_for_deploy(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(IloPXEBoot, self).prepare_instance(task) @METRICS.timer('IloPXEBoot.clean_up_instance') def clean_up_instance(self, task): @@ -621,6 +676,8 @@ class IloPXEBoot(pxe.PXEBoot): 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 @@ -629,5 +686,17 @@ class IloPXEBoot(pxe.PXEBoot): manager_utils.node_power_action(task, states.POWER_OFF) disable_secure_boot_if_supported(task) + driver_internal_info = task.node.driver_internal_info - super(IloPXEBoot, self).clean_up_instance(task) + 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(IloPXEBoot, self).clean_up_instance(task) diff --git a/ironic/drivers/modules/ilo/management.py b/ironic/drivers/modules/ilo/management.py index 5ffb48dc3d..5597df0773 100644 --- a/ironic/drivers/modules/ilo/management.py +++ b/ironic/drivers/modules/ilo/management.py @@ -34,6 +34,7 @@ from ironic.drivers.modules.ilo import common as ilo_common from ironic.drivers.modules.ilo import firmware_processor from ironic.drivers.modules import ipmitool from ironic.drivers import utils as driver_utils +from ironic.objects import volume_target LOG = logging.getLogger(__name__) @@ -44,7 +45,8 @@ ilo_error = importutils.try_import('proliantutils.exception') BOOT_DEVICE_MAPPING_TO_ILO = { boot_devices.PXE: 'NETWORK', boot_devices.DISK: 'HDD', - boot_devices.CDROM: 'CDROM' + boot_devices.CDROM: 'CDROM', + boot_devices.ISCSIBOOT: 'ISCSI' } BOOT_DEVICE_ILO_TO_GENERIC = { v: k for k, v in BOOT_DEVICE_MAPPING_TO_ILO.items()} @@ -513,3 +515,68 @@ class IloManagement(base.ManagementInterface): '%(node)s for "update_firmware_sum" clean step. ' 'Error: %(error)s', {'node': node.uuid, 'error': e}) + + @METRICS.timer('IloManagement.set_iscsi_boot_target') + def set_iscsi_boot_target(self, task): + """Set iSCSI details of the system in UEFI boot mode. + + The initiator is set with the target details like + IQN, LUN, IP, Port etc. + :param task: a task from TaskManager. + :raises: IloCommandNotSupportedInBiosError if system in BIOS boot mode. + :raises: IloError on an error from iLO. + """ + # Getting target info + node = task.node + boot_volume = node.driver_internal_info.get('boot_from_volume') + volume = volume_target.VolumeTarget.get_by_uuid(task.context, + boot_volume) + properties = volume.properties + username = properties.get('auth_username', None) + password = properties.get('auth_password', None) + portal = properties['target_portal'] + iqn = properties['target_iqn'] + lun = properties['target_lun'] + host, port = portal.split(':') + + ilo_object = ilo_common.get_ilo_object(task.node) + try: + if username is None: + ilo_object.set_iscsi_info(iqn, lun, host, port) + else: + ilo_object.set_iscsi_info(iqn, lun, host, port, 'CHAP', + username, password) + except ilo_error.IloCommandNotSupportedInBiosError as ilo_exception: + operation = (_("Setting of target IQN %(target_iqn)s for node " + "%(node)s") + % {'target_iqn': iqn, 'node': node.uuid}) + raise exception.IloOperationNotSupported(operation=operation, + error=ilo_exception) + except ilo_error.IloError as ilo_exception: + operation = (_("Setting of target IQN %(target_iqn)s for node " + "%(node)s") + % {'target_iqn': iqn, 'node': node.uuid}) + raise exception.IloOperationError(operation=operation, + error=ilo_exception) + + @METRICS.timer('IloManagement.clear_iscsi_boot_target') + def clear_iscsi_boot_target(self, task): + """Unset iSCSI details of the system in UEFI boot mode. + + :param task: a task from TaskManager. + :raises: IloCommandNotSupportedInBiosError if system in BIOS boot mode. + :raises: IloError on an error from iLO. + """ + ilo_object = ilo_common.get_ilo_object(task.node) + try: + ilo_object.unset_iscsi_info() + except ilo_error.IloCommandNotSupportedInBiosError as ilo_exception: + operation = (_("Unsetting of iSCSI target for node %(node)s") + % {'node': task.node.uuid}) + raise exception.IloOperationNotSupported(operation=operation, + error=ilo_exception) + except ilo_error.IloError as ilo_exception: + operation = (_("Unsetting of iSCSI target for node %(node)s") + % {'node': task.node.uuid}) + raise exception.IloOperationError(operation=operation, + error=ilo_exception) diff --git a/ironic/tests/unit/drivers/modules/ilo/test_boot.py b/ironic/tests/unit/drivers/modules/ilo/test_boot.py index 1a5fd5de6f..252016dc92 100644 --- a/ironic/tests/unit/drivers/modules/ilo/test_boot.py +++ b/ironic/tests/unit/drivers/modules/ilo/test_boot.py @@ -34,7 +34,9 @@ from ironic.conductor import utils as manager_utils 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 pxe +from ironic.drivers.modules.storage import noop as noop_storage from ironic.drivers import utils as driver_utils from ironic.tests.unit.conductor import mgr_utils from ironic.tests.unit.db import base as db_base @@ -674,12 +676,14 @@ class IloVirtualMediaBootTestCase(db_base.DbTestCase): self.node = obj_utils.create_test_node( self.context, driver='iscsi_ilo', driver_info=INFO_DICT) + @mock.patch.object(noop_storage.NoopStorage, 'should_write_image', + autospec=True) @mock.patch.object(ilo_boot, '_validate_driver_info', spec_set=True, autospec=True) @mock.patch.object(ilo_boot, '_validate_instance_image_info', spec_set=True, autospec=True) def test_validate(self, mock_val_instance_image_info, - mock_val_driver_info): + mock_val_driver_info, storage_mock): instance_info = self.node.instance_info instance_info['ilo_boot_iso'] = 'deploy-iso' instance_info['image_source'] = '6b2f0c0c-79e8-4db6-842e-43c9764204af' @@ -689,10 +693,24 @@ class IloVirtualMediaBootTestCase(db_base.DbTestCase): shared=False) as task: task.node.driver_info['ilo_deploy_iso'] = 'deploy-iso' + storage_mock.return_value = True task.driver.boot.validate(task) mock_val_instance_image_info.assert_called_once_with(task) mock_val_driver_info.assert_called_once_with(task) + @mock.patch.object(noop_storage.NoopStorage, 'should_write_image', + autospec=True) + @mock.patch.object(ilo_boot, '_validate_driver_info', + spec_set=True, autospec=True) + def test_validate_boot_from_volume(self, mock_val_driver_info, + storage_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.driver_info['ilo_deploy_iso'] = 'deploy-iso' + storage_mock.return_value = False + task.driver.boot.validate(task) + mock_val_driver_info.assert_called_once_with(task) + @mock.patch.object(ilo_boot, 'prepare_node_for_deploy', spec_set=True, autospec=True) @mock.patch.object(manager_utils, 'node_power_action', @@ -834,6 +852,8 @@ class IloVirtualMediaBootTestCase(db_base.DbTestCase): self.assertFalse(setup_vmedia_mock.called) self.assertFalse(set_boot_device_mock.called) + @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, @@ -844,7 +864,8 @@ class IloVirtualMediaBootTestCase(db_base.DbTestCase): spec_set=True, autospec=True) def test_clean_up_instance(self, cleanup_iso_mock, cleanup_vmedia_mock, node_power_mock, - update_secure_boot_mode_mock): + update_secure_boot_mode_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 @@ -853,6 +874,62 @@ class IloVirtualMediaBootTestCase(db_base.DbTestCase): "12312642-09d3-467f-8e09-12385826a123") task.node.driver_internal_info = driver_internal_info task.node.save() + is_iscsi_boot_mock.return_value = False + task.driver.boot.clean_up_instance(task) + cleanup_iso_mock.assert_called_once_with(task.node) + cleanup_vmedia_mock.assert_called_once_with(task) + driver_internal_info = task.node.driver_internal_info + self.assertNotIn('boot_iso_created_in_web_server', + driver_internal_info) + self.assertNotIn('root_uuid_or_disk_id', driver_internal_info) + node_power_mock.assert_called_once_with(task, + states.POWER_OFF) + update_secure_boot_mode_mock.assert_called_once_with(task, False) + + @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) + node_power_mock.assert_called_once_with(task, + states.POWER_OFF) + clear_iscsi_boot_target_mock.assert_called_once_with(mock.ANY, + task) + update_secure_boot_mode_mock.assert_called_once_with(task, False) + self.assertIsNone(self.node.driver_internal_info.get( + 'ilo_uefi_iscsi_boot')) + + @mock.patch.object(ilo_common, 'cleanup_vmedia_boot', spec_set=True, + autospec=True) + @mock.patch.object(ilo_boot, '_clean_up_boot_iso_for_instance', + spec_set=True, autospec=True) + @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) + def test_clean_up_instance_boot_from_volume_bios( + self, node_power_mock, update_secure_boot_mode_mock, + is_iscsi_boot_mock, cleanup_iso_mock, cleanup_vmedia_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + is_iscsi_boot_mock.return_value = True task.driver.boot.clean_up_instance(task) cleanup_iso_mock.assert_called_once_with(task.node) cleanup_vmedia_mock.assert_called_once_with(task) @@ -872,6 +949,8 @@ class IloVirtualMediaBootTestCase(db_base.DbTestCase): task.driver.boot.clean_up_ramdisk(task) cleanup_vmedia_mock.assert_called_once_with(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(ilo_common, 'update_boot_mode', spec_set=True, @@ -882,9 +961,11 @@ class IloVirtualMediaBootTestCase(db_base.DbTestCase): autospec=True) def _test_prepare_instance_whole_disk_image( self, cleanup_vmedia_boot_mock, set_boot_device_mock, - update_boot_mode_mock, update_secure_boot_mode_mock): + update_boot_mode_mock, update_secure_boot_mode_mock, + is_iscsi_boot_mock): self.node.driver_internal_info = {'is_whole_disk_image': True} self.node.save() + is_iscsi_boot_mock.return_value = False with task_manager.acquire(self.context, self.node.uuid, shared=False) as task: task.driver.boot.prepare_instance(task) @@ -904,6 +985,8 @@ class IloVirtualMediaBootTestCase(db_base.DbTestCase): def test_prepare_instance_whole_disk_image(self): self._test_prepare_instance_whole_disk_image() + @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(ilo_common, 'update_boot_mode', spec_set=True, @@ -915,20 +998,71 @@ class IloVirtualMediaBootTestCase(db_base.DbTestCase): autospec=True) def test_prepare_instance_partition_image( self, cleanup_vmedia_boot_mock, configure_vmedia_mock, - update_boot_mode_mock, update_secure_boot_mode_mock): + update_boot_mode_mock, update_secure_boot_mode_mock, + is_iscsi_boot_mock): self.node.driver_internal_info = {'root_uuid_or_disk_id': ( "12312642-09d3-467f-8e09-12385826a123")} self.node.save() + is_iscsi_boot_mock.return_value = False with task_manager.acquire(self.context, self.node.uuid, shared=False) as task: task.driver.boot.prepare_instance(task) - cleanup_vmedia_boot_mock.assert_called_once_with(task) configure_vmedia_mock.assert_called_once_with( mock.ANY, task, "12312642-09d3-467f-8e09-12385826a123") update_boot_mode_mock.assert_called_once_with(task) update_secure_boot_mode_mock.assert_called_once_with(task, True) + @mock.patch.object(ilo_common, 'cleanup_vmedia_boot', spec_set=True, + autospec=True) + @mock.patch.object(deploy_utils, 'is_iscsi_boot', + spec_set=True, autospec=True) + @mock.patch.object(deploy_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, cleanup_vmedia_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) + cleanup_vmedia_boot_mock.assert_called_once_with(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) + + @mock.patch.object(ilo_common, 'cleanup_vmedia_boot', spec_set=True, + autospec=True) + @mock.patch.object(deploy_utils, 'is_iscsi_boot', + spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'get_boot_mode_for_deploy', + spec_set=True, autospec=True) + def test_prepare_instance_boot_from_volume_bios( + self, get_boot_mode_mock, + is_iscsi_boot_mock, cleanup_vmedia_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 = 'bios' + self.assertRaisesRegex(exception.InstanceDeployFailure, + "Virtual media can not boot volume " + "in BIOS boot mode.", + task.driver.boot.prepare_instance, task) + cleanup_vmedia_boot_mock.assert_called_once_with(task) + class IloPXEBootTestCase(db_base.DbTestCase): @@ -972,6 +1106,8 @@ class IloPXEBootTestCase(db_base.DbTestCase): pxe_prepare_instance_mock.assert_called_once_with(mock.ANY, task, None) + @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, @@ -979,15 +1115,39 @@ class IloPXEBootTestCase(db_base.DbTestCase): @mock.patch.object(pxe.PXEBoot, '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): + 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(pxe.PXEBoot, '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(deploy_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, @@ -996,11 +1156,95 @@ class IloPXEBootTestCase(db_base.DbTestCase): autospec=True) def test_prepare_instance(self, pxe_prepare_instance_mock, update_boot_mode_mock, - update_secure_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) + + @mock.patch.object(deploy_utils, 'is_iscsi_boot', + spec_set=True, autospec=True) + @mock.patch.object(deploy_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(pxe.PXEBoot, '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) + + @mock.patch.object(deploy_utils, 'is_iscsi_boot', + spec_set=True, autospec=True) + @mock.patch.object(deploy_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.assertIsNone(self.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(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(self.node.driver_internal_info.get( + 'ilo_uefi_iscsi_boot')) diff --git a/ironic/tests/unit/drivers/modules/ilo/test_management.py b/ironic/tests/unit/drivers/modules/ilo/test_management.py index 94fbbc16fc..04fa6c7e1f 100644 --- a/ironic/tests/unit/drivers/modules/ilo/test_management.py +++ b/ironic/tests/unit/drivers/modules/ilo/test_management.py @@ -16,6 +16,7 @@ import mock from oslo_utils import importutils +from oslo_utils import uuidutils from ironic.common import boot_devices from ironic.common import exception @@ -63,7 +64,7 @@ class IloManagementTestCase(db_base.DbTestCase): with task_manager.acquire(self.context, self.node.uuid, shared=False) as task: expected = [boot_devices.PXE, boot_devices.DISK, - boot_devices.CDROM] + boot_devices.CDROM, boot_devices.ISCSIBOOT] self.assertEqual( sorted(expected), sorted(task.driver.management. @@ -709,3 +710,132 @@ class IloManagementTestCase(db_base.DbTestCase): task.driver.management._update_firmware_sum_final( task, command) self.assertTrue(log_mock.exception.called) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_set_iscsi_boot_target_with_auth(self, get_ilo_object_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + vol_id = uuidutils.generate_uuid() + obj_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='iscsi', + boot_index=0, volume_id='1234', uuid=vol_id, + properties={'target_lun': 0, + 'target_portal': 'fake_host:3260', + 'target_iqn': 'fake_iqn', + 'auth_username': 'fake_username', + 'auth_password': 'fake_password'}) + driver_internal_info = task.node.driver_internal_info + driver_internal_info['boot_from_volume'] = vol_id + task.node.driver_internal_info = driver_internal_info + task.node.save() + ilo_object_mock = get_ilo_object_mock.return_value + task.driver.management.set_iscsi_boot_target(task) + ilo_object_mock.set_iscsi_info.assert_called_once_with( + 'fake_iqn', 0, 'fake_host', '3260', + 'CHAP', 'fake_username', 'fake_password') + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_set_iscsi_boot_target_without_auth(self, get_ilo_object_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + vol_id = uuidutils.generate_uuid() + obj_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='iscsi', + boot_index=0, volume_id='1234', uuid=vol_id, + properties={'target_lun': 0, + 'target_portal': 'fake_host:3260', + 'target_iqn': 'fake_iqn'}) + driver_internal_info = task.node.driver_internal_info + driver_internal_info['boot_from_volume'] = vol_id + task.node.driver_internal_info = driver_internal_info + task.node.save() + ilo_object_mock = get_ilo_object_mock.return_value + task.driver.management.set_iscsi_boot_target(task) + ilo_object_mock.set_iscsi_info.assert_called_once_with( + 'fake_iqn', 0, 'fake_host', '3260') + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_set_iscsi_boot_target_failed(self, get_ilo_object_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + vol_id = uuidutils.generate_uuid() + obj_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='iscsi', + boot_index=0, volume_id='1234', uuid=vol_id, + properties={'target_lun': 0, + 'target_portal': 'fake_host:3260', + 'target_iqn': 'fake_iqn', + 'auth_username': 'fake_username', + 'auth_password': 'fake_password'}) + driver_internal_info = task.node.driver_internal_info + driver_internal_info['boot_from_volume'] = vol_id + task.node.driver_internal_info = driver_internal_info + task.node.save() + ilo_object_mock = get_ilo_object_mock.return_value + ilo_object_mock.set_iscsi_info.side_effect = ( + ilo_error.IloError) + self.assertRaises(exception.IloOperationError, + task.driver.management.set_iscsi_boot_target, + task) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_set_iscsi_boot_target_in_bios(self, get_ilo_object_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + vol_id = uuidutils.generate_uuid() + obj_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='iscsi', + boot_index=0, volume_id='1234', uuid=vol_id, + properties={'target_lun': 0, + 'target_portal': 'fake_host:3260', + 'target_iqn': 'fake_iqn', + 'auth_username': 'fake_username', + 'auth_password': 'fake_password'}) + driver_internal_info = task.node.driver_internal_info + driver_internal_info['boot_from_volume'] = vol_id + task.node.driver_internal_info = driver_internal_info + task.node.save() + ilo_object_mock = get_ilo_object_mock.return_value + ilo_object_mock.set_iscsi_info.side_effect = ( + ilo_error.IloCommandNotSupportedInBiosError) + self.assertRaises(exception.IloOperationNotSupported, + task.driver.management.set_iscsi_boot_target, + task) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_clear_iscsi_boot_target(self, get_ilo_object_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ilo_object_mock = get_ilo_object_mock.return_value + + task.driver.management.clear_iscsi_boot_target(task) + ilo_object_mock.unset_iscsi_info.assert_called_once() + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_clear_iscsi_boot_target_failed(self, get_ilo_object_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ilo_object_mock = get_ilo_object_mock.return_value + ilo_object_mock.unset_iscsi_info.side_effect = ( + ilo_error.IloError) + self.assertRaises(exception.IloOperationError, + task.driver.management.clear_iscsi_boot_target, + task) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_clear_iscsi_boot_target_in_bios(self, get_ilo_object_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ilo_object_mock = get_ilo_object_mock.return_value + ilo_object_mock.unset_iscsi_info.side_effect = ( + ilo_error.IloCommandNotSupportedInBiosError) + self.assertRaises(exception.IloOperationNotSupported, + task.driver.management.clear_iscsi_boot_target, + task) diff --git a/ironic/tests/unit/drivers/third_party_driver_mocks.py b/ironic/tests/unit/drivers/third_party_driver_mocks.py index f813a69c07..33d3005783 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mocks.py +++ b/ironic/tests/unit/drivers/third_party_driver_mocks.py @@ -61,6 +61,8 @@ if not proliantutils: proliantutils.exception.IloError = type('IloError', (Exception,), {}) command_exception = type('IloCommandNotSupportedError', (Exception,), {}) proliantutils.exception.IloCommandNotSupportedError = command_exception + proliantutils.exception.IloCommandNotSupportedInBiosError = type( + 'IloCommandNotSupportedInBiosError', (Exception,), {}) proliantutils.exception.InvalidInputError = type( 'InvalidInputError', (Exception,), {}) proliantutils.exception.ImageExtractionFailed = type( diff --git a/releasenotes/notes/ilo-boot-from-iscsi-volume-41e8d510979c5037.yaml b/releasenotes/notes/ilo-boot-from-iscsi-volume-41e8d510979c5037.yaml new file mode 100644 index 0000000000..b10647d13e --- /dev/null +++ b/releasenotes/notes/ilo-boot-from-iscsi-volume-41e8d510979c5037.yaml @@ -0,0 +1,9 @@ +--- +features: + - Enhanced boot interface 'ilo-pxe' and 'ilo-virtual-media' to support + firmware based booting from iSCSI volume. +upgrade: + - The ``update_persistent_boot`` and ``[un]set_iscsi_info`` interfaces + of 'proliantutils' library has been enhanced to support booting from + an iSCSI volume. To leverage this feature, the 'proliantutils' library + needs to be upgraded to version '2.5.0'.