Merge branch 'ynl-tests'

Jakub Kicinski says:

====================
selftests: net: groundwork for YNL-based tests

Currently the options for writing networking tests are C, bash or
some mix of the two. YAML/Netlink gives us the ability to easily
interface with Netlink in higher level laguages. In particular,
there is a Python library already available in tree, under tools/net.
Add the scaffolding which allows writing tests using this library.

The "scaffolding" is needed because the library lives under
tools/net and uses YAML files from under Documentation/.
So we need a small amount of glue code to find those things
and add them to TEST_FILES.

This series adds both a basic SW sanity test and driver
test which can be run against netdevsim or a real device.
When I develop core code I usually test with netdevsim,
then a real device, and then a backport to Meta's kernel.
Because of the lack of integration, until now I had
to throw away the (YNL-based) test script and netdevsim code.

Running tests in tree directly:

 $ ./tools/testing/selftests/net/nl_netdev.py
 KTAP version 1
 1..2
 ok 1 nl_netdev.empty_check
 ok 2 nl_netdev.lo_check
 # Totals: pass:2 fail:0 xfail:0 xpass:0 skip:0 error:0

in tree via make:

 $ make -C tools/testing/selftests/ TARGETS=net \
	TEST_PROGS=nl_netdev.py TEST_GEN_PROGS="" run_tests
  [ ... ]

and installed externally, all seem to work:

 $ make -C tools/testing/selftests/ TARGETS=net \
	install INSTALL_PATH=/tmp/ksft-net
 $ /tmp/ksft-net/run_kselftest.sh -t net:nl_netdev.py
  [ ... ]

For driver tests I followed the lead of net/forwarding and
get the device name from env and/or a config file.

v3:
 - fix up netdevsim C
 - various small nits in other patches (see changelog in patches)
v2: https://lore.kernel.org/all/20240403023426.1762996-1-kuba@kernel.org/
 - don't add to TARGETS, create a deperate variable with deps
 - support and use with
 - support and use passing arguments to tests
v1: https://lore.kernel.org/all/20240402010520.1209517-1-kuba@kernel.org/
====================

Signed-off-by: David S. Miller <davem@davemloft.net>
This commit is contained in:
David S. Miller 2024-04-08 11:40:41 +01:00
commit a15d80a16d
17 changed files with 617 additions and 1 deletions

View file

