Merge "Implement "ip neigh flush" with Pyroute2"

This commit is contained in:
Zuul 2021-02-26 22:51:45 +00:00 committed by Gerrit Code Review
commit dcb032777f
4 changed files with 83 additions and 19 deletions

View File

@ -659,11 +659,12 @@ class IPRoute(SubProcessBase):
class IpNeighCommand(IpDeviceCommandBase):
COMMAND = 'neigh'
def add(self, ip_address, mac_address, **kwargs):
def add(self, ip_address, mac_address, nud_state=None, **kwargs):
add_neigh_entry(ip_address,
mac_address,
self.name,
self._parent.namespace,
namespace=self._parent.namespace,
nud_state=nud_state,
**kwargs)
def delete(self, ip_address, mac_address, **kwargs):
@ -685,11 +686,20 @@ class IpNeighCommand(IpDeviceCommandBase):
Given address entry is removed from neighbour cache (ARP or NDP). To
flush all entries pass string 'all' as an address.
From https://man.archlinux.org/man/core/iproute2/ip-neighbour.8.en:
"the default neighbour states to be flushed do not include permanent
and noarp".
:param ip_version: Either 4 or 6 for IPv4 or IPv6 respectively
:param ip_address: The prefix selecting the neighbours to flush
:param ip_address: The prefix selecting the neighbours to flush or
"all"
"""
# NOTE(haleyb): There is no equivalent to 'flush' in pyroute2
self._as_root([ip_version], ('flush', 'to', ip_address))
cidr = netaddr.IPNetwork(ip_address) if ip_address != 'all' else None
for entry in self.dump(ip_version):
if entry['state'] in ('permanent', 'noarp'):
continue
if ip_address == 'all' or entry['dst'] in cidr:
self.delete(entry['dst'], entry['lladdr'])
class IpNetnsCommand(IpCommandBase):
@ -843,20 +853,25 @@ def get_routing_table(ip_version, namespace=None):
# NOTE(haleyb): These neighbour functions live outside the IpNeighCommand
# class since not all callers require it.
def add_neigh_entry(ip_address, mac_address, device, namespace=None, **kwargs):
def add_neigh_entry(ip_address, mac_address, device, namespace=None,
nud_state=None, **kwargs):
"""Add a neighbour entry.
:param ip_address: IP address of entry to add
:param mac_address: MAC address of entry to add
:param device: Device name to use in adding entry
:param namespace: The name of the namespace in which to add the entry
:param nud_state: The NUD (Neighbour Unreachability Detection) state of
the entry; defaults to "permanent"
"""
ip_version = common_utils.get_ip_version(ip_address)
nud_state = nud_state or 'permanent'
privileged.add_neigh_entry(ip_version,
ip_address,
mac_address,
device,
namespace,
nud_state,
**kwargs)

View File

@ -36,6 +36,8 @@ _IP_VERSION_FAMILY_MAP = {4: socket.AF_INET, 6: socket.AF_INET6}
NETNS_RUN_DIR = '/var/run/netns'
NUD_STATES = {state[1]: state[0] for state in ndmsg.states.items()}
def _get_scope_name(scope):
"""Return the name of the scope (given as a number), or the scope number
@ -458,13 +460,15 @@ def get_link_vfs(device, namespace):
@privileged.default.entrypoint
def add_neigh_entry(ip_version, ip_address, mac_address, device, namespace,
**kwargs):
nud_state, **kwargs):
"""Add a neighbour entry.
:param ip_address: IP address of entry to add
:param mac_address: MAC address of entry to add
:param device: Device name to use in adding entry
:param namespace: The name of the namespace in which to add the entry
:param nud_state: The NUD (Neighbour Unreachability Detection) state of
the entry
"""
family = _IP_VERSION_FAMILY_MAP[ip_version]
_run_iproute_neigh('replace',
@ -473,7 +477,7 @@ def add_neigh_entry(ip_version, ip_address, mac_address, device, namespace,
dst=ip_address,
lladdr=mac_address,
family=family,
state=ndmsg.states['permanent'],
state=ndmsg.states[nud_state],
**kwargs)
@ -526,9 +530,10 @@ def dump_neigh_entries(ip_version, device, namespace, **kwargs):
for entry in dump:
attrs = dict(entry['attrs'])
entries += [{'dst': attrs['NDA_DST'],
'lladdr': attrs.get('NDA_LLADDR'),
'device': device}]
entries.append({'dst': attrs['NDA_DST'],
'lladdr': attrs.get('NDA_LLADDR'),
'device': device,
'state': NUD_STATES[entry['state']]})
return entries

View File

@ -46,6 +46,12 @@ WRONG_IP = '0.0.0.0'
TEST_IP = '240.0.0.1'
TEST_IP_NEIGH = '240.0.0.2'
TEST_IP_SECONDARY = '240.0.0.3'
TEST_IP6_NEIGH = 'fd00::2'
TEST_IP6_SECONDARY = 'fd00::3'
TEST_IP_NUD_STATES = ((TEST_IP_NEIGH, 'permanent'),
(TEST_IP_SECONDARY, 'reachable'),
(TEST_IP6_NEIGH, 'permanent'),
(TEST_IP6_SECONDARY, 'reachable'))
class IpLibTestFramework(functional_base.BaseSudoTestCase):
@ -416,7 +422,8 @@ class IpLibTestCase(IpLibTestFramework):
expected_neighs = [{'dst': TEST_IP_NEIGH,
'lladdr': mac_address,
'device': attr.name}]
'device': attr.name,
'state': 'permanent'}]
neighs = device.neigh.dump(4)
self.assertItemsEqual(expected_neighs, neighs)
@ -449,6 +456,41 @@ class IpLibTestCase(IpLibTestFramework):
# trying to delete a non-existent entry shouldn't raise an error
device.neigh.delete(TEST_IP_NEIGH, mac_address)
def test_flush_neigh_ipv4(self):
# Entry with state "reachable" deleted.
self._flush_neigh(constants.IP_VERSION_4, TEST_IP_SECONDARY,
{TEST_IP_NEIGH})
# Entries belong to "ip_to_flush" passed CIDR, but "permanent" entry
# is not deleted.
self._flush_neigh(constants.IP_VERSION_4, '240.0.0.0/28',
{TEST_IP_NEIGH})
# "all" passed, but "permanent" entry is not deleted.
self._flush_neigh(constants.IP_VERSION_4, 'all', {TEST_IP_NEIGH})
def test_flush_neigh_ipv6(self):
# Entry with state "reachable" deleted.
self._flush_neigh(constants.IP_VERSION_6, TEST_IP6_SECONDARY,
{TEST_IP6_NEIGH})
# Entries belong to "ip_to_flush" passed CIDR, but "permanent" entry
# is not deleted.
self._flush_neigh(constants.IP_VERSION_6, 'fd00::0/64',
{TEST_IP6_NEIGH})
# "all" passed, but "permanent" entry is not deleted.
self._flush_neigh(constants.IP_VERSION_6, 'all', {TEST_IP6_NEIGH})
def _flush_neigh(self, version, ip_to_flush, ips_expected):
attr = self.generate_device_details(
ip_cidrs=['%s/24' % TEST_IP, 'fd00::1/64'],
namespace=utils.get_rand_name(20, 'ns-'))
device = self.manage_device(attr)
for test_ip, nud_state in TEST_IP_NUD_STATES:
mac_address = net.get_random_mac('fa:16:3e:00:00:00'.split(':'))
device.neigh.add(test_ip, mac_address, nud_state)
device.neigh.flush(version, ip_to_flush)
ips = {e['dst'] for e in device.neigh.dump(version)}
self.assertEqual(ips_expected, ips)
def _check_for_device_name(self, ip, name, should_exist):
exist = any(d for d in ip.get_devices() if d.name == name)
self.assertEqual(should_exist, exist)

