]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
dhcp fails if extra_dhcp_opts for stateless subnet enabled
authorvenkata anil <anil.venkata@enovance.com>
Wed, 24 Jun 2015 07:33:09 +0000 (07:33 +0000)
committervenkata anil <anil.venkata@enovance.com>
Wed, 24 Jun 2015 07:33:14 +0000 (07:33 +0000)
vm on a network having IPv4 and IPv6 dhcpv6 stateless subnets,
fails to get IPv4 address, when vm uses a port with extra_dhcp_opts.

neutron creates entries in dhcp host file for each subnet of a port.
Each of these entries will have same mac address as first field,
and may have client_id, fqdn, ipv4/ipv6 address for dhcp/dhcpv6 stateful,
or tag as other fields.
For dhcpv6 stateless subnet with extra_dhcp_opts,
host file will have only mac address and tag.

If the last entry in host file for the port with extra_dhcp_opts,
is for dhcpv6 stateless subnet, then dnsmasq tries to use this entry,
(as dnsmasq reads the hosts file from EOF) to resolve
dhcp request even for IPv4, treats as 'no address found'
and fails to send DHCPOFFER.

So we sort the fixed_ips, so that ipv6 subnets for the port are added
first in host file, to avoid this issue.

Change-Id: I3bea58d86a3508e49cbac1d03c6b640836b4a7a2
Closes-bug: #1466144

neutron/agent/linux/dhcp.py
neutron/tests/unit/agent/linux/test_dhcp.py

index 084a67d4171033a9dffac6b1a2697f9e7b63127b..53af635e933a73be4ffb3d1c671009c08753e730 100644 (file)
@@ -434,6 +434,44 @@ class Dnsmasq(DhcpLocalProcess):
         LOG.debug('Reloading allocations for network: %s', self.network.id)
         self.device_manager.update(self.network, self.interface_name)
 
+    def _sort_fixed_ips_for_dnsmasq(self, fixed_ips, v6_nets):
+        """Sort fixed_ips so that stateless IPv6 subnets appear first.
+
+        For example, If a port with v6 extra_dhcp_opts is on a network with
+        IPv4 and IPv6 stateless subnets. Then dhcp host file will have
+        below 2 entries for same MAC,
+
+        fa:16:3e:8f:9d:65,30.0.0.5,set:aabc7d33-4874-429e-9637-436e4232d2cd
+        (entry for IPv4 dhcp)
+        fa:16:3e:8f:9d:65,set:aabc7d33-4874-429e-9637-436e4232d2cd
+        (entry for stateless IPv6 for v6 options)
+
+        dnsmasq internal details for processing host file entries
+        1) dnsmaq reads the host file from EOF.
+        2) So it first picks up stateless IPv6 entry,
+           fa:16:3e:8f:9d:65,set:aabc7d33-4874-429e-9637-436e4232d2cd
+        3) But dnsmasq doesn't have sufficient checks to skip this entry and
+           pick next entry, to process dhcp IPv4 request.
+        4) So dnsmaq uses this this entry to process dhcp IPv4 request.
+        5) As there is no ip in this entry, dnsmaq logs "no address available"
+           and fails to send DHCPOFFER message.
+
+        As we rely on internal details of dnsmasq to understand and fix the
+        issue, Ihar sent a mail to dnsmasq-discuss mailing list
+        http://lists.thekelleys.org.uk/pipermail/dnsmasq-discuss/2015q2/
+        009650.html
+
+        So If we reverse the order of writing entries in host file,
+        so that entry for stateless IPv6 comes first,
+        then dnsmasq can correctly fetch the IPv4 address.
+        """
+        return sorted(
+            fixed_ips,
+            key=lambda fip: ((fip.subnet_id in v6_nets) and (
+                v6_nets[fip.subnet_id].ipv6_address_mode == (
+                    constants.DHCPV6_STATELESS))),
+            reverse=True)
+
     def _iter_hosts(self):
         """Iterate over hosts.
 
@@ -449,8 +487,11 @@ class Dnsmasq(DhcpLocalProcess):
         """
         v6_nets = dict((subnet.id, subnet) for subnet in
                        self.network.subnets if subnet.ip_version == 6)
