From 591715b86e086bc42065a9bae1690927414100ad Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Thu, 18 Feb 2021 16:56:39 +0000 Subject: [PATCH] Implement "ip neigh flush" with Pyroute2 Story: #2007686 Task: #41558 Change-Id: I00c676e234fd9f771d716def7e4388bf33004118 --- neutron/agent/linux/ip_lib.py | 27 +++++++++--- neutron/privileged/agent/linux/ip_lib.py | 15 ++++--- .../functional/agent/linux/test_ip_lib.py | 44 ++++++++++++++++++- neutron/tests/unit/agent/linux/test_ip_lib.py | 16 ++++--- 4 files changed, 83 insertions(+), 19 deletions(-) diff --git a/neutron/agent/linux/ip_lib.py b/neutron/agent/linux/ip_lib.py index e7544d18ade..7854c2e462c 100644 --- a/neutron/agent/linux/ip_lib.py +++ b/neutron/agent/linux/ip_lib.py @@ -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) diff --git a/neutron/privileged/agent/linux/ip_lib.py b/neutron/privileged/agent/linux/ip_lib.py index 0b5b2fbf097..c3106285a55 100644 --- a/neutron/privileged/agent/linux/ip_lib.py +++ b/neutron/privileged/agent/linux/ip_lib.py @@ -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 diff --git a/neutron/tests/functional/agent/linux/test_ip_lib.py b/neutron/tests/functional/agent/linux/test_ip_lib.py index 88fdbaddb1c..fc511850156 100644 --- a/neutron/tests/functional/agent/linux/test_ip_lib.py +++ b/neutron/tests/functional/agent/linux/test_ip_lib.py @@ -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) diff --git a/neutron/tests/unit/agent/linux/test_ip_lib.py b/neutron/tests/unit/agent/linux/test_ip_lib.py index 8b8b9a9303a..64d28182d50 100644 --- a/neutron/tests/unit/agent/linux/test_ip_lib.py +++ b/neutron/tests/unit/agent/linux/test_ip_lib.py @@ -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):