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:
Brian Haley 2017-01-10 17:44:58 -05:00
parent 62fb004af6
commit d7a6827e43
4 changed files with 264 additions and 24 deletions

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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')