Merge "Add UEFI support for iPXE"
This commit is contained in:
commit
35841e90d6
@ -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:
|
||||
|
@ -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})
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
19
ironic/tests/unit/drivers/ipxe_uefi_config.template
Normal file
19
ironic/tests/unit/drivers/ipxe_uefi_config.template
Normal 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
|
@ -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"}
|
||||
|
3
releasenotes/notes/ipxe-and-uefi-7722bd5db71df02c.yaml
Normal file
3
releasenotes/notes/ipxe-and-uefi-7722bd5db71df02c.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Adds support for using iPXE in UEFI mode.
|
Loading…
Reference in New Issue
Block a user