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
This commit is contained in:
Julia Kreger 2018-10-15 12:50:56 -07:00
parent b766343183
commit 90d58ede94
7 changed files with 150 additions and 59 deletions

View File

@ -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(

View File

@ -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)

View File

@ -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)

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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://<tftp_address>/<tftp_path>/<boot_file>``. See
`story 1744620 <https://storyboard.openstack.org/#!/story/1744620>`_
for more information.