diff options
author | Julia Kreger <juliaashleykreger@gmail.com> | 2020-03-20 12:57:47 -0700 |
---|---|---|
committer | Julia Kreger <juliaashleykreger@gmail.com> | 2020-04-13 06:32:15 -0700 |
commit | cf412bc81e4182a7c332545cfeb6e154015e3dc3 (patch) | |
tree | 68025766d3f31800fee36cd21d74b738a51eb1e1 | |
parent | 1dee25f554ab928ecdc6b7e3dd691eabf3db0ee4 (diff) | |
download | ironic-cf412bc81e4182a7c332545cfeb6e154015e3dc3.tar.gz |
"dual stack" support for PXE/iPXE
Adds functionality for dual stack capabilities and automatic
population to neutron with the correct response based upon the
IP version of the provisioning/cleaning/rescue or tenant ports.
This was origianlly intended to be separated from removing the
need for [pxe]ip_version, however the resulting code changes
from doing both this and making ironic support dual stacks
touched the same tests and some of the same code, so combined
is simpler.
Change-Id: If7a296001e204ae0c9a49495731052ab33379628
-rw-r--r-- | ironic/common/pxe_utils.py | 42 | ||||
-rw-r--r-- | ironic/conf/default.py | 17 | ||||
-rw-r--r-- | ironic/dhcp/base.py | 12 | ||||
-rw-r--r-- | ironic/dhcp/neutron.py | 43 | ||||
-rw-r--r-- | ironic/drivers/modules/pxe_base.py | 14 | ||||
-rw-r--r-- | ironic/tests/unit/dhcp/test_neutron.py | 66 | ||||
-rw-r--r-- | ironic/tests/unit/drivers/modules/test_ipxe.py | 19 | ||||
-rw-r--r-- | ironic/tests/unit/drivers/modules/test_pxe.py | 17 | ||||
-rw-r--r-- | releasenotes/notes/dual-stack-ironic-493ebc7b71263aaa.yaml | 16 |
9 files changed, 216 insertions, 30 deletions
diff --git a/ironic/common/pxe_utils.py b/ironic/common/pxe_utils.py index 334f7b9d9..4e49bcc78 100644 --- a/ironic/common/pxe_utils.py +++ b/ironic/common/pxe_utils.py @@ -373,13 +373,16 @@ def clean_up_pxe_config(task, ipxe_enabled=False): task.node.uuid)) -def _dhcp_option_file_or_url(task, urlboot=False): +def _dhcp_option_file_or_url(task, urlboot=False, ip_version=None): """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. + :param ip_version: Integer representing the version of IP of + to return options for DHCP. Possible options + are 4, and 6. """ boot_file = deploy_utils.get_pxe_boot_file(task.node) # NOTE(TheJulia): There are additional cases as we add new @@ -387,12 +390,16 @@ def _dhcp_option_file_or_url(task, urlboot=False): if not urlboot: return boot_file elif urlboot: - host = utils.wrap_ipv6(CONF.pxe.tftp_server) + if CONF.my_ipv6 and ip_version == 6: + host = utils.wrap_ipv6(CONF.my_ipv6) + else: + host = utils.wrap_ipv6(CONF.pxe.tftp_server) return "tftp://{host}/{boot_file}".format(host=host, boot_file=boot_file) -def dhcp_options_for_instance(task, ipxe_enabled=False, url_boot=False): +def dhcp_options_for_instance(task, ipxe_enabled=False, url_boot=False, + ip_version=None): """Retrieves the DHCP PXE boot options. :param task: A TaskManager instance. @@ -404,13 +411,19 @@ def dhcp_options_for_instance(task, ipxe_enabled=False, url_boot=False): If [pxe]ip_version is set to `6`, then this option has no effect as url_boot form is required by DHCPv6 standards. + :param ip_version: The IP version of options to return as values + differ by IP version. Default to [pxe]ip_version. + Possible options are integers 4 or 6. :returns: Dictionary to be sent to the networking service describing the DHCP options to be set. """ + if ip_version: + use_ip_version = ip_version + else: + use_ip_version = int(CONF.pxe.ip_version) dhcp_opts = [] - ip_version = int(CONF.pxe.ip_version) dhcp_provider_name = CONF.dhcp.dhcp_provider - if ip_version == 4: + if use_ip_version == 4: boot_file_param = DHCP_BOOTFILE_NAME else: # NOTE(TheJulia): Booting with v6 means it is always @@ -421,7 +434,7 @@ def dhcp_options_for_instance(task, ipxe_enabled=False, url_boot=False): # 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 = _dhcp_option_file_or_url(task, url_boot, use_ip_version) if ipxe_enabled: # TODO(TheJulia): DHCPv6 through dnsmasq + ipxe matching simply @@ -444,7 +457,7 @@ def dhcp_options_for_instance(task, ipxe_enabled=False, url_boot=False): # added in the Stein cycle which identifies the iPXE User-Class # directly and is only sent in DHCPv6. - if ip_version != 6: + if use_ip_version != 6: dhcp_opts.append( {'opt_name': "tag:!ipxe,%s" % boot_file_param, 'opt_value': boot_file} @@ -463,7 +476,7 @@ def dhcp_options_for_instance(task, ipxe_enabled=False, url_boot=False): else: # !175 == non-iPXE. # http://ipxe.org/howto/dhcpd#ipxe-specific_options - if ip_version == 6: + if use_ip_version == 6: LOG.warning('IPv6 is enabled and the DHCP driver appears set ' 'to a plugin aside from "neutron". Node %(name)s ' 'may not receive proper DHCPv6 provided ' @@ -512,7 +525,7 @@ def dhcp_options_for_instance(task, ipxe_enabled=False, url_boot=False): # Append the IP version for all the configuration options for opt in dhcp_opts: - opt.update({'ip_version': ip_version}) + opt.update({'ip_version': use_ip_version}) return dhcp_opts @@ -906,7 +919,16 @@ def prepare_instance_pxe_config(task, image_info, """ node = task.node - dhcp_opts = dhcp_options_for_instance(task, ipxe_enabled) + # Generate options for both IPv4 and IPv6, and they can be + # filtered down later based upon the port options. + # TODO(TheJulia): This should be re-tooled during the Victoria + # development cycle so that we call a single method and return + # combined options. The method we currently call is relied upon + # by two eternal projects, to changing the behavior is not ideal. + dhcp_opts = dhcp_options_for_instance(task, ipxe_enabled, + ip_version=4) + dhcp_opts += dhcp_options_for_instance(task, ipxe_enabled, + ip_version=6) provider = dhcp_factory.DHCPFactory() provider.update_dhcp(task, dhcp_opts) pxe_config_path = get_pxe_config_file_path( diff --git a/ironic/conf/default.py b/ironic/conf/default.py index 857acb029..9621c2db5 100644 --- a/ironic/conf/default.py +++ b/ironic/conf/default.py @@ -239,9 +239,20 @@ netconf_opts = [ cfg.StrOpt('my_ip', default=netutils.get_my_ipv4(), sample_default='127.0.0.1', - help=_('IP address of this host. If unset, will determine the ' - 'IP programmatically. If unable to do so, will use ' - '"127.0.0.1".')), + help=_('IPv4 address of this host. If unset, will determine ' + 'the IP programmatically. If unable to do so, will use ' + '"127.0.0.1". NOTE: This field does accept an IPv6 ' + 'address as an override for templates and URLs, ' + 'however it is recommended that [DEFAULT]my_ipv6 ' + 'is used along with DNS names for service URLs for ' + 'dual-stack environments.')), + cfg.StrOpt('my_ipv6', + default=None, + sample_default='2001:db8::1', + help=_('IP address of this host using IPv6. This value must ' + 'be supplied via the configuration and cannot be ' + 'adequately programmatically determined like the ' + '[DEFAULT]my_ip parameter for IPv4.')), ] notification_opts = [ diff --git a/ironic/dhcp/base.py b/ironic/dhcp/base.py index 8fad59ce7..57a4e7911 100644 --- a/ironic/dhcp/base.py +++ b/ironic/dhcp/base.py @@ -39,9 +39,11 @@ class BaseDHCP(object, metaclass=abc.ABCMeta): :: [{'opt_name': '67', - 'opt_value': 'pxelinux.0'}, + 'opt_value': 'pxelinux.0', + 'ip_version': 4}, {'opt_name': '66', - 'opt_value': '123.123.123.456'}] + 'opt_value': '123.123.123.456', + 'ip_version': 4}] :param token: An optional authentication token. Deprecated, use context :param context: request context :type context: ironic.common.context.RequestContext @@ -63,9 +65,11 @@ class BaseDHCP(object, metaclass=abc.ABCMeta): :: [{'opt_name': '67', - 'opt_value': 'pxelinux.0'}, + 'opt_value': 'pxelinux.0', + 'ip_version': 4}, {'opt_name': '66', - 'opt_value': '123.123.123.456'}] + 'opt_value': '123.123.123.456', + 'ip_version': 4}] :param vifs: A dict with keys 'ports' and 'portgroups' and dicts as values. Each dict has key/value pairs of the form diff --git a/ironic/dhcp/neutron.py b/ironic/dhcp/neutron.py index 224561455..4b0210cb3 100644 --- a/ironic/dhcp/neutron.py +++ b/ironic/dhcp/neutron.py @@ -14,6 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. +import ipaddress import time from neutronclient.common import exceptions as neutron_client_exc @@ -49,9 +50,11 @@ class NeutronDHCPApi(base.BaseDHCP): :: [{'opt_name': '67', - 'opt_value': 'pxelinux.0'}, + 'opt_value': 'pxelinux.0', + 'ip_version': 4}, {'opt_name': '66', - 'opt_value': '123.123.123.456'}] + 'opt_value': '123.123.123.456'}, + 'ip_version': 4}] :param token: optional auth token. Deprecated, use context. :param context: request context :type context: ironic.common.context.RequestContext @@ -59,8 +62,36 @@ class NeutronDHCPApi(base.BaseDHCP): """ super(NeutronDHCPApi, self).update_port_dhcp_opts( port_id, dhcp_options, token=token, context=context) - port_req_body = {'port': {'extra_dhcp_opts': dhcp_options}} try: + neutron_client = neutron.get_client(token=token, + context=context) + + fip = None + port = neutron_client.show_port(port_id).get('port') + try: + if port: + # TODO(TheJulia): We need to retool this down the + # road so that we handle ports and allow preferences + # for multi-address ports with different IP versions + # and enable operators to possibly select preferences + # for provisionioning operations. + # This is compounded by v6 mainly only being available + # with UEFI machines, so the support matrix also gets + # a little "weird". + # Ideally, we should work on this in Victoria. + fip = port.get('fixed_ips')[0] + except (TypeError, IndexError): + fip = None + update_opts = [] + if fip: + ip_version = ipaddress.ip_address(fip['ip_address']).version + for option in dhcp_options: + if option.get('ip_version', 4) == ip_version: + update_opts.append(option) + else: + LOG.error('Requested to update port for port %s, ' + 'however port lacks an IP address.', port_id) + port_req_body = {'port': {'extra_dhcp_opts': update_opts}} neutron.update_neutron_port(context, port_id, port_req_body) except neutron_client_exc.NeutronClientException: LOG.exception("Failed to update Neutron port %s.", port_id) @@ -75,9 +106,11 @@ class NeutronDHCPApi(base.BaseDHCP): :: [{'opt_name': '67', - 'opt_value': 'pxelinux.0'}, + 'opt_value': 'pxelinux.0', + 'ip_version': 4}, {'opt_name': '66', - 'opt_value': '123.123.123.456'}] + 'opt_value': '123.123.123.456', + 'ip_version': 4}] :param vifs: a dict of Neutron port/portgroup dicts to update DHCP options on. The port/portgroup dict key should be Ironic port UUIDs, and the values diff --git a/ironic/drivers/modules/pxe_base.py b/ironic/drivers/modules/pxe_base.py index 374a5dc10..290b005a1 100644 --- a/ironic/drivers/modules/pxe_base.py +++ b/ironic/drivers/modules/pxe_base.py @@ -169,8 +169,16 @@ class PXEBaseMixin(object): # or was deleted. pxe_utils.create_ipxe_boot_script() + # Generate options for both IPv4 and IPv6, and they can be + # filtered down later based upon the port options. + # TODO(TheJulia): This should be re-tooled during the Victoria + # development cycle so that we call a single method and return + # combined options. The method we currently call is relied upon + # by two eternal projects, to changing the behavior is not ideal. dhcp_opts = pxe_utils.dhcp_options_for_instance( - task, ipxe_enabled=self.ipxe_enabled) + task, ipxe_enabled=self.ipxe_enabled, ip_version=4) + dhcp_opts += pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=self.ipxe_enabled, ip_version=6) provider = dhcp_factory.DHCPFactory() provider.update_dhcp(task, dhcp_opts) @@ -259,7 +267,9 @@ class PXEBaseMixin(object): # If it's going to PXE boot we need to update the DHCP server dhcp_opts = pxe_utils.dhcp_options_for_instance( - task, ipxe_enabled=self.ipxe_enabled) + task, ipxe_enabled=self.ipxe_enabled, ip_version=4) + dhcp_opts += pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=self.ipxe_enabled, ip_version=6) provider = dhcp_factory.DHCPFactory() provider.update_dhcp(task, dhcp_opts) diff --git a/ironic/tests/unit/dhcp/test_neutron.py b/ironic/tests/unit/dhcp/test_neutron.py index 29983e7d3..391b1cf74 100644 --- a/ironic/tests/unit/dhcp/test_neutron.py +++ b/ironic/tests/unit/dhcp/test_neutron.py @@ -48,8 +48,9 @@ class TestNeutron(db_base.DbTestCase): dhcp_factory.DHCPFactory._dhcp_provider = None + @mock.patch('ironic.common.neutron.get_client', autospec=True) @mock.patch('ironic.common.neutron.update_neutron_port', autospec=True) - def test_update_port_dhcp_opts(self, update_mock): + def test_update_port_dhcp_opts(self, update_mock, client_mock): opts = [{'opt_name': 'bootfile-name', 'opt_value': 'pxelinux.0'}, {'opt_name': 'tftp-server', @@ -58,6 +59,56 @@ class TestNeutron(db_base.DbTestCase): 'opt_value': '1.1.1.1'}] port_id = 'fake-port-id' expected = {'port': {'extra_dhcp_opts': opts}} + port_data = { + "id": port_id, + "fixed_ips": [ + { + "ip_address": "192.168.1.3", + } + ], + } + client_mock.return_value.show_port.return_value = {'port': port_data} + + api = dhcp_factory.DHCPFactory() + with task_manager.acquire(self.context, self.node.uuid) as task: + api.provider.update_port_dhcp_opts(port_id, opts, + context=task.context) + update_mock.assert_called_once_with( + self.context, port_id, expected) + + @mock.patch('ironic.common.neutron.get_client', autospec=True) + @mock.patch('ironic.common.neutron.update_neutron_port', autospec=True) + def test_update_port_dhcp_opts_v6(self, update_mock, client_mock): + opts = [{'opt_name': 'bootfile-name', + 'opt_value': 'pxelinux.0', + 'ip_version': 4}, + {'opt_name': 'tftp-server', + 'opt_value': '1.1.1.1', + 'ip_version': 4}, + {'opt_name': 'server-ip-address', + 'opt_value': '1.1.1.1', + 'ip_version': 4}, + {'opt_name': 'bootfile-url', + 'opt_value': 'tftp://::1/file.name', + 'ip_version': 6}] + port_id = 'fake-port-id' + expected = { + 'port': { + 'extra_dhcp_opts': [{ + 'opt_name': 'bootfile-url', + 'opt_value': 'tftp://::1/file.name', + 'ip_version': 6}] + } + } + port_data = { + "id": port_id, + "fixed_ips": [ + { + "ip_address": "2001:db8::201", + } + ], + } + client_mock.return_value.show_port.return_value = {'port': port_data} api = dhcp_factory.DHCPFactory() with task_manager.acquire(self.context, self.node.uuid) as task: @@ -66,10 +117,21 @@ class TestNeutron(db_base.DbTestCase): update_mock.assert_called_once_with( task.context, port_id, expected) + @mock.patch('ironic.common.neutron.get_client', autospec=True) @mock.patch('ironic.common.neutron.update_neutron_port', autospec=True) - def test_update_port_dhcp_opts_with_exception(self, update_mock): + def test_update_port_dhcp_opts_with_exception(self, update_mock, + client_mock): opts = [{}] port_id = 'fake-port-id' + port_data = { + "id": port_id, + "fixed_ips": [ + { + "ip_address": "192.168.1.3", + } + ], + } + client_mock.return_value.show_port.return_value = {'port': port_data} update_mock.side_effect = ( neutron_client_exc.NeutronClientException()) diff --git a/ironic/tests/unit/drivers/modules/test_ipxe.py b/ironic/tests/unit/drivers/modules/test_ipxe.py index 2c78a7550..4b1d81670 100644 --- a/ironic/tests/unit/drivers/modules/test_ipxe.py +++ b/ironic/tests/unit/drivers/modules/test_ipxe.py @@ -270,11 +270,14 @@ class iPXEBootTestCase(db_base.DbTestCase): self.node.save() with task_manager.acquire(self.context, self.node.uuid) as task: dhcp_opts = pxe_utils.dhcp_options_for_instance( - task, ipxe_enabled=True) + task, ipxe_enabled=True, ip_version=4) + dhcp_opts += pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=True, ip_version=6) task.driver.boot.prepare_ramdisk(task, {'foo': 'bar'}) mock_deploy_img_info.assert_called_once_with(task.node, mode=mode, ipxe_enabled=True) - provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) + provider_mock.update_dhcp.assert_called_once_with( + task, dhcp_opts) if self.node.provision_state == states.DEPLOYING: get_boot_mode_mock.assert_called_once_with(task) set_boot_device_mock.assert_called_once_with(task, @@ -630,6 +633,8 @@ class iPXEBootTestCase(db_base.DbTestCase): with task_manager.acquire(self.context, self.node.uuid) as task: dhcp_opts = pxe_utils.dhcp_options_for_instance( task, ipxe_enabled=True) + dhcp_opts += pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=True, ip_version=6) pxe_config_path = pxe_utils.get_pxe_config_file_path( task.node.uuid, ipxe_enabled=True) task.node.properties['capabilities'] = 'boot_mode:bios' @@ -672,6 +677,8 @@ class iPXEBootTestCase(db_base.DbTestCase): with task_manager.acquire(self.context, self.node.uuid) as task: dhcp_opts = pxe_utils.dhcp_options_for_instance( task, ipxe_enabled=True) + dhcp_opts += pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=True, ip_version=6) pxe_config_path = pxe_utils.get_pxe_config_file_path( task.node.uuid, ipxe_enabled=True) task.node.properties['capabilities'] = 'boot_mode:bios' @@ -710,7 +717,9 @@ class iPXEBootTestCase(db_base.DbTestCase): 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, ipxe_enabled=True) + task, ipxe_enabled=True, ip_version=4) + dhcp_opts += pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=True, ip_version=6) task.node.properties['capabilities'] = 'boot_mode:bios' task.node.driver_internal_info['is_whole_disk_image'] = False @@ -742,6 +751,8 @@ class iPXEBootTestCase(db_base.DbTestCase): with task_manager.acquire(self.context, self.node.uuid) as task: dhcp_opts = pxe_utils.dhcp_options_for_instance( task, ipxe_enabled=True) + dhcp_opts += pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=True, ip_version=6) task.node.properties['capabilities'] = 'boot_mode:bios' task.node.driver_internal_info['is_whole_disk_image'] = True task.driver.boot.prepare_instance(task) @@ -786,6 +797,8 @@ class iPXEBootTestCase(db_base.DbTestCase): 'boot_from_volume': vol_id} dhcp_opts = pxe_utils.dhcp_options_for_instance(task, ipxe_enabled=True) + dhcp_opts += pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=True, ip_version=6) pxe_config_path = pxe_utils.get_pxe_config_file_path( task.node.uuid, ipxe_enabled=True) 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 33b487db2..f5ef8cd26 100644 --- a/ironic/tests/unit/drivers/modules/test_pxe.py +++ b/ironic/tests/unit/drivers/modules/test_pxe.py @@ -86,6 +86,7 @@ class PXEBootTestCase(db_base.DbTestCase): self.port = obj_utils.create_test_port(self.context, node_id=self.node.id) self.config(group='conductor', api_url='http://127.0.0.1:1234/') + self.config(my_ipv6='2001:db8::1') def test_get_properties(self): expected = pxe_base.COMMON_PROPERTIES @@ -267,6 +268,8 @@ class PXEBootTestCase(db_base.DbTestCase): with task_manager.acquire(self.context, self.node.uuid) as task: dhcp_opts = pxe_utils.dhcp_options_for_instance( task, ipxe_enabled=False) + dhcp_opts += pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=False, ip_version=6) task.driver.boot.prepare_ramdisk(task, {'foo': 'bar'}) mock_deploy_img_info.assert_called_once_with(task.node, mode=mode, @@ -552,7 +555,9 @@ class PXEBootTestCase(db_base.DbTestCase): 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, ipxe_enabled=False) + task, ipxe_enabled=False, ip_version=4) + dhcp_opts += pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=False, ip_version=6) pxe_config_path = pxe_utils.get_pxe_config_file_path( task.node.uuid) task.node.properties['capabilities'] = 'boot_mode:bios' @@ -595,6 +600,8 @@ class PXEBootTestCase(db_base.DbTestCase): with task_manager.acquire(self.context, self.node.uuid) as task: dhcp_opts = pxe_utils.dhcp_options_for_instance( task, ipxe_enabled=False) + dhcp_opts += pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=False, ip_version=6) pxe_config_path = pxe_utils.get_pxe_config_file_path( task.node.uuid) task.node.properties['capabilities'] = 'boot_mode:bios' @@ -634,6 +641,8 @@ class PXEBootTestCase(db_base.DbTestCase): with task_manager.acquire(self.context, self.node.uuid) as task: dhcp_opts = pxe_utils.dhcp_options_for_instance( task, ipxe_enabled=False) + dhcp_opts += pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=False, ip_version=6) task.node.properties['capabilities'] = 'boot_mode:bios' task.node.driver_internal_info['is_whole_disk_image'] = False @@ -663,6 +672,8 @@ class PXEBootTestCase(db_base.DbTestCase): with task_manager.acquire(self.context, self.node.uuid) as task: dhcp_opts = pxe_utils.dhcp_options_for_instance( task, ipxe_enabled=False) + dhcp_opts += pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=False, ip_version=6) task.node.properties['capabilities'] = 'boot_mode:bios' task.node.driver_internal_info['is_whole_disk_image'] = True task.driver.boot.prepare_instance(task) @@ -734,6 +745,8 @@ class PXEBootTestCase(db_base.DbTestCase): task.node.save() dhcp_opts = pxe_utils.dhcp_options_for_instance( task, ipxe_enabled=False) + dhcp_opts += pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=False, ip_version=6) pxe_config_path = pxe_utils.get_pxe_config_file_path( task.node.uuid) task.driver.boot.prepare_instance(task) @@ -830,6 +843,8 @@ class PXERamdiskDeployTestCase(db_base.DbTestCase): with task_manager.acquire(self.context, self.node.uuid) as task: dhcp_opts = pxe_utils.dhcp_options_for_instance( task, ipxe_enabled=False) + dhcp_opts += pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=False, ip_version=6) 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/dual-stack-ironic-493ebc7b71263aaa.yaml b/releasenotes/notes/dual-stack-ironic-493ebc7b71263aaa.yaml new file mode 100644 index 000000000..8f9884f12 --- /dev/null +++ b/releasenotes/notes/dual-stack-ironic-493ebc7b71263aaa.yaml @@ -0,0 +1,16 @@ +--- +features: + - | + Adds functionality with neutron integration to support dual-stack + (IPv4 and IPv6 environment configurations). This enables ironic to + look up the attached port(s) and supply DHCP options in alignment + with the protocol version allocated on the port. +upgrade: + - | + The ``[pxe]ip_version`` setting may no longer be required depending on + neutron integration. + - | + Operators that used the ``[DEFAULT]my_ip`` setting with an IPv6 address + may wish to explore migrating to the ``[DEFAULT]my_ipv6`` setting. Setting + both values enables the appropriate IP addresses based on protocol version + for PXE/iPXE. |