diff --git a/ironic/common/pxe_utils.py b/ironic/common/pxe_utils.py index 37e6ffd255..9aca80d8d3 100644 --- a/ironic/common/pxe_utils.py +++ b/ironic/common/pxe_utils.py @@ -43,9 +43,15 @@ PXE_CFG_DIR_NAME = CONF.pxe.pxe_config_subdir DHCP_CLIENT_ID = '61' # rfc2132 DHCP_TFTP_SERVER_NAME = '66' # rfc2132 DHCP_BOOTFILE_NAME = '67' # rfc2132 +DHCPV6_BOOTFILE_NAME = '59' # rfc5870 +# NOTE(TheJulia): adding note for the bootfile parameter +# field as defined by RFC 5870. No practical examples seem +# available. Neither grub2 or ipxe seem to leverage this. +# DHCPV6_BOOTFILE_PARAMS = '60' # rfc5870 DHCP_TFTP_SERVER_ADDRESS = '150' # rfc5859 DHCP_IPXE_ENCAP_OPTS = '175' # Tentatively Assigned DHCP_TFTP_PATH_PREFIX = '210' # rfc5071 + DEPLOY_KERNEL_RAMDISK_LABELS = ['deploy_kernel', 'deploy_ramdisk'] RESCUE_KERNEL_RAMDISK_LABELS = ['rescue_kernel', 'rescue_ramdisk'] KERNEL_RAMDISK_LABELS = {'deploy': DEPLOY_KERNEL_RAMDISK_LABELS, @@ -391,16 +397,58 @@ def clean_up_pxe_config(task): task.node.uuid)) -def dhcp_options_for_instance(task): +def _dhcp_option_file_or_url(task, urlboot=False): + """Returns the appropriate file or URL. + + :param task: A TaskManager object. + :param url_boot: Boolean value default False to indicate if a + URL should be returned to the file as opposed + to a file. + """ + boot_file = deploy_utils.get_pxe_boot_file(task.node) + # NOTE(TheJulia): There are additional cases as we add new + # features, so the logic below is in the form of if/elif/elif + if not urlboot: + return boot_file + elif urlboot: + path_prefix = get_tftp_path_prefix() + if path_prefix == '': + path_prefix = '/' + return ("tftp://" + CONF.pxe.tftp_server + + path_prefix + boot_file) + + +def dhcp_options_for_instance(task, ipxe_enabled=False, url_boot=False): """Retrieves the DHCP PXE boot options. :param task: A TaskManager instance. + :param ipxe_enabled: Default false boolean that siganls if iPXE + formatting should be returned by the method + for DHCP server configuration. + :param url_boot: Default false boolean to inform the method if + a URL should be returned to boot the node. + If [pxe]ip_version is set to `6`, then this option + has no effect as url_boot form is required by DHCPv6 + standards. + :returns: Dictionary to be sent to the networking service describing + the DHCP options to be set. """ dhcp_opts = [] + ip_version = int(CONF.pxe.ip_version) + if ip_version == 4: + boot_file_param = DHCP_BOOTFILE_NAME + else: + # NOTE(TheJulia): Booting with v6 means it is always + # a URL reply. + boot_file_param = DHCPV6_BOOTFILE_NAME + url_boot = True + # NOTE(TheJulia): The ip_version value config from the PXE config is + # guarded in the configuration, so there is no real sense in having + # anything else here in the event the value is something aside from + # 4 or 6, as there are no other possible values. + boot_file = _dhcp_option_file_or_url(task, url_boot) - boot_file = deploy_utils.get_pxe_boot_file(task.node) - - if is_ipxe_enabled(task): + if ipxe_enabled: script_name = os.path.basename(CONF.pxe.ipxe_boot_script) ipxe_script_url = '/'.join([CONF.deploy.http_url, script_name]) dhcp_provider_name = CONF.dhcp.dhcp_provider @@ -409,7 +457,7 @@ def dhcp_options_for_instance(task): if dhcp_provider_name == 'neutron': # 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,%s" % DHCP_BOOTFILE_NAME, + dhcp_opts.append({'opt_name': "tag:!ipxe,%s" % boot_file_param, 'opt_value': boot_file}) dhcp_opts.append({'opt_name': "tag:ipxe,%s" % DHCP_BOOTFILE_NAME, 'opt_value': ipxe_script_url}) @@ -417,25 +465,27 @@ def dhcp_options_for_instance(task): # !175 == non-iPXE. # http://ipxe.org/howto/dhcpd#ipxe-specific_options dhcp_opts.append({'opt_name': "!%s,%s" % (DHCP_IPXE_ENCAP_OPTS, - DHCP_BOOTFILE_NAME), + boot_file_param), 'opt_value': boot_file}) - dhcp_opts.append({'opt_name': DHCP_BOOTFILE_NAME, + dhcp_opts.append({'opt_name': boot_file_param, 'opt_value': ipxe_script_url}) else: - dhcp_opts.append({'opt_name': DHCP_BOOTFILE_NAME, + dhcp_opts.append({'opt_name': boot_file_param, 'opt_value': boot_file}) # 210 == tftp server path-prefix or tftp root, will be used to find # pxelinux.cfg directory. The pxelinux.0 loader infers this information # from it's own path, but Petitboot needs it to be specified by this # option since it doesn't use pxelinux.0 loader. - dhcp_opts.append({'opt_name': DHCP_TFTP_PATH_PREFIX, - 'opt_value': get_tftp_path_prefix()}) - - dhcp_opts.append({'opt_name': DHCP_TFTP_SERVER_NAME, - 'opt_value': CONF.pxe.tftp_server}) - dhcp_opts.append({'opt_name': DHCP_TFTP_SERVER_ADDRESS, - 'opt_value': CONF.pxe.tftp_server}) + if not url_boot: + dhcp_opts.append( + {'opt_name': DHCP_TFTP_PATH_PREFIX, + 'opt_value': get_tftp_path_prefix()}) + if not url_boot: + dhcp_opts.append({'opt_name': DHCP_TFTP_SERVER_NAME, + 'opt_value': CONF.pxe.tftp_server}) + dhcp_opts.append({'opt_name': DHCP_TFTP_SERVER_ADDRESS, + 'opt_value': CONF.pxe.tftp_server}) # NOTE(vsaienko) set this option specially for dnsmasq case as it always # sets `siaddr` field which is treated by pxe clients as TFTP server # see page 9 https://tools.ietf.org/html/rfc2131. @@ -449,12 +499,13 @@ def dhcp_options_for_instance(task): # unknown options but potentially it may blow up with others. # Related bug was opened on Neutron side: # https://bugs.launchpad.net/neutron/+bug/1723354 - dhcp_opts.append({'opt_name': 'server-ip-address', - 'opt_value': CONF.pxe.tftp_server}) + if not url_boot: + dhcp_opts.append({'opt_name': 'server-ip-address', + 'opt_value': CONF.pxe.tftp_server}) # Append the IP version for all the configuration options for opt in dhcp_opts: - opt.update({'ip_version': int(CONF.pxe.ip_version)}) + opt.update({'ip_version': ip_version}) return dhcp_opts @@ -593,10 +644,10 @@ def get_image_info(node, mode='deploy'): def build_deploy_pxe_options(task, pxe_info, mode='deploy'): pxe_opts = {} node = task.node - + # TODO(TheJulia): In the future this should become an argument + ipxe_enabled = is_ipxe_enabled(task) kernel_label = '%s_kernel' % mode ramdisk_label = '%s_ramdisk' % mode - ipxe_enabled = is_ipxe_enabled(task) for label, option in ((kernel_label, 'deployment_aki_path'), (ramdisk_label, 'deployment_ari_path')): if ipxe_enabled: @@ -839,7 +890,7 @@ def prepare_instance_pxe_config(task, image_info, """ node = task.node - dhcp_opts = dhcp_options_for_instance(task) + dhcp_opts = dhcp_options_for_instance(task, ipxe_enabled) provider = dhcp_factory.DHCPFactory() provider.update_dhcp(task, dhcp_opts) pxe_config_path = get_pxe_config_file_path( diff --git a/ironic/drivers/modules/ipxe.py b/ironic/drivers/modules/ipxe.py index e1f6069e86..4eb687eb87 100644 --- a/ironic/drivers/modules/ipxe.py +++ b/ironic/drivers/modules/ipxe.py @@ -142,7 +142,8 @@ class iPXEBoot(pxe_base.PXEBaseMixin, base.BootInterface): # or was deleted. pxe_utils.create_ipxe_boot_script() - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=True) provider = dhcp_factory.DHCPFactory() provider.update_dhcp(task, dhcp_opts) @@ -219,7 +220,8 @@ class iPXEBoot(pxe_base.PXEBaseMixin, base.BootInterface): pxe_utils.cache_ramdisk_kernel(task, instance_image_info) # If it's going to PXE boot we need to update the DHCP server - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance(task, + ipxe_enabled=True) provider = dhcp_factory.DHCPFactory() provider.update_dhcp(task, dhcp_opts) diff --git a/ironic/drivers/modules/pxe.py b/ironic/drivers/modules/pxe.py index 017a63199b..393dde4240 100644 --- a/ironic/drivers/modules/pxe.py +++ b/ironic/drivers/modules/pxe.py @@ -141,8 +141,8 @@ class PXEBoot(pxe_base.PXEBaseMixin, base.BootInterface): """ node = task.node mode = deploy_utils.rescue_or_deploy_mode(node) - - if CONF.pxe.ipxe_enabled: + ipxe_enabled = CONF.pxe.ipxe_enabled + if ipxe_enabled: # NOTE(mjturek): At this point, the ipxe boot script should # already exist as it is created at startup time. However, we # call the boot script create method here to assert its @@ -150,7 +150,8 @@ class PXEBoot(pxe_base.PXEBaseMixin, base.BootInterface): # or was deleted. pxe_utils.create_ipxe_boot_script() - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=ipxe_enabled) provider = dhcp_factory.DHCPFactory() provider.update_dhcp(task, dhcp_opts) @@ -224,7 +225,8 @@ class PXEBoot(pxe_base.PXEBaseMixin, base.BootInterface): pxe_utils.cache_ramdisk_kernel(task, instance_image_info) # If it's going to PXE boot we need to update the DHCP server - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled) provider = dhcp_factory.DHCPFactory() provider.update_dhcp(task, dhcp_opts) diff --git a/ironic/tests/unit/common/test_pxe_utils.py b/ironic/tests/unit/common/test_pxe_utils.py index 498d006150..2f5884d36f 100644 --- a/ironic/tests/unit/common/test_pxe_utils.py +++ b/ironic/tests/unit/common/test_pxe_utils.py @@ -719,22 +719,33 @@ class TestPXEUtils(db_base.DbTestCase): self.config(tftp_server='192.0.2.1', group='pxe') self.config(pxe_bootfile_name='fake-bootfile', group='pxe') self.config(tftp_root='/tftp-path/', group='pxe') - expected_info = [{'opt_name': '67', - 'opt_value': 'fake-bootfile', - 'ip_version': ip_version}, - {'opt_name': '210', - 'opt_value': '/tftp-path/', - 'ip_version': ip_version}, - {'opt_name': '66', - 'opt_value': '192.0.2.1', - 'ip_version': ip_version}, - {'opt_name': '150', - 'opt_value': '192.0.2.1', - 'ip_version': ip_version}, - {'opt_name': 'server-ip-address', - 'opt_value': '192.0.2.1', - 'ip_version': ip_version} - ] + + if ip_version == 6: + # NOTE(TheJulia): DHCPv6 RFCs seem to indicate that the prior + # options are not imported, although they may be supported + # by vendors. The apparent proper option is to return a + # URL in the field https://tools.ietf.org/html/rfc5970#section-3 + expected_info = [{'opt_name': '59', + 'opt_value': 'tftp://192.0.2.1/tftp-path' + '/fake-bootfile', + 'ip_version': ip_version}] + elif ip_version == 4: + expected_info = [{'opt_name': '67', + 'opt_value': 'fake-bootfile', + 'ip_version': ip_version}, + {'opt_name': '210', + 'opt_value': '/tftp-path/', + 'ip_version': ip_version}, + {'opt_name': '66', + 'opt_value': '192.0.2.1', + 'ip_version': ip_version}, + {'opt_name': '150', + 'opt_value': '192.0.2.1', + 'ip_version': ip_version}, + {'opt_name': 'server-ip-address', + 'opt_value': '192.0.2.1', + 'ip_version': ip_version} + ] with task_manager.acquire(self.context, self.node.uuid) as task: self.assertEqual(expected_info, pxe_utils.dhcp_options_for_instance(task)) @@ -817,7 +828,8 @@ class TestPXEUtils(db_base.DbTestCase): 'ip_version': 4}] self.assertItemsEqual(expected_info, - pxe_utils.dhcp_options_for_instance(task)) + pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=True)) self.config(dhcp_provider='neutron', group='dhcp') expected_boot_script_url = 'http://192.0.3.2:1234/boot.ipxe' @@ -838,7 +850,8 @@ class TestPXEUtils(db_base.DbTestCase): 'ip_version': 4}] self.assertItemsEqual(expected_info, - pxe_utils.dhcp_options_for_instance(task)) + pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=True)) def test_dhcp_options_for_instance_ipxe_bios(self): boot_file = 'fake-bootfile-bios' diff --git a/ironic/tests/unit/drivers/modules/test_ipxe.py b/ironic/tests/unit/drivers/modules/test_ipxe.py index 974a5ff5fc..e45f9d65f6 100644 --- a/ironic/tests/unit/drivers/modules/test_ipxe.py +++ b/ironic/tests/unit/drivers/modules/test_ipxe.py @@ -257,7 +257,8 @@ class iPXEBootTestCase(db_base.DbTestCase): 'rescue_ramdisk': 'r'} self.node.save() with task_manager.acquire(self.context, self.node.uuid) as task: - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=True) task.driver.boot.prepare_ramdisk(task, {'foo': 'bar'}) mock_deploy_img_info.assert_called_once_with(task.node, mode=mode) provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) @@ -538,7 +539,8 @@ class iPXEBootTestCase(db_base.DbTestCase): 'ramdisk': ('', '/path/to/ramdisk')} get_image_info_mock.return_value = image_info with task_manager.acquire(self.context, self.node.uuid) as task: - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=True) pxe_config_path = pxe_utils.get_pxe_config_file_path( task.node.uuid) task.node.properties['capabilities'] = 'boot_mode:bios' @@ -578,7 +580,8 @@ class iPXEBootTestCase(db_base.DbTestCase): self.node.provision_state = states.ACTIVE self.node.save() with task_manager.acquire(self.context, self.node.uuid) as task: - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=True) pxe_config_path = pxe_utils.get_pxe_config_file_path( task.node.uuid) task.node.properties['capabilities'] = 'boot_mode:bios' @@ -615,7 +618,8 @@ class iPXEBootTestCase(db_base.DbTestCase): 'ramdisk': ('', '/path/to/ramdisk')} get_image_info_mock.return_value = image_info with task_manager.acquire(self.context, self.node.uuid) as task: - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=True) task.node.properties['capabilities'] = 'boot_mode:bios' task.node.driver_internal_info['is_whole_disk_image'] = False @@ -644,7 +648,8 @@ class iPXEBootTestCase(db_base.DbTestCase): dhcp_factory_mock.return_value = provider_mock get_image_info_mock.return_value = {} with task_manager.acquire(self.context, self.node.uuid) as task: - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=True) task.node.properties['capabilities'] = 'boot_mode:bios' task.node.driver_internal_info['is_whole_disk_image'] = True task.driver.boot.prepare_instance(task) @@ -688,7 +693,8 @@ class iPXEBootTestCase(db_base.DbTestCase): with task_manager.acquire(self.context, self.node.uuid) as task: task.node.driver_internal_info = { 'boot_from_volume': vol_id} - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance(task, + ipxe_enabled=True) pxe_config_path = pxe_utils.get_pxe_config_file_path( task.node.uuid) task.node.properties['capabilities'] = 'boot_mode:bios' diff --git a/ironic/tests/unit/drivers/modules/test_pxe.py b/ironic/tests/unit/drivers/modules/test_pxe.py index 63e606728c..8e78877844 100644 --- a/ironic/tests/unit/drivers/modules/test_pxe.py +++ b/ironic/tests/unit/drivers/modules/test_pxe.py @@ -255,7 +255,8 @@ class PXEBootTestCase(db_base.DbTestCase): 'rescue_ramdisk': 'r'} self.node.save() with task_manager.acquire(self.context, self.node.uuid) as task: - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=CONF.pxe.ipxe_enabled) task.driver.boot.prepare_ramdisk(task, {'foo': 'bar'}) mock_deploy_img_info.assert_called_once_with(task.node, mode=mode) provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) @@ -543,7 +544,8 @@ class PXEBootTestCase(db_base.DbTestCase): 'ramdisk': ('', '/path/to/ramdisk')} get_image_info_mock.return_value = image_info with task_manager.acquire(self.context, self.node.uuid) as task: - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=CONF.pxe.ipxe_enabled) pxe_config_path = pxe_utils.get_pxe_config_file_path( task.node.uuid) task.node.properties['capabilities'] = 'boot_mode:bios' @@ -583,7 +585,8 @@ class PXEBootTestCase(db_base.DbTestCase): self.node.provision_state = states.ACTIVE self.node.save() with task_manager.acquire(self.context, self.node.uuid) as task: - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=CONF.pxe.ipxe_enabled) pxe_config_path = pxe_utils.get_pxe_config_file_path( task.node.uuid) task.node.properties['capabilities'] = 'boot_mode:bios' @@ -621,7 +624,8 @@ class PXEBootTestCase(db_base.DbTestCase): 'ramdisk': ('', '/path/to/ramdisk')} get_image_info_mock.return_value = image_info with task_manager.acquire(self.context, self.node.uuid) as task: - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=CONF.pxe.ipxe_enabled) task.node.properties['capabilities'] = 'boot_mode:bios' task.node.driver_internal_info['is_whole_disk_image'] = False @@ -647,7 +651,8 @@ class PXEBootTestCase(db_base.DbTestCase): dhcp_factory_mock.return_value = provider_mock get_image_info_mock.return_value = {} with task_manager.acquire(self.context, self.node.uuid) as task: - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance( + task, CONF.pxe.ipxe_enabled) task.node.properties['capabilities'] = 'boot_mode:bios' task.node.driver_internal_info['is_whole_disk_image'] = True task.driver.boot.prepare_instance(task) @@ -691,7 +696,8 @@ class PXEBootTestCase(db_base.DbTestCase): with task_manager.acquire(self.context, self.node.uuid) as task: task.node.driver_internal_info = { 'boot_from_volume': vol_id} - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance(task, + ipxe_enabled=True) pxe_config_path = pxe_utils.get_pxe_config_file_path( task.node.uuid) task.node.properties['capabilities'] = 'boot_mode:bios' @@ -780,7 +786,8 @@ class PXEBootTestCase(db_base.DbTestCase): instance_info['capabilities'] = {'boot_option': 'ramdisk'} task.node.instance_info = instance_info task.node.save() - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=CONF.pxe.ipxe_enabled) pxe_config_path = pxe_utils.get_pxe_config_file_path( task.node.uuid) task.driver.boot.prepare_instance(task) @@ -871,7 +878,8 @@ class PXERamdiskDeployTestCase(db_base.DbTestCase): 'ramdisk': ('', '/path/to/ramdisk')} get_image_info_mock.return_value = image_info with task_manager.acquire(self.context, self.node.uuid) as task: - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + dhcp_opts = pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=CONF.pxe.ipxe_enabled) pxe_config_path = pxe_utils.get_pxe_config_file_path( task.node.uuid) task.node.properties['capabilities'] = 'boot_option:netboot' diff --git a/releasenotes/notes/boot-from-url-98d21670e726c518.yaml b/releasenotes/notes/boot-from-url-98d21670e726c518.yaml new file mode 100644 index 0000000000..c97c59575c --- /dev/null +++ b/releasenotes/notes/boot-from-url-98d21670e726c518.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fixes a misunderstanding in how DHCPv6 booting of machines operates + in that only a URL to the boot loader is expected in that case, as opposed + to traditional TFTP parameters. Now a URL is sent to the client in the form + of ``tftp:////``. See + `story 1744620 `_ + for more information.