Re: [PATCH net-next 6/7] selftests: drivers: add scaffolding for Netlink tests in Python

David Wei
2024-04-03T03:06:36Z

On 2024-04-01 18:05, Jakub Kicinski wrote:
> Add drivers/net as a target for mixed-use tests.
> The setup is expected to work similarly to the forwarding tests.
> Since we only need one interface (unlike forwarding tests)
> read the target device name from NETIF. If not present we'll
> try to run the test against netdevsim.
>
> Signed-off-by: Jakub Kicinski <kuba@kernel.org>
> ---
> tools/testing/selftests/Makefile | 3 +-
> tools/testing/selftests/drivers/net/Makefile | 7 ++
> .../testing/selftests/drivers/net/README.rst | 30 +++++
> .../selftests/drivers/net/lib/py/__init__.py | 17 +++
> .../selftests/drivers/net/lib/py/env.py | 41 ++++++
> .../testing/selftests/net/lib/py/__init__.py | 1 +
> tools/testing/selftests/net/lib/py/nsim.py | 118 ++++++++++++++++++
> 7 files changed, 216 insertions(+), 1 deletion(-)
> create mode 100644 tools/testing/selftests/drivers/net/Makefile
> create mode 100644 tools/testing/selftests/drivers/net/README.rst
> create mode 100644 tools/testing/selftests/drivers/net/lib/py/__init__.py
> create mode 100644 tools/testing/selftests/drivers/net/lib/py/env.py
> create mode 100644 tools/testing/selftests/net/lib/py/nsim.py
>
> diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
> index 0cffdfb4b116..d015ec14a85e 100644
> --- a/tools/testing/selftests/Makefile
> +++ b/tools/testing/selftests/Makefile
> @@ -17,6 +17,7 @@ TARGETS += devices
> TARGETS += dmabuf-heaps
> TARGETS += drivers/dma-buf
> TARGETS += drivers/s390x/uvdevice
> +TARGETS += drivers/net
> TARGETS += drivers/net/bonding
> TARGETS += drivers/net/team
> TARGETS += dt
> @@ -117,7 +118,7 @@ TARGETS_HOTPLUG = cpu-hotplug
> TARGETS_HOTPLUG += memory-hotplug
>
> # Networking tests want the net/lib target, include it automatically
> -ifneq ($(filter net ,$(TARGETS)),)
> +ifneq ($(filter net drivers/net,$(TARGETS)),)
> ifeq ($(filter net/lib,$(TARGETS)),)
> override TARGETS := $(TARGETS) net/lib
> endif
> diff --git a/tools/testing/selftests/drivers/net/Makefile b/tools/testing/selftests/drivers/net/Makefile
> new file mode 100644
> index 000000000000..379cdb1960a7
> --- /dev/null
> +++ b/tools/testing/selftests/drivers/net/Makefile
> @@ -0,0 +1,7 @@
> +# SPDX-License-Identifier: GPL-2.0
> +
> +TEST_INCLUDES := $(wildcard lib/py/*.py)
> +
> +TEST_PROGS := stats.py
> +
> +include ../../lib.mk
> diff --git a/tools/testing/selftests/drivers/net/README.rst b/tools/testing/selftests/drivers/net/README.rst
> new file mode 100644
> index 000000000000..5ef7c417d431
> --- /dev/null
> +++ b/tools/testing/selftests/drivers/net/README.rst
> @@ -0,0 +1,30 @@
> +Running tests
> +=============
> +
> +Tests are executed within kselftest framework like any other tests.
> +By default tests execute against software drivers such as netdevsim.
> +All tests must support running against a real device (SW-only tests
> +should instead be placed in net/ or drivers/net/netdevsim, HW-only
> +tests in drivers/net/hw).
> +
> +Set appropriate variables to point the tests at a real device.
> +
> +Variables
> +=========
> +
> +Variables can be set in the environment or by creating a net.config
> +file in the same directory as this README file. Example::
> +
> + $ NETIF=eth0 ./some_test.sh
> +
> +or::
> +
> + $ cat tools/testing/selftests/drivers/net/net.config
> + # Variable set in a file
> + NETIF=eth0
> +
> +NETIF
> +~~~~~
> +
> +Name of the netdevice against which the test should be executed.
> +When empty or not set software devices will be used.
> diff --git a/tools/testing/selftests/drivers/net/lib/py/__init__.py b/tools/testing/selftests/drivers/net/lib/py/__init__.py
> new file mode 100644
> index 000000000000..4653dffcd962
> --- /dev/null
> +++ b/tools/testing/selftests/drivers/net/lib/py/__init__.py
> @@ -0,0 +1,17 @@
> +# SPDX-License-Identifier: GPL-2.0
> +
> +import sys
> +from pathlib import Path
> +
> +KSFT_DIR = (Path(__file__).parent / "../../../..").resolve()
> +
> +try:
> + sys.path.append(KSFT_DIR.as_posix())
> + from net.lib.py import *
> +except ModuleNotFoundError as e:
> + ksft_pr("Failed importing `net` library from kernel sources")
> + ksft_pr(str(e))
> + ktap_result(True, comment="SKIP")
> + sys.exit(4)
> +
> +from .env import *
> diff --git a/tools/testing/selftests/drivers/net/lib/py/env.py b/tools/testing/selftests/drivers/net/lib/py/env.py
> new file mode 100644
> index 000000000000..ee4a44555d83
> --- /dev/null
> +++ b/tools/testing/selftests/drivers/net/lib/py/env.py
> @@ -0,0 +1,41 @@
> +# SPDX-License-Identifier: GPL-2.0
> +
> +import os
> +import shlex
> +from pathlib import Path
> +from lib.py import ip
> +from lib.py import NetdevSimDev

nit: these could be on the same line.

> +
> +class NetDrvEnv:
> + def __init__(self, src_path):
> + self.env = os.environ.copy()
> + self._load_env_file(src_path)
> +
> + if 'NETIF' in self.env:
> + self._ns = None

My brain interprets 'ns' as 'namespace'. How about something like
nsimdev/nsdev/nsim?

> + self.dev = ip("link show dev " + self.env['NETIF'], json=True)[0]
> + else:
> + self._ns = NetdevSimDev()
> + self.dev = self._ns.nsims[0].dev
> + self.ifindex = self.dev['ifindex']
> +
> + def __del__(self):
> + if self._ns:
> + self._ns.remove()
> +
> + 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
> diff --git a/tools/testing/selftests/net/lib/py/__init__.py b/tools/testing/selftests/net/lib/py/__init__.py
> index 81a8d14b68f0..99cfc8dc4dca 100644
> --- a/tools/testing/selftests/net/lib/py/__init__.py
> +++ b/tools/testing/selftests/net/lib/py/__init__.py
> @@ -3,4 +3,5 @@
> from .ksft import *
> from .ynl import NlError, YnlFamily, EthtoolFamily, NetdevFamily, RtnlFamily
> from .consts import KSRC
> +from .nsim import *
> from .utils import *
> diff --git a/tools/testing/selftests/net/lib/py/nsim.py b/tools/testing/selftests/net/lib/py/nsim.py
> new file mode 100644
> index 000000000000..13eb42c82829
> --- /dev/null
> +++ b/tools/testing/selftests/net/lib/py/nsim.py
> @@ -0,0 +1,118 @@
> +# SPDX-License-Identifier: GPL-2.0
> +
> +import json
> +import os
> +import random
> +import re
> +import time
> +from .utils import cmd, ip
> +
> +
> +class NetdevSim:
> + """
> + Class for netdevsim netdevice and its attributes.
> + """
> +
> + def __init__(self, nsimdev, port_index, ifname, ns=None):
> + # In case udev renamed the netdev to according to new schema,
> + # check if the name matches the port_index.
> + nsimnamere = re.compile("eni\d+np(\d+)")
> + match = nsimnamere.match(ifname)
> + if match and int(match.groups()[0]) != port_index + 1:
> + raise Exception("netdevice name mismatches the expected one")
> +
> + self.ifname = ifname
> + self.nsimdev = nsimdev
> + self.port_index = port_index
> + self.ns = ns
> + self.dfs_dir = "%s/ports/%u/" % (nsimdev.dfs_dir, port_index)
> + ret = ip("-j link show dev %s" % ifname, ns=ns)
> + self.dev = json.loads(ret.stdout)[0]
> +
> + def dfs_write(self, path, val):
> + self.nsimdev.dfs_write(f'ports/{self.port_index}/' + path, val)
> +
> +
> +class NetdevSimDev:
> + """
> + Class for netdevsim bus device and its attributes.
> + """
> + @staticmethod
> + def ctrl_write(path, val):
> + fullpath = os.path.join("/sys/bus/netdevsim/", path)
> + with open(fullpath, "w") as f:
> + f.write(val)
> +
> + def dfs_write(self, path, val):
> + fullpath = os.path.join(f"/sys/kernel/debug/netdevsim/netdevsim{self.addr}/", path)
> + with open(fullpath, "w") as f:
> + f.write(val)
> +
> + def __init__(self, port_count=1, ns=None):
> + # nsim will spawn in init_net, we'll set to actual ns once we switch it the.sre
> + self.ns = None
> +
> + if not os.path.exists("/sys/bus/netdevsim"):
> + cmd("modprobe netdevsim")
> +
> + addr = random.randrange(1 << 15)
> + while True:
> + try:
> + self.ctrl_write("new_device", "%u %u" % (addr, port_count))
> + except OSError as e:
> + if e.errno == errno.ENOSPC:
> + addr = random.randrange(1 << 15)
> + continue
> + raise e
> + break
> + self.addr = addr
> +
> + # As probe of netdevsim device might happen from a workqueue,
> + # so wait here until all netdevs appear.
> + self.wait_for_netdevs(port_count)
> +
> + if ns:
> + cmd(f"devlink dev reload netdevsim/netdevsim{addr} netns {ns.name}")
> + self.ns = ns
> +
> + cmd("udevadm settle", ns=self.ns)
> + ifnames = self.get_ifnames()
> +
> + self.dfs_dir = "/sys/kernel/debug/netdevsim/netdevsim%u/" % addr
> +
> + self.nsims = []
> + for port_index in range(port_count):
> + self.nsims.append(NetdevSim(self, port_index, ifnames[port_index],
> + ns=ns))
> +
> + def get_ifnames(self):
> + ifnames = []
> + listdir = cmd(f"ls /sys/bus/netdevsim/devices/netdevsim{self.addr}/net/",
> + ns=self.ns).stdout.split()
> + for ifname in listdir:
> + ifnames.append(ifname)
> + ifnames.sort()
> + return ifnames
> +
> + def wait_for_netdevs(self, port_count):
> + timeout = 5
> + timeout_start = time.time()
> +
> + while True:
> + try:
> + ifnames = self.get_ifnames()
> + except FileNotFoundError as e:
> + ifnames = []
> + if len(ifnames) == port_count:
> + break
> + if time.time() < timeout_start + timeout:
> + continue
> + raise Exception("netdevices did not appear within timeout")
> +
> + def remove(self):
> + self.ctrl_write("del_device", "%u" % (self.addr, ))

I really want this to be in the dtor, but couldn't get it to work
because Python doesn't let you open files in __del__(). :(

> +
> + def remove_nsim(self, nsim):
> + self.nsims.remove(nsim)
> + self.ctrl_write("devices/netdevsim%u/del_port" % (self.addr, ),
> + "%u" % (nsim.port_index, ))