From a9c3a8b9d0adecfd394acf3dba6911a45ced4755 Mon Sep 17 00:00:00 2001 From: Peter Saveliev Date: Tue, 13 Feb 2024 18:37:52 +0100 Subject: [PATCH 1/7] rtnl: probe_msg draft version --- pyroute2/netlink/rtnl/__init__.py | 3 +++ pyroute2/netlink/rtnl/probe_msg.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 pyroute2/netlink/rtnl/probe_msg.py diff --git a/pyroute2/netlink/rtnl/__init__.py b/pyroute2/netlink/rtnl/__init__.py index 0859046d8..df52c98f7 100644 --- a/pyroute2/netlink/rtnl/__init__.py +++ b/pyroute2/netlink/rtnl/__init__.py @@ -155,6 +155,9 @@ RTM_NEWNETNS = 500 RTM_DELNETNS = 501 RTM_GETNETNS = 502 +RTM_NEWPROBE = 504 +RTM_DELPROBE = 505 +RTM_GETPROBE = 506 (RTM_NAMES, RTM_VALUES) = map_namespace('RTM_', globals()) TC_H_INGRESS = 0xFFFFFFF1 diff --git a/pyroute2/netlink/rtnl/probe_msg.py b/pyroute2/netlink/rtnl/probe_msg.py new file mode 100644 index 000000000..39a81a088 --- /dev/null +++ b/pyroute2/netlink/rtnl/probe_msg.py @@ -0,0 +1,30 @@ +from pyroute2.netlink import nlmsg + + +class probe_msg(nlmsg): + ''' + Fake message type to represent network probe info. + + This is a prototype, the NLA layout is subject to change without + notification. + ''' + + __slots__ = () + prefix = 'PROBE_' + + fields = ( + ('family', 'B'), + ('proto', 'B'), + ('port', 'H'), + ('dst_len', 'I'), + ('cmd', 'I'), + ) + + nla_map = ( + ('PROBE_UNSPEC', 'none'), + ('PROBE_KIND', 'asciiz'), + ('PROBE_STDOUT', 'asciiz'), + ('PROBE_STDERR', 'asciiz'), + ('PROBE_SRC', 'target'), + ('PROBE_DST', 'target'), + ) From aea5a9ef98008f6fdc1a860ff799db745b55cb7c Mon Sep 17 00:00:00 2001 From: Peter Saveliev Date: Tue, 13 Feb 2024 18:41:50 +0100 Subject: [PATCH 2/7] iproute: add probe() method --- pyroute2/iproute/linux.py | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/pyroute2/iproute/linux.py b/pyroute2/iproute/linux.py index a1f7e698d..61936b68f 100644 --- a/pyroute2/iproute/linux.py +++ b/pyroute2/iproute/linux.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- +import errno import logging import os +import shutil +import subprocess import time import warnings from functools import partial @@ -56,6 +59,7 @@ RTM_NEWNEIGH, RTM_NEWNETNS, RTM_NEWNSID, + RTM_NEWPROBE, RTM_NEWQDISC, RTM_NEWROUTE, RTM_NEWRULE, @@ -89,6 +93,7 @@ from pyroute2.netlink.rtnl.ndtmsg import ndtmsg from pyroute2.netlink.rtnl.nsidmsg import nsidmsg from pyroute2.netlink.rtnl.nsinfmsg import nsinfmsg +from pyroute2.netlink.rtnl.probe_msg import probe_msg from pyroute2.netlink.rtnl.riprsocket import RawIPRSocket from pyroute2.netlink.rtnl.rtmsg import rtmsg from pyroute2.netlink.rtnl.tcmsg import plugins as tc_plugins @@ -378,6 +383,58 @@ def poll(self, method, command, timeout=10, interval=0.2, **spec): pass raise TimeoutError() + # 8<--------------------------------------------------------------- + # + # Diagnostics + # + def probe(self, command, **kwarg): + response = probe_msg() + response['header']['sequence_number'] = 255 + response['header']['pid'] = 0 + response['header']['type'] = RTM_NEWPROBE + # + response['family'] = AF_INET + response['proto'] = 1 + response['port'] = 0 + response['dst_len'] = 32 + response['cmd'] = 1 + # + kind = kwarg.get('kind', 'ping') + dst = kwarg.get('dst', '0.0.0.0') + timeout = kwarg.get('timeout', 1) + num = kwarg.get('num', 1) + response['attrs'].append(['PROBE_KIND', kind]) + response['attrs'].append(['PROBE_DST', dst]) + + args = [ + shutil.which(kind), + '-c', + f'{num}', + '-W', + f'{timeout}', + f'{dst}', + ] + if args[0] is None: + raise NetlinkError(errno.ENOENT, "probe not found") + + process = subprocess.Popen( + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + try: + out, err = process.communicate(timeout=timeout) + response['attrs'].append(['PROBE_STDOUT', out]) + response['attrs'].append(['PROBE_STDERR', err]) + except subprocess.TimeoutExpired: + process.terminate() + raise NetlinkError(errno.ETIMEDOUT, "timeout expired") + finally: + process.stdout.close() + process.stderr.close() + return_code = process.wait() + if return_code != 0: + raise NetlinkError(errno.EHOSTUNREACH, "probe failed") + return [response] + # 8<--------------------------------------------------------------- # # Listing methods From 35f3d1667d33b1be68b7eee09041cb4e93e5170f Mon Sep 17 00:00:00 2001 From: Peter Saveliev Date: Tue, 13 Feb 2024 18:56:22 +0100 Subject: [PATCH 3/7] ndb: add probe interface --- pyroute2/ndb/main.py | 1 + pyroute2/ndb/objects/probe.py | 30 ++++++++++++++++++++++++++++++ pyroute2/ndb/schema.py | 4 ++-- pyroute2/ndb/view.py | 2 ++ 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 pyroute2/ndb/objects/probe.py diff --git a/pyroute2/ndb/main.py b/pyroute2/ndb/main.py index 2ab039509..4164cc186 100644 --- a/pyroute2/ndb/main.py +++ b/pyroute2/ndb/main.py @@ -321,6 +321,7 @@ def add_mock_netns(self, netns): ('af_bridge_fdb', 'fdb'), ('rules', 'rules'), ('netns', 'netns'), + ('probe', 'probe'), ('af_bridge_vlans', 'vlans'), ) diff --git a/pyroute2/ndb/objects/probe.py b/pyroute2/ndb/objects/probe.py new file mode 100644 index 000000000..10424ad7b --- /dev/null +++ b/pyroute2/ndb/objects/probe.py @@ -0,0 +1,30 @@ +from pyroute2.netlink.rtnl.probe_msg import probe_msg + +from ..objects import RTNL_Object + + +def load_probe_msg(schema, target, event): + pass + + +schema = probe_msg.sql_schema().unique_index() +init = { + 'specs': [['probe', schema]], + 'classes': [['probe', probe_msg]], + 'event_map': {probe_msg: [load_probe_msg]}, +} + + +class Probe(RTNL_Object): + + table = 'probe' + msg_class = probe_msg + api = 'probe' + + def __init__(self, *argv, **kwarg): + kwarg['iclass'] = probe_msg + self.event_map = {probe_msg: 'load_probe_msg'} + super().__init__(*argv, **kwarg) + + def check(self): + return True diff --git a/pyroute2/ndb/schema.py b/pyroute2/ndb/schema.py index 97bf83ec6..42a4514cc 100644 --- a/pyroute2/ndb/schema.py +++ b/pyroute2/ndb/schema.py @@ -131,7 +131,7 @@ from pyroute2.common import basestring, uuid32 # -from .objects import address, interface, neighbour, netns, route, rule +from .objects import address, interface, neighbour, netns, probe, route, rule try: import psycopg2 @@ -141,7 +141,7 @@ # # the order is important # -plugins = [interface, address, neighbour, route, netns, rule] +plugins = [interface, address, neighbour, route, netns, rule, probe] MAX_ATTEMPTS = 5 diff --git a/pyroute2/ndb/view.py b/pyroute2/ndb/view.py index 09da32ffb..a629acc5e 100644 --- a/pyroute2/ndb/view.py +++ b/pyroute2/ndb/view.py @@ -66,6 +66,7 @@ from .objects.interface import Interface, Vlan from .objects.neighbour import FDBRecord, Neighbour from .objects.netns import NetNS +from .objects.probe import Probe from .objects.route import Route from .objects.rule import Rule from .report import Record, RecordSet @@ -120,6 +121,7 @@ def __init__(self, ndb, table, chain=None, auth_managers=None): self.classes['routes'] = Route self.classes['rules'] = Rule self.classes['netns'] = NetNS + self.classes['probe'] = Probe self.classes['af_bridge_vlans'] = Vlan def __enter__(self): From 600c4f36d8710b293cd0112447c517022b847991 Mon Sep 17 00:00:00 2001 From: Peter Saveliev Date: Tue, 13 Feb 2024 20:33:46 +0100 Subject: [PATCH 4/7] rtnl: move probe to netlink proxy + add basic test --- pyroute2/iproute/linux.py | 52 ++++-------------------- pyroute2/netlink/rtnl/iprsocket.py | 2 + pyroute2/netlink/rtnl/marshal.py | 2 + pyroute2/netlink/rtnl/probe_msg.py | 54 +++++++++++++++++++++---- tests/test_linux/test_ipr/test_probe.py | 35 ++++++++++++++++ 5 files changed, 94 insertions(+), 51 deletions(-) create mode 100644 tests/test_linux/test_ipr/test_probe.py diff --git a/pyroute2/iproute/linux.py b/pyroute2/iproute/linux.py index 61936b68f..81400ede5 100644 --- a/pyroute2/iproute/linux.py +++ b/pyroute2/iproute/linux.py @@ -1,9 +1,6 @@ # -*- coding: utf-8 -*- -import errno import logging import os -import shutil -import subprocess import time import warnings from functools import partial @@ -388,52 +385,19 @@ def poll(self, method, command, timeout=10, interval=0.2, **spec): # Diagnostics # def probe(self, command, **kwarg): - response = probe_msg() - response['header']['sequence_number'] = 255 - response['header']['pid'] = 0 - response['header']['type'] = RTM_NEWPROBE + msg = probe_msg() # - response['family'] = AF_INET - response['proto'] = 1 - response['port'] = 0 - response['dst_len'] = 32 - response['cmd'] = 1 + msg['family'] = AF_INET + msg['proto'] = 1 + msg['port'] = 0 + msg['dst_len'] = 32 # kind = kwarg.get('kind', 'ping') dst = kwarg.get('dst', '0.0.0.0') - timeout = kwarg.get('timeout', 1) - num = kwarg.get('num', 1) - response['attrs'].append(['PROBE_KIND', kind]) - response['attrs'].append(['PROBE_DST', dst]) - - args = [ - shutil.which(kind), - '-c', - f'{num}', - '-W', - f'{timeout}', - f'{dst}', - ] - if args[0] is None: - raise NetlinkError(errno.ENOENT, "probe not found") + msg['attrs'].append(['PROBE_KIND', kind]) + msg['attrs'].append(['PROBE_DST', dst]) - process = subprocess.Popen( - args, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - try: - out, err = process.communicate(timeout=timeout) - response['attrs'].append(['PROBE_STDOUT', out]) - response['attrs'].append(['PROBE_STDERR', err]) - except subprocess.TimeoutExpired: - process.terminate() - raise NetlinkError(errno.ETIMEDOUT, "timeout expired") - finally: - process.stdout.close() - process.stderr.close() - return_code = process.wait() - if return_code != 0: - raise NetlinkError(errno.EHOSTUNREACH, "probe failed") - return [response] + return self.nlm_request(msg, msg_type=RTM_NEWPROBE, msg_flags=1) # 8<--------------------------------------------------------------- # diff --git a/pyroute2/netlink/rtnl/iprsocket.py b/pyroute2/netlink/rtnl/iprsocket.py index 5dc3b4877..a6f671e03 100644 --- a/pyroute2/netlink/rtnl/iprsocket.py +++ b/pyroute2/netlink/rtnl/iprsocket.py @@ -17,6 +17,7 @@ proxy_newlink, proxy_setlink, ) + from pyroute2.netlink.rtnl.probe_msg import proxy_newprobe class IPRSocketBase(object): @@ -37,6 +38,7 @@ def __init__(self, *argv, **kwarg): self._sproxy.pmap = { rtnl.RTM_NEWLINK: proxy_newlink, rtnl.RTM_SETLINK: proxy_setlink, + rtnl.RTM_NEWPROBE: proxy_newprobe, } def bind(self, groups=None, **kwarg): diff --git a/pyroute2/netlink/rtnl/marshal.py b/pyroute2/netlink/rtnl/marshal.py index 448b8bfea..40431dc54 100644 --- a/pyroute2/netlink/rtnl/marshal.py +++ b/pyroute2/netlink/rtnl/marshal.py @@ -7,6 +7,7 @@ from pyroute2.netlink.rtnl.ndmsg import ndmsg from pyroute2.netlink.rtnl.ndtmsg import ndtmsg from pyroute2.netlink.rtnl.nsidmsg import nsidmsg +from pyroute2.netlink.rtnl.probe_msg import probe_msg from pyroute2.netlink.rtnl.rtmsg import rtmsg from pyroute2.netlink.rtnl.tcmsg import tcmsg @@ -48,6 +49,7 @@ class MarshalRtnl(Marshal): rtnl.RTM_GETSTATS: ifstatsmsg, rtnl.RTM_NEWLINKPROP: ifinfmsg, rtnl.RTM_DELLINKPROP: ifinfmsg, + rtnl.RTM_NEWPROBE: probe_msg, } def fix_message(self, msg): diff --git a/pyroute2/netlink/rtnl/probe_msg.py b/pyroute2/netlink/rtnl/probe_msg.py index 39a81a088..23ddcee07 100644 --- a/pyroute2/netlink/rtnl/probe_msg.py +++ b/pyroute2/netlink/rtnl/probe_msg.py @@ -1,4 +1,9 @@ +import errno +import shutil +import subprocess + from pyroute2.netlink import nlmsg +from pyroute2.netlink.exceptions import NetlinkError class probe_msg(nlmsg): @@ -12,13 +17,7 @@ class probe_msg(nlmsg): __slots__ = () prefix = 'PROBE_' - fields = ( - ('family', 'B'), - ('proto', 'B'), - ('port', 'H'), - ('dst_len', 'I'), - ('cmd', 'I'), - ) + fields = (('family', 'B'), ('proto', 'B'), ('port', 'H'), ('dst_len', 'I')) nla_map = ( ('PROBE_UNSPEC', 'none'), @@ -28,3 +27,44 @@ class probe_msg(nlmsg): ('PROBE_SRC', 'target'), ('PROBE_DST', 'target'), ) + + +def proxy_newprobe(msg, nl): + num = 1 + timeout = 1 + dst = msg.get('dst') + kind = msg.get('kind') + + if kind.endswith('ping'): + args = [ + shutil.which(kind), + '-c', + f'{num}', + '-W', + f'{timeout}', + f'{dst}', + ] + if args[0] is None: + raise NetlinkError(errno.ENOENT, "probe not found") + + process = subprocess.Popen( + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + try: + out, err = process.communicate(timeout=timeout) + if out: + msg['attrs'].append(['PROBE_STDOUT', out]) + if err: + msg['attrs'].append(['PROBE_STDERR', err]) + except subprocess.TimeoutExpired: + process.terminate() + raise NetlinkError(errno.ETIMEDOUT, "timeout expired") + finally: + process.stdout.close() + process.stderr.close() + return_code = process.wait() + if return_code != 0: + raise NetlinkError(errno.EHOSTUNREACH, "probe failed") + msg.reset() + msg.encode() + return {'verdict': 'return', 'data': msg.data} diff --git a/tests/test_linux/test_ipr/test_probe.py b/tests/test_linux/test_ipr/test_probe.py new file mode 100644 index 000000000..de95f6385 --- /dev/null +++ b/tests/test_linux/test_ipr/test_probe.py @@ -0,0 +1,35 @@ +from socket import AF_INET + +import pytest +from pr2test.context_manager import make_test_matrix, skip_if_not_supported +from pr2test.marks import require_root + +pytestmark = [require_root()] +test_matrix = make_test_matrix(targets=['local', 'netns']) + + +@pytest.mark.parametrize('context', test_matrix, indirect=True) +@skip_if_not_supported +def test_ping_ok(context): + index, ifname = context.default_interface + ipaddr = context.new_ipaddr + + context.ipr.addr('add', index=index, address=ipaddr, prefixlen=24) + context.ipr.link('set', index=index, state='up') + context.ipr.link( + 'set', index=context.ipr.link_lookup(ifname='lo'), state='up' + ) + + context.ndb.interfaces.wait(ifname=ifname, state='up') + context.ndb.interfaces.wait(ifname='lo', state='up') + + probes = [x for x in context.ipr.probe('add', kind='ping', dst=ipaddr)] + probe = probes[0] + + assert len(probes) == 1 + assert probe['family'] == AF_INET + assert probe['proto'] == 1 + assert probe['port'] == 0 + assert probe['dst_len'] == 32 + assert probe.get('dst') == ipaddr + assert probe.get('kind') == 'ping' From 0bade23d7fc5ceeed2feeac57dcf8303ce574cbf Mon Sep 17 00:00:00 2001 From: Peter Saveliev Date: Wed, 14 Feb 2024 12:45:26 +0100 Subject: [PATCH 5/7] iproute: don't defer probe run --- pyroute2/iproute/linux.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyroute2/iproute/linux.py b/pyroute2/iproute/linux.py index 81400ede5..fd20f0806 100644 --- a/pyroute2/iproute/linux.py +++ b/pyroute2/iproute/linux.py @@ -396,8 +396,8 @@ def probe(self, command, **kwarg): dst = kwarg.get('dst', '0.0.0.0') msg['attrs'].append(['PROBE_KIND', kind]) msg['attrs'].append(['PROBE_DST', dst]) - - return self.nlm_request(msg, msg_type=RTM_NEWPROBE, msg_flags=1) + # iterate the results immediately, don't defer the probe run + return tuple(self.nlm_request(msg, msg_type=RTM_NEWPROBE, msg_flags=1)) # 8<--------------------------------------------------------------- # From 7ec7d32e4be4f48b3296eff57c2a484e4e853946 Mon Sep 17 00:00:00 2001 From: Peter Saveliev Date: Wed, 14 Feb 2024 12:46:04 +0100 Subject: [PATCH 6/7] tests: update probe tests --- tests/test_linux/test_ipr/test_probe.py | 42 ++++++++++++++++++ tests/test_linux/test_ndb/test_probe.py | 57 +++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 tests/test_linux/test_ndb/test_probe.py diff --git a/tests/test_linux/test_ipr/test_probe.py b/tests/test_linux/test_ipr/test_probe.py index de95f6385..df8b7a7cf 100644 --- a/tests/test_linux/test_ipr/test_probe.py +++ b/tests/test_linux/test_ipr/test_probe.py @@ -1,9 +1,12 @@ +import errno from socket import AF_INET import pytest from pr2test.context_manager import make_test_matrix, skip_if_not_supported from pr2test.marks import require_root +from pyroute2 import NetlinkError + pytestmark = [require_root()] test_matrix = make_test_matrix(targets=['local', 'netns']) @@ -33,3 +36,42 @@ def test_ping_ok(context): assert probe['dst_len'] == 32 assert probe.get('dst') == ipaddr assert probe.get('kind') == 'ping' + + +@pytest.mark.parametrize( + 'context', make_test_matrix(targets=['netns']), indirect=True +) +@skip_if_not_supported +def test_ping_fail_ehostunreach(context): + context.ipr.link( + 'set', index=context.ipr.link_lookup(ifname='lo'), state='down' + ) + with pytest.raises(NetlinkError) as e: + context.ipr.probe('add', kind='ping', dst='127.0.0.1') + assert e.value.code == errno.EHOSTUNREACH + + +@pytest.mark.parametrize( + 'context', make_test_matrix(targets=['netns']), indirect=True +) +@skip_if_not_supported +def test_ping_fail_etimedout(context): + index, ifname = context.default_interface + ipaddr = context.new_ipaddr + target = context.new_ipaddr + + context.ipr.addr('add', index=index, address=ipaddr, prefixlen=24) + context.ipr.link('set', index=index, state='up') + context.ipr.link( + 'set', index=context.ipr.link_lookup(ifname='lo'), state='up' + ) + + context.ndb.interfaces.wait(ifname=ifname, state='up') + context.ndb.interfaces.wait(ifname='lo', state='up') + + context.ipr.link( + 'set', index=context.ipr.link_lookup(ifname='lo'), state='up' + ) + with pytest.raises(NetlinkError) as e: + context.ipr.probe('add', kind='ping', dst=target) + assert e.value.code == errno.ETIMEDOUT diff --git a/tests/test_linux/test_ndb/test_probe.py b/tests/test_linux/test_ndb/test_probe.py new file mode 100644 index 000000000..a84e0e073 --- /dev/null +++ b/tests/test_linux/test_ndb/test_probe.py @@ -0,0 +1,57 @@ +import errno + +import pytest +from pr2test.context_manager import make_test_matrix, skip_if_not_supported +from pr2test.marks import require_root + +from pyroute2 import NetlinkError + +pytestmark = [require_root()] +test_matrix = make_test_matrix(targets=['local', 'netns']) + + +@pytest.mark.parametrize('context', test_matrix, indirect=True) +@skip_if_not_supported +def test_ping_ok(context): + index, ifname = context.default_interface + ipaddr = context.new_ipaddr + + with context.ndb.interfaces[ifname] as i: + i.add_ip(address=ipaddr, prefixlen=24) + i.set(state='up') + + with context.ndb.interfaces['lo'] as i: + i.set(state='up') + + context.ndb.probe.create(kind='ping', dst=ipaddr).commit() + + +@pytest.mark.parametrize( + 'context', make_test_matrix(targets=['netns']), indirect=True +) +@skip_if_not_supported +def test_ping_fail_ehostunreach(context): + with context.ndb.interfaces['lo'] as i: + i.set(state='down') + with pytest.raises(NetlinkError) as e: + context.ndb.probe.create(kind='ping', dst='127.0.0.1').commit() + assert e.value.code == errno.EHOSTUNREACH + + +@pytest.mark.parametrize( + 'context', make_test_matrix(targets=['netns']), indirect=True +) +@skip_if_not_supported +def test_ping_fail_etimedout(context): + index, ifname = context.default_interface + ipaddr = context.new_ipaddr + target = context.new_ipaddr + + with context.ndb.interfaces[ifname] as i: + i.add_ip(address=ipaddr, prefixlen=24) + i.set(state='up') + with context.ndb.interfaces['lo'] as i: + i.set(state='up') + with pytest.raises(NetlinkError) as e: + context.ndb.probe.create(kind='ping', dst=target).commit() + assert e.value.code == errno.ETIMEDOUT From 17aff11e5c4157d76d655a8f3579f54940626b77 Mon Sep 17 00:00:00 2001 From: Peter Saveliev Date: Wed, 14 Feb 2024 21:48:24 +0100 Subject: [PATCH 7/7] docs: add docs on network probes --- docs/ndb_probes.rst | 6 +++ docs/ndb_toc.rst | 1 + pyroute2/iproute/linux.py | 46 ++++++++++++++++++-- pyroute2/ndb/main.py | 2 +- pyroute2/ndb/objects/probe.py | 58 +++++++++++++++++++++++-- pyroute2/ndb/view.py | 2 +- pyroute2/netlink/rtnl/probe_msg.py | 14 +++--- tests/test_linux/test_ndb/test_probe.py | 6 +-- 8 files changed, 118 insertions(+), 17 deletions(-) create mode 100644 docs/ndb_probes.rst diff --git a/docs/ndb_probes.rst b/docs/ndb_probes.rst new file mode 100644 index 000000000..8c7240dc0 --- /dev/null +++ b/docs/ndb_probes.rst @@ -0,0 +1,6 @@ +.. ndbprobes: + +Network probes +============== + +.. automodule:: pyroute2.ndb.objects.probe diff --git a/docs/ndb_toc.rst b/docs/ndb_toc.rst index 812ba7ddd..6c64044cc 100644 --- a/docs/ndb_toc.rst +++ b/docs/ndb_toc.rst @@ -11,6 +11,7 @@ ndb_interfaces ndb_addresses ndb_routes + ndb_probes ndb_schema ndb_sources ndb_debug diff --git a/pyroute2/iproute/linux.py b/pyroute2/iproute/linux.py index fd20f0806..4e550a52a 100644 --- a/pyroute2/iproute/linux.py +++ b/pyroute2/iproute/linux.py @@ -385,6 +385,44 @@ def poll(self, method, command, timeout=10, interval=0.2, **spec): # Diagnostics # def probe(self, command, **kwarg): + ''' + Run a network probe. + + The API will trigger a network probe from the environment it + works in. For NetNS it will be the network namespace, for + remote IPRoute instances it will be the host it runs on. + + Running probes via API allows to test network connectivity + between the environments in a simple uniform way. + + Supported arguments: + + * kind -- probe type, for now only ping is supported + * dst -- target to run the probe against + * num -- number of probes to run + * timeout -- timeout for the whole request + + Examples:: + + ipr.probe("add", kind="ping", dst="10.0.0.1") + + By default ping probe will send one ICMP request towards + the target. To change this, use num argument:: + + ipr.probe( + "add", + kind="ping", + dst="10.0.0.1", + num=4, + timeout=10 + ) + + Timeout for the ping probe by default is 1 second, which + may not be enough to run multiple requests. + + In the next release more probe types are planned, like TCP + port probe. + ''' msg = probe_msg() # msg['family'] = AF_INET @@ -392,10 +430,10 @@ def probe(self, command, **kwarg): msg['port'] = 0 msg['dst_len'] = 32 # - kind = kwarg.get('kind', 'ping') - dst = kwarg.get('dst', '0.0.0.0') - msg['attrs'].append(['PROBE_KIND', kind]) - msg['attrs'].append(['PROBE_DST', dst]) + msg['attrs'].append(['PROBE_KIND', kwarg.get('kind', 'ping')]) + msg['attrs'].append(['PROBE_DST', kwarg.get('dst', '0.0.0.0')]) + msg['attrs'].append(['PROBE_NUM', kwarg.get('num', 1)]) + msg['attrs'].append(['PROBE_TIMEOUT', kwarg.get('timeout', 1)]) # iterate the results immediately, don't defer the probe run return tuple(self.nlm_request(msg, msg_type=RTM_NEWPROBE, msg_flags=1)) diff --git a/pyroute2/ndb/main.py b/pyroute2/ndb/main.py index 4164cc186..81b65238f 100644 --- a/pyroute2/ndb/main.py +++ b/pyroute2/ndb/main.py @@ -321,7 +321,7 @@ def add_mock_netns(self, netns): ('af_bridge_fdb', 'fdb'), ('rules', 'rules'), ('netns', 'netns'), - ('probe', 'probe'), + ('probes', 'probes'), ('af_bridge_vlans', 'vlans'), ) diff --git a/pyroute2/ndb/objects/probe.py b/pyroute2/ndb/objects/probe.py index 10424ad7b..724171c43 100644 --- a/pyroute2/ndb/objects/probe.py +++ b/pyroute2/ndb/objects/probe.py @@ -1,3 +1,55 @@ +''' +Run a network probe +=================== + +A successful network probe neither creates real network objects +like interfaces or addresses, nor database records. The only +important thing it does -- it raises no exception. + +On the contrary, an unsuccessful network probe raises a +`NetlinkError` exception, cancelling the whole transaction. + +A network probe is always run from the corresponding netlink +target: a local system, a remote system, a network namespace, +a container. + +An example scenario: + + * target alpha, set up eth0 10.0.0.2/24 + * target beta, set up eth0 10.0.0.4/24 + * ping 10.0.0.4 (beta) from target alpha + * ping 10.0.0.2 (alpha) from target beta + +The code below sets up the addresses and checks ICMP responses. If +any step fails, the whole transaction will be rolled back automatically:: + + with NDB(log='debug') as ndb: + ndb.sources.add(kind='remote', hostname='alpha', username='root') + ndb.sources.add(kind='remote', hostname='beta', username='root') + + with ndb.begin() as trx: + trx.push( + (ndb + .interfaces[{'target': 'alpha', 'ifname': 'eth0'}] + .set(state='up') + .add_ip(address='10.0.0.2', prefixlen=24) + ), + (ndb + .interfaces[{'target': 'beta', 'ifname': 'eth0'}] + .set(state='up') + .add_ip(address='10.0.0.4', prefixlen=24) + ), + (ndb + .probes + .create(target='alpha', kind='ping', dst='10.0.0.4') + ), + (ndb + .probes + .create(target='beta', kind='ping', dst='10.0.0.2') + ), + ) +''' + from pyroute2.netlink.rtnl.probe_msg import probe_msg from ..objects import RTNL_Object @@ -9,15 +61,15 @@ def load_probe_msg(schema, target, event): schema = probe_msg.sql_schema().unique_index() init = { - 'specs': [['probe', schema]], - 'classes': [['probe', probe_msg]], + 'specs': [['probes', schema]], + 'classes': [['probes', probe_msg]], 'event_map': {probe_msg: [load_probe_msg]}, } class Probe(RTNL_Object): - table = 'probe' + table = 'probes' msg_class = probe_msg api = 'probe' diff --git a/pyroute2/ndb/view.py b/pyroute2/ndb/view.py index a629acc5e..311b62b96 100644 --- a/pyroute2/ndb/view.py +++ b/pyroute2/ndb/view.py @@ -121,7 +121,7 @@ def __init__(self, ndb, table, chain=None, auth_managers=None): self.classes['routes'] = Route self.classes['rules'] = Rule self.classes['netns'] = NetNS - self.classes['probe'] = Probe + self.classes['probes'] = Probe self.classes['af_bridge_vlans'] = Vlan def __enter__(self): diff --git a/pyroute2/netlink/rtnl/probe_msg.py b/pyroute2/netlink/rtnl/probe_msg.py index 23ddcee07..eb5c2db21 100644 --- a/pyroute2/netlink/rtnl/probe_msg.py +++ b/pyroute2/netlink/rtnl/probe_msg.py @@ -26,12 +26,14 @@ class probe_msg(nlmsg): ('PROBE_STDERR', 'asciiz'), ('PROBE_SRC', 'target'), ('PROBE_DST', 'target'), + ('PROBE_NUM', 'uint8'), + ('PROBE_TIMEOUT', 'uint8'), ) def proxy_newprobe(msg, nl): - num = 1 - timeout = 1 + num = msg.get('num') + timeout = msg.get('timeout') dst = msg.get('dst') kind = msg.get('kind') @@ -45,7 +47,7 @@ def proxy_newprobe(msg, nl): f'{dst}', ] if args[0] is None: - raise NetlinkError(errno.ENOENT, "probe not found") + raise NetlinkError(errno.ENOENT, 'probe not found') process = subprocess.Popen( args, stdout=subprocess.PIPE, stderr=subprocess.PIPE @@ -58,13 +60,15 @@ def proxy_newprobe(msg, nl): msg['attrs'].append(['PROBE_STDERR', err]) except subprocess.TimeoutExpired: process.terminate() - raise NetlinkError(errno.ETIMEDOUT, "timeout expired") + raise NetlinkError(errno.ETIMEDOUT, 'timeout expired') finally: process.stdout.close() process.stderr.close() return_code = process.wait() if return_code != 0: - raise NetlinkError(errno.EHOSTUNREACH, "probe failed") + raise NetlinkError(errno.EHOSTUNREACH, 'probe failed') + else: + raise NetlinkError(errno.ENOTSUP, 'probe type not supported') msg.reset() msg.encode() return {'verdict': 'return', 'data': msg.data} diff --git a/tests/test_linux/test_ndb/test_probe.py b/tests/test_linux/test_ndb/test_probe.py index a84e0e073..72669f9f4 100644 --- a/tests/test_linux/test_ndb/test_probe.py +++ b/tests/test_linux/test_ndb/test_probe.py @@ -23,7 +23,7 @@ def test_ping_ok(context): with context.ndb.interfaces['lo'] as i: i.set(state='up') - context.ndb.probe.create(kind='ping', dst=ipaddr).commit() + context.ndb.probes.create(kind='ping', dst=ipaddr).commit() @pytest.mark.parametrize( @@ -34,7 +34,7 @@ def test_ping_fail_ehostunreach(context): with context.ndb.interfaces['lo'] as i: i.set(state='down') with pytest.raises(NetlinkError) as e: - context.ndb.probe.create(kind='ping', dst='127.0.0.1').commit() + context.ndb.probes.create(kind='ping', dst='127.0.0.1').commit() assert e.value.code == errno.EHOSTUNREACH @@ -53,5 +53,5 @@ def test_ping_fail_etimedout(context): with context.ndb.interfaces['lo'] as i: i.set(state='up') with pytest.raises(NetlinkError) as e: - context.ndb.probe.create(kind='ping', dst=target).commit() + context.ndb.probes.create(kind='ping', dst=target).commit() assert e.value.code == errno.ETIMEDOUT