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.
|
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
|
Other references
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
@ -628,6 +628,13 @@ def get_instance_image_info(task, ipxe_enabled=False):
|
|||||||
else:
|
else:
|
||||||
root_dir = get_root_dir()
|
root_dir = get_root_dir()
|
||||||
i_info = node.instance_info
|
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')
|
labels = ('kernel', 'ramdisk')
|
||||||
d_info = deploy_utils.get_image_instance_info(node)
|
d_info = deploy_utils.get_image_instance_info(node)
|
||||||
if not (i_info.get('kernel') and i_info.get('ramdisk')):
|
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'])
|
i_info[label] = str(iproperties[label + '_id'])
|
||||||
node.instance_info = i_info
|
node.instance_info = i_info
|
||||||
node.save()
|
node.save()
|
||||||
|
|
||||||
for label in labels:
|
for label in labels:
|
||||||
image_info[label] = (
|
image_info[label] = (
|
||||||
i_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']
|
pxe_opts['ramdisk_opts'] = i_info['ramdisk_kernel_arguments']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
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
|
return pxe_opts
|
||||||
|
|
||||||
@ -937,7 +951,6 @@ def prepare_instance_pxe_config(task, image_info,
|
|||||||
is in use by the caller.
|
is in use by the caller.
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
node = task.node
|
node = task.node
|
||||||
# Generate options for both IPv4 and IPv6, and they can be
|
# Generate options for both IPv4 and IPv6, and they can be
|
||||||
# filtered down later based upon the port options.
|
# 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
|
:raises: MissingParameterValue if the image doesn't contain
|
||||||
the mentioned properties.
|
the mentioned properties.
|
||||||
"""
|
"""
|
||||||
image_href = deploy_info['image_source']
|
image_href = deploy_info.get('image_source')
|
||||||
boot_iso = deploy_info.get('boot_iso')
|
boot_iso = deploy_info.get('boot_iso')
|
||||||
if image_href and boot_iso:
|
if image_href and boot_iso:
|
||||||
raise exception.InvalidParameterValue(_(
|
raise exception.InvalidParameterValue(_(
|
||||||
"An 'image_source' and 'boot_iso' parameter may not be "
|
"An 'image_source' and 'boot_iso' parameter may not be "
|
||||||
"specified at the same time."))
|
"specified at the same time."))
|
||||||
|
if not image_href:
|
||||||
|
image_href = boot_iso
|
||||||
try:
|
try:
|
||||||
img_service = image_service.get_image_service(image_href, context=ctx)
|
img_service = image_service.get_image_service(image_href, context=ctx)
|
||||||
image_props = img_service.show(image_href)['properties']
|
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')
|
is_whole_disk_image = node.driver_internal_info.get('is_whole_disk_image')
|
||||||
boot_iso = node.instance_info.get('boot_iso')
|
boot_iso = node.instance_info.get('boot_iso')
|
||||||
|
|
||||||
if not boot_iso:
|
if not boot_iso:
|
||||||
info['image_source'] = node.instance_info.get('image_source')
|
info['image_source'] = node.instance_info.get('image_source')
|
||||||
else:
|
else:
|
||||||
|
@ -33,9 +33,14 @@ boot
|
|||||||
|
|
||||||
:boot_ramdisk
|
:boot_ramdisk
|
||||||
imgfree
|
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
|
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
|
initrd {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ pxe_options.ari_path }} || goto boot_ramdisk
|
||||||
boot
|
boot
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
{%- if pxe_options.boot_from_volume %}
|
{%- if pxe_options.boot_from_volume %}
|
||||||
|
|
||||||
:boot_iscsi
|
:boot_iscsi
|
||||||
|
@ -244,7 +244,6 @@ class PXEBaseMixin(object):
|
|||||||
boot_option = deploy_utils.get_boot_option(node)
|
boot_option = deploy_utils.get_boot_option(node)
|
||||||
boot_device = None
|
boot_device = None
|
||||||
instance_image_info = {}
|
instance_image_info = {}
|
||||||
|
|
||||||
if boot_option == "ramdisk":
|
if boot_option == "ramdisk":
|
||||||
instance_image_info = pxe_utils.get_instance_image_info(
|
instance_image_info = pxe_utils.get_instance_image_info(
|
||||||
task, ipxe_enabled=self.ipxe_enabled)
|
task, ipxe_enabled=self.ipxe_enabled)
|
||||||
@ -365,6 +364,14 @@ class PXEBaseMixin(object):
|
|||||||
{'node': node.uuid})
|
{'node': node.uuid})
|
||||||
pxe_utils.validate_boot_parameters_for_trusted_boot(node)
|
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)
|
pxe_utils.parse_driver_info(node)
|
||||||
|
|
||||||
@METRICS.timer('PXEBaseMixin.validate')
|
@METRICS.timer('PXEBaseMixin.validate')
|
||||||
@ -393,6 +400,8 @@ class PXEBaseMixin(object):
|
|||||||
if (node.driver_internal_info.get('is_whole_disk_image')
|
if (node.driver_internal_info.get('is_whole_disk_image')
|
||||||
or deploy_utils.get_boot_option(node) == 'local'):
|
or deploy_utils.get_boot_option(node) == 'local'):
|
||||||
props = []
|
props = []
|
||||||
|
elif d_info.get('boot_iso'):
|
||||||
|
props = ['boot_iso']
|
||||||
elif service_utils.is_glance_image(d_info['image_source']):
|
elif service_utils.is_glance_image(d_info['image_source']):
|
||||||
props = ['kernel_id', 'ramdisk_id']
|
props = ['kernel_id', 'ramdisk_id']
|
||||||
else:
|
else:
|
||||||
|
@ -108,6 +108,12 @@ class TestPXEUtils(db_base.DbTestCase):
|
|||||||
self.ipxe_options_boot_from_volume_extra_volume.pop(
|
self.ipxe_options_boot_from_volume_extra_volume.pop(
|
||||||
'initrd_filename', None)
|
'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)
|
self.node = object_utils.create_test_node(self.context)
|
||||||
|
|
||||||
def test_default_pxe_config(self):
|
def test_default_pxe_config(self):
|
||||||
@ -218,6 +224,27 @@ class TestPXEUtils(db_base.DbTestCase):
|
|||||||
expected_template = f.read().rstrip()
|
expected_template = f.read().rstrip()
|
||||||
self.assertEqual(str(expected_template), rendered_template)
|
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):
|
def test_default_grub_config(self):
|
||||||
pxe_opts = self.pxe_options
|
pxe_opts = self.pxe_options
|
||||||
pxe_opts['boot_mode'] = 'uefi'
|
pxe_opts['boot_mode'] = 'uefi'
|
||||||
@ -1040,6 +1067,20 @@ class PXEInterfacesTestCase(db_base.DbTestCase):
|
|||||||
image_info = pxe_utils.get_instance_image_info(task)
|
image_info = pxe_utils.get_instance_image_info(task)
|
||||||
self.assertEqual({}, image_info)
|
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)
|
@mock.patch.object(deploy_utils, 'fetch_images', autospec=True)
|
||||||
def test__cache_tftp_images_master_path(self, mock_fetch_image):
|
def test__cache_tftp_images_master_path(self, mock_fetch_image):
|
||||||
temp_dir = tempfile.mkdtemp()
|
temp_dir = tempfile.mkdtemp()
|
||||||
@ -1414,7 +1455,8 @@ class iPXEBuildConfigOptionsTestCase(db_base.DbTestCase):
|
|||||||
ipxe_use_swift=False,
|
ipxe_use_swift=False,
|
||||||
debug=False,
|
debug=False,
|
||||||
boot_from_volume=False,
|
boot_from_volume=False,
|
||||||
mode='deploy'):
|
mode='deploy',
|
||||||
|
iso_boot=False):
|
||||||
self.config(debug=debug)
|
self.config(debug=debug)
|
||||||
self.config(pxe_append_params='test_param', group='pxe')
|
self.config(pxe_append_params='test_param', group='pxe')
|
||||||
self.config(ipxe_timeout=ipxe_timeout, 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('deployment_ari_path')
|
||||||
expected_options.pop('initrd_filename')
|
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,
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
shared=True) as task:
|
shared=True) as task:
|
||||||
options = pxe_utils.build_pxe_config_options(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',
|
self._test_build_pxe_config_options_ipxe(mode='rescue',
|
||||||
ipxe_timeout=120)
|
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.common.utils.rmtree_without_raise', autospec=True)
|
||||||
@mock.patch('ironic_lib.utils.unlink_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):
|
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,
|
self.assertRaises(exception.MissingParameterValue,
|
||||||
task.driver.boot.validate, task)
|
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):
|
def test_validate_fail_missing_image_source(self):
|
||||||
info = dict(INST_INFO_DICT)
|
info = dict(INST_INFO_DICT)
|
||||||
del info['image_source']
|
del info['image_source']
|
||||||
@ -820,6 +844,52 @@ class iPXEBootTestCase(db_base.DbTestCase):
|
|||||||
boot_devices.PXE,
|
boot_devices.PXE,
|
||||||
persistent=True)
|
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(manager_utils, 'node_set_boot_device', autospec=True)
|
||||||
@mock.patch.object(pxe_utils, 'clean_up_pxe_config', autospec=True)
|
@mock.patch.object(pxe_utils, 'clean_up_pxe_config', autospec=True)
|
||||||
def test_prepare_instance_localboot(self, clean_up_pxe_config_mock,
|
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