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):
|
class IpNeighCommand(IpDeviceCommandBase):
|
||||||
COMMAND = 'neigh'
|
COMMAND = 'neigh'
|
||||||
|
|
||||||
def add(self, ip_address, mac_address):
|
def add(self, ip_address, mac_address, **kwargs):
|
||||||
ip_version = get_ip_version(ip_address)
|
add_neigh_entry(ip_address,
|
||||||
self._as_root([ip_version],
|
mac_address,
|
||||||
('replace', ip_address,
|
self.name,
|
||||||
'lladdr', mac_address,
|
self._parent.namespace,
|
||||||
'nud', 'permanent',
|
**kwargs)
|
||||||
'dev', self.name))
|
|
||||||
|
|
||||||
def delete(self, ip_address, mac_address):
|
def delete(self, ip_address, mac_address, **kwargs):
|
||||||
ip_version = get_ip_version(ip_address)
|
delete_neigh_entry(ip_address,
|
||||||
self._as_root([ip_version],
|
mac_address,
|
||||||
('del', ip_address,
|
self.name,
|
||||||
'lladdr', mac_address,
|
self._parent.namespace,
|
||||||
'dev', self.name))
|
**kwargs)
|
||||||
|
|
||||||
|
def dump(self, ip_version, **kwargs):
|
||||||
|
return dump_neigh_entries(ip_version,
|
||||||
|
self.name,
|
||||||
|
self._parent.namespace,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
def show(self, ip_version):
|
def show(self, ip_version):
|
||||||
options = [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_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
|
||||||
"""
|
"""
|
||||||
|
# NOTE(haleyb): There is no equivalent to 'flush' in pyroute2
|
||||||
self._as_root([ip_version], ('flush', 'to', ip_address))
|
self._as_root([ip_version], ('flush', 'to', ip_address))
|
||||||
|
|
||||||
|
|
||||||
@ -941,6 +947,7 @@ def get_device_mac(device_name, namespace=None):
|
|||||||
|
|
||||||
|
|
||||||
NetworkNamespaceNotFound = privileged.NetworkNamespaceNotFound
|
NetworkNamespaceNotFound = privileged.NetworkNamespaceNotFound
|
||||||
|
NetworkInterfaceNotFound = privileged.NetworkInterfaceNotFound
|
||||||
|
|
||||||
|
|
||||||
def get_routing_table(ip_version, namespace=None):
|
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))
|
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):
|
def ensure_device_is_ready(device_name, namespace=None):
|
||||||
dev = IPDevice(device_name, namespace=namespace)
|
dev = IPDevice(device_name, namespace=namespace)
|
||||||
dev.set_log_fail_as_error(False)
|
dev.set_log_fail_as_error(False)
|
||||||
|
@ -15,6 +15,7 @@ import socket
|
|||||||
|
|
||||||
import pyroute2
|
import pyroute2
|
||||||
from pyroute2.netlink import rtnl
|
from pyroute2.netlink import rtnl
|
||||||
|
from pyroute2.netlink.rtnl import ndmsg
|
||||||
|
|
||||||
from neutron._i18n import _
|
from neutron._i18n import _
|
||||||
from neutron import privileged
|
from neutron import privileged
|
||||||
@ -38,6 +39,10 @@ class NetworkNamespaceNotFound(RuntimeError):
|
|||||||
self.message % {'netns_name': netns_name})
|
self.message % {'netns_name': netns_name})
|
||||||
|
|
||||||
|
|
||||||
|
class NetworkInterfaceNotFound(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@privileged.default.entrypoint
|
@privileged.default.entrypoint
|
||||||
def get_routing_table(ip_version, namespace=None):
|
def get_routing_table(ip_version, namespace=None):
|
||||||
"""Return a list of dictionaries, each representing a route.
|
"""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'])}
|
'scope': _get_scope_name(route['scope'])}
|
||||||
for route in ipdb_routes if route['family'] == family]
|
for route in ipdb_routes if route['family'] == family]
|
||||||
return routes
|
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'
|
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'
|
||||||
|
|
||||||
|
|
||||||
class IpLibTestFramework(functional_base.BaseSudoTestCase):
|
class IpLibTestFramework(functional_base.BaseSudoTestCase):
|
||||||
@ -227,6 +228,39 @@ class IpLibTestCase(IpLibTestFramework):
|
|||||||
with testtools.ExpectedException(ip_lib.NetworkNamespaceNotFound):
|
with testtools.ExpectedException(ip_lib.NetworkNamespaceNotFound):
|
||||||
ip_lib.get_routing_table(4, namespace="nonexistent-netns")
|
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):
|
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)
|
||||||
|
@ -19,6 +19,7 @@ import mock
|
|||||||
import netaddr
|
import netaddr
|
||||||
from neutron_lib import exceptions
|
from neutron_lib import exceptions
|
||||||
import pyroute2
|
import pyroute2
|
||||||
|
from pyroute2.netlink.rtnl import ndmsg
|
||||||
import testtools
|
import testtools
|
||||||
|
|
||||||
from neutron.agent.common import utils # noqa
|
from neutron.agent.common import utils # noqa
|
||||||
@ -1581,21 +1582,62 @@ class TestIpNeighCommand(TestIPCmdBase):
|
|||||||
self.parent.name = 'tap0'
|
self.parent.name = 'tap0'
|
||||||
self.command = 'neigh'
|
self.command = 'neigh'
|
||||||
self.neigh_cmd = ip_lib.IpNeighCommand(self.parent)
|
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.neigh_cmd.add('192.168.45.100', 'cc:dd:ee:ff:ab:cd')
|
||||||
self._assert_sudo([4],
|
mock_netns_enter.link_lookup.assert_called_once_with(ifname='tap0')
|
||||||
('replace', '192.168.45.100',
|
mock_netns_enter.neigh.assert_called_once_with(
|
||||||
'lladdr', 'cc:dd:ee:ff:ab:cd',
|
'replace',
|
||||||
'nud', 'permanent',
|
dst='192.168.45.100',
|
||||||
'dev', 'tap0'))
|
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.neigh_cmd.delete('192.168.45.100', 'cc:dd:ee:ff:ab:cd')
|
||||||
self._assert_sudo([4],
|
mock_netns_enter.link_lookup.assert_called_once_with(ifname='tap0')
|
||||||
('del', '192.168.45.100',
|
mock_netns_enter.neigh.assert_called_once_with(
|
||||||
'lladdr', 'cc:dd:ee:ff:ab:cd',
|
'delete',
|
||||||
'dev', 'tap0'))
|
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):
|
def test_flush(self):
|
||||||
self.neigh_cmd.flush(4, '192.168.0.1')
|
self.neigh_cmd.flush(4, '192.168.0.1')
|
||||||
|
Loading…
Reference in New Issue
Block a user