Skip to content

Commit 8d03c15

Browse files
committed
Merge branch 'selftests-drv-net-support-testing-with-a-remote-system'
Jakub Kicinski says: ==================== selftests: drv-net: support testing with a remote system Implement support for tests which require access to a remote system / endpoint which can generate traffic. This series concludes the "groundwork" for upstream driver tests. I wanted to support the three models which came up in discussions: - SW testing with netdevsim - "local" testing with two ports on the same system in a loopback - "remote" testing via SSH so there is a tiny bit of an abstraction which wraps up how "remote" commands are executed. Otherwise hopefully there's nothing surprising. I'm only adding a ping test. I had a bigger one written but I was worried we'll get into discussing the details of the test itself and how I chose to hack up netdevsim, instead of the test infra... So that test will be a follow up :) v4: https://lore.kernel.org/all/20240418233844.2762396-1-kuba@kernel.org v3: https://lore.kernel.org/all/20240417231146.2435572-1-kuba@kernel.org v2: https://lore.kernel.org/all/20240416004556.1618804-1-kuba@kernel.org v1: https://lore.kernel.org/all/20240412233705.1066444-1-kuba@kernel.org ==================== Link: https://lore.kernel.org/r/20240420025237.3309296-1-kuba@kernel.org Signed-off-by: Jakub Kicinski <kuba@kernel.org>
2 parents b2c8599 + f1e68a1 commit 8d03c15

File tree

12 files changed

+417
-30
lines changed

12 files changed

+417
-30
lines changed

