diff --git a/ironic/common/neutron.py b/ironic/common/neutron.py index f915ccef6f..1cd34fd16f 100644 --- a/ironic/common/neutron.py +++ b/ironic/common/neutron.py @@ -12,6 +12,7 @@ import copy +import netaddr from neutronclient.common import exceptions as neutron_exceptions from neutronclient.v2_0 import client as clientv20 from oslo_log import log @@ -192,6 +193,32 @@ def _verify_security_groups(security_groups, client): raise exception.NetworkError(msg) +def _add_ip_addresses_for_ipv6_stateful(port, client): + """Add additional IP addresses to the ipv6 stateful neutron port + + When network booting with DHCPv6-stateful we cannot control the CLID/IAID + used by the different clients, UEFI, iPXE, Ironic IPA etc. Multiple + IP address reservation is required in the DHCPv6 server to avoid + NoAddrsAvail issues. + + :param port: A neutron port + :param client: Neutron client + """ + fixed_ips = port['port']['fixed_ips'] + if (not fixed_ips + or netaddr.IPAddress(fixed_ips[0]['ip_address']).version != 6): + return + + subnet = client.show_subnet( + port['port']['fixed_ips'][0]['subnet_id']).get('subnet') + if subnet and subnet['ipv6_address_mode'] == 'dhcpv6-stateful': + for i in range(1, CONF.neutron.dhcpv6_stateful_address_count): + fixed_ips.append({'subnet_id': subnet['id']}) + + body = {'port': {'fixed_ips': fixed_ips}} + client.update_port(port['port']['id'], body) + + def add_ports_to_network(task, network_uuid, security_groups=None): """Create neutron ports to boot the ramdisk. @@ -295,6 +322,8 @@ def add_ports_to_network(task, network_uuid, security_groups=None): wait_for_host_agent(client, port_body['port']['binding:host_id']) port = client.create_port(port_body) + if CONF.neutron.dhcpv6_stateful_address_count > 1: + _add_ip_addresses_for_ipv6_stateful(port, client) if is_smart_nic: wait_for_port_status(client, port['port']['id'], 'ACTIVE') except neutron_exceptions.NeutronClientException as e: diff --git a/ironic/conf/neutron.py b/ironic/conf/neutron.py index 3edb73bc28..3775996366 100644 --- a/ironic/conf/neutron.py +++ b/ironic/conf/neutron.py @@ -102,6 +102,16 @@ opts = [ '"neutron" network interface and not used for the ' '"flat" or "noop" network interfaces. If not ' 'specified, the default security group is used.')), + cfg.IntOpt('dhcpv6_stateful_address_count', + default=4, + help=_('Number of IPv6 addresses to allocate for ports created ' + 'for provisioning, cleaning, rescue or inspection on ' + 'DHCPv6-stateful networks. Different stages of the ' + 'chain-loading process will request addresses with ' + 'different CLID/IAID. Due to non-identical identifiers ' + 'multiple addresses must be reserved for the host to ' + 'ensure each step of the boot process can successfully ' + 'lease addresses.')) ] diff --git a/ironic/tests/unit/common/test_neutron.py b/ironic/tests/unit/common/test_neutron.py index 34894de34d..86b9b23464 100644 --- a/ironic/tests/unit/common/test_neutron.py +++ b/ironic/tests/unit/common/test_neutron.py @@ -161,7 +161,8 @@ class TestNeutronNetworkActions(db_base.DbTestCase): )] # Very simple neutron port representation self.neutron_port = {'id': '132f871f-eaec-4fed-9475-0d54465e0f00', - 'mac_address': '52:54:00:cf:2d:32'} + 'mac_address': '52:54:00:cf:2d:32', + 'fixed_ips': []} self.network_uuid = uuidutils.generate_uuid() self.client_mock = mock.Mock() self.client_mock.list_agents.return_value = { @@ -217,7 +218,8 @@ class TestNeutronNetworkActions(db_base.DbTestCase): expected_body2['port']['mac_address'] = port2.address expected_body2['fixed_ips'] = [] neutron_port2 = {'id': '132f871f-eaec-4fed-9475-0d54465e0f01', - 'mac_address': port2.address} + 'mac_address': port2.address, + 'fixed_ips': []} self.client_mock.create_port.side_effect = [ {'port': self.neutron_port}, {'port': neutron_port2} @@ -259,6 +261,36 @@ class TestNeutronNetworkActions(db_base.DbTestCase): self._test_add_ports_to_network(is_client_id=False, security_groups=sg_ids) + def test__add_ip_addresses_for_ipv6_stateful(self): + subnet_id = uuidutils.generate_uuid() + self.client_mock.show_subnet.return_value = { + 'subnet': { + 'id': subnet_id, + 'ip_version': 6, + 'ipv6_address_mode': 'dhcpv6-stateful' + } + } + self.neutron_port['fixed_ips'] = [{'subnet_id': subnet_id, + 'ip_address': '2001:db8::1'}] + + expected_body = { + 'port': { + 'fixed_ips': [ + {'subnet_id': subnet_id, 'ip_address': '2001:db8::1'}, + {'subnet_id': subnet_id}, + {'subnet_id': subnet_id}, + {'subnet_id': subnet_id} + ] + } + } + + neutron._add_ip_addresses_for_ipv6_stateful( + {'port': self.neutron_port}, + self.client_mock + ) + self.client_mock.update_port.assert_called_once_with( + self.neutron_port['id'], expected_body) + def test_verify_sec_groups(self): sg_ids = [] for i in range(2): @@ -655,6 +687,19 @@ class TestNeutronNetworkActions(db_base.DbTestCase): self.assertFalse(res) self.assertTrue(log_mock.error.called) + @mock.patch.object(neutron, 'LOG', autospec=True) + def test_validate_port_info_neutron_with_network_type_unmanaged( + self, log_mock): + self.node.network_interface = 'neutron' + self.node.save() + llc = {'network_type': 'unmanaged'} + port = object_utils.create_test_port( + self.context, node_id=self.node.id, uuid=uuidutils.generate_uuid(), + address='52:54:00:cf:2d:33', local_link_connection=llc) + res = neutron.validate_port_info(self.node, port) + self.assertTrue(res) + self.assertFalse(log_mock.warning.called) + def test_validate_agent_up(self): self.client_mock.list_agents.return_value = { 'agents': [{'alive': True}]} diff --git a/releasenotes/notes/dhcpv6-stateful-address-count-0f94ac6a55bd9e51.yaml b/releasenotes/notes/dhcpv6-stateful-address-count-0f94ac6a55bd9e51.yaml new file mode 100644 index 0000000000..8267f99d45 --- /dev/null +++ b/releasenotes/notes/dhcpv6-stateful-address-count-0f94ac6a55bd9e51.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + For baremetal operations on DHCPv6-stateful networks multiple IPv6 + addresses can now be allocated for neutron ports created for provisioning, + cleaning, rescue or inspection. The new parameter + ``[neutron]/dhcpv6_stateful_address_count`` controls the number of addresses + to allocate (Default: 4). +