From 03e88cd72a8f547133563e4a7a0f099c303be6b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Jens=C3=A5s?= Date: Fri, 17 Jan 2020 12:29:10 +0100 Subject: [PATCH] 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 592c2f8d91c3172c75cc5a2464350891b0a303f1) --- neutron/agent/linux/dhcp.py | 107 +++++++++++++++--- neutron/cmd/sanity/checks.py | 16 +++ neutron/conf/agent/dhcp.py | 3 + neutron/tests/unit/agent/linux/test_dhcp.py | 44 +++++++ ...t-addr6-list-support-45d104b3f7ce220e.yaml | 25 ++++ 5 files changed, 179 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/dhcp-dnsmasq-dhcp-host-addr6-list-support-45d104b3f7ce220e.yaml diff --git a/neutron/agent/linux/dhcp.py b/neutron/agent/linux/dhcp.py index 47f6cf5e5ae..7353deccf4a 100644 --- a/neutron/agent/linux/dhcp.py +++ b/neutron/agent/linux/dhcp.py @@ -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): diff --git a/neutron/cmd/sanity/checks.py b/neutron/cmd/sanity/checks.py index cbb2cef7b5d..425c5378218 100644 --- a/neutron/cmd/sanity/checks.py +++ b/neutron/cmd/sanity/checks.py @@ -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) diff --git a/neutron/conf/agent/dhcp.py b/neutron/conf/agent/dhcp.py index 9823dd1bd9a..a9c629a388b 100644 --- a/neutron/conf/agent/dhcp.py +++ b/neutron/conf/agent/dhcp.py @@ -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.")) ] diff --git a/neutron/tests/unit/agent/linux/test_dhcp.py b/neutron/tests/unit/agent/linux/test_dhcp.py index a7c509f269b..f17b26f0f04 100644 --- a/neutron/tests/unit/agent/linux/test_dhcp.py +++ b/neutron/tests/unit/agent/linux/test_dhcp.py @@ -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' diff --git a/releasenotes/notes/dhcp-dnsmasq-dhcp-host-addr6-list-support-45d104b3f7ce220e.yaml b/releasenotes/notes/dhcp-dnsmasq-dhcp-host-addr6-list-support-45d104b3f7ce220e.yaml new file mode 100644 index 00000000000..f96201b62bc --- /dev/null +++ b/releasenotes/notes/dhcp-dnsmasq-dhcp-host-addr6-list-support-45d104b3f7ce220e.yaml @@ -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 `_) + + .. 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``. +