tools/testing/selftests/drivers/net/Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
TEST_INCLUDES := $(wildcard lib/py/*.py)
44

5-
TEST_PROGS := stats.py
5+
TEST_PROGS := \
6+
ping.py \
7+
stats.py \
8+
# end of TEST_PROGS
69

710
include ../../lib.mk

tools/testing/selftests/drivers/net/README.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,41 @@ or::
2323
# Variable set in a file
2424
NETIF=eth0
2525

26+
Please note that the config parser is very simple, if there are
27+
any non-alphanumeric characters in the value it needs to be in
28+
double quotes.
29+
2630
NETIF
2731
~~~~~
2832

2933
Name of the netdevice against which the test should be executed.
3034
When empty or not set software devices will be used.
35+
36+
LOCAL_V4, LOCAL_V6, REMOTE_V4, REMOTE_V6
37+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
38+
39+
Local and remote endpoint IP addresses.
40+
41+
REMOTE_TYPE
42+
~~~~~~~~~~~
43+
44+
Communication method used to run commands on the remote endpoint.
45+
Test framework has built-in support for ``netns`` and ``ssh`` channels.
46+
``netns`` assumes the "remote" interface is part of the same
47+
host, just moved to the specified netns.
48+
``ssh`` communicates with remote endpoint over ``ssh`` and ``scp``.
49+
Using persistent SSH connections is strongly encouraged to avoid
50+
the latency of SSH connection setup on every command.
51+
52+
Communication methods are defined by classes in ``lib/py/remote_{name}.py``.
53+
It should be possible to add a new method without modifying any of
54+
the framework, by simply adding an appropriately named file to ``lib/py``.
55+
56+
REMOTE_ARGS
57+
~~~~~~~~~~~
58+
59+
Arguments used to construct the communication channel.
60+
Communication channel dependent::
61+
62+
for netns - name of the "remote" namespace
63+
for ssh - name/address of the remote host

tools/testing/selftests/drivers/net/lib/py/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
sys.exit(4)
1616

1717
from .env import *
18+
from .remote import Remote

tools/testing/selftests/drivers/net/lib/py/env.py

Lines changed: 157 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,41 @@
33
import os
44
import shlex
55
from pathlib import Path
6-
from lib.py import ip
7-
from lib.py import NetdevSimDev
6+
from lib.py import KsftSkipEx
7+
from lib.py import cmd, ip
8+
from lib.py import NetNS, NetdevSimDev
9+
from .remote import Remote
10+
11+
12+
def _load_env_file(src_path):
13+
env = os.environ.copy()
14+
15+
src_dir = Path(src_path).parent.resolve()
16+
if not (src_dir / "net.config").exists():
17+
return env
18+
19+
lexer = shlex.shlex(open((src_dir / "net.config").as_posix(), 'r').read())
20+
k = None
21+
for token in lexer:
22+
if k is None:
23+
k = token
24+
env[k] = ""
25+
elif token == "=":
26+
pass
27+
else:
28+
env[k] = token
29+
k = None
30+
return env
31+
832

933
class NetDrvEnv:
34+
"""
35+
Class for a single NIC / host env, with no remote end
36+
"""
1037
def __init__(self, src_path):
1138
self._ns = None
1239

13-
self.env = os.environ.copy()
14-
self._load_env_file(src_path)
40+
self.env = _load_env_file(src_path)
1541

1642
if 'NETIF' in self.env:
1743
self.dev = ip("link show dev " + self.env['NETIF'], json=True)[0]
@@ -34,19 +60,130 @@ def __del__(self):
3460
self._ns.remove()
3561
self._ns = None
3662

37-
def _load_env_file(self, src_path):
38-
src_dir = Path(src_path).parent.resolve()
39-
if not (src_dir / "net.config").exists():
40-
return
41-
42-
lexer = shlex.shlex(open((src_dir / "net.config").as_posix(), 'r').read())
43-
k = None
44-
for token in lexer:
45-
if k is None:
46-
k = token
47-
self.env[k] = ""
48-
elif token == "=":
49-
pass
50-
else:
51-
self.env[k] = token
52-
k = None
63+
64+
class NetDrvEpEnv:
65+
"""
66+
Class for an environment with a local device and "remote endpoint"
67+
which can be used to send traffic in.
68+
69+
For local testing it creates two network namespaces and a pair
70+
of netdevsim devices.
71+
"""
72+
73+
# Network prefixes used for local tests
74+
nsim_v4_pfx = "192.0.2."
75+
nsim_v6_pfx = "2001:db8::"
76+
77+
def __init__(self, src_path):
78+
79+
self.env = _load_env_file(src_path)
80+
81+
# Things we try to destroy
82+
self.remote = None
83+
# These are for local testing state
84+
self._netns = None
85+
self._ns = None
86+
self._ns_peer = None
87+
88+
if "NETIF" in self.env:
89+
self.dev = ip("link show dev " + self.env['NETIF'], json=True)[0]
90+
91+
self.v4 = self.env.get("LOCAL_V4")
92+
self.v6 = self.env.get("LOCAL_V6")
93+
self.remote_v4 = self.env.get("REMOTE_V4")
94+
self.remote_v6 = self.env.get("REMOTE_V6")
95+
kind = self.env["REMOTE_TYPE"]
96+
args = self.env["REMOTE_ARGS"]
97+
else:
98+
self.create_local()
99+
100+
self.dev = self._ns.nsims[0].dev
101+
102+
self.v4 = self.nsim_v4_pfx + "1"
103+
self.v6 = self.nsim_v6_pfx + "1"
104+
self.remote_v4 = self.nsim_v4_pfx + "2"
105+
self.remote_v6 = self.nsim_v6_pfx + "2"
106+
kind = "netns"
107+
args = self._netns.name
108+
109+
self.remote = Remote(kind, args, src_path)
110+
111+
self.addr = self.v6 if self.v6 else self.v4
112+
self.remote_addr = self.remote_v6 if self.remote_v6 else self.remote_v4
113+
114+
self.addr_ipver = "6" if self.v6 else "4"
115+
# Bracketed addresses, some commands need IPv6 to be inside []
116+
self.baddr = f"[{self.v6}]" if self.v6 else self.v4
117+
self.remote_baddr = f"[{self.remote_v6}]" if self.remote_v6 else self.remote_v4
118+
119+
self.ifname = self.dev['ifname']
120+
self.ifindex = self.dev['ifindex']
121+
122+
self._required_cmd = {}
123+
124+
def create_local(self):
125+
self._netns = NetNS()
126+
self._ns = NetdevSimDev()
127+
self._ns_peer = NetdevSimDev(ns=self._netns)
128+
129+
with open("/proc/self/ns/net") as nsfd0, \
130+
open("/var/run/netns/" + self._netns.name) as nsfd1:
131+
ifi0 = self._ns.nsims[0].ifindex
132+
ifi1 = self._ns_peer.nsims[0].ifindex
133+
NetdevSimDev.ctrl_write('link_device',
134+
f'{nsfd0.fileno()}:{ifi0} {nsfd1.fileno()}:{ifi1}')
135+
136+
ip(f" addr add dev {self._ns.nsims[0].ifname} {self.nsim_v4_pfx}1/24")
137+
ip(f"-6 addr add dev {self._ns.nsims[0].ifname} {self.nsim_v6_pfx}1/64 nodad")
138+
ip(f" link set dev {self._ns.nsims[0].ifname} up")
139+
140+
ip(f" addr add dev {self._ns_peer.nsims[0].ifname} {self.nsim_v4_pfx}2/24", ns=self._netns)
141+
ip(f"-6 addr add dev {self._ns_peer.nsims[0].ifname} {self.nsim_v6_pfx}2/64 nodad", ns=self._netns)
142+
ip(f" link set dev {self._ns_peer.nsims[0].ifname} up", ns=self._netns)
143+
144+
def __enter__(self):
145+
return self
146+
147+
def __exit__(self, ex_type, ex_value, ex_tb):
148+
"""
149+
__exit__ gets called at the end of a "with" block.
150+
"""
151+
self.__del__()
152+
153+
def __del__(self):
154+
if self._ns:
155+
self._ns.remove()
156+
self._ns = None
157+
if self._ns_peer:
158+
self._ns_peer.remove()
159+
self._ns_peer = None
160+
if self._netns:
161+
del self._netns
162+
self._netns = None
163+
if self.remote:
164+
del self.remote
165+
self.remote = None
166+
167+
def require_v4(self):
168+
if not self.v4 or not self.remote_v4:
169+
raise KsftSkipEx("Test requires IPv4 connectivity")
170+
171+
def require_v6(self):
172+
if not self.v6 or not self.remote_v6:
173+
raise KsftSkipEx("Test requires IPv6 connectivity")
174+
175+
def _require_cmd(self, comm, key, host=None):
176+
cached = self._required_cmd.get(comm, {})
177+
if cached.get(key) is None:
178+
cached[key] = cmd("command -v -- " + comm, fail=False,
179+
shell=True, host=host).ret == 0
180+
self._required_cmd[comm] = cached
181+
return cached[key]
182+
183+
def require_cmd(self, comm, local=True, remote=False):
184+
if local:
185+
if not self._require_cmd(comm, "local"):
186+
raise KsftSkipEx("Test requires command: " + comm)
187+
if remote:
188+
if not self._require_cmd(comm, "remote"):
189+
raise KsftSkipEx("Test requires (remote) command: " + comm)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
3+
import os
4+
import importlib
5+
6+
_modules = {}
7+
8+
def Remote(kind, args, src_path):
9+
global _modules
10+
11+
if kind not in _modules:
12+
_modules[kind] = importlib.import_module("..remote_" + kind, __name__)
13+
14+
dir_path = os.path.abspath(src_path + "/../")
15+
return getattr(_modules[kind], "Remote")(args, dir_path)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
3+
import os
4+
import subprocess
5+
6+
from lib.py import cmd
7+
8+
9+
class Remote:
10+
def __init__(self, name, dir_path):
11+
self.name = name
12+
self.dir_path = dir_path
13+
14+
def cmd(self, comm):
15+
return subprocess.Popen(["ip", "netns", "exec", self.name, "bash", "-c", comm],
16+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
17+
18+
def deploy(self, what):
19+
if os.path.isabs(what):
20+
return what
21+
return os.path.abspath(self.dir_path + "/" + what)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
3+
import os
4+
import string
5+
import subprocess
6+
import random
7+
8+
from lib.py import cmd
9+
10+
11+
class Remote:
12+
def __init__(self, name, dir_path):
13+
self.name = name
14+
self.dir_path = dir_path
15+
self._tmpdir = None
16+
17+
def __del__(self):
18+
if self._tmpdir:
19+
cmd("rm -rf " + self._tmpdir, host=self)
20+
self._tmpdir = None
21+
22+
def cmd(self, comm):
23+
return subprocess.Popen(["ssh", "-q", self.name, comm],
24+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
25+
26+
def _mktmp(self):
27+
return ''.join(random.choice(string.ascii_lowercase) for _ in range(8))
28+
29+
def deploy(self, what):
30+
if not self._tmpdir:
31+
self._tmpdir = "/tmp/" + self._mktmp()
32+
cmd("mkdir " + self._tmpdir, host=self)
33+
file_name = self._tmpdir + "/" + self._mktmp() + os.path.basename(what)
34+
35+
if not os.path.isabs(what):
36+
what = os.path.abspath(self.dir_path + "/" + what)
37+
38+
cmd(f"scp {what} {self.name}:{file_name}")
39+
return file_name
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: GPL-2.0
3+
4+
from lib.py import ksft_run, ksft_exit
5+
from lib.py import ksft_eq
6+
from lib.py import NetDrvEpEnv
7+
from lib.py import bkg, cmd, wait_port_listen, rand_port
8+
9+
10+
def test_v4(cfg) -> None:
11+
cfg.require_v4()
12+
13+
cmd(f"ping -c 1 -W0.5 {cfg.remote_v4}")
14+
cmd(f"ping -c 1 -W0.5 {cfg.v4}", host=cfg.remote)
15+
16+
17+
def test_v6(cfg) -> None:
18+
cfg.require_v6()
19+
20+
cmd(f"ping -c 1 -W0.5 {cfg.remote_v6}")
21+
cmd(f"ping -c 1 -W0.5 {cfg.v6}", host=cfg.remote)
22+
23+
24+
def test_tcp(cfg) -> None:
25+
cfg.require_cmd("socat", remote=True)
26+
27+
port = rand_port()
28+
listen_cmd = f"socat -{cfg.addr_ipver} -t 2 -u TCP-LISTEN:{port},reuseport STDOUT"
29+
30+
with bkg(listen_cmd, exit_wait=True) as nc:
31+
wait_port_listen(port)
32+
33+
cmd(f"echo ping | socat -t 2 -u STDIN TCP:{cfg.baddr}:{port}",
34+
shell=True, host=cfg.remote)
35+
ksft_eq(nc.stdout.strip(), "ping")
36+
37+
with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as nc:
38+
wait_port_listen(port, host=cfg.remote)
39+
40+
cmd(f"echo ping | socat -t 2 -u STDIN TCP:{cfg.remote_baddr}:{port}", shell=True)
41+
ksft_eq(nc.stdout.strip(), "ping")
42+
43+
44+
def main() -> None:
45+
with NetDrvEpEnv(__file__) as cfg:
46+
ksft_run(globs=globals(), case_pfx={"test_"}, args=(cfg, ))
47+
ksft_exit()
48+
49+
50+
if __name__ == "__main__":
51+
main()

tools/testing/selftests/net/lib/py/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from .consts import KSRC
44
from .ksft import *
5+
from .netns import NetNS
56
from .nsim import *
67
from .utils import *
78
from .ynl import NlError, YnlFamily, EthtoolFamily, NetdevFamily, RtnlFamily

0 commit comments

Comments
 (0)