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.
"""
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
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
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]}
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,