View File

@ -551,11 +551,6 @@ class TestIPCmdBase(base.BaseTestCase):
self.parent._run.assert_has_calls([
mock.call(options, self.command, args)])
def _assert_sudo(self, options, args, use_root_namespace=False):
self.parent._as_root.assert_has_calls(
[mock.call(options, self.command, args,
use_root_namespace=use_root_namespace)])
class TestIpRuleCommand(TestIPCmdBase):
def setUp(self):
@ -1367,8 +1362,15 @@ class TestIpNeighCommand(TestIPCmdBase):
ifindex=1)
def test_flush(self):
self.neigh_cmd.flush(4, '192.168.0.1')
self._assert_sudo([4], ('flush', 'to', '192.168.0.1'))
with mock.patch.object(self.neigh_cmd, 'dump') as mock_dump, \
mock.patch.object(self.neigh_cmd, 'delete') as mock_delete:
mock_dump.return_value = (
{'state': 'permanent', 'dst': '1.2.3.4', 'lladdr': 'mac_1'},
{'state': 'reachable', 'dst': '1.2.3.5', 'lladdr': 'mac_2'})
self.neigh_cmd.flush(4, '1.2.3.4')
mock_delete.assert_not_called()
self.neigh_cmd.flush(4, '1.2.3.5')
mock_delete.assert_called_once_with('1.2.3.5', 'mac_2')
class TestArpPing(TestIPCmdBase):