Retry dhcp_release on failures

Sometimes calls to dhcp_release(6) do not result in removal
of a lease from the leases file, for example, when the release
packet is not received by dnsmasq.  Trying more than once is
recommended in this case.

Instead of blindly trying some number of times, we monitor
the lease file contents, and retry the dhcp_release(6) call
when an entry still remains.  This is possible since
dhcp_release(6) is being run from the DHCP server itself.
We try three times and wait 0.3 seconds between tries.

We also now check for any stale leases in the leases file
that are unknown to neutron, also trying to remove them.

Change-Id: Ic1864f7efbc94db1369ac7f3e2879fda86f95a11
Closes-bug: #1764481
This commit is contained in:
Brian Haley 2018-04-11 21:22:23 -04:00
parent 2f3fb39fa2
commit fab032b426
2 changed files with 308 additions and 69 deletions

View File

@ -58,6 +58,8 @@ METADATA_PORT = 80
WIN2k3_STATIC_DNS = 249 WIN2k3_STATIC_DNS = 249
NS_PREFIX = 'qdhcp-' NS_PREFIX = 'qdhcp-'
DNSMASQ_SERVICE_NAME = 'dnsmasq' DNSMASQ_SERVICE_NAME = 'dnsmasq'
DHCP_RELEASE_TRIES = 3
DHCP_RELEASE_TRIES_SLEEP = 0.3
class DictModel(dict): class DictModel(dict):
@ -470,10 +472,10 @@ class Dnsmasq(DhcpLocalProcess):
"will not call it again.") "will not call it again.")
return self._IS_DHCP_RELEASE6_SUPPORTED return self._IS_DHCP_RELEASE6_SUPPORTED
def _release_lease(self, mac_address, ip, client_id=None, def _release_lease(self, mac_address, ip, ip_version, client_id=None,
server_id=None, iaid=None): server_id=None, iaid=None):
"""Release a DHCP lease.""" """Release a DHCP lease."""
if netaddr.IPAddress(ip).version == constants.IP_VERSION_6: if ip_version == constants.IP_VERSION_6:
if not self._is_dhcp_release6_supported(): if not self._is_dhcp_release6_supported():
return return
cmd = ['dhcp_release6', '--iface', self.interface_name, cmd = ['dhcp_release6', '--iface', self.interface_name,
@ -754,15 +756,12 @@ class Dnsmasq(DhcpLocalProcess):
LOG.debug('Error while reading hosts file %s', filename) LOG.debug('Error while reading hosts file %s', filename)
return leases return leases
def _read_v6_leases_file_leases(self, filename): def _read_leases_file_leases(self, filename, ip_version=None):
""" """
reading information from leases file which is needed to pass to Read information from leases file, which is needed to pass to
dhcp_release6 command line utility if some of these leases are not dhcp_release6 command line utility if some of these leases are not
needed anymore needed anymore
in this method ipv4 entries in leases file are ignored, as info in
hosts file is enough
each line in dnsmasq leases file is one of the following each line in dnsmasq leases file is one of the following
* duid entry: duid server_duid * duid entry: duid server_duid
There MUST be single duid entry per file There MUST be single duid entry per file
@ -791,7 +790,8 @@ class Dnsmasq(DhcpLocalProcess):
dnsmasq-discuss/2016q2/010595.html dnsmasq-discuss/2016q2/010595.html
:param filename: leases file :param filename: leases file
:return: dict, keys are IPv6 addresses, values are dicts containing :param ip_version: IP version of entries to return, or None for all
:return: dict, keys are IP(v6) addresses, values are dicts containing
iaid, client_id and server_id iaid, client_id and server_id
""" """
leases = {} leases = {}
@ -812,7 +812,8 @@ class Dnsmasq(DhcpLocalProcess):
parts = l.strip().split() parts = l.strip().split()
(iaid, ip, client_id) = parts[1], parts[2], parts[4] (iaid, ip, client_id) = parts[1], parts[2], parts[4]
ip = ip.strip('[]') ip = ip.strip('[]')
if netaddr.IPAddress(ip).version == constants.IP_VERSION_4: if (ip_version and
netaddr.IPAddress(ip).version != ip_version):
continue continue
leases[ip] = {'iaid': iaid, leases[ip] = {'iaid': iaid,
'client_id': client_id, 'client_id': client_id,
@ -824,26 +825,68 @@ class Dnsmasq(DhcpLocalProcess):
filename = self.get_conf_file_name('host') filename = self.get_conf_file_name('host')
old_leases = self._read_hosts_file_leases(filename) old_leases = self._read_hosts_file_leases(filename)
leases_filename = self.get_conf_file_name('leases') leases_filename = self.get_conf_file_name('leases')
# here is dhcpv6 stuff needed to craft dhcpv6 packet cur_leases = self._read_leases_file_leases(leases_filename)
v6_leases = self._read_v6_leases_file_leases(leases_filename) if not cur_leases:
return
v4_leases = set()
for (k, v) in cur_leases.items():
# IPv4 leases have a MAC, IPv6 ones do not, so we must ignore
if netaddr.IPAddress(k).version == constants.IP_VERSION_4:
# treat '*' as None, see note in _read_leases_file_leases()
client_id = v['client_id']
if client_id is '*':
client_id = None
v4_leases.add((k, v['iaid'], client_id))
new_leases = set() new_leases = set()
for port in self.network.ports: for port in self.network.ports:
client_id = self._get_client_id(port) client_id = self._get_client_id(port)
for alloc in port.fixed_ips: for alloc in port.fixed_ips:
new_leases.add((alloc.ip_address, port.mac_address, client_id)) new_leases.add((alloc.ip_address, port.mac_address, client_id))
for ip, mac, client_id in old_leases - new_leases: # If an entry is in the leases or host file(s), but doesn't have
entry = v6_leases.get(ip, None) # a fixed IP on a corresponding neutron port, consider it stale.
version = netaddr.IPAddress(ip).version entries_to_release = (v4_leases | old_leases) - new_leases
if entry: if not entries_to_release:
# must release IPv6 lease return
self._release_lease(mac, ip, entry['client_id'],
# Try DHCP_RELEASE_TRIES times to release a lease, re-reading the
# file each time to see if it's still there. We loop +1 times to
# check the lease file one last time before logging any remaining
# entries.
for i in range(DHCP_RELEASE_TRIES + 1):
entries_not_present = set()
for ip, mac, client_id in entries_to_release:
try:
entry = cur_leases[ip]
except KeyError:
entries_not_present.add((ip, mac, client_id))
continue
# if not the final loop, try and release
if i < DHCP_RELEASE_TRIES:
ip_version = netaddr.IPAddress(ip).version
if ip_version == constants.IP_VERSION_6:
client_id = entry['client_id']
self._release_lease(mac, ip, ip_version, client_id,
entry['server_id'], entry['iaid']) entry['server_id'], entry['iaid'])
# must release only if v4 lease. If we have ipv6 address missing
# in old_leases, that means it's released already and nothing to do # Remove elements that were not in the current leases file,
# here # no need to look for them again, and see if we're done.
elif version == constants.IP_VERSION_4: entries_to_release -= entries_not_present
self._release_lease(mac, ip, client_id) if not entries_to_release:
break
if i < DHCP_RELEASE_TRIES:
time.sleep(DHCP_RELEASE_TRIES_SLEEP)
cur_leases = self._read_leases_file_leases(leases_filename)
if not cur_leases:
break
else:
LOG.warning("Could not release DHCP leases for these IP "
"addresses after %d tries: %s",
DHCP_RELEASE_TRIES,
', '.join(ip for ip, m, c in entries_to_release))
def _output_addn_hosts_file(self): def _output_addn_hosts_file(self):
"""Writes a dnsmasq compatible additional hosts file. """Writes a dnsmasq compatible additional hosts file.

View File

@ -1915,18 +1915,26 @@ class TestDnsmasq(TestBase):
mac1 = '00:00:80:aa:bb:cc' mac1 = '00:00:80:aa:bb:cc'
ip2 = '192.168.1.3' ip2 = '192.168.1.3'
mac2 = '00:00:80:cc:bb:aa' mac2 = '00:00:80:cc:bb:aa'
ip3 = '0001:0002:0003:004:0005:0006:0007:0008' ip3 = '0001:0002:0003:0004:0005:0006:0007:0008'
mac3 = '00:00:80:bb:aa:cc' mac3 = '00:00:80:bb:aa:cc'
old_leases = {(ip1, mac1, None), (ip2, mac2, None), (ip3, mac3, None)} old_leases = {(ip1, mac1, None), (ip2, mac2, None), (ip3, mac3, None)}
dnsmasq._read_hosts_file_leases = mock.Mock(return_value=old_leases) dnsmasq._read_hosts_file_leases = mock.Mock(return_value=old_leases)
dnsmasq._read_v6_leases_file_leases = mock.Mock( # Because the lease release code could fire multiple times, the
return_value={ # second read of the lease file must not have the entries that
'0001:0002:0003:004:0005:0006:0007:0008': # would have been released.
{'iaid': 0xff, dnsmasq._read_leases_file_leases = mock.Mock(
side_effect=[{ip1: {'iaid': mac1,
'client_id': 'client_id', 'client_id': 'client_id',
'server_id': 'server_id'}} 'server_id': 'server_id'},
) ip2: {'iaid': mac2,
'client_id': 'client_id',
'server_id': 'server_id'},
ip3: {'iaid': 0xff,
'client_id': 'client_id',
'server_id': 'server_id'}
},
{}])
dnsmasq._output_hosts_file = mock.Mock() dnsmasq._output_hosts_file = mock.Mock()
dnsmasq._release_lease = mock.Mock() dnsmasq._release_lease = mock.Mock()
@ -1935,11 +1943,15 @@ class TestDnsmasq(TestBase):
dnsmasq._release_unused_leases() dnsmasq._release_unused_leases()
dnsmasq._release_lease.assert_has_calls([mock.call(mac1, ip1, None), dnsmasq._release_lease.assert_has_calls([mock.call(mac1, ip1,
mock.call(mac2, ip2, None), constants.IP_VERSION_4,
None, 'server_id', mac1),
mock.call(mac2, ip2,
constants.IP_VERSION_4,
None, 'server_id', mac2),
mock.call(mac3, ip3, mock.call(mac3, ip3,
'client_id', constants.IP_VERSION_6,
'server_id', 'client_id', 'server_id',
0xff), 0xff),
], ],
any_order=True) any_order=True)
@ -1952,13 +1964,20 @@ class TestDnsmasq(TestBase):
ip2 = '192.168.1.3' ip2 = '192.168.1.3'
mac2 = '00:00:80:cc:bb:aa' mac2 = '00:00:80:cc:bb:aa'
old_leases = set([(ip1, mac1, None), (ip2, mac2, None)]) old_leases = set([(ip1, mac1, 'client_id'), (ip2, mac2, None)])
dnsmasq._read_hosts_file_leases = mock.Mock(return_value=old_leases) dnsmasq._read_hosts_file_leases = mock.Mock(return_value=old_leases)
dnsmasq._read_v6_leases_file_leases = mock.Mock( # Because the lease release code could fire multiple times, the
return_value={'fdca:3ba5:a17a::1': {'iaid': 0xff, # second read of the lease file must not have the entries that
# would have been released.
dnsmasq._read_leases_file_leases = mock.Mock(
side_effect=[{ip1: {'iaid': 0xff,
'client_id': 'client_id', 'client_id': 'client_id',
'server_id': 'server_id'},
ip2: {'iaid': mac2,
'client_id': None,
'server_id': 'server_id'} 'server_id': 'server_id'}
}) },
{}])
ipw = mock.patch( ipw = mock.patch(
'neutron.agent.linux.ip_lib.IpNetnsCommand.execute').start() 'neutron.agent.linux.ip_lib.IpNetnsCommand.execute').start()
dnsmasq._release_unused_leases() dnsmasq._release_unused_leases()
@ -1981,7 +2000,7 @@ class TestDnsmasq(TestBase):
old_leases = set([(ip1, mac1, None)]) old_leases = set([(ip1, mac1, None)])
dnsmasq._read_hosts_file_leases = mock.Mock(return_value=old_leases) dnsmasq._read_hosts_file_leases = mock.Mock(return_value=old_leases)
dnsmasq._read_v6_leases_file_leases = mock.Mock( dnsmasq._read_leases_file_leases = mock.Mock(
return_value={'fdca:3ba5:a17a::1': {'iaid': 0xff, return_value={'fdca:3ba5:a17a::1': {'iaid': 0xff,
'client_id': 'client_id', 'client_id': 'client_id',
'server_id': 'server_id'} 'server_id': 'server_id'}
@ -2003,7 +2022,7 @@ class TestDnsmasq(TestBase):
old_leases = set([(ip1, mac1, None), (ip2, mac2, None)]) old_leases = set([(ip1, mac1, None), (ip2, mac2, None)])
dnsmasq._read_hosts_file_leases = mock.Mock(return_value=old_leases) dnsmasq._read_hosts_file_leases = mock.Mock(return_value=old_leases)
dnsmasq._read_v6_leases_file_leases = mock.Mock( dnsmasq._read_leases_file_leases = mock.Mock(
return_value={ip6: {'iaid': 0xff, return_value={ip6: {'iaid': 0xff,
'client_id': 'client_id', 'client_id': 'client_id',
'server_id': 'server_id'} 'server_id': 'server_id'}
@ -2031,11 +2050,24 @@ class TestDnsmasq(TestBase):
old_leases = set([(ip1, mac1, client_id1), (ip2, mac2, client_id2)]) old_leases = set([(ip1, mac1, client_id1), (ip2, mac2, client_id2)])
dnsmasq._read_hosts_file_leases = mock.Mock(return_value=old_leases) dnsmasq._read_hosts_file_leases = mock.Mock(return_value=old_leases)
dnsmasq._read_v6_leases_file_leases = mock.Mock( # Because the lease release code could fire multiple times, the
return_value={ip6: {'iaid': 0xff, # second read of the lease file must not have the entries that
# would have been released.
dnsmasq._read_leases_file_leases = mock.Mock(
side_effect=[{ip6: {'iaid': 0xff,
'client_id': 'client_id',
'server_id': 'server_id'},
ip1: {'iaid': mac1,
'client_id': client_id1,
'server_id': 'server_id'},
ip2: {'iaid': mac2,
'client_id': client_id2,
'server_id': 'server_id'}
},
{ip6: {'iaid': 0xff,
'client_id': 'client_id', 'client_id': 'client_id',
'server_id': 'server_id'} 'server_id': 'server_id'}
}) }])
dnsmasq._output_hosts_file = mock.Mock() dnsmasq._output_hosts_file = mock.Mock()
dnsmasq._release_lease = mock.Mock() dnsmasq._release_lease = mock.Mock()
dnsmasq.network.ports = [] dnsmasq.network.ports = []
@ -2043,8 +2075,10 @@ class TestDnsmasq(TestBase):
dnsmasq._release_unused_leases() dnsmasq._release_unused_leases()
dnsmasq._release_lease.assert_has_calls( dnsmasq._release_lease.assert_has_calls(
[mock.call(mac1, ip1, client_id1), [mock.call(mac1, ip1, constants.IP_VERSION_4, client_id1,
mock.call(mac2, ip2, client_id2)], 'server_id', mac1),
mock.call(mac2, ip2, constants.IP_VERSION_4, client_id2,
'server_id', mac2)],
any_order=True) any_order=True)
def test_release_unused_leases_one_lease(self): def test_release_unused_leases_one_lease(self):
@ -2058,11 +2092,21 @@ class TestDnsmasq(TestBase):
old_leases = set([(ip1, mac1, None), (ip2, mac2, None)]) old_leases = set([(ip1, mac1, None), (ip2, mac2, None)])
dnsmasq._read_hosts_file_leases = mock.Mock(return_value=old_leases) dnsmasq._read_hosts_file_leases = mock.Mock(return_value=old_leases)
dnsmasq._read_v6_leases_file_leases = mock.Mock( # Because the lease release code could fire multiple times, the
return_value={ip6: {'iaid': 0xff, # second read of the lease file must not have the entries that
# would have been released.
dnsmasq._read_leases_file_leases = mock.Mock(
side_effect=[{ip6: {'iaid': 0xff,
'client_id': 'client_id',
'server_id': 'server_id'},
ip2: {'iaid': mac2,
'client_id': None,
'server_id': 'server_id'}
},
{ip6: {'iaid': 0xff,
'client_id': 'client_id', 'client_id': 'client_id',
'server_id': 'server_id'} 'server_id': 'server_id'}
}) }])
dnsmasq._output_hosts_file = mock.Mock() dnsmasq._output_hosts_file = mock.Mock()
dnsmasq._release_lease = mock.Mock() dnsmasq._release_lease = mock.Mock()
dnsmasq.network.ports = [FakePort1()] dnsmasq.network.ports = [FakePort1()]
@ -2070,7 +2114,7 @@ class TestDnsmasq(TestBase):
dnsmasq._release_unused_leases() dnsmasq._release_unused_leases()
dnsmasq._release_lease.assert_called_once_with( dnsmasq._release_lease.assert_called_once_with(
mac2, ip2, None) mac2, ip2, constants.IP_VERSION_4, None, 'server_id', mac2)
def test_release_unused_leases_one_lease_with_client_id(self): def test_release_unused_leases_one_lease_with_client_id(self):
dnsmasq = self._get_dnsmasq(FakeDualNetwork()) dnsmasq = self._get_dnsmasq(FakeDualNetwork())
@ -2086,18 +2130,138 @@ class TestDnsmasq(TestBase):
old_leases = set([(ip1, mac1, client_id1), (ip2, mac2, client_id2)]) old_leases = set([(ip1, mac1, client_id1), (ip2, mac2, client_id2)])
dnsmasq._read_hosts_file_leases = mock.Mock(return_value=old_leases) dnsmasq._read_hosts_file_leases = mock.Mock(return_value=old_leases)
dnsmasq._output_hosts_file = mock.Mock() dnsmasq._output_hosts_file = mock.Mock()
dnsmasq._read_v6_leases_file_leases = mock.Mock( # Because the lease release code could fire multiple times, the
return_value={ip6: {'iaid': 0xff, # second read of the lease file must not have the entries that
# would have been released.
dnsmasq._read_leases_file_leases = mock.Mock(
side_effect=[{ip6: {'iaid': 0xff,
'client_id': 'client_id',
'server_id': 'server_id'},
ip1: {'iaid': mac1,
'client_id': client_id1,
'server_id': 'server_id'}
},
{ip6: {'iaid': 0xff,
'client_id': 'client_id', 'client_id': 'client_id',
'server_id': 'server_id'} 'server_id': 'server_id'}
}) }])
dnsmasq._release_lease = mock.Mock() dnsmasq._release_lease = mock.Mock()
dnsmasq.network.ports = [FakePort5()] dnsmasq.network.ports = [FakePort5()]
dnsmasq._release_unused_leases() dnsmasq._release_unused_leases()
dnsmasq._release_lease.assert_called_once_with( dnsmasq._release_lease.assert_called_once_with(
mac1, ip1, client_id1) mac1, ip1, constants.IP_VERSION_4, client_id1, 'server_id', mac1)
def test_release_unused_leases_one_lease_from_leases_file(self):
# leases file has a stale entry that is not in the host file
dnsmasq = self._get_dnsmasq(FakeDualNetwork())
ip1 = '192.168.0.2'
mac1 = '00:00:80:aa:bb:cc'
ip2 = '192.168.0.3'
mac2 = '00:00:80:cc:bb:aa'
ip6 = '2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d'
old_leases = set([(ip1, mac1, None)])
dnsmasq._read_hosts_file_leases = mock.Mock(return_value=old_leases)
# Because the lease release code could fire multiple times, the
# second read of the lease file must not have the entries that
# would have been released.
dnsmasq._read_leases_file_leases = mock.Mock(
side_effect=[{ip6: {'iaid': 0xff,
'client_id': 'client_id',
'server_id': 'server_id'},
ip2: {'iaid': mac2,
'client_id': None,
'server_id': 'server_id'}
},
{ip6: {'iaid': 0xff,
'client_id': 'client_id',
'server_id': 'server_id'}
}])
dnsmasq._output_hosts_file = mock.Mock()
dnsmasq._release_lease = mock.Mock()
dnsmasq.network.ports = [FakePort1()]
dnsmasq._release_unused_leases()
dnsmasq._release_lease.assert_called_once_with(
mac2, ip2, constants.IP_VERSION_4, None, 'server_id', mac2)
@mock.patch.object(dhcp.LOG, 'warn')
def _test_release_unused_leases_one_lease_mult_times(self, mock_log_warn,
removed):
# Simulate a dhcp_release failure where the lease remains in the
# lease file despite multiple dhcp_release calls
dnsmasq = self._get_dnsmasq(FakeDualNetwork())
ip1 = '192.168.0.2'
mac1 = '00:00:80:aa:bb:cc'
ip2 = '192.168.0.3'
mac2 = '00:00:80:cc:bb:aa'
ip6 = '2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d'
old_leases = set([(ip1, mac1, None), (ip2, mac2, None)])
dnsmasq._read_hosts_file_leases = mock.Mock(return_value=old_leases)
# Because the lease release code could fire multiple times, the
# second and subsequent reads of the lease file must have the
# entries that were not released.
side_effect = [{ip6: {'iaid': 0xff,
'client_id': 'client_id',
'server_id': 'server_id'},
ip2: {'iaid': mac2,
'client_id': None,
'server_id': 'server_id'}
},
{ip6: {'iaid': 0xff,
'client_id': 'client_id',
'server_id': 'server_id'},
ip2: {'iaid': mac2,
'client_id': None,
'server_id': 'server_id'}
},
{ip6: {'iaid': 0xff,
'client_id': 'client_id',
'server_id': 'server_id'},
ip2: {'iaid': mac2,
'client_id': None,
'server_id': 'server_id'}
}]
# entry did/didn't go away after final dhcp_release try
if not removed:
side_effect.append(
{ip6: {'iaid': 0xff,
'client_id': 'client_id',
'server_id': 'server_id'},
ip2: {'iaid': mac2,
'client_id': None,
'server_id': 'server_id'}
})
else:
side_effect.append({})
dnsmasq._read_leases_file_leases = mock.Mock(side_effect=side_effect)
dnsmasq._output_hosts_file = mock.Mock()
dnsmasq._release_lease = mock.Mock()
dnsmasq.network.ports = [FakePort1()]
dnsmasq._release_unused_leases()
self.assertEqual(dhcp.DHCP_RELEASE_TRIES,
dnsmasq._release_lease.call_count)
self.assertEqual(dhcp.DHCP_RELEASE_TRIES + 1,
dnsmasq._read_leases_file_leases.call_count)
if not removed:
self.assertTrue(mock_log_warn.called)
def test_release_unused_leases_one_lease_mult_times_not_removed(self):
self._test_release_unused_leases_one_lease_mult_times(False)
def test_release_unused_leases_one_lease_mult_times_removed(self):
self._test_release_unused_leases_one_lease_mult_times(True)
def test_read_hosts_file_leases(self): def test_read_hosts_file_leases(self):
filename = '/path/to/file' filename = '/path/to/file'
@ -2145,12 +2309,12 @@ class TestDnsmasq(TestBase):
("fdca:3ba5:a17a::1", "00:00:80:aa:bb:cc", ("fdca:3ba5:a17a::1", "00:00:80:aa:bb:cc",
'client2')]), leases) 'client2')]), leases)
def test_read_v6_leases_file_leases(self): def _test_read_leases_file_leases(self, ip_version):
filename = '/path/to/file' filename = '/path/to/file'
lines = [ lines = [
"1472673289 aa:bb:cc:00:00:01 192.168.1.2 host-192-168-1-2 *", "1472673289 aa:bb:cc:00:00:02 192.168.1.2 host-192-168-1-2 *",
"1472673289 aa:bb:cc:00:00:01 192.168.1.3 host-192-168-1-3 *", "1472673289 aa:bb:cc:00:00:03 192.168.1.3 host-192-168-1-3 *",
"1472673289 aa:bb:cc:00:00:01 192.168.1.4 host-192-168-1-4 *", "1472673289 aa:bb:cc:00:00:04 192.168.1.4 host-192-168-1-4 *",
"duid 00:01:00:01:02:03:04:05:06:07:08:09:0a:0b", "duid 00:01:00:01:02:03:04:05:06:07:08:09:0a:0b",
"1472597740 1044800001 [2001:DB8::a] host-2001-db8--a " "1472597740 1044800001 [2001:DB8::a] host-2001-db8--a "
"00:04:4a:d0:d2:34:19:2b:49:08:84:e8:34:bd:0c:dc:b9:3b", "00:04:4a:d0:d2:34:19:2b:49:08:84:e8:34:bd:0c:dc:b9:3b",
@ -2164,14 +2328,13 @@ class TestDnsmasq(TestBase):
dnsmasq = self._get_dnsmasq(FakeDualNetwork()) dnsmasq = self._get_dnsmasq(FakeDualNetwork())
with mock.patch('os.path.exists', return_value=True): with mock.patch('os.path.exists', return_value=True):
leases = dnsmasq._read_v6_leases_file_leases(filename) leases = dnsmasq._read_leases_file_leases(filename, ip_version)
server_id = '00:01:00:01:02:03:04:05:06:07:08:09:0a:0b' server_id = '00:01:00:01:02:03:04:05:06:07:08:09:0a:0b'
entry1 = {'iaid': '1044800001', entry1 = {'iaid': '1044800001',
'client_id': '00:04:4a:d0:d2:34:19:2b:49:08:84:' 'client_id': '00:04:4a:d0:d2:34:19:2b:49:08:84:'
'e8:34:bd:0c:dc:b9:3b', 'e8:34:bd:0c:dc:b9:3b',
'server_id': server_id 'server_id': server_id
} }
entry2 = {'iaid': '1044800002', entry2 = {'iaid': '1044800002',
'client_id': '00:04:ce:96:53:3d:f2:c2:4c:4c:81:' 'client_id': '00:04:ce:96:53:3d:f2:c2:4c:4c:81:'
'7d:db:c9:8d:d2:74:22:3b:0a', '7d:db:c9:8d:d2:74:22:3b:0a',
@ -2182,13 +2345,46 @@ class TestDnsmasq(TestBase):
'7f:5c:33:31:37:5d:80:77:b4', '7f:5c:33:31:37:5d:80:77:b4',
'server_id': server_id 'server_id': server_id
} }
expected = {'2001:DB8::a': entry1, v6_expected = {'2001:DB8::a': entry1,
'2001:DB8::b': entry2, '2001:DB8::b': entry2,
'2001:DB8::c': entry3 '2001:DB8::c': entry3
} }
self.assertEqual(expected, leases) entry4 = {'iaid': 'aa:bb:cc:00:00:02',
'client_id': '*',
'server_id': None
}
entry5 = {'iaid': 'aa:bb:cc:00:00:03',
'client_id': '*',
'server_id': None
}
entry6 = {'iaid': 'aa:bb:cc:00:00:04',
'client_id': '*',
'server_id': None
}
v4_expected = {'192.168.1.2': entry4,
'192.168.1.3': entry5,
'192.168.1.4': entry6
}
expected = {}
if not ip_version or ip_version == constants.IP_VERSION_6:
expected.update(v6_expected)
if not ip_version or ip_version == constants.IP_VERSION_4:
expected.update(v4_expected)
mock_open.assert_called_once_with(filename) mock_open.assert_called_once_with(filename)
self.assertEqual(expected, leases)
def test_read_v6_leases_file_leases(self):
self._test_read_leases_file_leases(constants.IP_VERSION_6)
def test_read_v4_leases_file_leases(self):
self._test_read_leases_file_leases(constants.IP_VERSION_4)
def test_read_all_leases_file_leases(self):
self._test_read_leases_file_leases(None)
def test_make_subnet_interface_ip_map(self): def test_make_subnet_interface_ip_map(self):
with mock.patch('neutron.agent.linux.ip_lib.IPDevice') as ip_dev: with mock.patch('neutron.agent.linux.ip_lib.IPDevice') as ip_dev: