From 2a5005d946de35625adc910dff912404aa0ade70 Mon Sep 17 00:00:00 2001 From: Lucas Alvares Gomes Date: Mon, 14 Dec 2015 16:55:21 +0000 Subject: [PATCH] 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 --- doc/source/deploy/install-guide.rst | 23 ++-- ironic/common/pxe_utils.py | 21 ++-- ironic/drivers/modules/ipxe_config.template | 2 +- ironic/drivers/modules/pxe.py | 8 -- ironic/tests/unit/common/test_pxe_utils.py | 115 ++++++++++++++++-- .../tests/unit/drivers/ipxe_config.template | 2 +- .../unit/drivers/ipxe_uefi_config.template | 19 +++ ironic/tests/unit/drivers/modules/test_pxe.py | 14 --- .../notes/ipxe-and-uefi-7722bd5db71df02c.yaml | 3 + 9 files changed, 155 insertions(+), 52 deletions(-) create mode 100644 ironic/tests/unit/drivers/ipxe_uefi_config.template create mode 100644 releasenotes/notes/ipxe-and-uefi-7722bd5db71df02c.yaml diff --git a/doc/source/deploy/install-guide.rst b/doc/source/deploy/install-guide.rst index e611b0d91f..21bc81bb57 100644 --- a/doc/source/deploy/install-guide.rst +++ b/doc/source/deploy/install-guide.rst @@ -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: diff --git a/ironic/common/pxe_utils.py b/ironic/common/pxe_utils.py index 96bd14295a..5fece7c8cf 100644 --- a/ironic/common/pxe_utils.py +++ b/ironic/common/pxe_utils.py @@ -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}) diff --git a/ironic/drivers/modules/ipxe_config.template b/ironic/drivers/modules/ipxe_config.template index 9564301177..1254310bdb 100644 --- a/ironic/drivers/modules/ipxe_config.template +++ b/ironic/drivers/modules/ipxe_config.template @@ -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 diff --git a/ironic/drivers/modules/pxe.py b/ironic/drivers/modules/pxe.py index 7381ddbc46..3424df2f37 100644 --- a/ironic/drivers/modules/pxe.py +++ b/ironic/drivers/modules/pxe.py @@ -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) diff --git a/ironic/tests/unit/common/test_pxe_utils.py b/ironic/tests/unit/common/test_pxe_utils.py index 9f01f0a403..dbc0914328 100644 --- a/ironic/tests/unit/common/test_pxe_utils.py +++ b/ironic/tests/unit/common/test_pxe_utils.py @@ -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)) diff --git a/ironic/tests/unit/drivers/ipxe_config.template b/ironic/tests/unit/drivers/ipxe_config.template index 1ca14c8f3b..5dbf2747dc 100644 --- a/ironic/tests/unit/drivers/ipxe_config.template +++ b/ironic/tests/unit/drivers/ipxe_config.template @@ -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 diff --git a/ironic/tests/unit/drivers/ipxe_uefi_config.template b/ironic/tests/unit/drivers/ipxe_uefi_config.template new file mode 100644 index 0000000000..b4471c1ed6 --- /dev/null +++ b/ironic/tests/unit/drivers/ipxe_uefi_config.template @@ -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 diff --git a/ironic/tests/unit/drivers/modules/test_pxe.py b/ironic/tests/unit/drivers/modules/test_pxe.py index e50a07a560..0b5936fbda 100644 --- a/ironic/tests/unit/drivers/modules/test_pxe.py +++ b/ironic/tests/unit/drivers/modules/test_pxe.py @@ -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"} diff --git a/releasenotes/notes/ipxe-and-uefi-7722bd5db71df02c.yaml b/releasenotes/notes/ipxe-and-uefi-7722bd5db71df02c.yaml new file mode 100644 index 0000000000..02daa335e7 --- /dev/null +++ b/releasenotes/notes/ipxe-and-uefi-7722bd5db71df02c.yaml @@ -0,0 +1,3 @@ +--- +features: + - Adds support for using iPXE in UEFI mode.