Additional IP addresses to IPv6 stateful ports

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.

A new option to control the number of addresses to
allocate for IPv6 stateful ports created by Ironic
for provisioning, cleaning, rescuing etc is added.

Story: 2007315
Task:  38820
Change-Id: I8c595830b8c7974c651a9e524b57a6540e4f3d1f
This commit is contained in:
Harald Jensås 2019-12-19 15:29:04 +01:00
parent c40d221fca
commit cb3185274a
4 changed files with 95 additions and 2 deletions

View File

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

View File

@ -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.'))
]

View File

@ -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}]}

View File

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