DHCPv6 - Use addr6_list in dnsmasq
Adds a new bool option dnsmasq_enable_addr6_list, when
enabled configuration for dnsmasq will be created with a
single dhcp-host entry specifying a list of ip addresses
allocated for a port.
Previously the dnsmasq dhcp-agent driver would write a
separate dhcp-host entry for each fixed-ip of a port in
the dnsmasq hosts file. The result of the previous
behaviour is that dnsmasq will only use one of the config
entries, i.e the first one matching the mac identifier.
The trade-off is that only a single dns_assignment will
be used for IPv6 addresses within the same subnet. (But
in practice, this was always the case since only the
first config entry would be used by dnsmasq.)
Why is this neccecary:
This is done to enable ironic provisioning over IPv6
using DHCPv6-stateful. For background info, please
read dnsmasq-discuss thread:
http://lists.thekelleys.org.uk/pipermail/dnsmasq-discuss/2020q1/thread.html#13671
Conflicts:
neutron/cmd/sanity/checks.py
Closes-Bug: #1861032
Change-Id: I833840e7daed2efa7efaece27cfd1ba28e0feb90
(cherry picked from commit 592c2f8d91
)
This commit is contained in:
parent
6a1c6b6e58
commit
03e88cd72a
@ -567,7 +567,89 @@ class Dnsmasq(DhcpLocalProcess):
|
||||
constants.DHCPV6_STATELESS))),
|
||||
reverse=True)
|
||||
|
||||
def _iter_hosts(self):
|
||||
def _merge_alloc_addr6_list(self, fixed_ips, v6_nets):
|
||||
"""Merge fixed_ips to ipv6 addr lists
|
||||
|
||||
If a port have multiple IPv6 addresses in the same subnet, merge the
|
||||
into one entry listing all the addresess, creating a single dhcp-host
|
||||
entry with the list of addresses defined allow dnsmasq to make all
|
||||
addresses available as requests for leases arrive.
|
||||
|
||||
See dnsmasq-discuss mailing list: http://lists.thekelleys.org.uk/
|
||||
pipermail/dnsmasq-discuss/2020q1/013743.html
|
||||
|
||||
"""
|
||||
by_subnet = {}
|
||||
NewFip = collections.namedtuple('NewFip', 'subnet_id ip_address')
|
||||
merged = []
|
||||
|
||||
for fip in fixed_ips:
|
||||
if (fip.subnet_id in v6_nets and
|
||||
v6_nets[fip.subnet_id].ipv6_address_mode == (
|
||||
constants.DHCPV6_STATEFUL)):
|
||||
if fip.subnet_id not in by_subnet:
|
||||
by_subnet.update({fip.subnet_id: []})
|
||||
by_subnet[fip.subnet_id].append(fip.ip_address)
|
||||
else:
|
||||
merged.append(fip)
|
||||
|
||||
for subnet_id in by_subnet:
|
||||
addr6_list = ','.join([self._format_address_for_dnsmasq(ip)
|
||||
for ip in by_subnet[subnet_id]])
|
||||
merged.append(NewFip(subnet_id=subnet_id,
|
||||
ip_address=addr6_list))
|
||||
|
||||
return merged
|
||||
|
||||
def _get_dns_assignment(self, ip_address, dns_assignment):
|
||||
"""Get DNS assignment hostname and fqdn
|
||||
|
||||
In dnsmasq it is not possible to configure two dhcp-host
|
||||
entries mapped to a single client mac address with IP
|
||||
addresses in the same subnet. When recieving a requst
|
||||
dnsmasq will match on the first entry in it's config,
|
||||
and lease that address. The second entry will never be
|
||||
used.
|
||||
|
||||
For IPv6 it is possible to add multiple IPv6 addresses
|
||||
to a single dhcp-host entry by placing a list of addresses
|
||||
in brackets, i.e [addr1][addr2][...]. See dnsmasq mailing
|
||||
list: http://lists.thekelleys.org.uk/pipermail/
|
||||
dnsmasq-discuss/2020q1/013671.html. Since we cannot have
|
||||
two hostnames in the dhcp-host entry this method picks the
|
||||
first hostname and fqdn it find's matching one of the IP's
|
||||
in the fixed-ips in dns_assignment or the hostname is
|
||||
generated based on the first fixed-ip.
|
||||
|
||||
:param ip_address: IP address or a list of IPv6 addresses
|
||||
:param dns_ip_map: DNS IP Mapping
|
||||
:param dns_assignment: DNS assignments
|
||||
:return: hostname, fqdn
|
||||
"""
|
||||
hostname, fqdn = None, None
|
||||
ip_addresses = ip_address.replace('[', '').split(']')
|
||||
|
||||
if dns_assignment:
|
||||
dns_ip_map = {d.ip_address: d for d in dns_assignment}
|
||||
for addr in ip_addresses:
|
||||
# If dns_name attribute is supported by ports API, return the
|
||||
# dns_assignment generated by the Neutron server. Otherwise,
|
||||
# generate hostname and fqdn locally (previous behaviour)
|
||||
if addr in dns_ip_map:
|
||||
hostname = dns_ip_map[addr].hostname
|
||||
fqdn = dns_ip_map[addr].fqdn
|
||||
break
|
||||
|
||||
if hostname is None:
|
||||
hostname = ('host-%s' %
|
||||
ip_addresses[0].replace('.', '-').replace(':', '-'))
|
||||
fqdn = hostname
|
||||
if self.conf.dns_domain:
|
||||
fqdn = '%s.%s' % (fqdn, self.conf.dns_domain)
|
||||
|
||||
return hostname, fqdn
|
||||
|
||||
def _iter_hosts(self, merge_addr6_list=False):
|
||||
"""Iterate over hosts.
|
||||
|
||||
For each host on the network we yield a tuple containing:
|
||||
@ -590,11 +672,13 @@ class Dnsmasq(DhcpLocalProcess):
|
||||
for port in self.network.ports:
|
||||
fixed_ips = self._sort_fixed_ips_for_dnsmasq(port.fixed_ips,
|
||||
v6_nets)
|
||||
# TODO(hjensas): Drop this conditional and option once distros
|
||||
# generally have dnsmasq supporting addr6 list and range.
|
||||
if self.conf.dnsmasq_enable_addr6_list and merge_addr6_list:
|
||||
fixed_ips = self._merge_alloc_addr6_list(fixed_ips, v6_nets)
|
||||
# Confirm whether Neutron server supports dns_name attribute in the
|
||||
# ports API
|
||||
dns_assignment = getattr(port, 'dns_assignment', None)
|
||||
if dns_assignment:
|
||||
dns_ip_map = {d.ip_address: d for d in dns_assignment}
|
||||
for alloc in fixed_ips:
|
||||
no_dhcp = False
|
||||
no_opts = False
|
||||
@ -606,18 +690,9 @@ class Dnsmasq(DhcpLocalProcess):
|
||||
# to provide options for a client that won't use DHCP
|
||||
no_opts = addr_mode == constants.IPV6_SLAAC
|
||||
|
||||
# If dns_name attribute is supported by ports API, return the
|
||||
# dns_assignment generated by the Neutron server. Otherwise,
|
||||
# generate hostname and fqdn locally (previous behaviour)
|
||||
if dns_assignment:
|
||||
hostname = dns_ip_map[alloc.ip_address].hostname
|
||||
fqdn = dns_ip_map[alloc.ip_address].fqdn
|
||||
else:
|
||||
hostname = 'host-%s' % alloc.ip_address.replace(
|
||||
'.', '-').replace(':', '-')
|
||||
fqdn = hostname
|
||||
if self.conf.dns_domain:
|
||||
fqdn = '%s.%s' % (fqdn, self.conf.dns_domain)
|
||||
hostname, fqdn = self._get_dns_assignment(alloc.ip_address,
|
||||
dns_assignment)
|
||||
|
||||
yield (port, alloc, hostname, fqdn, no_dhcp, no_opts)
|
||||
|
||||
def _get_port_extra_dhcp_opts(self, port):
|
||||
@ -702,7 +777,7 @@ class Dnsmasq(DhcpLocalProcess):
|
||||
if s.enable_dhcp]
|
||||
# NOTE(ihrachyshka): the loop should not log anything inside it, to
|
||||
# avoid potential performance drop when lots of hosts are dumped
|
||||
for host_tuple in self._iter_hosts():
|
||||
for host_tuple in self._iter_hosts(merge_addr6_list=True):
|
||||
port, alloc, hostname, name, no_dhcp, no_opts = host_tuple
|
||||
if no_dhcp:
|
||||
if not no_opts and self._get_port_extra_dhcp_opts(port):
|
||||
|
@ -42,6 +42,7 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
MINIMUM_DNSMASQ_VERSION = 2.67
|
||||
DNSMASQ_VERSION_DHCP_RELEASE6 = 2.76
|
||||
DNSMASQ_VERSION_HOST_ADDR6_LIST = 2.81
|
||||
MINIMUM_DIBBLER_VERSION = '1.0.1'
|
||||
CONNTRACK_GRE_MODULE = 'nf_conntrack_proto_gre'
|
||||
|
||||
@ -199,6 +200,10 @@ def get_dnsmasq_version_with_dhcp_release6():
|
||||
return DNSMASQ_VERSION_DHCP_RELEASE6
|
||||
|
||||
|
||||
def get_dnsmasq_version_with_host_addr6_list():
|
||||
return DNSMASQ_VERSION_HOST_ADDR6_LIST
|
||||
|
||||
|
||||
def dnsmasq_local_service_supported():
|
||||
cmd = ['dnsmasq', '--test', '--local-service']
|
||||
env = {'LC_ALL': 'C'}
|
||||
@ -223,6 +228,17 @@ def dnsmasq_version_supported():
|
||||
ver = float(m.group(1)) if m else 0
|
||||
if ver < MINIMUM_DNSMASQ_VERSION:
|
||||
return False
|
||||
if (cfg.CONF.dnsmasq_enable_addr6_list is True and
|
||||
ver < DNSMASQ_VERSION_HOST_ADDR6_LIST):
|
||||
LOG.warning('Support for multiple IPv6 addresses in host '
|
||||
'entries was introduced in dnsmasq version '
|
||||
'%(required)s. Found dnsmasq version %(current)s, '
|
||||
'which does not support this feature. Unless support '
|
||||
'for multiple IPv6 addresses was backported to the '
|
||||
'running build of dnsmasq, the configuration option '
|
||||
'dnsmasq_enable_addr6_list should be set to False.',
|
||||
{'required': DNSMASQ_VERSION_HOST_ADDR6_LIST,
|
||||
'current': ver})
|
||||
except (OSError, RuntimeError, IndexError, ValueError) as e:
|
||||
LOG.debug("Exception while checking minimal dnsmasq version. "
|
||||
"Exception: %s", e)
|
||||
|
@ -110,6 +110,9 @@ DNSMASQ_OPTS = [
|
||||
cfg.IntOpt('dhcp_rebinding_time', default=0,
|
||||
help=_("DHCP rebinding time T2 (in seconds). If set to 0, it "
|
||||
"will default to 7/8 of the lease time.")),
|
||||
cfg.BoolOpt('dnsmasq_enable_addr6_list', default=False,
|
||||
help=_("Enable dhcp-host entry with list of addresses when "
|
||||
"port has multiple IPv6 addresses in the same subnet."))
|
||||
]
|
||||
|
||||
|
||||
|
@ -229,6 +229,25 @@ class FakeV6PortExtraOpt(object):
|
||||
ip_version=constants.IP_VERSION_6)]
|
||||
|
||||
|
||||
class FakeV6PortMultipleFixedIpsSameSubnet(object):
|
||||
def __init__(self, domain='openstacklocal'):
|
||||
self.id = 'hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh'
|
||||
self.admin_state_up = True
|
||||
self.device_owner = 'foo3'
|
||||
self.fixed_ips = [
|
||||
FakeIPAllocation('fdca:3ba5:a17a:4ba3::2',
|
||||
'ffffffff-ffff-ffff-ffff-ffffffffffff'),
|
||||
FakeIPAllocation('fdca:3ba5:a17a:4ba3::4',
|
||||
'ffffffff-ffff-ffff-ffff-ffffffffffff')]
|
||||
self.mac_address = '00:00:f3:aa:bb:cc'
|
||||
self.device_id = 'fake_port6'
|
||||
self.extra_dhcp_opts = []
|
||||
self.dns_assignment = [FakeDNSAssignment('fdca:3ba5:a17a:4ba3::2',
|
||||
domain=domain),
|
||||
FakeDNSAssignment('fdca:3ba5:a17a:4ba3::4',
|
||||
domain=domain)]
|
||||
|
||||
|
||||
class FakeDualPortWithV6ExtraOpt(object):
|
||||
def __init__(self):
|
||||
self.id = 'hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh'
|
||||
@ -1005,6 +1024,14 @@ class FakeNetworkWithV6SatelessAndV4DHCPSubnets(object):
|
||||
self.namespace = 'qdhcp-ns'
|
||||
|
||||
|
||||
class FakeV6NetworkStatefulDHCPSameSubnetFixedIps(object):
|
||||
def __init__(self):
|
||||
self.id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'
|
||||
self.subnets = [FakeV6SubnetDHCPStateful()]
|
||||
self.ports = [FakeV6PortMultipleFixedIpsSameSubnet()]
|
||||
self.namespace = 'qdhcp-ns'
|
||||
|
||||
|
||||
class LocalChild(dhcp.DhcpLocalProcess):
|
||||
PORTS = {4: [4], 6: [6]}
|
||||
|
||||
@ -2765,6 +2792,23 @@ class TestDnsmasq(TestBase):
|
||||
self.safe.assert_has_calls([mock.call(exp_host_name, exp_host_data),
|
||||
mock.call(exp_opt_name, exp_opt_data)])
|
||||
|
||||
def test_host_and_opts_file_on_stateful_dhcpv6_same_subnet_fixedips(self):
|
||||
self.conf.set_override('dnsmasq_enable_addr6_list', True)
|
||||
exp_host_name = '/dhcp/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/host'
|
||||
exp_host_data = (
|
||||
'00:00:f3:aa:bb:cc,host-fdca-3ba5-a17a-4ba3--2.openstacklocal.,'
|
||||
'[fdca:3ba5:a17a:4ba3::2],[fdca:3ba5:a17a:4ba3::4]\n'.lstrip())
|
||||
exp_opt_name = '/dhcp/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/opts'
|
||||
exp_opt_data = ('tag:subnet-ffffffff-ffff-ffff-ffff-ffffffffffff,'
|
||||
'option6:dns-server,[2001:0200:feed:7ac0::1]\n'
|
||||
'tag:subnet-ffffffff-ffff-ffff-ffff-ffffffffffff,'
|
||||
'option6:domain-search,openstacklocal').lstrip()
|
||||
dm = self._get_dnsmasq(FakeV6NetworkStatefulDHCPSameSubnetFixedIps())
|
||||
dm._output_hosts_file()
|
||||
dm._output_opts_file()
|
||||
self.safe.assert_has_calls([mock.call(exp_host_name, exp_host_data),
|
||||
mock.call(exp_opt_name, exp_opt_data)])
|
||||
|
||||
def test_host_and_opts_file_on_stateless_dhcpv6_network_no_dns(self):
|
||||
exp_host_name = '/dhcp/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/host'
|
||||
exp_opt_name = '/dhcp/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/opts'
|
||||
|
@ -0,0 +1,25 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Adds support for configuring a list of IPv6 addresses for a dhcp-host entry
|
||||
in the dnsmasq DHCP agent driver. For a port with multiple IPv6 fixed-ips
|
||||
in the same subnet a single dhcp-host entry including all the addresses are
|
||||
written to the dnsmasq dhcp-hostsfile.
|
||||
|
||||
Reserving multiple addresses for a host eases problems related to network
|
||||
and chain-booting where each step in the boot process requests an address
|
||||
using different DUID/IAID combinations. With a single address, only one
|
||||
gets the "static" address and the boot process will fail on the following
|
||||
steps. By reserving enough addresses for all the stages of the boot process
|
||||
this problem is resolved. (See bug:
|
||||
`#1861032 <https://bugs.launchpad.net/neutron/+bug/1861032>`_)
|
||||
|
||||
.. NOTE:: This requires dnsmasq version 2.81 or later. Some distributions
|
||||
may backport this feauture to earlier dnsmasq version as part of
|
||||
the packaging, check the distributions releasenotes.
|
||||
|
||||
Since the new configuration format is invalid in previous versions
|
||||
of dnsmasq this feauture is *disabled* by default. To *enable* the
|
||||
feature set the option ``dnsmasq_enable_addr6_list`` in DHCP agent
|
||||
configuration to ``True``.
|
||||
|
Loading…
Reference in New Issue
Block a user