Merge "DHCPv6 - Use addr6_list in dnsmasq" into stable/train
This commit is contained in:
commit
7b406a832c
|
@ -567,7 +567,89 @@ class Dnsmasq(DhcpLocalProcess):
|
||||||
constants.DHCPV6_STATELESS))),
|
constants.DHCPV6_STATELESS))),
|
||||||
reverse=True)
|
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.
|
"""Iterate over hosts.
|
||||||
|
|
||||||
For each host on the network we yield a tuple containing:
|
For each host on the network we yield a tuple containing:
|
||||||
|
@ -590,11 +672,13 @@ class Dnsmasq(DhcpLocalProcess):
|
||||||
for port in self.network.ports:
|
for port in self.network.ports:
|
||||||
fixed_ips = self._sort_fixed_ips_for_dnsmasq(port.fixed_ips,
|
fixed_ips = self._sort_fixed_ips_for_dnsmasq(port.fixed_ips,
|
||||||
v6_nets)
|
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
|
# Confirm whether Neutron server supports dns_name attribute in the
|
||||||
# ports API
|
# ports API
|
||||||
dns_assignment = getattr(port, 'dns_assignment', None)
|
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:
|
for alloc in fixed_ips:
|
||||||
no_dhcp = False
|
no_dhcp = False
|
||||||
no_opts = False
|
no_opts = False
|
||||||
|
@ -606,18 +690,9 @@ class Dnsmasq(DhcpLocalProcess):
|
||||||
# to provide options for a client that won't use DHCP
|
# to provide options for a client that won't use DHCP
|
||||||
no_opts = addr_mode == constants.IPV6_SLAAC
|
no_opts = addr_mode == constants.IPV6_SLAAC
|
||||||
|
|
||||||
# If dns_name attribute is supported by ports API, return the
|
hostname, fqdn = self._get_dns_assignment(alloc.ip_address,
|
||||||
# dns_assignment generated by the Neutron server. Otherwise,
|
dns_assignment)
|
||||||
# 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)
|
|
||||||
yield (port, alloc, hostname, fqdn, no_dhcp, no_opts)
|
yield (port, alloc, hostname, fqdn, no_dhcp, no_opts)
|
||||||
|
|
||||||
def _get_port_extra_dhcp_opts(self, port):
|
def _get_port_extra_dhcp_opts(self, port):
|
||||||
|
@ -702,7 +777,7 @@ class Dnsmasq(DhcpLocalProcess):
|
||||||
if s.enable_dhcp]
|
if s.enable_dhcp]
|
||||||
# NOTE(ihrachyshka): the loop should not log anything inside it, to
|
# NOTE(ihrachyshka): the loop should not log anything inside it, to
|
||||||
# avoid potential performance drop when lots of hosts are dumped
|
# 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
|
port, alloc, hostname, name, no_dhcp, no_opts = host_tuple
|
||||||
if no_dhcp:
|
if no_dhcp:
|
||||||
if not no_opts and self._get_port_extra_dhcp_opts(port):
|
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
|
MINIMUM_DNSMASQ_VERSION = 2.67
|
||||||
DNSMASQ_VERSION_DHCP_RELEASE6 = 2.76
|
DNSMASQ_VERSION_DHCP_RELEASE6 = 2.76
|
||||||
|
DNSMASQ_VERSION_HOST_ADDR6_LIST = 2.81
|
||||||
MINIMUM_DIBBLER_VERSION = '1.0.1'
|
MINIMUM_DIBBLER_VERSION = '1.0.1'
|
||||||
CONNTRACK_GRE_MODULE = 'nf_conntrack_proto_gre'
|
CONNTRACK_GRE_MODULE = 'nf_conntrack_proto_gre'
|
||||||
|
|
||||||
|
@ -199,6 +200,10 @@ def get_dnsmasq_version_with_dhcp_release6():
|
||||||
return DNSMASQ_VERSION_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():
|
def dnsmasq_local_service_supported():
|
||||||
cmd = ['dnsmasq', '--test', '--local-service']
|
cmd = ['dnsmasq', '--test', '--local-service']
|
||||||
env = {'LC_ALL': 'C'}
|
env = {'LC_ALL': 'C'}
|
||||||
|
@ -223,6 +228,17 @@ def dnsmasq_version_supported():
|
||||||
ver = float(m.group(1)) if m else 0
|
ver = float(m.group(1)) if m else 0
|
||||||
if ver < MINIMUM_DNSMASQ_VERSION:
|
if ver < MINIMUM_DNSMASQ_VERSION:
|
||||||
return False
|
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:
|
except (OSError, RuntimeError, IndexError, ValueError) as e:
|
||||||
LOG.debug("Exception while checking minimal dnsmasq version. "
|
LOG.debug("Exception while checking minimal dnsmasq version. "
|
||||||
"Exception: %s", e)
|
"Exception: %s", e)
|
||||||
|
|
|
@ -110,6 +110,9 @@ DNSMASQ_OPTS = [
|
||||||
cfg.IntOpt('dhcp_rebinding_time', default=0,
|
cfg.IntOpt('dhcp_rebinding_time', default=0,
|
||||||
help=_("DHCP rebinding time T2 (in seconds). If set to 0, it "
|
help=_("DHCP rebinding time T2 (in seconds). If set to 0, it "
|
||||||
"will default to 7/8 of the lease time.")),
|
"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)]
|
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):
|
class FakeDualPortWithV6ExtraOpt(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.id = 'hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh'
|
self.id = 'hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh'
|
||||||
|
@ -1005,6 +1024,14 @@ class FakeNetworkWithV6SatelessAndV4DHCPSubnets(object):
|
||||||
self.namespace = 'qdhcp-ns'
|
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):
|
class LocalChild(dhcp.DhcpLocalProcess):
|
||||||
PORTS = {4: [4], 6: [6]}
|
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),
|
self.safe.assert_has_calls([mock.call(exp_host_name, exp_host_data),
|
||||||
mock.call(exp_opt_name, exp_opt_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):
|
def test_host_and_opts_file_on_stateless_dhcpv6_network_no_dns(self):
|
||||||
exp_host_name = '/dhcp/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/host'
|
exp_host_name = '/dhcp/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/host'
|
||||||
exp_opt_name = '/dhcp/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/opts'
|
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