+
         for port in self.network.ports:
-            for alloc in port.fixed_ips:
+            fixed_ips = self._sort_fixed_ips_for_dnsmasq(port.fixed_ips,
+                                                         v6_nets)
+            for alloc in fixed_ips:
                 # Note(scollins) Only create entries that are
                 # associated with the subnet being managed by this
                 # dhcp agent
index 44a017245a34884b9efe0b1a582466b4c734ba33..0850c27d6c99b02c68562b28198817b6f6119f79 100644 (file)
@@ -160,6 +160,23 @@ class FakeV6PortExtraOpt(object):
                     ip_version=6)]
 
 
+class FakeDualPortWithV6ExtraOpt(object):
+    id = 'hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh'
+    admin_state_up = True
+    device_owner = 'foo3'
+    fixed_ips = [FakeIPAllocation('192.168.0.3',
+                                  'dddddddd-dddd-dddd-dddd-dddddddddddd'),
+                 FakeIPAllocation('ffea:3ba5:a17a:4ba3:0216:3eff:fec2:771d',
+                                  'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee')]
+    mac_address = '00:16:3e:c2:77:1d'
+
+    def __init__(self):
+        self.extra_dhcp_opts = [
+            DhcpOpt(opt_name='dns-server',
+                    opt_value='ffea:3ba5:a17a:4ba3::100',
+                    ip_version=6)]
+
+
 class FakeDualPort(object):
     id = 'hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh'
     admin_state_up = True
@@ -617,6 +634,14 @@ class FakeV6NetworkStatelessDHCP(object):
     namespace = 'qdhcp-ns'
 
 
+class FakeNetworkWithV6SatelessAndV4DHCPSubnets(object):
+    id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'
+
+    subnets = [FakeV6SubnetStateless(), FakeV4Subnet()]
+    ports = [FakeDualPortWithV6ExtraOpt(), FakeRouterPort()]
+    namespace = 'qdhcp-ns'
+
+
 class LocalChild(dhcp.DhcpLocalProcess):
     PORTS = {4: [4], 6: [6]}
 
@@ -1638,6 +1663,33 @@ 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_net_with_V6_stateless_and_V4_subnets(
+                                                                    self):
+        exp_host_name = '/dhcp/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/host'
+        exp_host_data = (
+            '00:16:3e:c2:77:1d,set:hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh\n'
+            '00:16:3e:c2:77:1d,host-192-168-0-3.openstacklocal,'
+            '192.168.0.3,set:hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh\n'
+            '00:00:0f:rr:rr:rr,'
+            'host-192-168-0-1.openstacklocal,192.168.0.1\n').lstrip()
+        exp_opt_name = '/dhcp/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/opts'
+        exp_opt_data = (
+            'tag:tag0,option6:domain-search,openstacklocal\n'
+            'tag:tag1,option:dns-server,8.8.8.8\n'
+            'tag:tag1,option:classless-static-route,20.0.0.1/24,20.0.0.1,'
+            '169.254.169.254/32,192.168.0.1,0.0.0.0/0,192.168.0.1\n'
+            'tag:tag1,249,20.0.0.1/24,20.0.0.1,169.254.169.254/32,'
+            '192.168.0.1,0.0.0.0/0,192.168.0.1\n'
+            'tag:tag1,option:router,192.168.0.1\n'
+            'tag:hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh,'
+            'option6:dns-server,ffea:3ba5:a17a:4ba3::100').lstrip()
+
+        dm = self._get_dnsmasq(FakeNetworkWithV6SatelessAndV4DHCPSubnets())
+        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_should_enable_metadata_namespaces_disabled_returns_false(self):
         self.conf.set_override('use_namespaces', False)
         self.assertFalse(dhcp.Dnsmasq.should_enable_metadata(self.conf,