Add UEFI support for iPXE

This patch adds UEFI support for iPXE, the changes made are:

* Remove conditional preventing iPXE to be configured with UEFI

* Add the boot_mode= kernel parameter to the iPXE template

* Add initrd=deploy_ramdisk kernel parameter to the iPXE template. The
  UEFI support in iPXE requires the kernel argument to match what the
  initrd expects. For more information see [0]

[0] http://forum.ipxe.org/showthread.php?tid=7589&pid=11843#pid11843

Closes-Bug: #1525989
Change-Id: I6e74bc6332c5aba92ef0de8694fd4259c596cf03
This commit is contained in:
Lucas Alvares Gomes 2015-12-14 16:55:21 +00:00
parent effb9638aa
commit 2a5005d946
9 changed files with 155 additions and 52 deletions

View File

@ -985,20 +985,20 @@ on the Bare Metal service node(s) where ``ironic-conductor`` is running.
Fedora 22 or higher:
dnf install ipxe-bootimgs
#. Copy the iPXE boot image (undionly.kpxe) to ``/tftpboot``. The binary
might be found at::
#. Copy the iPXE boot image (``undionly.kpxe`` for **BIOS** and
``ipxe.efi`` for **UEFI**) to ``/tftpboot``. The binary might
be found at::
Ubuntu:
cp /usr/lib/ipxe/undionly.kpxe /tftpboot
cp /usr/lib/ipxe/{undionly.kpxe,ipxe.efi} /tftpboot
Fedora/RHEL7/CentOS7:
cp /usr/share/ipxe/undionly.kpxe /tftpboot
cp /usr/share/ipxe/{undionly.kpxe,ipxe.efi} /tftpboot
.. note::
If the packaged version of the iPXE boot image doesn't work, you
can download a prebuilt one from http://boot.ipxe.org/undionly.kpxe
or build one image from source, see http://ipxe.org/download for
more information.
If the packaged version of the iPXE boot image doesn't work, you can
download a prebuilt one from http://boot.ipxe.org or build one image
from source, see http://ipxe.org/download for more information.
#. Enable/Configure iPXE in the Bare Metal Service's configuration file
(/etc/ironic/ironic.conf)::
@ -1011,9 +1011,16 @@ on the Bare Metal service node(s) where ``ironic-conductor`` is running.
# Neutron bootfile DHCP parameter. (string value)
pxe_bootfile_name=undionly.kpxe
# Bootfile DHCP parameter for UEFI boot mode. (string value)
uefi_pxe_bootfile_name=ipxe.efi
# Template file for PXE configuration. (string value)
pxe_config_template=$pybasedir/drivers/modules/ipxe_config.template
# Template file for PXE configuration for UEFI boot loader.
# (string value)
uefi_pxe_config_template=$pybasedir/drivers/modules/ipxe_config.template
#. Restart the ``ironic-conductor`` process::
Fedora/RHEL7/CentOS7:

View File

