Implement "ip neigh flush" with Pyroute2

Story: #2007686
Task: #41558

Change-Id: I00c676e234fd9f771d716def7e4388bf33004118
This commit is contained in:
Rodolfo Alonso Hernandez 2021-02-18 16:56:39 +00:00
parent 5a419cbc84
commit 591715b86e
4 changed files with 83 additions and 19 deletions

View File

@ -659,11 +659,12 @@ class IPRoute(SubProcessBase):
class IpNeighCommand(IpDeviceCommandBase): class IpNeighCommand(IpDeviceCommandBase):
COMMAND = 'neigh' 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, add_neigh_entry(ip_address,
mac_address, mac_address,
self.name, self.name,
self._parent.namespace, namespace=self._parent.namespace,
nud_state=nud_state,
**kwargs) **kwargs)
def delete(self, ip_address, mac_address, **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 Given address entry is removed from neighbour cache (ARP or NDP). To
flush all entries pass string 'all' as an address. 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_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 cidr = netaddr.IPNetwork(ip_address) if ip_address != 'all' else None
self._as_root([ip_version], ('flush', 'to', ip_address)) 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): class IpNetnsCommand(IpCommandBase):
@ -843,20 +853,25 @@ def get_routing_table(ip_version, namespace=None):
# NOTE(haleyb): These neighbour functions live outside the IpNeighCommand # NOTE(haleyb): These neighbour functions live outside the IpNeighCommand
# class since not all callers require it. # 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. """Add a neighbour entry.
:param ip_address: IP address of entry to add :param ip_address: IP address of entry to add
:param mac_address: MAC address of entry to add :param mac_address: MAC address of entry to add
:param device: Device name to use in adding entry :param device: Device name to use in adding entry
:param namespace: The name of the namespace in which to add the 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) ip_version = common_utils.get_ip_version(ip_address)
nud_state = nud_state or 'permanent'
privileged.add_neigh_entry(ip_version, privileged.add_neigh_entry(ip_version,
ip_address, ip_address,
mac_address, mac_address,
device, device,
namespace, namespace,
nud_state,
**kwargs) **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' NETNS_RUN_DIR = '/var/run/netns'
NUD_STATES = {state[1]: state[0] for state in ndmsg.states.items()}
def _get_scope_name(scope): def _get_scope_name(scope):
"""Return the name of the scope (given as a number), or the scope number """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 @privileged.default.entrypoint
def add_neigh_entry(ip_version, ip_address, mac_address, device, namespace, def add_neigh_entry(ip_version, ip_address, mac_address, device, namespace,
**kwargs): nud_state, **kwargs):
"""Add a neighbour entry. """Add a neighbour entry.
:param ip_address: IP address of entry to add :param ip_address: IP address of entry to add
:param mac_address: MAC address of entry to add :param mac_address: MAC address of entry to add
:param device: Device name to use in adding entry :param device: Device name to use in adding entry
:param namespace: The name of the namespace in which to add the 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] family = _IP_VERSION_FAMILY_MAP[ip_version]
_run_iproute_neigh('replace', _run_iproute_neigh('replace',
@ -473,7 +477,7 @@ def add_neigh_entry(ip_version, ip_address, mac_address, device, namespace,
dst=ip_address, dst=ip_address,
lladdr=mac_address, lladdr=mac_address,
family=family, family=family,
state=ndmsg.states['permanent'], state=ndmsg.states[nud_state],
**kwargs) **kwargs)
@ -526,9 +530,10 @@ def dump_neigh_entries(ip_version, device, namespace, **kwargs):
for entry in dump: for entry in dump:
attrs = dict(entry['attrs']) attrs = dict(entry['attrs'])
entries += [{'dst': attrs['NDA_DST'], entries.append({'dst': attrs['NDA_DST'],
'lladdr': attrs.get('NDA_LLADDR'), 'lladdr': attrs.get('NDA_LLADDR'),
'device': device}] 'device': device,
'state': NUD_STATES[entry['state']]})
return entries return entries

View File

@ -46,6 +46,12 @@ WRONG_IP = '0.0.0.0'
TEST_IP = '240.0.0.1' TEST_IP = '240.0.0.1'
TEST_IP_NEIGH = '240.0.0.2' TEST_IP_NEIGH = '240.0.0.2'
TEST_IP_SECONDARY = '240.0.0.3' 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): class IpLibTestFramework(functional_base.BaseSudoTestCase):
@ -416,7 +422,8 @@ class IpLibTestCase(IpLibTestFramework):
expected_neighs = [{'dst': TEST_IP_NEIGH, expected_neighs = [{'dst': TEST_IP_NEIGH,
'lladdr': mac_address, 'lladdr': mac_address,
'device': attr.name}] 'device': attr.name,
'state': 'permanent'}]
neighs = device.neigh.dump(4) neighs = device.neigh.dump(4)
self.assertItemsEqual(expected_neighs, neighs) self.assertItemsEqual(expected_neighs, neighs)
@ -449,6 +456,41 @@ class IpLibTestCase(IpLibTestFramework):
# trying to delete a non-existent entry shouldn't raise an error # trying to delete a non-existent entry shouldn't raise an error
device.neigh.delete(TEST_IP_NEIGH, mac_address) 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): def _check_for_device_name(self, ip, name, should_exist):
exist = any(d for d in ip.get_devices() if d.name == name) exist = any(d for d in ip.get_devices() if d.name == name)
self.assertEqual(should_exist, exist) self.assertEqual(should_exist, exist)

View File

@ -551,11 +551,6 @@ class TestIPCmdBase(base.BaseTestCase):
self.parent._run.assert_has_calls([ self.parent._run.assert_has_calls([
mock.call(options, self.command, args)]) 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): class TestIpRuleCommand(TestIPCmdBase):
def setUp(self): def setUp(self):
@ -1367,8 +1362,15 @@ class TestIpNeighCommand(TestIPCmdBase):
ifindex=1) ifindex=1)
def test_flush(self): def test_flush(self):
self.neigh_cmd.flush(4, '192.168.0.1') with mock.patch.object(self.neigh_cmd, 'dump') as mock_dump, \
self._assert_sudo([4], ('flush', 'to', '192.168.0.1')) 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): class TestArpPing(TestIPCmdBase):