From 90d58ede94daa439aa784240b8157bdf8039ee37 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Mon, 15 Oct 2018 12:50:56 -0700 Subject: [PATCH] Fix DHCPv6 support Adds logic to handle the appropriate replies for DHCPv6 responses. The IPv6 nature was discovered while researching differences and finding that the field ID value changes between IPv4 and IPv6 DHCP clients, as DHCPv6 is purely booting from a URL. Change-Id: I63572bea9bfb150aaeb4708dfa57e71adf4f64ab Task: 9788 Story: 1744620 Story: 2003934 --- ironic/common/pxe_utils.py | 93 ++++++++++++++----- ironic/drivers/modules/ipxe.py | 6 +- ironic/drivers/modules/pxe.py | 10 +- ironic/tests/unit/common/test_pxe_utils.py | 49 ++++++---- .../tests/unit/drivers/modules/test_ipxe.py | 18 ++-- ironic/tests/unit/drivers/modules/test_pxe.py | 24 +++-- .../notes/boot-from-url-98d21670e726c518.yaml | 9 ++ 7 files changed, 150 insertions(+), 59 deletions(-) create mode 100644 releasenotes/notes/boot-from-url-98d21670e726c518.yaml 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.