@ -243,7 +243,7 @@ def create_pxe_config(task, pxe_options, template=None):
pxe_config_disk_ident)
utils.write_to_file(pxe_config_file_path, pxe_config)
if is_uefi_boot_mode:
if is_uefi_boot_mode and not CONF.pxe.ipxe_enabled:
_link_ip_address_pxe_configs(task, hex_form)
else:
_link_mac_pxe_configs(task)
@ -257,7 +257,9 @@ def clean_up_pxe_config(task):
"""
LOG.debug("Cleaning up PXE config for node %s", task.node.uuid)
if deploy_utils.get_boot_mode_for_deploy(task.node) == 'uefi':
is_uefi_boot_mode = (deploy_utils.get_boot_mode_for_deploy(task.node) ==
'uefi')
if is_uefi_boot_mode and not CONF.pxe.ipxe_enabled:
api = dhcp_factory.DHCPFactory().provider
ip_addresses = api.get_ip_addresses(task)
if not ip_addresses:
@ -297,6 +299,12 @@ def dhcp_options_for_instance(task):
:param task: A TaskManager instance.
"""
dhcp_opts = []
if deploy_utils.get_boot_mode_for_deploy(task.node) == 'uefi':
boot_file = CONF.pxe.uefi_pxe_bootfile_name
else:
boot_file = CONF.pxe.pxe_bootfile_name
if CONF.pxe.ipxe_enabled:
script_name = os.path.basename(CONF.pxe.ipxe_boot_script)
ipxe_script_url = '/'.join([CONF.deploy.http_url, script_name])
@ -307,22 +315,17 @@ def dhcp_options_for_instance(task):
# Neutron use dnsmasq as default DHCP agent, add extra config
# to neutron "dhcp-match=set:ipxe,175" and use below option
dhcp_opts.append({'opt_name': 'tag:!ipxe,bootfile-name',
'opt_value': CONF.pxe.pxe_bootfile_name})
'opt_value': boot_file})
dhcp_opts.append({'opt_name': 'tag:ipxe,bootfile-name',
'opt_value': ipxe_script_url})
else:
# !175 == non-iPXE.
# http://ipxe.org/howto/dhcpd#ipxe-specific_options
dhcp_opts.append({'opt_name': '!175,bootfile-name',
'opt_value': CONF.pxe.pxe_bootfile_name})
'opt_value': boot_file})
dhcp_opts.append({'opt_name': 'bootfile-name',
'opt_value': ipxe_script_url})
else:
if deploy_utils.get_boot_mode_for_deploy(task.node) == 'uefi':
boot_file = CONF.pxe.uefi_pxe_bootfile_name
else:
boot_file = CONF.pxe.pxe_bootfile_name
dhcp_opts.append({'opt_name': 'bootfile-name',
'opt_value': boot_file})

View File

@ -5,7 +5,7 @@ dhcp
goto deploy
:deploy
kernel {{ pxe_options.deployment_aki_path }} selinux=0 disk={{ pxe_options.disk }} iscsi_target_iqn={{ pxe_options.iscsi_target_iqn }} deployment_id={{ pxe_options.deployment_id }} deployment_key={{ pxe_options.deployment_key }} ironic_api_url={{ pxe_options.ironic_api_url }} troubleshoot=0 text {{ pxe_options.pxe_append_params|default("", true) }} boot_option={{ pxe_options.boot_option }} ip=${ip}:${next-server}:${gateway}:${netmask} BOOTIF=${mac} {% if pxe_options.root_device %}root_device={{ pxe_options.root_device }}{% endif %} ipa-api-url={{ pxe_options['ipa-api-url'] }} ipa-driver-name={{ pxe_options['ipa-driver-name'] }} coreos.configdrive=0
kernel {{ pxe_options.deployment_aki_path }} selinux=0 disk={{ pxe_options.disk }} iscsi_target_iqn={{ pxe_options.iscsi_target_iqn }} deployment_id={{ pxe_options.deployment_id }} deployment_key={{ pxe_options.deployment_key }} ironic_api_url={{ pxe_options.ironic_api_url }} troubleshoot=0 text {{ pxe_options.pxe_append_params|default("", true) }} boot_option={{ pxe_options.boot_option }} ip=${ip}:${next-server}:${gateway}:${netmask} BOOTIF=${mac} {% if pxe_options.root_device %}root_device={{ pxe_options.root_device }}{% endif %} ipa-api-url={{ pxe_options['ipa-api-url'] }} ipa-driver-name={{ pxe_options['ipa-driver-name'] }} boot_mode={{ pxe_options['boot_mode'] }} initrd=deploy_ramdisk coreos.configdrive=0
initrd {{ pxe_options.deployment_ari_path }}
boot

View File

@ -404,14 +404,6 @@ class PXEBoot(base.BootInterface):
raise exception.MissingParameterValue(_(
"iPXE boot is enabled but no HTTP URL or HTTP "
"root was specified."))
# iPXE and UEFI should not be configured together.
if boot_mode == 'uefi':
LOG.error(_LE("UEFI boot mode is not supported with "
"iPXE boot enabled."))
raise exception.InvalidParameterValue(_(
"Conflict: iPXE is enabled, but cannot be used with node"
"%(node_uuid)s configured to use UEFI boot") %
{'node_uuid': node.uuid})
if boot_mode == 'uefi':
validate_boot_option_for_uefi(node)

View File

