Skip to content

Commit

Permalink
rtnl: Merge pull request #1175 from svinota/rtnl-probe
Browse files Browse the repository at this point in the history
Add probe functionaly

Bug-Url: #1175
  • Loading branch information
svinota authored Feb 14, 2024
2 parents eef9470 + 17aff11 commit 34d0768
Show file tree
Hide file tree
Showing 13 changed files with 368 additions and 2 deletions.
6 changes: 6 additions & 0 deletions docs/ndb_probes.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.. ndbprobes:
Network probes
==============

.. automodule:: pyroute2.ndb.objects.probe
1 change: 1 addition & 0 deletions docs/ndb_toc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
ndb_interfaces
ndb_addresses
ndb_routes
ndb_probes
ndb_schema
ndb_sources
ndb_debug
Expand Down
59 changes: 59 additions & 0 deletions pyroute2/iproute/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
RTM_NEWNEIGH,
RTM_NEWNETNS,
RTM_NEWNSID,
RTM_NEWPROBE,
RTM_NEWQDISC,
RTM_NEWROUTE,
RTM_NEWRULE,
Expand Down Expand Up @@ -89,6 +90,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
Expand Down Expand Up @@ -378,6 +380,63 @@ def poll(self, method, command, timeout=10, interval=0.2, **spec):
pass
raise TimeoutError()

# 8<---------------------------------------------------------------
#
# 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
msg['proto'] = 1
msg['port'] = 0
msg['dst_len'] = 32
#
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))

# 8<---------------------------------------------------------------
#
# Listing methods
Expand Down
1 change: 1 addition & 0 deletions pyroute2/ndb/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ def add_mock_netns(self, netns):
('af_bridge_fdb', 'fdb'),
('rules', 'rules'),
('netns', 'netns'),
('probes', 'probes'),
('af_bridge_vlans', 'vlans'),
)

Expand Down
82 changes: 82 additions & 0 deletions pyroute2/ndb/objects/probe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'''
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


def load_probe_msg(schema, target, event):
pass


schema = probe_msg.sql_schema().unique_index()
init = {
'specs': [['probes', schema]],
'classes': [['probes', probe_msg]],
'event_map': {probe_msg: [load_probe_msg]},
}


class Probe(RTNL_Object):

table = 'probes'
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
4 changes: 2 additions & 2 deletions pyroute2/ndb/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
2 changes: 2 additions & 0 deletions pyroute2/ndb/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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['probes'] = Probe
self.classes['af_bridge_vlans'] = Vlan

def __enter__(self):
Expand Down
3 changes: 3 additions & 0 deletions pyroute2/netlink/rtnl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pyroute2/netlink/rtnl/iprsocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
proxy_newlink,
proxy_setlink,
)
from pyroute2.netlink.rtnl.probe_msg import proxy_newprobe


class IPRSocketBase(object):
Expand All @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions pyroute2/netlink/rtnl/marshal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down
74 changes: 74 additions & 0 deletions pyroute2/netlink/rtnl/probe_msg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import errno
import shutil
import subprocess

from pyroute2.netlink import nlmsg
from pyroute2.netlink.exceptions import NetlinkError


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

nla_map = (
('PROBE_UNSPEC', 'none'),
('PROBE_KIND', 'asciiz'),
('PROBE_STDOUT', 'asciiz'),
('PROBE_STDERR', 'asciiz'),
('PROBE_SRC', 'target'),
('PROBE_DST', 'target'),
('PROBE_NUM', 'uint8'),
('PROBE_TIMEOUT', 'uint8'),
)


def proxy_newprobe(msg, nl):
num = msg.get('num')
timeout = msg.get('timeout')
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')
else:
raise NetlinkError(errno.ENOTSUP, 'probe type not supported')
msg.reset()
msg.encode()
return {'verdict': 'return', 'data': msg.data}
Loading

0 comments on commit 34d0768

Please sign in to comment.