iPXE ISO Ramdisk booting
Adds an iPXE interface to boot via a virtual media ISO as if it was virtual media. Story: 2007644 Task: 39823 Change-Id: Ie7971692758f3a5421f0826fdaf3d2366f652236
This commit is contained in:
parent
bd0033611d
commit
0cbb0397b1
@ -270,6 +270,78 @@ For iLO drivers, fields that should be provided are:
|
||||
images, the file system modification time is used.
|
||||
|
||||
|
||||
Ramdisk booting
|
||||
---------------
|
||||
|
||||
Advanced operators, specifically ones working with ephemeral workloads,
|
||||
may find it more useful to explicitly treat a node as one that would always
|
||||
boot from a Ramdisk.
|
||||
|
||||
This functionality is largely intended for network booting, however some
|
||||
other boot interface, such as the ``redfish-virtual-media`` support enabling
|
||||
the same basic functionality through the existing interfaces.
|
||||
|
||||
To use, a few different settings must be modified.
|
||||
|
||||
#. Change the ``deploy_interface`` on the node to ``ramdisk``::
|
||||
|
||||
openstack baremetal node set $NODE_UUID \
|
||||
--deploy-interface ramdisk
|
||||
|
||||
#. Set a kernel and ramdisk to be utilized::
|
||||
|
||||
openstack baremetal node set $NODE_UUID \
|
||||
--instance-info kernel=$KERNEL_URL \
|
||||
--instance-info ramdisk=$RAMDISK_URL
|
||||
|
||||
#. Deploy the node::
|
||||
|
||||
openstack baremetal node deploy $NODE_UUID
|
||||
|
||||
.. warning::
|
||||
Configuration drives, also known as a configdrive, is not supported
|
||||
with the ``ramdisk`` deploy interface. Please ensure your ramdisk
|
||||
CPIO archive contains all necessary configuration and credentials.
|
||||
This is as no disk image is written to the disk of the node being
|
||||
provisioned with a ramdisk.
|
||||
|
||||
The node ramdisk components will then be assembled by the conductor,
|
||||
appropriate configuration put in place, and the node will then be powered
|
||||
on. From there, normal node booting will occur. Upon undeployment of the node,
|
||||
normal cleaning proceedures will occur as configured with-in the conductor.
|
||||
|
||||
Ramdisk booting with ISO media
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Currently supported for the use of ramdisks with the ``redfish-virtual-media``
|
||||
and ``ipxe`` boot interfaces, an operator may request an explict ISO file to
|
||||
be booted.
|
||||
|
||||
#. Store the URL to the ISO image to ``instance_info/boot_iso``,
|
||||
instead of a ``kernel`` or ``ramdisk`` setting::
|
||||
|
||||
openstack barmetal node set $NODE_UUID \
|
||||
--instance-info boot_iso=$BOOT_ISO_URL
|
||||
|
||||
#. Deploy the node::
|
||||
|
||||
openstack baremetal node deploy $NODE_UUID
|
||||
|
||||
|
||||
.. warning::
|
||||
This feature, when utilized with the ``ipxe`` ``boot_interface``,
|
||||
will only allow a kernel and ramdisk to be booted from the
|
||||
supplied ISO file. Any additional contents, such as additional
|
||||
ramdisk contents or installer package files will be unavailable
|
||||
after the boot of the Operating System. Operators wishing to leverage
|
||||
this functionality for actions such as OS installation should explore
|
||||
use of the standard ``ramdisk`` ``deploy_interface`` along with the
|
||||
``instance_info/kernel_append_params`` setting to pass arbitrary
|
||||
settings such as a mirror URL for the initial ramdisk to load data from.
|
||||
This is a limitation of iPXE and the overall boot process of the
|
||||
operating system where memory allocated by iPXE is released.
|
||||
|
||||
|
||||
Other references
|
||||
----------------
|
||||
|
||||
|
@ -628,6 +628,13 @@ def get_instance_image_info(task, ipxe_enabled=False):
|
||||
else:
|
||||
root_dir = get_root_dir()
|
||||
i_info = node.instance_info
|
||||
if i_info.get('boot_iso'):
|
||||
image_info['boot_iso'] = (
|
||||
i_info['boot_iso'],
|
||||
os.path.join(root_dir, node.uuid, 'boot_iso'))
|
||||
|
||||
return image_info
|
||||
|
||||
labels = ('kernel', 'ramdisk')
|
||||
d_info = deploy_utils.get_image_instance_info(node)
|
||||
if not (i_info.get('kernel') and i_info.get('ramdisk')):
|
||||
@ -637,7 +644,6 @@ def get_instance_image_info(task, ipxe_enabled=False):
|
||||
i_info[label] = str(iproperties[label + '_id'])
|
||||
node.instance_info = i_info
|
||||
node.save()
|
||||
|
||||
for label in labels:
|
||||
image_info[label] = (
|
||||
i_info[label],
|
||||
@ -726,6 +732,14 @@ def build_instance_pxe_options(task, pxe_info, ipxe_enabled=False):
|
||||
pxe_opts['ramdisk_opts'] = i_info['ramdisk_kernel_arguments']
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
# TODO(TheJulia): Boot iso should change at a later point
|
||||
# if we serve more than just as a pass-through.
|
||||
if i_info.get('boot_iso'):
|
||||
pxe_opts['boot_iso_url'] = '/'.join(
|
||||
[CONF.deploy.http_url, node.uuid, 'boot_iso'])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return pxe_opts
|
||||
|
||||
@ -937,7 +951,6 @@ def prepare_instance_pxe_config(task, image_info,
|
||||
is in use by the caller.
|
||||
:returns: None
|
||||
"""
|
||||
|
||||
node = task.node
|
||||
# Generate options for both IPv4 and IPv6, and they can be
|
||||
# filtered down later based upon the port options.
|
||||
|
@ -510,12 +510,14 @@ def validate_image_properties(ctx, deploy_info, properties):
|
||||
:raises: MissingParameterValue if the image doesn't contain
|
||||
the mentioned properties.
|
||||
"""
|
||||
image_href = deploy_info['image_source']
|
||||
image_href = deploy_info.get('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."))
|
||||
if not image_href:
|
||||
image_href = boot_iso
|
||||
try:
|
||||
img_service = image_service.get_image_service(image_href, context=ctx)
|
||||
image_props = img_service.show(image_href)['properties']
|
||||
@ -710,7 +712,6 @@ def get_image_instance_info(node):
|
||||
|
||||
is_whole_disk_image = node.driver_internal_info.get('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:
|
||||
|
@ -33,9 +33,14 @@ boot
|
||||
|
||||
:boot_ramdisk
|
||||
imgfree
|
||||
{%- if pxe_options.boot_iso_url %}
|
||||
sanboot {{ pxe_options.boot_iso_url }}
|
||||
{%- else %}
|
||||
kernel {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ pxe_options.aki_path }} root=/dev/ram0 text {{ pxe_options.pxe_append_params|default("", true) }} {{ pxe_options.ramdisk_opts|default('', true) }} initrd=ramdisk || goto boot_ramdisk
|
||||
initrd {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ pxe_options.ari_path }} || goto boot_ramdisk
|
||||
boot
|
||||
{%- endif %}
|
||||
|
||||
{%- if pxe_options.boot_from_volume %}
|
||||
|
||||
:boot_iscsi
|
||||
|
@ -244,7 +244,6 @@ class PXEBaseMixin(object):
|
||||
boot_option = deploy_utils.get_boot_option(node)
|
||||
boot_device = None
|
||||
instance_image_info = {}
|
||||
|
||||
if boot_option == "ramdisk":
|
||||
instance_image_info = pxe_utils.get_instance_image_info(
|
||||
task, ipxe_enabled=self.ipxe_enabled)
|
||||
@ -365,6 +364,14 @@ class PXEBaseMixin(object):
|
||||
{'node': node.uuid})
|
||||
pxe_utils.validate_boot_parameters_for_trusted_boot(node)
|
||||
|
||||
# Check if we have invalid parameters being passed which will not work
|
||||
# for ramdisk configurations.
|
||||
if (node.instance_info.get('image_source')
|
||||
and node.instance_info.get('boot_iso')):
|
||||
raise exception.InvalidParameterValue(_(
|
||||
"An 'image_source' and 'boot_iso' parameter may not be "
|
||||
"specified at the same time."))
|
||||
|
||||
pxe_utils.parse_driver_info(node)
|
||||
|
||||
@METRICS.timer('PXEBaseMixin.validate')
|
||||
@ -393,6 +400,8 @@ class PXEBaseMixin(object):
|
||||
if (node.driver_internal_info.get('is_whole_disk_image')
|
||||
or deploy_utils.get_boot_option(node) == 'local'):
|
||||
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']
|
||||
else:
|
||||
|
@ -108,6 +108,12 @@ class TestPXEUtils(db_base.DbTestCase):
|
||||
self.ipxe_options_boot_from_volume_extra_volume.pop(
|
||||
'initrd_filename', None)
|
||||
|
||||
self.ipxe_options_boot_from_iso = self.ipxe_options.copy()
|
||||
self.ipxe_options_boot_from_iso.update({
|
||||
'boot_from_iso': True,
|
||||
'boot_iso_url': 'http://1.2.3.4:1234/uuid/iso'
|
||||
})
|
||||
|
||||
self.node = object_utils.create_test_node(self.context)
|
||||
|
||||
def test_default_pxe_config(self):
|
||||
@ -218,6 +224,27 @@ class TestPXEUtils(db_base.DbTestCase):
|
||||
expected_template = f.read().rstrip()
|
||||
self.assertEqual(str(expected_template), rendered_template)
|
||||
|
||||
def test_default_ipxe_boot_from_iso(self):
|
||||
self.config(
|
||||
pxe_config_template='ironic/drivers/modules/ipxe_config.template',
|
||||
group='pxe'
|
||||
)
|
||||
self.config(http_url='http://1.2.3.4:1234', group='deploy')
|
||||
|
||||
pxe_options = self.ipxe_options_boot_from_iso
|
||||
|
||||
rendered_template = utils.render_template(
|
||||
CONF.pxe.pxe_config_template,
|
||||
{'pxe_options': pxe_options,
|
||||
'ROOT': '{{ ROOT }}'},
|
||||
)
|
||||
|
||||
templ_file = 'ironic/tests/unit/drivers/' \
|
||||
'ipxe_config_boot_from_iso.template'
|
||||
with open(templ_file) as f:
|
||||
expected_template = f.read().rstrip()
|
||||
self.assertEqual(str(expected_template), rendered_template)
|
||||
|
||||
def test_default_grub_config(self):
|
||||
pxe_opts = self.pxe_options
|
||||
pxe_opts['boot_mode'] = 'uefi'
|
||||
@ -1040,6 +1067,20 @@ class PXEInterfacesTestCase(db_base.DbTestCase):
|
||||
image_info = pxe_utils.get_instance_image_info(task)
|
||||
self.assertEqual({}, image_info)
|
||||
|
||||
@mock.patch('ironic.drivers.modules.deploy_utils.get_boot_option',
|
||||
return_value='ramdisk')
|
||||
def test_get_instance_image_info_boot_iso(self, boot_opt_mock):
|
||||
self.node.instance_info = {'boot_iso': 'http://localhost/boot.iso'}
|
||||
self.node.save()
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
image_info = pxe_utils.get_instance_image_info(
|
||||
task, ipxe_enabled=True)
|
||||
self.assertEqual('http://localhost/boot.iso',
|
||||
image_info['boot_iso'][0])
|
||||
|
||||
boot_opt_mock.assert_called_once_with(task.node)
|
||||
|
||||
@mock.patch.object(deploy_utils, 'fetch_images', autospec=True)
|
||||
def test__cache_tftp_images_master_path(self, mock_fetch_image):
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
@ -1414,7 +1455,8 @@ class iPXEBuildConfigOptionsTestCase(db_base.DbTestCase):
|
||||
ipxe_use_swift=False,
|
||||
debug=False,
|
||||
boot_from_volume=False,
|
||||
mode='deploy'):
|
||||
mode='deploy',
|
||||
iso_boot=False):
|
||||
self.config(debug=debug)
|
||||
self.config(pxe_append_params='test_param', group='pxe')
|
||||
self.config(ipxe_timeout=ipxe_timeout, group='pxe')
|
||||
@ -1520,6 +1562,19 @@ class iPXEBuildConfigOptionsTestCase(db_base.DbTestCase):
|
||||
expected_options.pop('deployment_ari_path')
|
||||
expected_options.pop('initrd_filename')
|
||||
|
||||
if iso_boot:
|
||||
self.node.instance_info = {'boot_iso': 'http://test.url/file.iso'}
|
||||
self.node.save()
|
||||
print(expected_options)
|
||||
print(image_info)
|
||||
iso_url = os.path.join(http_url, self.node.uuid, 'boot_iso')
|
||||
expected_options.update(
|
||||
{
|
||||
'boot_iso_url': iso_url
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
options = pxe_utils.build_pxe_config_options(task,
|
||||
@ -1708,6 +1763,9 @@ class iPXEBuildConfigOptionsTestCase(db_base.DbTestCase):
|
||||
self._test_build_pxe_config_options_ipxe(mode='rescue',
|
||||
ipxe_timeout=120)
|
||||
|
||||
def test_build_pxe_config_options_ipxe_boot_iso(self):
|
||||
self._test_build_pxe_config_options_ipxe(iso_boot=True)
|
||||
|
||||
@mock.patch('ironic.common.utils.rmtree_without_raise', autospec=True)
|
||||
@mock.patch('ironic_lib.utils.unlink_without_raise', autospec=True)
|
||||
def test_clean_up_ipxe_config_uefi(self, unlink_mock, rmtree_mock):
|
||||
|
39
ironic/tests/unit/drivers/ipxe_config_boot_from_iso.template
Normal file
39
ironic/tests/unit/drivers/ipxe_config_boot_from_iso.template
Normal file
@ -0,0 +1,39 @@
|
||||
#!ipxe
|
||||
|
||||
set attempts:int32 10
|
||||
set i:int32 0
|
||||
|
||||
goto deploy
|
||||
|
||||
:deploy
|
||||
imgfree
|
||||
kernel http://1.2.3.4:1234/deploy_kernel selinux=0 troubleshoot=0 text test_param BOOTIF=${mac} initrd=deploy_ramdisk || goto retry
|
||||
|
||||
initrd http://1.2.3.4:1234/deploy_ramdisk || goto retry
|
||||
boot
|
||||
|
||||
:retry
|
||||
iseq ${i} ${attempts} && goto fail ||
|
||||
inc i
|
||||
echo No response, retrying in {i} seconds.
|
||||
sleep ${i}
|
||||
goto deploy
|
||||
|
||||
:fail
|
||||
echo Failed to get a response after ${attempts} attempts
|
||||
echo Powering off in 30 seconds.
|
||||
sleep 30
|
||||
poweroff
|
||||
|
||||
:boot_partition
|
||||
imgfree
|
||||
kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param initrd=ramdisk || goto boot_partition
|
||||
initrd http://1.2.3.4:1234/ramdisk || goto boot_partition
|
||||
boot
|
||||
|
||||
:boot_ramdisk
|
||||
imgfree
|
||||
sanboot http://1.2.3.4:1234/uuid/iso
|
||||
|
||||
:boot_whole_disk
|
||||
sanboot --no-describe
|
@ -131,6 +131,30 @@ class iPXEBootTestCase(db_base.DbTestCase):
|
||||
self.assertRaises(exception.MissingParameterValue,
|
||||
task.driver.boot.validate, task)
|
||||
|
||||
@mock.patch.object(image_service.GlanceImageService, 'show', autospec=True)
|
||||
@mock.patch('ironic.drivers.modules.deploy_utils.get_boot_option',
|
||||
return_value='ramdisk', autospec=True)
|
||||
def test_validate_with_boot_iso(self, mock_boot_option, mock_glance):
|
||||
i_info = self.node.driver_info
|
||||
i_info['boot_iso'] = "http://localhost:1234/boot.iso"
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
task.driver.boot.validate(task)
|
||||
self.assertTrue(mock_boot_option.called)
|
||||
self.assertTrue(mock_glance.called)
|
||||
|
||||
def test_validate_with_boot_iso_and_image_source(self):
|
||||
i_info = self.node.instance_info
|
||||
i_info['image_source'] = "http://localhost:1234/image"
|
||||
i_info['boot_iso'] = "http://localhost:1234/boot.iso"
|
||||
self.node.instance_info = i_info
|
||||
self.node.save()
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
task.driver.boot.validate,
|
||||
task)
|
||||
|
||||
def test_validate_fail_missing_image_source(self):
|
||||
info = dict(INST_INFO_DICT)
|
||||
del info['image_source']
|
||||
@ -820,6 +844,52 @@ class iPXEBootTestCase(db_base.DbTestCase):
|
||||
boot_devices.PXE,
|
||||
persistent=True)
|
||||
|
||||
@mock.patch('os.path.isfile', lambda filename: False)
|
||||
@mock.patch.object(pxe_utils, 'create_pxe_config', autospec=True)
|
||||
@mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'switch_pxe_config', autospec=True)
|
||||
@mock.patch.object(dhcp_factory, 'DHCPFactory', autospec=True)
|
||||
@mock.patch.object(pxe_utils, 'cache_ramdisk_kernel', autospec=True)
|
||||
@mock.patch.object(pxe_utils, 'get_instance_image_info', autospec=True)
|
||||
def test_prepare_instance_netboot_ramdisk(
|
||||
self, get_image_info_mock, cache_mock,
|
||||
dhcp_factory_mock, switch_pxe_config_mock,
|
||||
set_boot_device_mock, create_pxe_config_mock):
|
||||
http_url = 'http://192.1.2.3:1234'
|
||||
self.config(http_url=http_url, group='deploy')
|
||||
provider_mock = mock.MagicMock()
|
||||
dhcp_factory_mock.return_value = provider_mock
|
||||
self.node.instance_info = {'boot_iso': 'http://1.2.3.4:1234/boot.iso',
|
||||
'capabilities': {'boot_option': 'ramdisk'}}
|
||||
image_info = {'kernel': ('', '/path/to/kernel'),
|
||||
'deploy_kernel': ('', '/path/to/kernel'),
|
||||
'ramdisk': ('', '/path/to/ramdisk'),
|
||||
'deploy_ramdisk': ('', '/path/to/ramdisk')}
|
||||
get_image_info_mock.return_value = image_info
|
||||
self.node.provision_state = states.DEPLOYING
|
||||
self.node.save()
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
print(task.node)
|
||||
dhcp_opts = pxe_utils.dhcp_options_for_instance(task,
|
||||
ipxe_enabled=True)
|
||||
dhcp_opts += pxe_utils.dhcp_options_for_instance(
|
||||
task, ipxe_enabled=True, ip_version=6)
|
||||
pxe_config_path = pxe_utils.get_pxe_config_file_path(
|
||||
task.node.uuid, ipxe_enabled=True)
|
||||
task.driver.boot.prepare_instance(task)
|
||||
self.assertTrue(get_image_info_mock.called)
|
||||
self.assertTrue(cache_mock.called)
|
||||
provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts)
|
||||
create_pxe_config_mock.assert_called_once_with(
|
||||
task, mock.ANY, CONF.pxe.ipxe_config_template,
|
||||
ipxe_enabled=True)
|
||||
switch_pxe_config_mock.assert_called_once_with(
|
||||
pxe_config_path, None, boot_modes.LEGACY_BIOS, False,
|
||||
ipxe_enabled=True, iscsi_boot=False, ramdisk_boot=True)
|
||||
set_boot_device_mock.assert_called_once_with(task,
|
||||
boot_devices.PXE,
|
||||
persistent=True)
|
||||
|
||||
@mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True)
|
||||
@mock.patch.object(pxe_utils, 'clean_up_pxe_config', autospec=True)
|
||||
def test_prepare_instance_localboot(self, clean_up_pxe_config_mock,
|
||||
|
@ -0,0 +1,12 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Adds functionality to the ``ipxe`` boot interface to support use of an
|
||||
``instance_info\boot_iso`` value with the ``ramdisk`` deployment interface.
|
||||
other:
|
||||
- |
|
||||
Support for iPXE booting a ISO medium will only work if the ramdisk loaded
|
||||
by the bootloader contains all artifacts required for the booting operating
|
||||
system to load. This is a limitation of iPXE and x86 systems architecture,
|
||||
as the memory allocated for the rest of the ISO disk image in memory is
|
||||
freed by the booting kernel.
|
Loading…
Reference in New Issue
Block a user