Change neighbour commands to use pyroute2
Change ip_lib's IpNeighCommand class to use pyroute2 for adding, deleting and dumping entries, rather than using 'ip neigh'. This will increase performance when many ARP updates happen at once. Change-Id: Idd528c0b402d1c9fc4b030f2aaa6d641d86ec02a Partial-Bug: #1511134
This commit is contained in:
parent
62fb004af6
commit
d7a6827e43
@ -822,20 +822,25 @@ class IPRoute(SubProcessBase):
|
||||
class IpNeighCommand(IpDeviceCommandBase):
|
||||
COMMAND = 'neigh'
|
||||
|
||||
def add(self, ip_address, mac_address):
|
||||
ip_version = get_ip_version(ip_address)
|
||||
self._as_root([ip_version],
|
||||
('replace', ip_address,
|
||||
'lladdr', mac_address,
|
||||
'nud', 'permanent',
|
||||
'dev', self.name))
|
||||
def add(self, ip_address, mac_address, **kwargs):
|
||||
add_neigh_entry(ip_address,
|
||||
mac_address,
|
||||
self.name,
|
||||
self._parent.namespace,
|
||||
**kwargs)
|
||||
|
||||
def delete(self, ip_address, mac_address):
|
||||
ip_version = get_ip_version(ip_address)
|
||||
self._as_root([ip_version],
|
||||
('del', ip_address,
|
||||
'lladdr', mac_address,
|
||||
'dev', self.name))
|
||||
def delete(self, ip_address, mac_address, **kwargs):
|
||||
delete_neigh_entry(ip_address,
|
||||
mac_address,
|
||||
self.name,
|
||||
self._parent.namespace,
|
||||
**kwargs)
|
||||
|
||||
def dump(self, ip_version, **kwargs):
|
||||
return dump_neigh_entries(ip_version,
|
||||
self.name,
|
||||
self._parent.namespace,
|
||||
**kwargs)
|
||||
|
||||
def show(self, ip_version):
|
||||
options = [ip_version]
|
||||
@ -852,6 +857,7 @@ class IpNeighCommand(IpDeviceCommandBase):
|
||||
:param ip_version: Either 4 or 6 for IPv4 or IPv6 respectively
|
||||
:param ip_address: The prefix selecting the neighbours to flush
|
||||
"""
|
||||
# NOTE(haleyb): There is no equivalent to 'flush' in pyroute2
|
||||
self._as_root([ip_version], ('flush', 'to', ip_address))
|
||||
|
||||
|
||||
@ -941,6 +947,7 @@ def get_device_mac(device_name, namespace=None):
|
||||
|
||||
|
||||
NetworkNamespaceNotFound = privileged.NetworkNamespaceNotFound
|
||||
NetworkInterfaceNotFound = privileged.NetworkInterfaceNotFound
|
||||
|
||||
|
||||
def get_routing_table(ip_version, namespace=None):
|
||||
@ -958,6 +965,61 @@ def get_routing_table(ip_version, namespace=None):
|
||||
return list(privileged.get_routing_table(ip_version, namespace))
|
||||
|
||||
|
||||
# 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):
|
||||
"""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
|
||||
"""
|
||||
ip_version = get_ip_version(ip_address)
|
||||
privileged.add_neigh_entry(ip_version,
|
||||
ip_address,
|
||||
mac_address,
|
||||
device,
|
||||
namespace,
|
||||
**kwargs)
|
||||
|
||||
|
||||
def delete_neigh_entry(ip_address, mac_address, device, namespace=None,
|
||||
**kwargs):
|
||||
"""Delete a neighbour entry.
|
||||
|
||||
:param ip_address: IP address of entry to delete
|
||||
:param mac_address: MAC address of entry to delete
|
||||
:param device: Device name to use in deleting entry
|
||||
:param namespace: The name of the namespace in which to delete the entry
|
||||
"""
|
||||
ip_version = get_ip_version(ip_address)
|
||||
privileged.delete_neigh_entry(ip_version,
|
||||
ip_address,
|
||||
mac_address,
|
||||
device,
|
||||
namespace,
|
||||
**kwargs)
|
||||
|
||||
|
||||
def dump_neigh_entries(ip_version, device=None, namespace=None, **kwargs):
|
||||
"""Dump all neighbour entries.
|
||||
|
||||
:param ip_version: IP version of entries to show (4 or 6)
|
||||
:param device: Device name to use in dumping entries
|
||||
:param namespace: The name of the namespace in which to dump the entries
|
||||
:param kwargs: Callers add any filters they use as kwargs
|
||||
:return: a list of dictionaries, each representing a neighbour.
|
||||
The dictionary format is: {'dst': ip_address,
|
||||
'lladdr': mac_address,
|
||||
'device': device_name}
|
||||
"""
|
||||
return list(privileged.dump_neigh_entries(ip_version,
|
||||
device,
|
||||
namespace,
|
||||
**kwargs))
|
||||
|
||||
|
||||
def ensure_device_is_ready(device_name, namespace=None):
|
||||
dev = IPDevice(device_name, namespace=namespace)
|
||||
dev.set_log_fail_as_error(False)
|
||||
|
@ -15,6 +15,7 @@ import socket
|
||||
|
||||
import pyroute2
|
||||
from pyroute2.netlink import rtnl
|
||||
from pyroute2.netlink.rtnl import ndmsg
|
||||
|
||||
from neutron._i18n import _
|
||||
from neutron import privileged
|
||||
@ -38,6 +39,10 @@ class NetworkNamespaceNotFound(RuntimeError):
|
||||
self.message % {'netns_name': netns_name})
|
||||
|
||||
|
||||
class NetworkInterfaceNotFound(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@privileged.default.entrypoint
|
||||
def get_routing_table(ip_version, namespace=None):
|
||||
"""Return a list of dictionaries, each representing a route.
|
||||
@ -66,3 +71,100 @@ def get_routing_table(ip_version, namespace=None):
|
||||
'scope': _get_scope_name(route['scope'])}
|
||||
for route in ipdb_routes if route['family'] == family]
|
||||
return routes
|
||||
|
||||
|
||||
def _get_iproute(namespace):
|
||||
# From iproute.py:
|
||||
# `IPRoute` -- RTNL API to the current network namespace
|
||||
# `NetNS` -- RTNL API to another network namespace
|
||||
if namespace:
|
||||
# do not try and create the namespace
|
||||
return pyroute2.NetNS(namespace, flags=0)
|
||||
else:
|
||||
return pyroute2.IPRoute()
|
||||
|
||||
|
||||
def _run_iproute(command, device, namespace, **kwargs):
|
||||
try:
|
||||
with _get_iproute(namespace) as ip:
|
||||
idx = ip.link_lookup(ifname=device)[0]
|
||||
return ip.neigh(command, ifindex=idx, **kwargs)
|
||||
except IndexError:
|
||||
msg = _("Network interface %(device)s not found in namespace "
|
||||
"%(namespace)s.") % {'device': device,
|
||||
'namespace': namespace}
|
||||
raise NetworkInterfaceNotFound(msg)
|
||||
except OSError as e:
|
||||
if e.errno == errno.ENOENT:
|
||||
raise NetworkNamespaceNotFound(netns_name=namespace)
|
||||
raise
|
||||
|
||||
|
||||
@privileged.default.entrypoint
|
||||
def add_neigh_entry(ip_version, ip_address, mac_address, device, namespace,
|
||||
**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
|
||||
"""
|
||||
family = _IP_VERSION_FAMILY_MAP[ip_version]
|
||||
_run_iproute('replace',
|
||||
device,
|
||||
namespace,
|
||||
dst=ip_address,
|
||||
lladdr=mac_address,
|
||||
family=family,
|
||||
state=ndmsg.states['permanent'],
|
||||
**kwargs)
|
||||
|
||||
|
||||
@privileged.default.entrypoint
|
||||
def delete_neigh_entry(ip_version, ip_address, mac_address, device, namespace,
|
||||
**kwargs):
|
||||
"""Delete a neighbour entry.
|
||||
|
||||
:param ip_address: IP address of entry to delete
|
||||
:param mac_address: MAC address of entry to delete
|
||||
:param device: Device name to use in deleting entry
|
||||
:param namespace: The name of the namespace in which to delete the entry
|
||||
"""
|
||||
family = _IP_VERSION_FAMILY_MAP[ip_version]
|
||||
_run_iproute('delete',
|
||||
device,
|
||||
namespace,
|
||||
dst=ip_address,
|
||||
lladdr=mac_address,
|
||||
family=family,
|
||||
**kwargs)
|
||||
|
||||
|
||||
@privileged.default.entrypoint
|
||||
def dump_neigh_entries(ip_version, device, namespace, **kwargs):
|
||||
"""Dump all neighbour entries.
|
||||
|
||||
:param ip_version: IP version of entries to show (4 or 6)
|
||||
:param device: Device name to use in dumping entries
|
||||
:param namespace: The name of the namespace in which to dump the entries
|
||||
:param kwargs: Callers add any filters they use as kwargs
|
||||
:return: a list of dictionaries, each representing a neighbour.
|
||||
The dictionary format is: {'dst': ip_address,
|
||||
'lladdr': mac_address,
|
||||
'device': device_name}
|
||||
"""
|
||||
family = _IP_VERSION_FAMILY_MAP[ip_version]
|
||||
entries = []
|
||||
dump = _run_iproute('dump',
|
||||
device,
|
||||
namespace,
|
||||
family=family,
|
||||
**kwargs)
|
||||
|
||||
for entry in dump:
|
||||
attrs = dict(entry['attrs'])
|
||||
entries += [{'dst': attrs['NDA_DST'],
|
||||
'lladdr': attrs.get('NDA_LLADDR'),
|
||||
'device': device}]
|
||||
return entries
|
||||
|
@ -35,6 +35,7 @@ Device = collections.namedtuple('Device',
|
||||
|
||||
WRONG_IP = '0.0.0.0'
|
||||
TEST_IP = '240.0.0.1'
|
||||
TEST_IP_NEIGH = '240.0.0.2'
|
||||
|
||||
|
||||
class IpLibTestFramework(functional_base.BaseSudoTestCase):
|
||||
@ -227,6 +228,39 @@ class IpLibTestCase(IpLibTestFramework):
|
||||
with testtools.ExpectedException(ip_lib.NetworkNamespaceNotFound):
|
||||
ip_lib.get_routing_table(4, namespace="nonexistent-netns")
|
||||
|
||||
def test_get_neigh_entries(self):
|
||||
attr = self.generate_device_details(
|
||||
ip_cidrs=["%s/24" % TEST_IP, "fd00::1/64"]
|
||||
)
|
||||
mac_address = utils.get_random_mac('fa:16:3e:00:00:00'.split(':'))
|
||||
device = self.manage_device(attr)
|
||||
device.neigh.add(TEST_IP_NEIGH, mac_address)
|
||||
|
||||
expected_neighs = [{'dst': TEST_IP_NEIGH,
|
||||
'lladdr': mac_address,
|
||||
'device': attr.name}]
|
||||
|
||||
neighs = device.neigh.dump(4)
|
||||
self.assertItemsEqual(expected_neighs, neighs)
|
||||
self.assertIsInstance(neighs, list)
|
||||
|
||||
device.neigh.delete(TEST_IP_NEIGH, mac_address)
|
||||
neighs = device.neigh.dump(4, dst=TEST_IP_NEIGH, lladdr=mac_address)
|
||||
self.assertEqual([], neighs)
|
||||
|
||||
def test_get_neigh_entries_no_namespace(self):
|
||||
with testtools.ExpectedException(ip_lib.NetworkNamespaceNotFound):
|
||||
ip_lib.dump_neigh_entries(4, namespace="nonexistent-netns")
|
||||
|
||||
def test_get_neigh_entries_no_interface(self):
|
||||
attr = self.generate_device_details(
|
||||
ip_cidrs=["%s/24" % TEST_IP, "fd00::1/64"]
|
||||
)
|
||||
self.manage_device(attr)
|
||||
with testtools.ExpectedException(ip_lib.NetworkInterfaceNotFound):
|
||||
ip_lib.dump_neigh_entries(4, device="nosuchdevice",
|
||||
namespace=attr.namespace)
|
||||
|
||||
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)
|
||||
|
@ -19,6 +19,7 @@ import mock
|
||||
import netaddr
|
||||
from neutron_lib import exceptions
|
||||
import pyroute2
|
||||
from pyroute2.netlink.rtnl import ndmsg
|
||||
import testtools
|
||||
|
||||
from neutron.agent.common import utils # noqa
|
||||
@ -1581,21 +1582,62 @@ class TestIpNeighCommand(TestIPCmdBase):
|
||||
self.parent.name = 'tap0'
|
||||
self.command = 'neigh'
|
||||
self.neigh_cmd = ip_lib.IpNeighCommand(self.parent)
|
||||
self.addCleanup(privileged.default.set_client_mode, True)
|
||||
privileged.default.set_client_mode(False)
|
||||
|
||||
def test_add_entry(self):
|
||||
@mock.patch.object(pyroute2, 'NetNS')
|
||||
def test_add_entry(self, mock_netns):
|
||||
mock_netns_instance = mock_netns.return_value
|
||||
mock_netns_enter = mock_netns_instance.__enter__.return_value
|
||||
mock_netns_enter.link_lookup.return_value = [1]
|
||||
self.neigh_cmd.add('192.168.45.100', 'cc:dd:ee:ff:ab:cd')
|
||||
self._assert_sudo([4],
|
||||
('replace', '192.168.45.100',
|
||||
'lladdr', 'cc:dd:ee:ff:ab:cd',
|
||||
'nud', 'permanent',
|
||||
'dev', 'tap0'))
|
||||
mock_netns_enter.link_lookup.assert_called_once_with(ifname='tap0')
|
||||
mock_netns_enter.neigh.assert_called_once_with(
|
||||
'replace',
|
||||
dst='192.168.45.100',
|
||||
lladdr='cc:dd:ee:ff:ab:cd',
|
||||
family=2,
|
||||
ifindex=1,
|
||||
state=ndmsg.states['permanent'])
|
||||
|
||||
def test_delete_entry(self):
|
||||
@mock.patch.object(pyroute2, 'NetNS')
|
||||
def test_add_entry_nonexistent_namespace(self, mock_netns):
|
||||
mock_netns.side_effect = OSError(errno.ENOENT, None)
|
||||
with testtools.ExpectedException(ip_lib.NetworkNamespaceNotFound):
|
||||
self.neigh_cmd.add('192.168.45.100', 'cc:dd:ee:ff:ab:cd')
|
||||
|
||||
@mock.patch.object(pyroute2, 'NetNS')
|
||||
def test_add_entry_other_error(self, mock_netns):
|
||||
expected_exception = OSError(errno.EACCES, None)
|
||||
mock_netns.side_effect = expected_exception
|
||||
with testtools.ExpectedException(expected_exception.__class__):
|
||||
self.neigh_cmd.add('192.168.45.100', 'cc:dd:ee:ff:ab:cd')
|
||||
|
||||
@mock.patch.object(pyroute2, 'NetNS')
|
||||
def test_delete_entry(self, mock_netns):
|
||||
mock_netns_instance = mock_netns.return_value
|
||||
mock_netns_enter = mock_netns_instance.__enter__.return_value
|
||||
mock_netns_enter.link_lookup.return_value = [1]
|
||||
self.neigh_cmd.delete('192.168.45.100', 'cc:dd:ee:ff:ab:cd')
|
||||
self._assert_sudo([4],
|
||||
('del', '192.168.45.100',
|
||||
'lladdr', 'cc:dd:ee:ff:ab:cd',
|
||||
'dev', 'tap0'))
|
||||
mock_netns_enter.link_lookup.assert_called_once_with(ifname='tap0')
|
||||
mock_netns_enter.neigh.assert_called_once_with(
|
||||
'delete',
|
||||
dst='192.168.45.100',
|
||||
lladdr='cc:dd:ee:ff:ab:cd',
|
||||
family=2,
|
||||
ifindex=1)
|
||||
|
||||
@mock.patch.object(pyroute2, 'NetNS')
|
||||
def test_dump_entries(self, mock_netns):
|
||||
mock_netns_instance = mock_netns.return_value
|
||||
mock_netns_enter = mock_netns_instance.__enter__.return_value
|
||||
mock_netns_enter.link_lookup.return_value = [1]
|
||||
self.neigh_cmd.dump(4)
|
||||
mock_netns_enter.link_lookup.assert_called_once_with(ifname='tap0')
|
||||
mock_netns_enter.neigh.assert_called_once_with(
|
||||
'dump',
|
||||
family=2,
|
||||
ifindex=1)
|
||||
|
||||
def test_flush(self):
|
||||
self.neigh_cmd.flush(4, '192.168.0.1')
|
||||
|
Loading…
Reference in New Issue
Block a user