Skip to content

Commit

Permalink
Merge branch 'selftests-drv-net-support-testing-with-a-remote-system'
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
Jakub Kicinski committed Apr 23, 2024
2 parents b2c8599 + f1e68a1 commit 8d03c15
Show file tree
Hide file tree
Showing 12 changed files with 417 additions and 30 deletions.
5 changes: 4 additions & 1 deletion tools/testing/selftests/drivers/net/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

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

TEST_PROGS := stats.py
TEST_PROGS := \
ping.py \
stats.py \
# end of TEST_PROGS

include ../../lib.mk
33 changes: 33 additions & 0 deletions tools/testing/selftests/drivers/net/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,41 @@ or::
# Variable set in a file
NETIF=eth0

Please note that the config parser is very simple, if there are
any non-alphanumeric characters in the value it needs to be in
double quotes.

NETIF
~~~~~

Name of the netdevice against which the test should be executed.
When empty or not set software devices will be used.

LOCAL_V4, LOCAL_V6, REMOTE_V4, REMOTE_V6
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Local and remote endpoint IP addresses.

REMOTE_TYPE
~~~~~~~~~~~

Communication method used to run commands on the remote endpoint.
Test framework has built-in support for ``netns`` and ``ssh`` channels.
``netns`` assumes the "remote" interface is part of the same
host, just moved to the specified netns.
``ssh`` communicates with remote endpoint over ``ssh`` and ``scp``.
Using persistent SSH connections is strongly encouraged to avoid
the latency of SSH connection setup on every command.

Communication methods are defined by classes in ``lib/py/remote_{name}.py``.
It should be possible to add a new method without modifying any of
the framework, by simply adding an appropriately named file to ``lib/py``.

REMOTE_ARGS
~~~~~~~~~~~

Arguments used to construct the communication channel.
Communication channel dependent::

for netns - name of the "remote" namespace
for ssh - name/address of the remote host
1 change: 1 addition & 0 deletions tools/testing/selftests/drivers/net/lib/py/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
sys.exit(4)

from .env import *
from .remote import Remote
177 changes: 157 additions & 20 deletions tools/testing/selftests/drivers/net/lib/py/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,41 @@
import os
import shlex
from pathlib import Path
from lib.py import ip
from lib.py import NetdevSimDev
from lib.py import KsftSkipEx
from lib.py import cmd, ip
from lib.py import NetNS, NetdevSimDev
from .remote import Remote


def _load_env_file(src_path):
env = os.environ.copy()

src_dir = Path(src_path).parent.resolve()
if not (src_dir / "net.config").exists():
return env

lexer = shlex.shlex(open((src_dir / "net.config").as_posix(), 'r').read())
k = None
for token in lexer:
if k is None:
k = token
env[k] = ""
elif token == "=":
pass
else:
env[k] = token
k = None
return env


class NetDrvEnv:
"""
Class for a single NIC / host env, with no remote end
"""
def __init__(self, src_path):
self._ns = None

self.env = os.environ.copy()
self._load_env_file(src_path)
self.env = _load_env_file(src_path)

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

def _load_env_file(self, src_path):
src_dir = Path(src_path).parent.resolve()
if not (src_dir / "net.config").exists():
return

lexer = shlex.shlex(open((src_dir / "net.config").as_posix(), 'r').read())
k = None
for token in lexer:
if k is None:
k = token
self.env[k] = ""
elif token == "=":
pass
else:
self.env[k] = token
k = None

class NetDrvEpEnv:
"""
Class for an environment with a local device and "remote endpoint"
which can be used to send traffic in.
For local testing it creates two network namespaces and a pair
of netdevsim devices.
"""

# Network prefixes used for local tests
nsim_v4_pfx = "192.0.2."
nsim_v6_pfx = "2001:db8::"

def __init__(self, src_path):

self.env = _load_env_file(src_path)

# Things we try to destroy
self.remote = None
# These are for local testing state
self._netns = None
self._ns = None
self._ns_peer = None

if "NETIF" in self.env:
self.dev = ip("link show dev " + self.env['NETIF'], json=True)[0]

