Iso booting via redfish virtual media

Adds support to signal a pass-through request to the
redfish-virtual-media boot interface so a user can supply
a boot ISO to boot the machine.

Tested on an HPE Edgeline e910 series machine using the
``redfish-virtual-media`` boot interface.

Story: 2007633
Task: 39823

Change-Id: Ie74472969c75994794dc0ca19bbe7cfd395855c9
This commit is contained in:
Julia Kreger 2020-05-12 16:19:34 -07:00
parent bf65acf6ba
commit bd0033611d
8 changed files with 332 additions and 32 deletions

View File

@ -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

View File

@ -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,

View File

@ -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')

View File

@ -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']

View File

@ -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):

View File

@ -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)

View File

@ -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):

View File

@ -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.