@ -140,6 +140,13 @@ nsim_set_fecparam(struct net_device *dev, struct ethtool_fecparam *fecparam)
return 0;
}
static void
nsim_get_fec_stats(struct net_device *dev, struct ethtool_fec_stats *fec_stats)
{
fec_stats->corrected_blocks.total = 123;
fec_stats->uncorrectable_blocks.total = 4;
}
static int nsim_get_ts_info(struct net_device *dev,
struct ethtool_ts_info *info)
{
@ -163,6 +170,7 @@ static const struct ethtool_ops nsim_ethtool_ops = {
.set_channels = nsim_set_channels,
.get_fecparam = nsim_get_fecparam,
.set_fecparam = nsim_set_fecparam,
.get_fec_stats = nsim_get_fec_stats,
.get_ts_info = nsim_get_ts_info,
};
@ -182,6 +190,9 @@ void nsim_ethtool_init(struct netdevsim *ns)
nsim_ethtool_ring_init(ns);
ns->ethtool.pauseparam.report_stats_rx = true;
ns->ethtool.pauseparam.report_stats_tx = true;
ns->ethtool.fec.fec = ETHTOOL_FEC_NONE;
ns->ethtool.fec.active_fec = ETHTOOL_FEC_NONE;

View file

@ -19,6 +19,7 @@
#include <linux/module.h>
#include <linux/netdevice.h>
#include <linux/slab.h>
#include <net/netdev_queues.h>
#include <net/netlink.h>
#include <net/pkt_cls.h>
#include <net/rtnetlink.h>
@ -330,6 +331,53 @@ static const struct net_device_ops nsim_vf_netdev_ops = {
.ndo_set_features = nsim_set_features,
};
/* We don't have true per-queue stats, yet, so do some random fakery here.
* Only report stuff for queue 0.
*/
static void nsim_get_queue_stats_rx(struct net_device *dev, int idx,
struct netdev_queue_stats_rx *stats)
{
struct rtnl_link_stats64 rtstats = {};
if (!idx)
nsim_get_stats64(dev, &rtstats);
stats->packets = rtstats.rx_packets - !!rtstats.rx_packets;
stats->bytes = rtstats.rx_bytes;
}
static void nsim_get_queue_stats_tx(struct net_device *dev, int idx,
struct netdev_queue_stats_tx *stats)
{
struct rtnl_link_stats64 rtstats = {};
if (!idx)
nsim_get_stats64(dev, &rtstats);
stats->packets = rtstats.tx_packets - !!rtstats.tx_packets;
stats->bytes = rtstats.tx_bytes;
}
static void nsim_get_base_stats(struct net_device *dev,
struct netdev_queue_stats_rx *rx,
struct netdev_queue_stats_tx *tx)
{
struct rtnl_link_stats64 rtstats = {};
nsim_get_stats64(dev, &rtstats);
rx->packets = !!rtstats.rx_packets;
rx->bytes = 0;
tx->packets = !!rtstats.tx_packets;
tx->bytes = 0;
}
static const struct netdev_stat_ops nsim_stat_ops = {
.get_queue_stats_tx = nsim_get_queue_stats_tx,
.get_queue_stats_rx = nsim_get_queue_stats_rx,
.get_base_stats = nsim_get_base_stats,
};
static void nsim_setup(struct net_device *dev)
{
ether_setup(dev);
@ -360,6 +408,7 @@ static int nsim_init_netdevsim(struct netdevsim *ns)
ns->phc = phc;
ns->netdev->netdev_ops = &nsim_netdev_ops;
ns->netdev->stat_ops = &nsim_stat_ops;
err = nsim_udp_tunnels_info_create(ns->nsim_dev, ns->netdev);
if (err)

View file

@ -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
@ -116,6 +117,13 @@ TARGETS += zram
TARGETS_HOTPLUG = cpu-hotplug
TARGETS_HOTPLUG += memory-hotplug
# Networking tests want the net/lib target, include it automatically
ifneq ($(filter net drivers/net,$(TARGETS)),)
ifeq ($(filter net/lib,$(TARGETS)),)
INSTALL_DEP_TARGETS := net/lib
endif
endif
# User can optionally provide a TARGETS skiplist. By default we skip
# BPF since it has cutting edge build time dependencies which require
# more effort to install.
@ -245,7 +253,7 @@ ifdef INSTALL_PATH
install -m 744 run_kselftest.sh $(INSTALL_PATH)/
rm -f $(TEST_LIST)
@ret=1; \
for TARGET in $(TARGETS); do \
for TARGET in $(TARGETS) $(INSTALL_DEP_TARGETS); do \
BUILD_TARGET=$$BUILD/$$TARGET; \
$(MAKE) OUTPUT=$$BUILD_TARGET -C $$TARGET install \
INSTALL_PATH=$(INSTALL_PATH)/$$TARGET \

View file

@ -0,0 +1,7 @@
# SPDX-License-Identifier: GPL-2.0
TEST_INCLUDES := $(wildcard lib/py/*.py)
TEST_PROGS := stats.py
include ../../lib.mk

View file

@ -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.

View file

@ -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 *

View file

@ -0,0 +1,52 @@
# SPDX-License-Identifier: GPL-2.0
import os
import shlex
from pathlib import Path
from lib.py import ip
from lib.py import NetdevSimDev
class NetDrvEnv:
def __init__(self, src_path):
self._ns = None
self.env = os.environ.copy()
self._load_env_file(src_path)
if 'NETIF' in self.env:
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 __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
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

View file

@ -0,0 +1,86 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
from lib.py import ksft_run, ksft_in, ksft_true, KsftSkipEx, KsftXfailEx
from lib.py import EthtoolFamily, NetdevFamily, RtnlFamily, NlError
from lib.py import NetDrvEnv
ethnl = EthtoolFamily()
netfam = NetdevFamily()
rtnl = RtnlFamily()
def check_pause(cfg) -> None:
global ethnl
try:
ethnl.pause_get({"header": {"dev-index": cfg.ifindex}})
except NlError as e:
if e.error == 95:
raise KsftXfailEx("pause not supported by the device")
raise
data = ethnl.pause_get({"header": {"dev-index": cfg.ifindex,
"flags": {'stats'}}})
ksft_true(data['stats'], "driver does not report stats")
def check_fec(cfg) -> None:
global ethnl
try:
ethnl.fec_get({"header": {"dev-index": cfg.ifindex}})
except NlError as e:
if e.error == 95:
raise KsftXfailEx("FEC not supported by the device")
raise
data = ethnl.fec_get({"header": {"dev-index": cfg.ifindex,
"flags": {'stats'}}})
ksft_true(data['stats'], "driver does not report stats")
def pkt_byte_sum(cfg) -> None:
global netfam, rtnl
def get_qstat(test):
global netfam
stats = netfam.qstats_get({}, dump=True)
if stats:
for qs in stats:
if qs["ifindex"]== test.ifindex:
return qs
qstat = get_qstat(cfg)
if qstat is None:
raise KsftSkipEx("qstats not supported by the device")
for key in ['tx-packets', 'tx-bytes', 'rx-packets', 'rx-bytes']:
ksft_in(key, qstat, "Drivers should always report basic keys")
# Compare stats, rtnl stats and qstats must match,
# but the interface may be up, so do a series of dumps
# each time the more "recent" stats must be higher or same.
def stat_cmp(rstat, qstat):
for key in ['tx-packets', 'tx-bytes', 'rx-packets', 'rx-bytes']:
if rstat[key] != qstat[key]:
return rstat[key] - qstat[key]
return 0
for _ in range(10):
rtstat = rtnl.getlink({"ifi-index": cfg.ifindex})['stats']
if stat_cmp(rtstat, qstat) < 0:
raise Exception("RTNL stats are lower, fetched later")
qstat = get_qstat(cfg)
if stat_cmp(rtstat, qstat) > 0:
raise Exception("Qstats are lower, fetched later")
def main() -> None:
with NetDrvEnv(__file__) as cfg:
ksft_run([check_pause, check_fec, pkt_byte_sum],
args=(cfg, ))
if __name__ == "__main__":
main()

View file

@ -34,6 +34,7 @@ TEST_PROGS += gre_gso.sh
TEST_PROGS += cmsg_so_mark.sh
TEST_PROGS += cmsg_time.sh cmsg_ipv6.sh
TEST_PROGS += netns-name.sh
TEST_PROGS += nl_netdev.py
TEST_PROGS += srv6_end_dt46_l3vpn_test.sh
TEST_PROGS += srv6_end_dt4_l3vpn_test.sh
TEST_PROGS += srv6_end_dt6_l3vpn_test.sh

View file

@ -0,0 +1,8 @@
# SPDX-License-Identifier: GPL-2.0
TEST_FILES := ../../../../../Documentation/netlink/specs
TEST_FILES += ../../../../net/ynl
TEST_INCLUDES := $(wildcard py/*.py)
include ../../lib.mk

View file

@ -0,0 +1,7 @@
# SPDX-License-Identifier: GPL-2.0
from .consts import KSRC
from .ksft import *
from .nsim import *
from .utils import *
from .ynl import NlError, YnlFamily, EthtoolFamily, NetdevFamily, RtnlFamily

View file

@ -0,0 +1,9 @@
# SPDX-License-Identifier: GPL-2.0
import sys
from pathlib import Path
KSFT_DIR = (Path(__file__).parent / "../../..").resolve()
KSRC = (Path(__file__).parent / "../../../../../..").resolve()
KSFT_MAIN_NAME = Path(sys.argv[0]).with_suffix("").name

View file

@ -0,0 +1,96 @@
# SPDX-License-Identifier: GPL-2.0
import builtins
from .consts import KSFT_MAIN_NAME
KSFT_RESULT = None
class KsftSkipEx(Exception):
pass
class KsftXfailEx(Exception):
pass
def ksft_pr(*objs, **kwargs):
print("#", *objs, **kwargs)
def ksft_eq(a, b, comment=""):
global KSFT_RESULT
if a != b:
KSFT_RESULT = False
ksft_pr("Check failed", a, "!=", b, comment)
def ksft_true(a, comment=""):
global KSFT_RESULT
if not a:
KSFT_RESULT = False
ksft_pr("Check failed", a, "does not eval to True", comment)
def ksft_in(a, b, comment=""):
global KSFT_RESULT
if a not in b:
KSFT_RESULT = False
ksft_pr("Check failed", a, "not in", b, comment)
def ksft_ge(a, b, comment=""):
global KSFT_RESULT
if a < b:
KSFT_RESULT = False
ksft_pr("Check failed", a, "<", b, comment)
def ktap_result(ok, cnt=1, case="", comment=""):
res = ""
if not ok:
res += "not "
res += "ok "
res += str(cnt) + " "
res += KSFT_MAIN_NAME
if case:
res += "." + str(case.__name__)
if comment:
res += " # " + comment
print(res)
def ksft_run(cases, args=()):
totals = {"pass": 0, "fail": 0, "skip": 0, "xfail": 0}
print("KTAP version 1")
print("1.." + str(len(cases)))
global KSFT_RESULT
cnt = 0
for case in cases:
KSFT_RESULT = True
cnt += 1
try:
case(*args)
except KsftSkipEx as e:
ktap_result(True, cnt, case, comment="SKIP " + str(e))
totals['skip'] += 1
continue
except KsftXfailEx as e:
ktap_result(True, cnt, case, comment="XFAIL " + str(e))
totals['xfail'] += 1
continue
except Exception as e:
for line in str(e).split('\n'):
ksft_pr("Exception|", line)
ktap_result(False, cnt, case)
totals['fail'] += 1
continue
ktap_result(KSFT_RESULT, cnt, case)
totals['pass'] += 1
print(
f"# Totals: pass:{totals['pass']} fail:{totals['fail']} xfail:{totals['xfail']} xpass:0 skip:{totals['skip']} error:0"
)

View file

@ -0,0 +1,115 @@
# 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(r"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.nsimdev = nsimdev
self.port_index = 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 there
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, ))
def remove_nsim(self, nsim):
self.nsims.remove(nsim)
self.ctrl_write("devices/netdevsim%u/del_port" % (self.addr, ),
"%u" % (nsim.port_index, ))

View file

@ -0,0 +1,47 @@
# SPDX-License-Identifier: GPL-2.0
import json as _json
import subprocess
class cmd:
def __init__(self, comm, shell=True, fail=True, ns=None, background=False):
if ns:
if isinstance(ns, NetNS):
ns = ns.name
comm = f'ip netns exec {ns} ' + comm
self.stdout = None
self.stderr = None
self.ret = None
self.comm = comm
self.proc = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
if not background:
self.process(terminate=False, fail=fail)
def process(self, terminate=True, fail=None):
if terminate:
self.proc.terminate()
stdout, stderr = self.proc.communicate()
self.stdout = stdout.decode("utf-8")
self.stderr = stderr.decode("utf-8")
self.proc.stdout.close()
self.proc.stderr.close()
self.ret = self.proc.returncode
if self.proc.returncode != 0 and fail:
if len(stderr) > 0 and stderr[-1] == "\n":
stderr = stderr[:-1]
raise Exception("Command failed: %s\n%s" % (self.proc.args, stderr))
def ip(args, json=None, ns=None):
cmd_str = "ip "
if json:
cmd_str += '-j '
cmd_str += args
cmd_obj = cmd(cmd_str, ns=ns)
if json:
return _json.loads(cmd_obj.stdout)
return cmd_obj

View file

@ -0,0 +1,49 @@
# SPDX-License-Identifier: GPL-2.0
import sys
from pathlib import Path
from .consts import KSRC, KSFT_DIR
from .ksft import ksft_pr, ktap_result
# Resolve paths
try:
if (KSFT_DIR / "kselftest-list.txt").exists():
# Running in "installed" selftests
tools_full_path = KSFT_DIR
SPEC_PATH = KSFT_DIR / "net/lib/specs"
sys.path.append(tools_full_path.as_posix())
from net.lib.ynl.lib import YnlFamily, NlError
else:
# Running in tree
tools_full_path = KSRC / "tools"
SPEC_PATH = KSRC / "Documentation/netlink/specs"
sys.path.append(tools_full_path.as_posix())
from net.ynl.lib import YnlFamily, NlError
except ModuleNotFoundError as e:
ksft_pr("Failed importing `ynl` library from kernel sources")
ksft_pr(str(e))
ktap_result(True, comment="SKIP")
sys.exit(4)
#
# Wrapper classes, loading the right specs
# Set schema='' to avoid jsonschema validation, it's slow
#
class EthtoolFamily(YnlFamily):
def __init__(self):
super().__init__((SPEC_PATH / Path('ethtool.yaml')).as_posix(),
schema='')
class RtnlFamily(YnlFamily):
def __init__(self):
super().__init__((SPEC_PATH / Path('rt_link.yaml')).as_posix(),
schema='')
class NetdevFamily(YnlFamily):
def __init__(self):
super().__init__((SPEC_PATH / Path('netdev.yaml')).as_posix(),
schema='')

View file

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
from lib.py import ksft_run, ksft_pr, ksft_eq, ksft_ge, NetdevFamily
def empty_check(nf) -> None:
devs = nf.dev_get({}, dump=True)
ksft_ge(len(devs), 1)
def lo_check(nf) -> None:
lo_info = nf.dev_get({"ifindex": 1})
ksft_eq(len(lo_info['xdp-features']), 0)
ksft_eq(len(lo_info['xdp-rx-metadata-features']), 0)
def main() -> None:
nf = NetdevFamily()
ksft_run([empty_check, lo_check], args=(nf, ))
if __name__ == "__main__":
main()