self.v4 = self.env.get("LOCAL_V4")
self.v6 = self.env.get("LOCAL_V6")
self.remote_v4 = self.env.get("REMOTE_V4")
self.remote_v6 = self.env.get("REMOTE_V6")
kind = self.env["REMOTE_TYPE"]
args = self.env["REMOTE_ARGS"]
else:
self.create_local()

self.dev = self._ns.nsims[0].dev

self.v4 = self.nsim_v4_pfx + "1"
self.v6 = self.nsim_v6_pfx + "1"
self.remote_v4 = self.nsim_v4_pfx + "2"
self.remote_v6 = self.nsim_v6_pfx + "2"
kind = "netns"
args = self._netns.name

self.remote = Remote(kind, args, src_path)

self.addr = self.v6 if self.v6 else self.v4
self.remote_addr = self.remote_v6 if self.remote_v6 else self.remote_v4

self.addr_ipver = "6" if self.v6 else "4"
# Bracketed addresses, some commands need IPv6 to be inside []
self.baddr = f"[{self.v6}]" if self.v6 else self.v4
self.remote_baddr = f"[{self.remote_v6}]" if self.remote_v6 else self.remote_v4

self.ifname = self.dev['ifname']
self.ifindex = self.dev['ifindex']

self._required_cmd = {}

def create_local(self):
self._netns = NetNS()
self._ns = NetdevSimDev()
self._ns_peer = NetdevSimDev(ns=self._netns)

with open("/proc/self/ns/net") as nsfd0, \
open("/var/run/netns/" + self._netns.name) as nsfd1:
ifi0 = self._ns.nsims[0].ifindex
ifi1 = self._ns_peer.nsims[0].ifindex
NetdevSimDev.ctrl_write('link_device',
f'{nsfd0.fileno()}:{ifi0} {nsfd1.fileno()}:{ifi1}')

ip(f" addr add dev {self._ns.nsims[0].ifname} {self.nsim_v4_pfx}1/24")
ip(f"-6 addr add dev {self._ns.nsims[0].ifname} {self.nsim_v6_pfx}1/64 nodad")
ip(f" link set dev {self._ns.nsims[0].ifname} up")

ip(f" addr add dev {self._ns_peer.nsims[0].ifname} {self.nsim_v4_pfx}2/24", ns=self._netns)
ip(f"-6 addr add dev {self._ns_peer.nsims[0].ifname} {self.nsim_v6_pfx}2/64 nodad", ns=self._netns)
ip(f" link set dev {self._ns_peer.nsims[0].ifname} up", ns=self._netns)

def __enter__(self):
return self

def __exit__(self, ex_type, ex_value, ex_tb):
"""
__exit__ gets called at the end of a "with" block.
"""
self.__del__()

def __del__(self):
if self._ns:
self._ns.remove()
self._ns = None
if self._ns_peer:
self._ns_peer.remove()
self._ns_peer = None
if self._netns:
del self._netns
self._netns = None
if self.remote:
del self.remote
self.remote = None

def require_v4(self):
if not self.v4 or not self.remote_v4:
raise KsftSkipEx("Test requires IPv4 connectivity")

def require_v6(self):
if not self.v6 or not self.remote_v6:
raise KsftSkipEx("Test requires IPv6 connectivity")

def _require_cmd(self, comm, key, host=None):
cached = self._required_cmd.get(comm, {})
if cached.get(key) is None:
cached[key] = cmd("command -v -- " + comm, fail=False,
shell=True, host=host).ret == 0
self._required_cmd[comm] = cached
return cached[key]

def require_cmd(self, comm, local=True, remote=False):
if local:
if not self._require_cmd(comm, "local"):
raise KsftSkipEx("Test requires command: " + comm)
if remote:
if not self._require_cmd(comm, "remote"):
raise KsftSkipEx("Test requires (remote) command: " + comm)
15 changes: 15 additions & 0 deletions tools/testing/selftests/drivers/net/lib/py/remote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: GPL-2.0

import os
import importlib

