summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2020-04-21 16:46:12 +0000
committerGerrit Code Review <review@openstack.org>2020-04-21 16:46:12 +0000
commit81d38a836b4bd1b2cecd4acc2c285e599b64b2c5 (patch)
treec08cc1464fa9314957a9f5e123719c9a10810af1
parent2a42947b0b0d2257a763e97f898fda7ff7665ac0 (diff)
parentcf412bc81e4182a7c332545cfeb6e154015e3dc3 (diff)
downloadironic-81d38a836b4bd1b2cecd4acc2c285e599b64b2c5.tar.gz
Merge ""dual stack" support for PXE/iPXE"
-rw-r--r--ironic/common/pxe_utils.py42
-rw-r--r--ironic/conf/default.py17
-rw-r--r--ironic/dhcp/base.py12
-rw-r--r--ironic/dhcp/neutron.py43
-rw-r--r--ironic/drivers/modules/pxe_base.py14
-rw-r--r--ironic/tests/unit/dhcp/test_neutron.py66
-rw-r--r--ironic/tests/unit/drivers/modules/test_ipxe.py19
-rw-r--r--ironic/tests/unit/drivers/modules/test_pxe.py17
-rw-r--r--releasenotes/notes/dual-stack-ironic-493ebc7b71263aaa.yaml16
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 1b4b0a48c..cbec1bb0d 100644
--- a/ironic/tests/unit/drivers/modules/test_ipxe.py
+++ b/ironic/tests/unit/drivers/modules/test_ipxe.py
@@ -272,11 +272,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,
@@ -633,6 +636,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'
@@ -677,6 +682,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'
@@ -717,7 +724,9 @@ class iPXEBootTestCase(db_base.DbTestCase):
instance_info = {"boot_option": "netboot"}
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.instance_info['capabilities'] = instance_info
task.node.driver_internal_info['is_whole_disk_image'] = False
@@ -751,6 +760,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.instance_info['capabilities'] = instance_info
task.node.driver_internal_info['is_whole_disk_image'] = True
@@ -796,6 +807,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 7f10297a2..ed4fa2b63 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
@@ -269,6 +270,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,
@@ -554,7 +557,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'
@@ -599,6 +604,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'
@@ -639,6 +646,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.instance_info['capabilities'] = instance_info
task.node.driver_internal_info['is_whole_disk_image'] = False
@@ -670,6 +679,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.instance_info['capabilities'] = instance_info
task.node.driver_internal_info['is_whole_disk_image'] = True
@@ -742,6 +753,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)
@@ -838,6 +851,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.