diff --git a/doc/source/admin/drivers/redfish.rst b/doc/source/admin/drivers/redfish.rst index 2e5a50a2b4..f784740dc8 100644 --- a/doc/source/admin/drivers/redfish.rst +++ b/doc/source/admin/drivers/redfish.rst @@ -185,6 +185,29 @@ property can be used to pass user-specified kernel command line parameters. For ramdisk kernel, ``[instance_info]/kernel_append_params`` property serves the same purpose. +Virtual Media Ramdisk +~~~~~~~~~~~~~~~~~~~~~ + +The ``ramdisk`` deploy interface can be used in concert with the the +``redfish-virtual-media`` boot interface to facilitate the boot of a remote +node utilizing pre-supplied virtual media. + +Instead of supplying an ``[instance_info]/image_source`` parameter, a +``[instance_info]/boot_iso`` parameter can be supplied. The image will +be downloaded by the conductor, and the instance will be booted using +the supplied ISO image. In accordance with the ``ramdisk`` deployment +interface behavior, once booted the machine will have a ``provision_state`` +of ``ACTIVE``. + +.. code-block:: bash + + openstack baremetal node set \ + --instance_info boot_iso=http://url/to.iso node-0 + +This initial interface does not support bootloader configuration +parameter injection, as such the ``[instance_info]/kernel_append_params`` +setting is ignored. + .. _Redfish: http://redfish.dmtf.org/ .. _Sushy: https://opendev.org/openstack/sushy .. _TLS: https://en.wikipedia.org/wiki/Transport_Layer_Security diff --git a/ironic/common/images.py b/ironic/common/images.py index f1dc7ad15a..31332df761 100644 --- a/ironic/common/images.py +++ b/ironic/common/images.py @@ -526,7 +526,7 @@ def get_temp_url_for_glance_image(context, image_uuid): def create_boot_iso(context, output_filename, kernel_href, ramdisk_href, deploy_iso_href=None, esp_image_href=None, root_uuid=None, kernel_params=None, boot_mode=None, - configdrive_href=None): + configdrive_href=None, base_iso=None): """Creates a bootable ISO image for a node. Given the hrefs for kernel, ramdisk, root partition's UUID and @@ -553,14 +553,26 @@ def create_boot_iso(context, output_filename, kernel_href, :param configdrive_href: URL to ISO9660 or FAT-formatted OpenStack config drive image. This image will be embedded into the built ISO image. Optional. + :param base_iso: URL or glance UUID of a to be used as an override of + what should be retrieved for to use, instead of building an ISO + bootable ramdisk. :raises: ImageCreationFailed, if creating boot ISO failed. """ with utils.tempdir() as tmpdir: - kernel_path = os.path.join(tmpdir, kernel_href.split('/')[-1]) - ramdisk_path = os.path.join(tmpdir, ramdisk_href.split('/')[-1]) - - fetch(context, kernel_href, kernel_path) - fetch(context, ramdisk_href, ramdisk_path) + if base_iso: + # NOTE(TheJulia): Eventually we want to use the creation method + # to perform the massaging of the image, because oddly enough + # we need to do all the same basic things, just a little + # differently. + fetch(context, base_iso, output_filename) + # Temporary, return to the caller until we support the combined + # operation. + return + else: + kernel_path = os.path.join(tmpdir, kernel_href.split('/')[-1]) + ramdisk_path = os.path.join(tmpdir, ramdisk_href.split('/')[-1]) + fetch(context, kernel_href, kernel_path) + fetch(context, ramdisk_href, ramdisk_path) if configdrive_href: configdrive_path = os.path.join( @@ -592,7 +604,11 @@ def create_boot_iso(context, output_filename, kernel_href, elif CONF.esp_image: esp_image_path = CONF.esp_image - + # TODO(TheJulia): we should opportunisticly try to make bios + # bootable and UEFI. In other words, collapse a lot of this + # path since they are not mutually exclusive. + # UEFI boot mode, but Network iPXE -> ISO means bios bootable + # contents are still required. create_esp_image_for_uefi( output_filename, kernel_path, ramdisk_path, deploy_iso=deploy_iso_path, esp_image=esp_image_path, diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py index 510c256346..71aaf7bc6c 100644 --- a/ironic/drivers/modules/deploy_utils.py +++ b/ironic/drivers/modules/deploy_utils.py @@ -511,6 +511,11 @@ def validate_image_properties(ctx, deploy_info, properties): the mentioned properties. """ image_href = deploy_info['image_source'] + boot_iso = deploy_info.get('boot_iso') + if image_href and boot_iso: + raise exception.InvalidParameterValue(_( + "An 'image_source' and 'boot_iso' parameter may not be " + "specified at the same time.")) try: img_service = image_service.get_image_service(image_href, context=ctx) image_props = img_service.show(image_href)['properties'] @@ -697,11 +702,21 @@ def get_image_instance_info(node): instance_info. Also raises same exception if kernel/ramdisk is missing in instance_info for non-glance images. """ + # TODO(TheJulia): We seem to have a lack of direct unit testing of this + # method, but that is likely okay. If memory serves we test this at + # a few different levels. That being said, it would be good for some + # more explicit unit testing to exist. info = {} - info['image_source'] = node.instance_info.get('image_source') is_whole_disk_image = node.driver_internal_info.get('is_whole_disk_image') - if not is_whole_disk_image: + boot_iso = node.instance_info.get('boot_iso') + + if not boot_iso: + info['image_source'] = node.instance_info.get('image_source') + else: + info['boot_iso'] = boot_iso + + if not is_whole_disk_image and not boot_iso: if not service_utils.is_glance_image(info['image_source']): info['kernel'] = node.instance_info.get('kernel') info['ramdisk'] = node.instance_info.get('ramdisk') diff --git a/ironic/drivers/modules/redfish/boot.py b/ironic/drivers/modules/redfish/boot.py index 8c5bfc8a98..445ee0d578 100644 --- a/ironic/drivers/modules/redfish/boot.py +++ b/ironic/drivers/modules/redfish/boot.py @@ -458,7 +458,7 @@ def _parse_deploy_info(node): def _prepare_iso_image(task, kernel_href, ramdisk_href, bootloader_href=None, configdrive=None, - root_uuid=None, params=None): + root_uuid=None, params=None, base_iso=None): """Prepare an ISO to boot the node. Build bootable ISO out of `kernel_href` and `ramdisk_href` (and @@ -486,23 +486,28 @@ def _prepare_iso_image(task, kernel_href, ramdisk_href, value. :raises: ImageCreationFailed, if creating ISO image failed. """ - if not kernel_href or not ramdisk_href: + if (not kernel_href or not ramdisk_href) and not base_iso: raise exception.InvalidParameterValue(_( - "Unable to find kernel or ramdisk for " - "building ISO for %(node)s") % + "Unable to find kernel, ramdisk for " + "building ISO, or explicit ISO for %(node)s") % {'node': task.node.uuid}) i_info = task.node.instance_info + # NOTE(TheJulia): Until we support modifying a base iso, most of + # this logic actually does nothing in the end. But it should! if deploy_utils.get_boot_option(task.node) == "ramdisk": - kernel_params = "root=/dev/ram0 text " - kernel_params += i_info.get("ramdisk_kernel_arguments", "") + if not base_iso: + kernel_params = "root=/dev/ram0 text " + kernel_params += i_info.get("ramdisk_kernel_arguments", "") + else: + kernel_params = None else: kernel_params = i_info.get( 'kernel_append_params', CONF.redfish.kernel_append_params) - if params: + if params and not base_iso: kernel_params = ' '.join( (kernel_params, ' '.join( '%s=%s' % kv for kv in params.items()))) @@ -527,7 +532,11 @@ def _prepare_iso_image(task, kernel_href, ramdisk_href, configdrive_href = configdrive - if configdrive: + # FIXME(TheJulia): This is treated as conditional with + # a base_iso as the intent, eventually, is to support + # injection into the supplied image. + + if configdrive and not base_iso: parsed_url = urlparse.urlparse(configdrive) if not parsed_url.scheme: cfgdrv_blob = base64.decode_as_bytes(configdrive) @@ -549,7 +558,8 @@ def _prepare_iso_image(task, kernel_href, ramdisk_href, configdrive_href=configdrive_href, root_uuid=root_uuid, kernel_params=kernel_params, - boot_mode=boot_mode) + boot_mode=boot_mode, + base_iso=base_iso) iso_object_name = _get_iso_image_name(task.node) @@ -597,6 +607,9 @@ def _prepare_deploy_iso(task, params, mode): ramdisk_href = d_info.get('%s_ramdisk' % mode) bootloader_href = d_info.get('bootloader') + # TODO(TheJulia): At some point we should support something like + # boot_iso for the deploy interface, perhaps when we support config + # injection. prepare_iso_image = functools.partial( _prepare_iso_image, task, kernel_href, ramdisk_href, bootloader_href=bootloader_href, params=params) @@ -656,8 +669,9 @@ def _prepare_boot_iso(task, root_uuid=None): kernel_href = node.instance_info.get('kernel') ramdisk_href = node.instance_info.get('ramdisk') + base_iso = node.instance_info.get('boot_iso') - if not kernel_href or not ramdisk_href: + if (not kernel_href or not ramdisk_href) and not base_iso: image_href = d_info['image_source'] @@ -671,17 +685,17 @@ def _prepare_boot_iso(task, root_uuid=None): if not ramdisk_href: ramdisk_href = image_properties.get('ramdisk_id') - if not kernel_href or not ramdisk_href: - raise exception.InvalidParameterValue(_( - "Unable to find kernel or ramdisk for " - "to generate boot ISO for %(node)s") % - {'node': task.node.uuid}) + if (not kernel_href or not ramdisk_href): + raise exception.InvalidParameterValue(_( + "Unable to find kernel or ramdisk for " + "to generate boot ISO for %(node)s") % + {'node': task.node.uuid}) bootloader_href = d_info.get('bootloader') return _prepare_iso_image( task, kernel_href, ramdisk_href, bootloader_href, - root_uuid=root_uuid) + root_uuid=root_uuid, base_iso=base_iso) class RedfishVirtualMediaBoot(base.BootInterface): @@ -767,7 +781,8 @@ class RedfishVirtualMediaBoot(base.BootInterface): if node.driver_internal_info.get('is_whole_disk_image'): props = [] - + elif d_info.get('boot_iso'): + props = ['boot_iso'] elif service_utils.is_glance_image(d_info['image_source']): props = ['kernel_id', 'ramdisk_id'] diff --git a/ironic/tests/unit/common/test_images.py b/ironic/tests/unit/common/test_images.py index 437bb41ef0..8a90ab55bb 100644 --- a/ironic/tests/unit/common/test_images.py +++ b/ironic/tests/unit/common/test_images.py @@ -911,6 +911,27 @@ class FsImageTestCase(base.TestCase): 'output_file', 'tmpdir/kernel-uuid', 'tmpdir/ramdisk-uuid', configdrive='tmpdir/configdrive', kernel_params=params) + @mock.patch.object(images, 'create_isolinux_image_for_bios', autospec=True) + @mock.patch.object(images, 'fetch', autospec=True) + @mock.patch.object(utils, 'tempdir', autospec=True) + def test_create_boot_iso_for_existing_iso(self, tempdir_mock, + fetch_images_mock, + create_isolinux_mock): + mock_file_handle = mock.MagicMock(spec=io.BytesIO) + mock_file_handle.__enter__.return_value = 'tmpdir' + tempdir_mock.return_value = mock_file_handle + base_iso = 'http://fake.local:1234/fake.iso' + images.create_boot_iso('ctx', 'output_file', 'kernel-uuid', + 'ramdisk-uuid', 'deploy_iso-uuid', + 'efiboot-uuid', None, + None, None, 'http://configdrive', + base_iso=base_iso) + + fetch_images_mock.assert_any_call( + 'ctx', base_iso, 'output_file') + + create_isolinux_mock.assert_not_called() + @mock.patch.object(image_service, 'get_image_service', autospec=True) def test_get_glance_image_properties_no_such_prop(self, image_service_mock): diff --git a/ironic/tests/unit/drivers/modules/redfish/test_boot.py b/ironic/tests/unit/drivers/modules/redfish/test_boot.py index 51177c5ee1..b365eb8307 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_boot.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_boot.py @@ -340,7 +340,8 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): url = redfish_boot._prepare_iso_image( task, 'http://kernel/img', 'http://ramdisk/img', - 'http://bootloader/img', root_uuid=task.node.uuid) + 'http://bootloader/img', root_uuid=task.node.uuid, + base_iso=None) object_name = 'boot-%s' % task.node.uuid @@ -352,7 +353,8 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): boot_mode='uefi', esp_image_href='http://bootloader/img', configdrive_href=mock.ANY, kernel_params='nofb nomodeset vga=normal', - root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123') + root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123', + base_iso=None) self.assertEqual(expected_url, url) @@ -381,7 +383,8 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): boot_mode=None, esp_image_href=None, configdrive_href=mock.ANY, kernel_params='nofb nomodeset vga=normal', - root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123') + root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123', + base_iso=None) self.assertEqual(expected_url, url) @@ -397,14 +400,39 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): redfish_boot._prepare_iso_image( task, 'http://kernel/img', 'http://ramdisk/img', - bootloader_href=None, root_uuid=task.node.uuid) + bootloader_href=None, root_uuid=task.node.uuid, + base_iso=None) mock_create_boot_iso.assert_called_once_with( mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img', boot_mode=None, esp_image_href=None, configdrive_href=mock.ANY, kernel_params=kernel_params, - root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123') + root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123', + base_iso=None) + + @mock.patch.object(redfish_boot, '_publish_image', autospec=True) + @mock.patch.object(images, 'create_boot_iso', autospec=True) + def test__prepare_iso_image_boot_iso( + self, mock_create_boot_iso, mock__publish_image): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + + task.node.instance_info = {'boot_iso': 'http://host/boot.iso', + 'capabilities': { + 'boot_option': 'ramdisk'}} + + redfish_boot._prepare_iso_image( + task, None, None, root_uuid=None, + base_iso='http://host/boot.iso') + + mock_create_boot_iso.assert_called_once_with( + mock.ANY, mock.ANY, None, None, + boot_mode=None, esp_image_href=None, + configdrive_href=None, + kernel_params=None, + root_uuid=None, + base_iso='http://host/boot.iso') @mock.patch.object(redfish_boot, '_prepare_iso_image', autospec=True) def test__prepare_deploy_iso(self, mock__prepare_iso_image): @@ -474,7 +502,30 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): mock__prepare_iso_image.assert_called_once_with( mock.ANY, 'http://kernel/img', 'http://ramdisk/img', - 'bootloader', root_uuid=task.node.uuid) + 'bootloader', root_uuid=task.node.uuid, base_iso=None) + + @mock.patch.object(redfish_boot, '_prepare_iso_image', autospec=True) + @mock.patch.object(images, 'create_boot_iso', autospec=True) + def test__prepare_boot_iso_user_supplied(self, mock_create_boot_iso, + mock__prepare_iso_image): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.driver_info.update( + {'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader'} + ) + + task.node.instance_info.update( + {'boot_iso': 'http://boot/iso'}) + + redfish_boot._prepare_boot_iso( + task, root_uuid=task.node.uuid) + + mock__prepare_iso_image.assert_called_once_with( + mock.ANY, None, None, + 'bootloader', root_uuid=task.node.uuid, + base_iso='http://boot/iso') @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) @mock.patch.object(deploy_utils, 'validate_image_properties', @@ -534,6 +585,64 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): mock_validate_image_properties.assert_called_once_with( mock.ANY, mock.ANY, mock.ANY) + @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) + @mock.patch.object(deploy_utils, 'validate_image_properties', + autospec=True) + @mock.patch.object(boot_mode_utils, 'get_boot_mode_for_deploy', + autospec=True) + def test_validate_bios_boot_iso(self, mock_get_boot_mode, + mock_validate_image_properties, + mock_parse_driver_info): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.instance_info.update( + {'boot_iso': 'http://localhost/file.iso'} + ) + + task.node.driver_info.update( + {'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader'} + ) + # NOTE(TheJulia): Boot mode doesn't matter for this + # test scenario. + mock_get_boot_mode.return_value = 'bios' + + task.driver.boot.validate(task) + + mock_validate_image_properties.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY) + + @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) + @mock.patch.object(deploy_utils, 'validate_image_properties', + autospec=True) + @mock.patch.object(boot_mode_utils, 'get_boot_mode_for_deploy', + autospec=True) + def test_validate_bios_boot_iso_conflicting_image_source( + self, mock_get_boot_mode, + mock_validate_image_properties, + mock_parse_driver_info): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.instance_info.update( + {'boot_iso': 'http://localhost/file.iso', + 'image_source': 'http://localhost/file.img'} + ) + + task.node.driver_info.update( + {'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader'} + ) + # NOTE(TheJulia): Boot mode doesn't matter for this + # test scenario. + mock_get_boot_mode.return_value = 'bios' + + task.driver.boot.validate(task) + + mock_validate_image_properties.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY) + @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) @mock.patch.object(deploy_utils, 'validate_image_properties', autospec=True) @@ -841,6 +950,85 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + 'clean_up_instance', autospec=True) + @mock.patch.object(redfish_boot, '_prepare_boot_iso', autospec=True) + @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True) + @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True) + @mock.patch.object(redfish_boot, '_parse_driver_info', autospec=True) + @mock.patch.object(redfish_boot, 'manager_utils', autospec=True) + @mock.patch.object(redfish_boot, 'deploy_utils', autospec=True) + @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) + def test_prepare_instance_ramdisk_boot_iso( + self, mock_boot_mode_utils, mock_deploy_utils, mock_manager_utils, + mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia, + mock__prepare_boot_iso, mock_clean_up_instance): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.DEPLOYING + task.node.driver_internal_info[ + 'root_uuid_or_disk_id'] = self.node.uuid + task.node.instance_info = {'boot_iso': 'http://host/boot.iso'} + + mock_deploy_utils.get_boot_option.return_value = 'ramdisk' + + mock__prepare_boot_iso.return_value = 'image-url' + + task.driver.boot.prepare_instance(task) + + mock__prepare_boot_iso.assert_called_once_with(task) + + mock__eject_vmedia.assert_called_once_with( + task, sushy.VIRTUAL_MEDIA_CD) + + mock__insert_vmedia.assert_called_once_with( + task, 'image-url', sushy.VIRTUAL_MEDIA_CD) + + mock_manager_utils.node_set_boot_device.assert_called_once_with( + task, boot_devices.CDROM, persistent=True) + + mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) + + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + 'clean_up_instance', autospec=True) + @mock.patch.object(redfish_boot, '_prepare_boot_iso', autospec=True) + @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True) + @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True) + @mock.patch.object(redfish_boot, '_parse_driver_info', autospec=True) + @mock.patch.object(redfish_boot, 'manager_utils', autospec=True) + @mock.patch.object(redfish_boot, 'deploy_utils', autospec=True) + @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) + def test_prepare_instance_ramdisk_boot_iso_boot( + self, mock_boot_mode_utils, mock_deploy_utils, mock_manager_utils, + mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia, + mock__prepare_boot_iso, mock_clean_up_instance): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.DEPLOYING + i_info = task.node.instance_info + i_info['boot_iso'] = "super-magic" + task.node.instance_info = i_info + mock_deploy_utils.get_boot_option.return_value = 'ramdisk' + + mock__prepare_boot_iso.return_value = 'image-url' + + task.driver.boot.prepare_instance(task) + + mock__prepare_boot_iso.assert_called_once_with(task) + + mock__eject_vmedia.assert_called_once_with( + task, sushy.VIRTUAL_MEDIA_CD) + + mock__insert_vmedia.assert_called_once_with( + task, 'image-url', sushy.VIRTUAL_MEDIA_CD) + + mock_manager_utils.node_set_boot_device.assert_called_once_with( + task, boot_devices.CDROM, persistent=True) + + mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) + @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True) @mock.patch.object(redfish_boot, '_cleanup_iso_image', autospec=True) @mock.patch.object(redfish_boot, 'manager_utils', autospec=True) diff --git a/ironic/tests/unit/drivers/modules/test_deploy_utils.py b/ironic/tests/unit/drivers/modules/test_deploy_utils.py index c474f5b1b2..0307fd8d24 100644 --- a/ironic/tests/unit/drivers/modules/test_deploy_utils.py +++ b/ironic/tests/unit/drivers/modules/test_deploy_utils.py @@ -1397,6 +1397,20 @@ class ValidateImagePropertiesTestCase(db_base.DbTestCase): inst_info, ['kernel', 'ramdisk']) self.assertEqual(expected_error, str(error)) + def test_validate_image_properties_boot_iso_conflict(self): + instance_info = { + 'image_source': 'http://ubuntu', + 'boot_iso': 'http://ubuntu.iso', + } + expected_error = ("An 'image_source' and 'boot_iso' " + "parameter may not be specified at " + "the same time.") + error = self.assertRaises(exception.InvalidParameterValue, + utils.validate_image_properties, + self.context, + instance_info, []) + self.assertEqual(expected_error, str(error)) + class ValidateParametersTestCase(db_base.DbTestCase): diff --git a/releasenotes/notes/add-redfish-boot_iso-pass-through-8a6f4d0c98ada1d5.yaml b/releasenotes/notes/add-redfish-boot_iso-pass-through-8a6f4d0c98ada1d5.yaml new file mode 100644 index 0000000000..8239910201 --- /dev/null +++ b/releasenotes/notes/add-redfish-boot_iso-pass-through-8a6f4d0c98ada1d5.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Adds functionality to allow a user to supply a node + ``instance_info/boot_iso`` parameter on machines utilizing the + ``redfish-virtual-media`` boot interface. When combined with the + ``ramdisk`` deployment interface, this allows an instance to boot + into a user supplied ISO image.