_modules = {}

def Remote(kind, args, src_path):
global _modules

if kind not in _modules:
_modules[kind] = importlib.import_module("..remote_" + kind, __name__)

dir_path = os.path.abspath(src_path + "/../")
return getattr(_modules[kind], "Remote")(args, dir_path)
21 changes: 21 additions & 0 deletions tools/testing/selftests/drivers/net/lib/py/remote_netns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SPDX-License-Identifier: GPL-2.0

import os
import subprocess

from lib.py import cmd


class Remote:
def __init__(self, name, dir_path):
self.name = name
self.dir_path = dir_path

def cmd(self, comm):
return subprocess.Popen(["ip", "netns", "exec", self.name, "bash", "-c", comm],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)

def deploy(self, what):
if os.path.isabs(what):
return what
return os.path.abspath(self.dir_path + "/" + what)
39 changes: 39 additions & 0 deletions tools/testing/selftests/drivers/net/lib/py/remote_ssh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# SPDX-License-Identifier: GPL-2.0

import os
import string
import subprocess
import random

from lib.py import cmd


class Remote:
def __init__(self, name, dir_path):
self.name = name
self.dir_path = dir_path
self._tmpdir = None

def __del__(self):
if self._tmpdir:
cmd("rm -rf " + self._tmpdir, host=self)
self._tmpdir = None

def cmd(self, comm):
return subprocess.Popen(["ssh", "-q", self.name, comm],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)

def _mktmp(self):
return ''.join(random.choice(string.ascii_lowercase) for _ in range(8))

def deploy(self, what):
if not self._tmpdir:
self._tmpdir = "/tmp/" + self._mktmp()
cmd("mkdir " + self._tmpdir, host=self)
file_name = self._tmpdir + "/" + self._mktmp() + os.path.basename(what)

if not os.path.isabs(what):
what = os.path.abspath(self.dir_path + "/" + what)

cmd(f"scp {what} {self.name}:{file_name}")
return file_name
51 changes: 51 additions & 0 deletions tools/testing/selftests/drivers/net/ping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0

from lib.py import ksft_run, ksft_exit
from lib.py import ksft_eq
from lib.py import NetDrvEpEnv
from lib.py import bkg, cmd, wait_port_listen, rand_port


def test_v4(cfg) -> None:
cfg.require_v4()

cmd(f"ping -c 1 -W0.5 {cfg.remote_v4}")
cmd(f"ping -c 1 -W0.5 {cfg.v4}", host=cfg.remote)


def test_v6(cfg) -> None:
cfg.require_v6()

cmd(f"ping -c 1 -W0.5 {cfg.remote_v6}")
cmd(f"ping -c 1 -W0.5 {cfg.v6}", host=cfg.remote)


def test_tcp(cfg) -> None:
cfg.require_cmd("socat", remote=True)

port = rand_port()
listen_cmd = f"socat -{cfg.addr_ipver} -t 2 -u TCP-LISTEN:{port},reuseport STDOUT"

with bkg(listen_cmd, exit_wait=True) as nc:
wait_port_listen(port)

cmd(f"echo ping | socat -t 2 -u STDIN TCP:{cfg.baddr}:{port}",
shell=True, host=cfg.remote)
ksft_eq(nc.stdout.strip(), "ping")

with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as nc:
wait_port_listen(port, host=cfg.remote)

cmd(f"echo ping | socat -t 2 -u STDIN TCP:{cfg.remote_baddr}:{port}", shell=True)
ksft_eq(nc.stdout.strip(), "ping")


def main() -> None:
with NetDrvEpEnv(__file__) as cfg:
ksft_run(globs=globals(), case_pfx={"test_"}, args=(cfg, ))
ksft_exit()


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions tools/testing/selftests/net/lib/py/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from .consts import KSRC
from .ksft import *
from .netns import NetNS
from .nsim import *
from .utils import *
from .ynl import NlError, YnlFamily, EthtoolFamily, NetdevFamily, RtnlFamily
Loading

0 comments on commit 8d03c15

Please sign in to comment.