@ -84,6 +84,16 @@ class TestPXEUtils(db_base.DbTestCase):
'ari_path': 'http://1.2.3.4:1234/ramdisk',
})
self.ipxe_options_bios = {
'boot_mode': 'bios',
}
self.ipxe_options_bios.update(self.ipxe_options)
self.ipxe_options_uefi = {
'boot_mode': 'uefi',
}
self.ipxe_options_uefi.update(self.ipxe_options)
self.node = object_utils.create_test_node(self.context)
def test__build_pxe_config(self):
@ -108,7 +118,7 @@ class TestPXEUtils(db_base.DbTestCase):
self.assertEqual(six.text_type(expected_template), rendered_template)
def test__build_ipxe_config(self):
def test__build_ipxe_bios_config(self):
# NOTE(lucasagomes): iPXE is just an extension of the PXE driver,
# it doesn't have it's own configuration option for template.
# More info:
@ -119,7 +129,7 @@ class TestPXEUtils(db_base.DbTestCase):
)
self.config(http_url='http://1.2.3.4:1234', group='deploy')
rendered_template = pxe_utils._build_pxe_config(
self.ipxe_options, CONF.pxe.pxe_config_template,
self.ipxe_options_bios, CONF.pxe.pxe_config_template,
'{{ ROOT }}', '{{ DISK_IDENTIFIER }}')
expected_template = open(
@ -127,6 +137,26 @@ class TestPXEUtils(db_base.DbTestCase):
self.assertEqual(six.text_type(expected_template), rendered_template)
def test__build_ipxe_uefi_config(self):
# NOTE(lucasagomes): iPXE is just an extension of the PXE driver,
# it doesn't have it's own configuration option for template.
# More info:
# http://docs.openstack.org/developer/ironic/deploy/install-guide.html
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')
rendered_template = pxe_utils._build_pxe_config(
self.ipxe_options_uefi, CONF.pxe.pxe_config_template,
'{{ ROOT }}', '{{ DISK_IDENTIFIER }}')
expected_template = open(
'ironic/tests/unit/drivers/'
'ipxe_uefi_config.template').read().rstrip()
self.assertEqual(six.text_type(expected_template), rendered_template)
def test__build_elilo_config(self):
pxe_opts = self.pxe_options
pxe_opts['boot_mode'] = 'uefi'
@ -311,6 +341,36 @@ class TestPXEUtils(db_base.DbTestCase):
pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid)
write_mock.assert_called_with(pxe_cfg_file_path, self.pxe_options_uefi)
@mock.patch('ironic.common.pxe_utils._link_mac_pxe_configs',
autospec=True)
@mock.patch('ironic.common.utils.write_to_file', autospec=True)
@mock.patch('ironic.common.pxe_utils._build_pxe_config', autospec=True)
@mock.patch('oslo_utils.fileutils.ensure_tree', autospec=True)
def test_create_pxe_config_uefi_ipxe(self, ensure_tree_mock, build_mock,
write_mock, link_mac_pxe_mock):
self.config(ipxe_enabled=True, group='pxe')
build_mock.return_value = self.ipxe_options_uefi
ipxe_template = "ironic/drivers/modules/ipxe_config.template"
with task_manager.acquire(self.context, self.node.uuid) as task:
task.node.properties['capabilities'] = 'boot_mode:uefi'
pxe_utils.create_pxe_config(task, self.ipxe_options_uefi,
ipxe_template)
ensure_calls = [
mock.call(os.path.join(CONF.deploy.http_root, self.node.uuid)),
mock.call(os.path.join(CONF.deploy.http_root, 'pxelinux.cfg'))
]
ensure_tree_mock.assert_has_calls(ensure_calls)
build_mock.assert_called_with(self.ipxe_options_uefi,
ipxe_template,
'{{ ROOT }}',
'{{ DISK_IDENTIFIER }}')
link_mac_pxe_mock.assert_called_once_with(task)
pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid)
write_mock.assert_called_with(pxe_cfg_file_path,
self.ipxe_options_uefi)
@mock.patch('ironic.common.utils.rmtree_without_raise', autospec=True)
@mock.patch('ironic.common.utils.unlink_without_raise', autospec=True)
def test_clean_up_pxe_config(self, unlink_mock, rmtree_mock):
@ -422,9 +482,8 @@ class TestPXEUtils(db_base.DbTestCase):
node_uuid,
driver_info)
def test_dhcp_options_for_instance_ipxe(self):
def _dhcp_options_for_instance_ipxe(self, task, boot_file):
self.config(tftp_server='192.0.2.1', group='pxe')
self.config(pxe_bootfile_name='fake-bootfile', group='pxe')
self.config(ipxe_enabled=True, group='pxe')
self.config(http_url='http://192.0.3.2:1234', group='deploy')
self.config(ipxe_boot_script='/test/boot.ipxe', group='pxe')
@ -432,7 +491,7 @@ class TestPXEUtils(db_base.DbTestCase):
self.config(dhcp_provider='isc', group='dhcp')
expected_boot_script_url = 'http://192.0.3.2:1234/boot.ipxe'
expected_info = [{'opt_name': '!175,bootfile-name',
'opt_value': 'fake-bootfile',
'opt_value': boot_file,
'ip_version': 4},
{'opt_name': 'server-ip-address',
'opt_value': '192.0.2.1',
@ -443,14 +502,14 @@ class TestPXEUtils(db_base.DbTestCase):
{'opt_name': 'bootfile-name',
'opt_value': expected_boot_script_url,
'ip_version': 4}]
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertItemsEqual(expected_info,
pxe_utils.dhcp_options_for_instance(task))
self.assertItemsEqual(expected_info,
pxe_utils.dhcp_options_for_instance(task))
self.config(dhcp_provider='neutron', group='dhcp')
expected_boot_script_url = 'http://192.0.3.2:1234/boot.ipxe'
expected_info = [{'opt_name': 'tag:!ipxe,bootfile-name',
'opt_value': 'fake-bootfile',
'opt_value': boot_file,
'ip_version': 4},
{'opt_name': 'server-ip-address',
'opt_value': '192.0.2.1',
@ -461,9 +520,22 @@ class TestPXEUtils(db_base.DbTestCase):
{'opt_name': 'tag:ipxe,bootfile-name',
'opt_value': expected_boot_script_url,
'ip_version': 4}]
self.assertItemsEqual(expected_info,
pxe_utils.dhcp_options_for_instance(task))
def test_dhcp_options_for_instance_ipxe_bios(self):
boot_file = 'fake-bootfile-bios'
self.config(pxe_bootfile_name=boot_file, group='pxe')
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertItemsEqual(expected_info,
pxe_utils.dhcp_options_for_instance(task))
self._dhcp_options_for_instance_ipxe(task, boot_file)
def test_dhcp_options_for_instance_ipxe_uefi(self):
boot_file = 'fake-bootfile-uefi'
self.config(uefi_pxe_bootfile_name=boot_file, group='pxe')
with task_manager.acquire(self.context, self.node.uuid) as task:
task.node.properties['capabilities'] = 'boot_mode:uefi'
self._dhcp_options_for_instance_ipxe(task, boot_file)
@mock.patch('ironic.common.utils.rmtree_without_raise', autospec=True)
@mock.patch('ironic.common.utils.unlink_without_raise', autospec=True)
@ -514,3 +586,24 @@ class TestPXEUtils(db_base.DbTestCase):
unlink_mock.assert_has_calls(unlink_calls)
rmtree_mock.assert_called_once_with(
os.path.join(CONF.pxe.tftp_root, self.node.uuid))
@mock.patch('ironic.common.utils.rmtree_without_raise', autospec=True)
@mock.patch('ironic.common.utils.unlink_without_raise', autospec=True)
def test_clean_up_ipxe_config_uefi(self, unlink_mock, rmtree_mock):
self.config(ipxe_enabled=True, group='pxe')
address = "aa:aa:aa:aa:aa:aa"
properties = {'capabilities': 'boot_mode:uefi'}
object_utils.create_test_port(self.context, node_id=self.node.id,
address=address)
with task_manager.acquire(self.context, self.node.uuid) as task:
task.node.properties = properties
pxe_utils.clean_up_pxe_config(task)
unlink_calls = [
mock.call('/httpboot/pxelinux.cfg/aa-aa-aa-aa-aa-aa'),
mock.call('/httpboot/pxelinux.cfg/aaaaaaaaaaaa')
]
unlink_mock.assert_has_calls(unlink_calls)
rmtree_mock.assert_called_once_with(
os.path.join(CONF.deploy.http_root, self.node.uuid))

View File

@ -5,7 +5,7 @@ dhcp
goto deploy
:deploy
kernel http://1.2.3.4:1234/deploy_kernel selinux=0 disk=cciss/c0d0,sda,hda,vda iscsi_target_iqn=iqn-1be26c0b-03f2-4d2e-ae87-c02d7f33c123 deployment_id=1be26c0b-03f2-4d2e-ae87-c02d7f33c123 deployment_key=0123456789ABCDEFGHIJKLMNOPQRSTUV ironic_api_url=http://192.168.122.184:6385 troubleshoot=0 text test_param boot_option=netboot ip=${ip}:${next-server}:${gateway}:${netmask} BOOTIF=${mac} root_device=vendor=fake,size=123 ipa-api-url=http://192.168.122.184:6385 ipa-driver-name=pxe_ssh coreos.configdrive=0
kernel http://1.2.3.4:1234/deploy_kernel selinux=0 disk=cciss/c0d0,sda,hda,vda iscsi_target_iqn=iqn-1be26c0b-03f2-4d2e-ae87-c02d7f33c123 deployment_id=1be26c0b-03f2-4d2e-ae87-c02d7f33c123 deployment_key=0123456789ABCDEFGHIJKLMNOPQRSTUV ironic_api_url=http://192.168.122.184:6385 troubleshoot=0 text test_param boot_option=netboot ip=${ip}:${next-server}:${gateway}:${netmask} BOOTIF=${mac} root_device=vendor=fake,size=123 ipa-api-url=http://192.168.122.184:6385 ipa-driver-name=pxe_ssh boot_mode=bios initrd=deploy_ramdisk coreos.configdrive=0
initrd http://1.2.3.4:1234/deploy_ramdisk
boot

View File

@ -0,0 +1,19 @@
#!ipxe
dhcp
goto deploy
:deploy
kernel http://1.2.3.4:1234/deploy_kernel selinux=0 disk=cciss/c0d0,sda,hda,vda iscsi_target_iqn=iqn-1be26c0b-03f2-4d2e-ae87-c02d7f33c123 deployment_id=1be26c0b-03f2-4d2e-ae87-c02d7f33c123 deployment_key=0123456789ABCDEFGHIJKLMNOPQRSTUV ironic_api_url=http://192.168.122.184:6385 troubleshoot=0 text test_param boot_option=netboot ip=${ip}:${next-server}:${gateway}:${netmask} BOOTIF=${mac} root_device=vendor=fake,size=123 ipa-api-url=http://192.168.122.184:6385 ipa-driver-name=pxe_ssh boot_mode=uefi initrd=deploy_ramdisk coreos.configdrive=0
initrd http://1.2.3.4:1234/deploy_ramdisk
boot
:boot_partition
kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param
initrd http://1.2.3.4:1234/ramdisk
boot
:boot_whole_disk
sanboot --no-describe

View File

@ -582,20 +582,6 @@ class PXEBootTestCase(db_base.DbTestCase):
self.assertRaises(exception.MissingParameterValue,
task.driver.boot.validate, task)
@mock.patch.object(base_image_service.BaseImageService, '_show',
autospec=True)
def test_validate_fail_invalid_config_uefi_ipxe(self, mock_glance):
properties = {'capabilities': 'boot_mode:uefi,cap2:value2'}
mock_glance.return_value = {'properties': {'kernel_id': 'fake-kernel',
'ramdisk_id': 'fake-initr'}}
self.config(ipxe_enabled=True, group='pxe')
self.config(http_url='dummy_url', group='deploy')
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
task.node.properties = properties
self.assertRaises(exception.InvalidParameterValue,
task.driver.boot.validate, task)
def test_validate_fail_invalid_config_uefi_whole_disk_image(self):
properties = {'capabilities': 'boot_mode:uefi,boot_option:netboot'}
instance_info = {"boot_option": "netboot"}

View File

@ -0,0 +1,3 @@
---
features:
- Adds support for using iPXE in UEFI mode.