diff options
Diffstat (limited to 'tools/testing/selftests/drivers')
31 files changed, 2303 insertions, 164 deletions
diff --git a/tools/testing/selftests/drivers/net/bonding/bond_topo_2d1c.sh b/tools/testing/selftests/drivers/net/bonding/bond_topo_2d1c.sh index 167aa4a4a12a..903c7a6c7287 100644 --- a/tools/testing/selftests/drivers/net/bonding/bond_topo_2d1c.sh +++ b/tools/testing/selftests/drivers/net/bonding/bond_topo_2d1c.sh @@ -48,7 +48,7 @@ gateway_create() ip -n ${g_ns} link add br0 type bridge ip -n ${g_ns} link set br0 up ip -n ${g_ns} addr add ${g_ip4}/24 dev br0 - ip -n ${g_ns} addr add ${g_ip6}/24 dev br0 + ip -n ${g_ns} addr add ${g_ip6}/24 dev br0 nodad } gateway_destroy() @@ -75,7 +75,7 @@ server_create() ip -n ${s_ns} link set bond0 up ip -n ${s_ns} addr add ${s_ip4}/24 dev bond0 - ip -n ${s_ns} addr add ${s_ip6}/24 dev bond0 + ip -n ${s_ns} addr add ${s_ip6}/24 dev bond0 nodad } # Reset bond with new mode and options @@ -97,9 +97,7 @@ bond_reset() ip -n ${s_ns} link set bond0 up ip -n ${s_ns} addr add ${s_ip4}/24 dev bond0 - ip -n ${s_ns} addr add ${s_ip6}/24 dev bond0 - # Wait for IPv6 address ready as it needs DAD - slowwait 2 ip netns exec ${s_ns} ping6 ${c_ip6} -c 1 -W 0.1 &> /dev/null + ip -n ${s_ns} addr add ${s_ip6}/24 dev bond0 nodad } server_destroy() @@ -124,7 +122,7 @@ client_create() ip -n ${c_ns} link set eth0 up ip -n ${c_ns} addr add ${c_ip4}/24 dev eth0 - ip -n ${c_ns} addr add ${c_ip6}/24 dev eth0 + ip -n ${c_ns} addr add ${c_ip6}/24 dev eth0 nodad } client_destroy() diff --git a/tools/testing/selftests/drivers/net/config b/tools/testing/selftests/drivers/net/config index 91d4fd410914..4838adf27fa1 100644 --- a/tools/testing/selftests/drivers/net/config +++ b/tools/testing/selftests/drivers/net/config @@ -4,6 +4,12 @@ CONFIG_DEBUG_INFO_BTF_MODULES=n CONFIG_INET_PSP=y CONFIG_IPV6=y CONFIG_MACSEC=m +CONFIG_NET_ACT_SKBEDIT=m +CONFIG_NET_CLS_ACT=y +CONFIG_NET_CLS_BPF=y +CONFIG_NET_CLS_FLOWER=m +CONFIG_NET_CLS_FW=m +CONFIG_NET_CLS_MATCHALL=m CONFIG_NETCONSOLE=m CONFIG_NETCONSOLE_DYNAMIC=y CONFIG_NETCONSOLE_EXTENDED_LOG=y @@ -11,7 +17,10 @@ CONFIG_NETDEVSIM=m CONFIG_NETKIT=y CONFIG_NET_SCH_ETF=m CONFIG_NET_SCH_FQ=m +CONFIG_NET_SCH_INGRESS=y +CONFIG_NET_SCH_PRIO=m CONFIG_PPP=y CONFIG_PPPOE=y +CONFIG_TLS=y CONFIG_VLAN_8021Q=m CONFIG_XDP_SOCKETS=y diff --git a/tools/testing/selftests/drivers/net/hw/Makefile b/tools/testing/selftests/drivers/net/hw/Makefile index fd0535a96d84..78bb0169350b 100644 --- a/tools/testing/selftests/drivers/net/hw/Makefile +++ b/tools/testing/selftests/drivers/net/hw/Makefile @@ -19,7 +19,7 @@ TEST_GEN_FILES := \ TEST_PROGS = \ csum.py \ - devlink_port_split.py \ + devlink_rate_cross_esw.py \ devlink_rate_tc_bw.py \ devmem.py \ ethtool.sh \ @@ -53,6 +53,10 @@ TEST_PROGS = \ xsk_reconfig.py \ # +TEST_PROGS_EXTENDED := \ + devlink_port_split.py \ +# end of TEST_PROGS_EXTENDED + TEST_FILES := \ devmem_lib.py \ ethtool_lib.sh \ diff --git a/tools/testing/selftests/drivers/net/hw/config b/tools/testing/selftests/drivers/net/hw/config index ed8642b68094..d89a9ba17655 100644 --- a/tools/testing/selftests/drivers/net/hw/config +++ b/tools/testing/selftests/drivers/net/hw/config @@ -15,11 +15,13 @@ CONFIG_IPV6_SIT=y CONFIG_IPV6_TUNNEL=y CONFIG_NET_CLS_ACT=y CONFIG_NET_CLS_BPF=y +CONFIG_NET_DEVMEM=y CONFIG_NET_IPGRE=y CONFIG_NET_IPGRE_DEMUX=y CONFIG_NET_IPIP=y CONFIG_NETKIT=y CONFIG_NET_SCH_INGRESS=y +CONFIG_SYNC_FILE=y CONFIG_UDMABUF=y CONFIG_USER_NS=y CONFIG_VXLAN=y diff --git a/tools/testing/selftests/drivers/net/hw/csum.py b/tools/testing/selftests/drivers/net/hw/csum.py index 3e3a89a34afe..0e99198f8d39 100755 --- a/tools/testing/selftests/drivers/net/hw/csum.py +++ b/tools/testing/selftests/drivers/net/hw/csum.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0 -"""Run the tools/testing/selftests/net/csum testsuite.""" +"""Run the tools/testing/selftests/net/lib/csum testsuite.""" from os import path diff --git a/tools/testing/selftests/drivers/net/hw/devlink_rate_cross_esw.py b/tools/testing/selftests/drivers/net/hw/devlink_rate_cross_esw.py new file mode 100755 index 000000000000..4416f024cb76 --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/devlink_rate_cross_esw.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +""" +Devlink Rate Cross-eswitch Scheduling Test Suite +================================================== + +Control-plane tests for cross-eswitch TX scheduling via devlink-rate. +Validates that VFs from different PFs on the same chip can share +rate groups using the cross-device parent-dev attribute. + +Preconditions: +- NETIF points to a bond device with exactly two interfaces. +- the interfaces must be two PFs from different devices sharing the same chip. +- (for mlx5): the two interfaces are in switchdev mode and configured in a LAG: + - devlink dev eswitch set $DEV1 mode switchdev + - devlink dev eswitch set $DEV2 mode switchdev + - devlink dev param set $DEV1 name esw_multiport value 1 cmode runtime + - devlink dev param set $DEV2 name esw_multiport value 1 cmode runtime +- test cases will be skipped if: + - the number of interfaces in the bond device is != 2. + - the kernel doesn't support devlink rates. + - the devlink API doesn't support cross-device parents (ENODEV). + - cross-esw rate scheduling returns EOPNOTSUPP. +""" + +import errno +import glob +import os +import time + +from lib.py import ksft_pr, ksft_eq, ksft_run, ksft_exit +from lib.py import KsftSkipEx, KsftFailEx +from lib.py import NetDrvEnv, DevlinkFamily +from lib.py import NlError +from lib.py import cmd, defer, ip, tool + + +# --- Discovery and setup --- + + +def get_bond_slaves(bond_ifname): + """Returns sorted list of slave netdev names for a bond.""" + pattern = f"/sys/class/net/{bond_ifname}/lower_*" + lowers = glob.glob(pattern) + if not lowers: + raise KsftSkipEx(f"No bond slaves for {bond_ifname}") + slaves = [] + for path in sorted(lowers): + name = os.path.basename(path) + if name.startswith("lower_"): + name = name[len("lower_"):] + slaves.append(name) + return slaves + + +def discover_pfs(cfg): + """Discovers both PFs from bond slaves.""" + slaves = get_bond_slaves(cfg.ifname) + if len(slaves) != 2: + raise KsftSkipEx(f"Need 2 bond slaves, found {len(slaves)}") + + pf0, pf1 = slaves[0], slaves[1] + ksft_pr(f"PF0: {pf0} PF1: {pf1}") + return pf0, pf1 + + +def get_pci_addr(ifname): + """Resolves PCI address for a network interface.""" + return os.path.basename(os.path.realpath(f"/sys/class/net/{ifname}/device")) + + +def get_vf_port_index(pf_pci): + """Finds devlink port-index for vf0 under pf_pci.""" + ports = tool("devlink", "port show", json=True)["port"] + for port_name, props in ports.items(): + if port_name.startswith(f"pci/{pf_pci}/") and props.get("vfnum") == 0: + return int(port_name.split("/")[-1]) + raise KsftSkipEx(f"VF port not found for {pf_pci}") + + +def cleanup_esw(pf): + """Removes VFs if created by tests.""" + cmd(f"echo 0 > /sys/class/net/{pf}/device/sriov_numvfs", shell=True, fail=False) + + +def setup_esw(pf): + """Creates 1 VF on 'pf'.""" + path = f"/sys/class/net/{pf}/device/sriov_numvfs" + cmd(f"echo 0 > {path}", shell=True) + cmd(f"echo 1 > {path}", shell=True) + defer(cleanup_esw, pf) + time.sleep(2) + + vf_dir = f"/sys/class/net/{pf}/device/virtfn0/net" + entries = os.listdir(vf_dir) if os.path.isdir(vf_dir) else [] + if not entries: + raise KsftSkipEx(f"VF not found for {pf}") + ip(f"link set dev {entries[0]} up") + + pf_pci = get_pci_addr(pf) + vf_idx = get_vf_port_index(pf_pci) + ksft_pr(f"Created VF {vf_idx} on PF {pf} ({pf_pci})") + return pf_pci, vf_idx + + +# --- Rate operation helpers --- + + +def rate_new(devnl, dev_pci, node_name, **kwargs): + """Creates rate node.""" + params = { + "bus-name": "pci", + "dev-name": dev_pci, + "rate-node-name": node_name, + } + params.update(kwargs) + try: + devnl.rate_new(params) + except NlError as e: + if e.error == errno.EOPNOTSUPP: + raise KsftSkipEx("rate_new not supported") from e + raise KsftFailEx("rate_new failed") from e + + +def rate_get(devnl, dev_pci, node_name): + """Gets rate node.""" + params = { + "bus-name": "pci", + "dev-name": dev_pci, + "rate-node-name": node_name, + } + return devnl.rate_get(params) + + +def rate_get_leaf(devnl, dev_pci, port_index): + """Gets rate leaf (VF).""" + params = { + "bus-name": "pci", + "dev-name": dev_pci, + "port-index": port_index, + } + return devnl.rate_get(params) + + +def rate_del(devnl, dev_pci, node_name): + """Deletes rate node.""" + devnl.rate_del({ + "bus-name": "pci", + "dev-name": dev_pci, + "rate-node-name": node_name, + }) + + +def rate_set_leaf(devnl, dev_pci, port_index, **kwargs): + """Sets rate attributes on a leaf (VF).""" + params = { + "bus-name": "pci", + "dev-name": dev_pci, + "port-index": port_index, + } + params.update(kwargs) + try: + devnl.rate_set(params) + except NlError as e: + if e.error == errno.EOPNOTSUPP: + raise KsftSkipEx("rate_set not supported") from e + raise KsftFailEx("rate_set failed") from e + + +def rate_set_leaf_parent(devnl, dev_pci, port_index, + parent_name, parent_dev_pci=None): + """Sets a leaf's parent, optionally cross-esw.""" + params = { + "bus-name": "pci", + "dev-name": dev_pci, + "port-index": port_index, + "rate-parent-node-name": parent_name, + } + if parent_dev_pci: + params["parent-dev"] = { + "bus-name": "pci", + "dev-name": parent_dev_pci, + } + try: + devnl.rate_set(params) + except NlError as e: + if e.error == errno.EOPNOTSUPP: + raise KsftSkipEx("rate_set not supported") from e + if parent_dev_pci and e.error == errno.ENODEV: + raise KsftSkipEx("Cross-esw scheduling not supported") from e + raise KsftFailEx("rate_set failed") from e + + +def rate_clear_leaf_parent(devnl, dev_pci, port_index): + """Clears a leaf's parent.""" + rate_set_leaf_parent(devnl, dev_pci, port_index, "") + + +def rate_set_node(devnl, dev_pci, node_name, **kwargs): + """Sets rate attributes on a node.""" + params = { + "bus-name": "pci", + "dev-name": dev_pci, + "rate-node-name": node_name, + } + params.update(kwargs) + devnl.rate_set(params) + + +# --- Test cases --- + + +def test_same_esw_parent(cfg): + """Assigns PF0's VF to PF0's group (same esw baseline).""" + pf0, _ = discover_pfs(cfg) + pf0_pci, vf0_idx = setup_esw(pf0) + + rate_new(cfg.devnl, pf0_pci, "group0") + defer(rate_del, cfg.devnl, pf0_pci, "group0") + ksft_pr("rate-new succeeded") + + rate_set_leaf_parent(cfg.devnl, pf0_pci, vf0_idx, "group0") + defer(rate_clear_leaf_parent, cfg.devnl, pf0_pci, vf0_idx) + + ksft_pr("Same-esw parent assignment succeeded") + + +def test_cross_esw_parent(cfg): + """Sets cross-esw parent, then clear it.""" + pf0, pf1 = discover_pfs(cfg) + pf0_pci, _ = setup_esw(pf0) + pf1_pci, vf1_idx = setup_esw(pf1) + + rate_new(cfg.devnl, pf0_pci, "group1") + defer(rate_del, cfg.devnl, pf0_pci, "group1") + ksft_pr("rate-new succeeded") + + rate_set_leaf_parent(cfg.devnl, pf1_pci, vf1_idx, + "group1", parent_dev_pci=pf0_pci) + defer(rate_clear_leaf_parent, cfg.devnl, pf1_pci, vf1_idx) + + ksft_pr("Cross-esw parent set and clear succeeded") + + +def test_tx_rates_on_cross_esw(cfg): + """Sets tx_max on group and tx_share on leaves in a cross-esw setup.""" + pf0, pf1 = discover_pfs(cfg) + pf0_pci, vf0_idx = setup_esw(pf0) + pf1_pci, vf1_idx = setup_esw(pf1) + + rate_new(cfg.devnl, pf0_pci, "group2", **{"rate-tx-max": 10000000}) + defer(rate_del, cfg.devnl, pf0_pci, "group2") + ksft_pr("rate-new succeeded") + + rate_set_leaf_parent(cfg.devnl, pf1_pci, vf1_idx, + "group2", parent_dev_pci=pf0_pci) + defer(rate_clear_leaf_parent, cfg.devnl, pf1_pci, vf1_idx) + ksft_pr("set parent cross-esw succeeded") + + rate_set_leaf_parent(cfg.devnl, pf0_pci, vf0_idx, "group2") + defer(rate_clear_leaf_parent, cfg.devnl, pf0_pci, vf0_idx) + ksft_pr("set parent same esw succeeded") + + rate_set_leaf(cfg.devnl, pf0_pci, vf0_idx, **{"rate-tx-share": 1000000}) + rate = rate_get_leaf(cfg.devnl, pf0_pci, vf0_idx) + ksft_eq(rate["rate-tx-share"], 1000000) + rate_set_leaf(cfg.devnl, pf1_pci, vf1_idx, **{"rate-tx-share": 2000000}) + rate = rate_get_leaf(cfg.devnl, pf1_pci, vf1_idx) + ksft_eq(rate["rate-tx-share"], 2000000) + rate_set_node(cfg.devnl, pf0_pci, "group2", **{"rate-tx-max": 250000000}) + rate = rate_get(cfg.devnl, pf0_pci, "group2") + ksft_eq(rate["rate-tx-max"], 250000000) + + ksft_pr("tx_max and tx_share set on cross-esw group") + + +def main() -> None: + """Main function.""" + + with NetDrvEnv(__file__, nsim_test=False) as cfg: + cfg.devnl = DevlinkFamily() + + ksft_run( + cases=[ + test_same_esw_parent, + test_cross_esw_parent, + test_tx_rates_on_cross_esw, + ], + args=(cfg,), + ) + ksft_exit() + + +if __name__ == "__main__": + main() diff --git a/tools/testing/selftests/drivers/net/hw/devmem.py b/tools/testing/selftests/drivers/net/hw/devmem.py index 031cf9905f65..82c11ffc4add 100755 --- a/tools/testing/selftests/drivers/net/hw/devmem.py +++ b/tools/testing/selftests/drivers/net/hw/devmem.py @@ -2,7 +2,8 @@ # SPDX-License-Identifier: GPL-2.0 from os import path -from devmem_lib import setup_test, run_rx, run_tx, run_tx_chunks, run_rx_hds +from devmem_lib import (setup_test, run_rx, run_tx, run_tx_chunks, run_rx_hds, + run_rx_large_niov) from lib.py import ksft_run, ksft_exit, ksft_disruptive from lib.py import NetDrvEpEnv @@ -30,11 +31,17 @@ def check_rx_hds(cfg) -> None: run_rx_hds(cfg) +def check_rx_large_niov(cfg) -> None: + """Run the devmem RX test with rx-page-size = 16 KiB.""" + run_rx_large_niov(cfg) + + def main() -> None: """Run the devmem test cases.""" with NetDrvEpEnv(__file__) as cfg: setup_test(cfg, path.abspath(path.dirname(__file__) + "/ncdevmem")) - ksft_run([check_rx, check_tx, check_tx_chunks, check_rx_hds], + ksft_run([check_rx, check_tx, check_tx_chunks, check_rx_hds, + check_rx_large_niov], args=(cfg,)) ksft_exit() diff --git a/tools/testing/selftests/drivers/net/hw/devmem_lib.py b/tools/testing/selftests/drivers/net/hw/devmem_lib.py index 0921ff03eb81..3554954a6691 100644 --- a/tools/testing/selftests/drivers/net/hw/devmem_lib.py +++ b/tools/testing/selftests/drivers/net/hw/devmem_lib.py @@ -1,6 +1,8 @@ # SPDX-License-Identifier: GPL-2.0 +# pylint: disable=invalid-name,too-many-arguments """Shared helpers for devmem TCP selftests.""" +import os import re from lib.py import (bkg, cmd, defer, ethtool, rand_port, wait_port_listen, @@ -8,19 +10,82 @@ from lib.py import (bkg, cmd, defer, ethtool, rand_port, wait_port_listen, NetdevFamily) -def require_devmem(cfg): - """Probe ncdevmem on cfg.ifname and SKIP the test if devmem isn't supported.""" - if not hasattr(cfg, "devmem_probed"): - probe_command = f"{cfg.bin_local} -f {cfg.ifname}" - cfg.devmem_supported = cmd(probe_command, fail=False, shell=True).ret == 0 - cfg.devmem_probed = True +RX_PAGE_SIZE_DEFAULT = 0 +RX_PAGE_SIZE_16K = 16384 + +PROBE_RX_PAGE_SIZES = (RX_PAGE_SIZE_DEFAULT, RX_PAGE_SIZE_16K) + +NR_HUGEPAGES_FILE = "/proc/sys/vm/nr_hugepages" + + +def _is_aligned(value, alignment): + """Equivalent of the kernel IS_ALIGNED(value, alignment). + + alignment must be a power of two. + """ + return (value & (alignment - 1)) == 0 + + +def _restore_nr_hugepages(nr_hugepages): + with open(NR_HUGEPAGES_FILE, 'w', encoding='utf-8') as f: + f.write(str(nr_hugepages)) + + +def _reserve_hugepages(want=64): + """Raise nr_hugepages to @want and arrange for it to be restored.""" + with open(NR_HUGEPAGES_FILE, 'r+', encoding='utf-8') as f: + nr_hugepages = int(f.read().strip()) + if nr_hugepages >= want: + return + f.seek(0) + f.write(str(want)) + defer(_restore_nr_hugepages, nr_hugepages) + + +def _probe_devmem(cfg, rx_page_size): + """Return True if ncdevmem can bind cfg.ifname at @rx_page_size.""" + probe_command = f"{cfg.bin_local} -f {cfg.ifname}" + if rx_page_size != RX_PAGE_SIZE_DEFAULT: + probe_command += f" -b {rx_page_size}" + return cmd(probe_command, fail=False, shell=True).ret == 0 + - if not cfg.devmem_supported: +def require_devmem(cfg, rx_page_size=RX_PAGE_SIZE_DEFAULT): + """Probe ncdevmem on cfg.ifname and SKIP the test if devmem isn't supported.""" + if rx_page_size not in PROBE_RX_PAGE_SIZES: + raise RuntimeError( + f"rx-page-size={rx_page_size} is missing from " + f"PROBE_RX_PAGE_SIZES, so it was never probed.") + + if not hasattr(cfg, "devmem_supported"): + _reserve_hugepages() + # Probe every size upfront: in nk tests a leased queue may land in + # ncdevmem's queue range and cause the probe to fail. + cfg.devmem_supported = {size: _probe_devmem(cfg, size) + for size in PROBE_RX_PAGE_SIZES} + + if not cfg.devmem_supported[RX_PAGE_SIZE_DEFAULT]: raise KsftSkipEx("Test requires devmem support") + if rx_page_size != RX_PAGE_SIZE_DEFAULT: + page_size = os.sysconf("SC_PAGE_SIZE") + if not _is_aligned(rx_page_size, page_size): + raise KsftSkipEx( + f"rx-page-size={rx_page_size} is invalid for this platform " + f"(must be a multiple of PAGE_SIZE={page_size})") + + if not cfg.devmem_supported[rx_page_size]: + raise KsftSkipEx( + f"Test requires devmem rx-page-size={rx_page_size} support") + def configure_nic(cfg): """Channels, rings, RSS, queue lease for netkit devmem.""" + if not hasattr(cfg, "devmem_supported"): + raise RuntimeError( + "require_devmem() must be called before configure_nic(), which " + "may lease a queue away and make later probes fail.") + if not hasattr(cfg, 'netns'): return @@ -37,14 +102,13 @@ def configure_nic(cfg): rings = ethnl.rings_get({'header': {'dev-index': cfg.ifindex}}) orig_rx_rings = rings['rx'] orig_hds_thresh = rings.get('hds-thresh', 0) - orig_data_split = rings.get('tcp-data-split', 'unknown') ethnl.rings_set({'header': {'dev-index': cfg.ifindex}, 'tcp-data-split': 'enabled', 'hds-thresh': 0, 'rx': min(64, orig_rx_rings)}) defer(ethnl.rings_set, {'header': {'dev-index': cfg.ifindex}, - 'tcp-data-split': orig_data_split, + 'tcp-data-split': 'unknown', 'hds-thresh': orig_hds_thresh, 'rx': orig_rx_rings}) @@ -76,7 +140,8 @@ def set_flow_rule(cfg, port): return int(re.search(r'ID (\d+)', output).group(1)) -def ncdevmem_rx(cfg, port, verify=True, fail_on_linear=False, flow_steer=False): +def ncdevmem_rx(cfg, port, verify=True, fail_on_linear=False, flow_steer=False, + rx_page_size=RX_PAGE_SIZE_DEFAULT): """Build the ncdevmem RX listener command.""" if hasattr(cfg, 'netns'): flow_rule_id = set_flow_rule(cfg, port) @@ -96,6 +161,8 @@ def ncdevmem_rx(cfg, port, verify=True, fail_on_linear=False, flow_steer=False): extras.append("-v 7") if fail_on_linear: extras.append("-L") + if rx_page_size != RX_PAGE_SIZE_DEFAULT: + extras.append(f"-b {rx_page_size}") parts = [cfg.bin_local, "-l", f"-f {ifname}", f"-s {addr}", f"-p {port}", *extras] @@ -202,6 +269,33 @@ def run_tx_chunks(cfg): ksft_eq(socat.stdout.strip(), "hello\nworld") +def run_rx_large_niov(cfg): + """Run the devmem RX test with a large niov (rx-page-size > PAGE_SIZE). + + Sweep payload sizes that straddle the niov boundary: below, equal to, + and above rx_page_size, to exercise sub-niov, exact-niov, and multi-niov + RX paths. + """ + require_devmem(cfg, rx_page_size=RX_PAGE_SIZE_16K) + _reserve_hugepages() + configure_nic(cfg) + netns = getattr(cfg, "netns", None) + + for size in [1024, 4096, 8192, 16384, 32768, 65536]: + port = rand_port() + socat = socat_send(cfg, port) + listen_cmd = ncdevmem_rx(cfg, port, + flow_steer=not netns, + rx_page_size=RX_PAGE_SIZE_16K) + data_pipe = (f"yes $(echo -e \x01\x02\x03\x04\x05\x06) | " + f"head -c {size} | {socat}") + with bkg(listen_cmd, exit_wait=True, ns=netns) as ncdevmem: + wait_port_listen(port, proto="tcp", ns=netns) + cmd(data_pipe, host=cfg.remote, shell=True) + ksft_eq(ncdevmem.ret, 0, + f"large-niov failed for payload size {size}") + + def run_rx_hds(cfg): """Run the HDS test by running devmem RX across a segment size sweep.""" require_devmem(cfg) diff --git a/tools/testing/selftests/drivers/net/hw/ethtool_rmon.sh b/tools/testing/selftests/drivers/net/hw/ethtool_rmon.sh index 2ec19edddfaa..a074834cbe59 100755 --- a/tools/testing/selftests/drivers/net/hw/ethtool_rmon.sh +++ b/tools/testing/selftests/drivers/net/hw/ethtool_rmon.sh @@ -65,6 +65,8 @@ bucket_test() run_on "$iface" \ "$MZ" "$iface" -q -c "$num_tx" -p "$len" -a own -b bcast -d 10us + hw_stats_settle "$iface" + after=$(run_on "$iface" ethtool --json -S "$iface" --groups rmon | \ jq -r ".[0].rmon[\"${set}-pktsNtoM\"][$bucket].val") diff --git a/tools/testing/selftests/drivers/net/hw/ethtool_std_stats.sh b/tools/testing/selftests/drivers/net/hw/ethtool_std_stats.sh index c085d2a4c989..09f8128c51f3 100755 --- a/tools/testing/selftests/drivers/net/hw/ethtool_std_stats.sh +++ b/tools/testing/selftests/drivers/net/hw/ethtool_std_stats.sh @@ -43,10 +43,12 @@ traffic_test() done # shellcheck disable=SC2086 # needs split options - run_on "$iface" "$MZ" "$iface" -q -c "$num_tx" $pkt_format + run_on "$iface" "$MZ" "$iface" -q -d 10usec -c "$num_tx" $pkt_format # shellcheck disable=SC2086 # needs split options - run_on "$neigh" "$MZ" "$neigh" -q -c "$num_rx" $pkt_format + run_on "$neigh" "$MZ" "$neigh" -q -d 10usec -c "$num_rx" $pkt_format + + hw_stats_settle "$int" for i in "${!counters[@]}"; do read -r int grp cnt target exact_check xfail_message \ diff --git a/tools/testing/selftests/drivers/net/hw/ncdevmem.c b/tools/testing/selftests/drivers/net/hw/ncdevmem.c index d96e8a3b5a65..918e3b51f3b8 100644 --- a/tools/testing/selftests/drivers/net/hw/ncdevmem.c +++ b/tools/testing/selftests/drivers/net/hw/ncdevmem.c @@ -40,6 +40,7 @@ #include <linux/uio.h> #include <stdarg.h> +#include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> @@ -61,6 +62,7 @@ #include <sys/time.h> #include <linux/memfd.h> +#include <sys/param.h> #include <linux/dma-buf.h> #include <linux/errqueue.h> #include <linux/udmabuf.h> @@ -79,6 +81,7 @@ #define PAGE_SHIFT 12 #define TEST_PREFIX "ncdevmem" #define NUM_PAGES 16000 +#define MB(x) ((x) << 20) #ifndef MSG_SOCK_DEVMEM #define MSG_SOCK_DEVMEM 0x2000000 @@ -100,6 +103,7 @@ static unsigned int dmabuf_id; static uint32_t tx_dmabuf_id; static int waittime_ms = 500; static bool fail_on_linear; +static uint32_t rx_page_size; /* System state loaded by current_config_load() */ #define MAX_FLOWS 8 @@ -142,6 +146,7 @@ static struct memory_buffer *udmabuf_alloc(size_t size) { struct udmabuf_create create; struct memory_buffer *ctx; + unsigned int memfd_flags; int ret; ctx = malloc(sizeof(*ctx)); @@ -150,15 +155,20 @@ static struct memory_buffer *udmabuf_alloc(size_t size) ctx->size = size; - ctx->devfd = open("/dev/udmabuf", O_RDWR); + ctx->devfd = open("/dev/udmabuf", O_RDONLY); if (ctx->devfd < 0) { pr_err("[skip,no-udmabuf: Unable to access DMA buffer device file]"); goto err_free_ctx; } - ctx->memfd = memfd_create("udmabuf-test", MFD_ALLOW_SEALING); + memfd_flags = MFD_ALLOW_SEALING; + if (rx_page_size > getpagesize()) + memfd_flags |= MFD_HUGETLB | MFD_HUGE_2MB; + + ctx->memfd = memfd_create("udmabuf-test", memfd_flags); if (ctx->memfd < 0) { - pr_err("[skip,no-memfd]"); + pr_err("[skip,no-memfd%s]", + (memfd_flags & MFD_HUGETLB) ? " (need hugepages)" : ""); goto err_close_dev; } @@ -168,6 +178,11 @@ static struct memory_buffer *udmabuf_alloc(size_t size) goto err_close_memfd; } + if (memfd_flags & MFD_HUGETLB) { + size = roundup(size, MB(2)); + ctx->size = size; + } + ret = ftruncate(ctx->memfd, size); if (ret == -1) { pr_err("[FAIL,memfd-truncate]"); @@ -699,6 +714,8 @@ static int bind_rx_queue(unsigned int ifindex, unsigned int dmabuf_fd, netdev_bind_rx_req_set_ifindex(req, ifindex); netdev_bind_rx_req_set_fd(req, dmabuf_fd); __netdev_bind_rx_req_set_queues(req, queues, n_queue_index); + if (rx_page_size) + netdev_bind_rx_req_set_rx_page_size(req, rx_page_size); rsp = netdev_bind_rx(*ys, req); if (!rsp) { @@ -1411,7 +1428,7 @@ int main(int argc, char *argv[]) int is_server = 0, opt; int ret, err = 1; - while ((opt = getopt(argc, argv, "Lls:c:p:v:q:t:f:z:n")) != -1) { + while ((opt = getopt(argc, argv, "Lls:c:p:v:q:t:f:z:nb:")) != -1) { switch (opt) { case 'L': fail_on_linear = true; @@ -1446,6 +1463,19 @@ int main(int argc, char *argv[]) case 'n': skip_config = 1; break; + case 'b': { + unsigned long val; + + errno = 0; + val = strtoul(optarg, NULL, 0); + if ((val == ULONG_MAX && errno == ERANGE) || + val > UINT32_MAX) { + pr_err("invalid rx_page_size: %s", optarg); + return 1; + } + rx_page_size = val; + break; + } case '?': fprintf(stderr, "unknown option: %c\n", optopt); break; diff --git a/tools/testing/selftests/drivers/net/hw/nk_devmem.py b/tools/testing/selftests/drivers/net/hw/nk_devmem.py index 300ed2a70ab4..61c6f31f01e5 100755 --- a/tools/testing/selftests/drivers/net/hw/nk_devmem.py +++ b/tools/testing/selftests/drivers/net/hw/nk_devmem.py @@ -3,7 +3,8 @@ """Test devmem TCP with netkit.""" import os -from devmem_lib import setup_test, run_rx, run_tx, run_tx_chunks, run_rx_hds +from devmem_lib import (setup_test, run_rx, run_tx, run_tx_chunks, run_rx_hds, + run_rx_large_niov) from lib.py import ksft_run, ksft_exit, ksft_disruptive from lib.py import NetDrvContEnv @@ -31,6 +32,11 @@ def check_nk_rx_hds(cfg) -> None: run_rx_hds(cfg) +def check_nk_rx_large_niov(cfg) -> None: + """Run the devmem RX large-niov test through netkit.""" + run_rx_large_niov(cfg) + + def main() -> None: """Run the netkit devmem test cases.""" with NetDrvContEnv(__file__, rxqueues=2, primary_rx_redirect=True) as cfg: @@ -38,7 +44,7 @@ def main() -> None: os.path.join(os.path.dirname(os.path.abspath(__file__)), "ncdevmem")) ksft_run([check_nk_rx, check_nk_tx, check_nk_tx_chunks, - check_nk_rx_hds], args=(cfg,)) + check_nk_rx_hds, check_nk_rx_large_niov], args=(cfg,)) ksft_exit() diff --git a/tools/testing/selftests/drivers/net/hw/rss_ctx.py b/tools/testing/selftests/drivers/net/hw/rss_ctx.py index f36f76d6ca59..5b25fa89c629 100755 --- a/tools/testing/selftests/drivers/net/hw/rss_ctx.py +++ b/tools/testing/selftests/drivers/net/hw/rss_ctx.py @@ -651,9 +651,14 @@ def test_rss_context_overlap(cfg, other_ctx=0): ntuple = defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}") # Test the main context - cnts = _get_rx_cnts(cfg) - GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000) - cnts = _get_rx_cnts(cfg, prev=cnts) + attempts = 3 + for attempt in range(attempts): + cnts = _get_rx_cnts(cfg) + GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000) + cnts = _get_rx_cnts(cfg, prev=cnts) + if sum(cnts[:2]) >= 7000 and sum(cnts[2:4]) >= 7000: + break + ksft_pr(f"Skewed queue distribution, attempt {attempt + 1}/{attempts}: " + str(cnts)) ksft_ge(sum(cnts[ :4]), 20000, "traffic on main context: " + str(cnts)) ksft_ge(sum(cnts[ :2]), 7000, "traffic on main context (1/2): " + str(cnts)) diff --git a/tools/testing/selftests/drivers/net/hw/toeplitz.py b/tools/testing/selftests/drivers/net/hw/toeplitz.py index cd7e080e6f84..571732198b93 100755 --- a/tools/testing/selftests/drivers/net/hw/toeplitz.py +++ b/tools/testing/selftests/drivers/net/hw/toeplitz.py @@ -21,6 +21,8 @@ from lib.py import ksft_variants, KsftNamedVariant, KsftSkipEx, KsftFailEx ETH_RSS_HASH_TOP = 1 # Must match RPS_MAX_CPUS in toeplitz.c RPS_MAX_CPUS = 16 +# Cap Rx queues so IRQ pinning leaves free CPUs in the RPS_MAX_CPUS range +QUEUE_CAP = 8 def _check_rps_and_rfs_not_configured(cfg): @@ -48,6 +50,25 @@ def _get_cpu_for_irq(irq): return int(data) +def _cap_queue_count(cfg): + ehdr = {"header": {"dev-index": cfg.ifindex}} + chans = cfg.ethnl.channels_get(ehdr) + + config = {} + restore = {} + for key in ("combined-count", "rx-count"): + cur = chans.get(key, 0) + if cur > QUEUE_CAP: + config[key] = QUEUE_CAP + restore[key] = cur + + if not config: + return + + cfg.ethnl.channels_set(ehdr | config) + defer(cfg.ethnl.channels_set, ehdr | restore) + + def _get_irq_cpus(cfg): """ Read the list of IRQs for the device Rx queues. @@ -177,6 +198,7 @@ def test(cfg, proto_flag, ipver, grp): ] if grp: + _cap_queue_count(cfg) _check_rps_and_rfs_not_configured(cfg) if grp == "rss": irq_cpus = ",".join([str(x) for x in _get_irq_cpus(cfg)]) diff --git a/tools/testing/selftests/drivers/net/hw/tso.py b/tools/testing/selftests/drivers/net/hw/tso.py index 802bb4868046..67f6c9ca9a64 100755 --- a/tools/testing/selftests/drivers/net/hw/tso.py +++ b/tools/testing/selftests/drivers/net/hw/tso.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0 -"""Run the tools/testing/selftests/net/csum testsuite.""" +"""A simple test for TSO.""" import fcntl import socket diff --git a/tools/testing/selftests/drivers/net/lib/py/env.py b/tools/testing/selftests/drivers/net/lib/py/env.py index e4ab99b905b1..25903f580b40 100644 --- a/tools/testing/selftests/drivers/net/lib/py/env.py +++ b/tools/testing/selftests/drivers/net/lib/py/env.py @@ -7,7 +7,7 @@ import time import json from pathlib import Path from lib.py import KsftSkipEx, KsftXfailEx -from lib.py import ksft_setup, wait_file +from lib.py import ksft_pr, ksft_setup, wait_file from lib.py import cmd, ethtool, ip, CmdExitFailure from lib.py import NetNS, NetdevSimDev, UserNetNS from .remote import Remote @@ -31,6 +31,7 @@ class NetDrvEnvBase: # Following attrs must be set be inheriting classes self.dev = None + self.ifname = None def _load_env_file(self): env = os.environ.copy() @@ -58,6 +59,22 @@ class NetDrvEnvBase: def __del__(self): pass + def _print_dev_info(self): + """ + Show whether the test ran on real hardware or netdevsim. + Useful to confirm when results are shared on the mailing list. + """ + driver = "unknown" + try: + info = ethtool(f"-i {self.ifname}").stdout + for line in info.splitlines(): + if line.startswith("driver:"): + driver = line.split(':', 1)[1].strip() or driver + break + except (CmdExitFailure, FileNotFoundError): + pass + ksft_pr(f"Interface: {self.ifname}, driver: {driver}") + def __enter__(self): ip(f"link set dev {self.dev['ifname']} up") wait_file(f"/sys/class/net/{self.dev['ifname']}/carrier", @@ -94,6 +111,7 @@ class NetDrvEnv(NetDrvEnvBase): self.dev = self._ns.nsims[0].dev self.ifname = self.dev['ifname'] self.ifindex = self.dev['ifindex'] + self._print_dev_info() def __del__(self): if self._ns: @@ -114,10 +132,11 @@ class NetDrvEpEnv(NetDrvEnvBase): nsim_v4_pfx = "192.0.2." nsim_v6_pfx = "2001:db8::" - def __init__(self, src_path, nsim_test=None): + def __init__(self, src_path, nsim_test=None, queue_count=None): super().__init__(src_path) self._stats_settle_time = None + self._queue_count = queue_count # Things we try to destroy self.remote = None @@ -159,16 +178,11 @@ class NetDrvEpEnv(NetDrvEnvBase): self.remote = Remote(kind, args, src_path) - self.addr_ipver = "6" if self.addr_v["6"] else "4" - self.addr = self.addr_v[self.addr_ipver] - self.remote_addr = self.remote_addr_v[self.addr_ipver] - - # Bracketed addresses, some commands need IPv6 to be inside [] - self.baddr = f"[{self.addr_v['6']}]" if self.addr_v["6"] else self.addr_v["4"] - self.remote_baddr = f"[{self.remote_addr_v['6']}]" if self.remote_addr_v["6"] else self.remote_addr_v["4"] + self.set_ipver("6" if self.addr_v["6"] else "4") self.ifname = self.dev['ifname'] self.ifindex = self.dev['ifindex'] + self._print_dev_info() # resolve remote interface name self.remote_ifname = self.resolve_remote_ifc() @@ -179,9 +193,13 @@ class NetDrvEpEnv(NetDrvEnvBase): self._required_cmd = {} def create_local(self): + nsim_kwargs = {} + if self._queue_count: + nsim_kwargs["queue_count"] = self._queue_count + self._netns = NetNS() - self._ns = NetdevSimDev() - self._ns_peer = NetdevSimDev(ns=self._netns) + self._ns = NetdevSimDev(**nsim_kwargs) + self._ns_peer = NetdevSimDev(ns=self._netns, **nsim_kwargs) with open("/proc/self/ns/net") as nsfd0, \ open("/var/run/netns/" + self._netns.name) as nsfd1: @@ -252,6 +270,25 @@ class NetDrvEpEnv(NetDrvEnvBase): if not self.addr_v[ipver] or not self.remote_addr_v[ipver]: raise KsftSkipEx(f"Test requires IPv{ipver} connectivity") + def set_ipver(self, ipver): + """ + Modify the IP version used by the generic address fields. + """ + if ipver == getattr(self, "addr_ipver", None): + return + + self.require_ipver(ipver) + + self.addr_ipver = ipver + self.addr = self.addr_v[ipver] + self.remote_addr = self.remote_addr_v[ipver] + + # Bracketed addresses, some commands need IPv6 to be inside [] + self.baddr = (f"[{self.addr_v['6']}]" if ipver == "6" + else self.addr_v["4"]) + self.remote_baddr = (f"[{self.remote_addr_v['6']}]" if ipver == "6" + else self.remote_addr_v["4"]) + def require_nsim(self, nsim_test=True): """Require or exclude netdevsim for this test""" if nsim_test and self._ns is None: diff --git a/tools/testing/selftests/drivers/net/netconsole/Makefile b/tools/testing/selftests/drivers/net/netconsole/Makefile index b56c70b7e274..f0674c0017fc 100644 --- a/tools/testing/selftests/drivers/net/netconsole/Makefile +++ b/tools/testing/selftests/drivers/net/netconsole/Makefile @@ -13,6 +13,7 @@ TEST_PROGS := \ netcons_resume.sh \ netcons_sysdata.sh \ netcons_torture.sh \ + netcons_userdata.sh \ # end of TEST_PROGS include ../../../lib.mk diff --git a/tools/testing/selftests/drivers/net/netconsole/netcons_cmdline.sh b/tools/testing/selftests/drivers/net/netconsole/netcons_cmdline.sh index 96d704b8d9d9..4436567abc94 100755 --- a/tools/testing/selftests/drivers/net/netconsole/netcons_cmdline.sh +++ b/tools/testing/selftests/drivers/net/netconsole/netcons_cmdline.sh @@ -50,7 +50,7 @@ do # Send the message echo "${MSG}: ${TARGET}" > /dev/kmsg # Wait until socat saves the file to disk - busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" + busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" || true # Make sure the message was received in the dst part # and exit validate_msg "${OUTPUT_FILE}" diff --git a/tools/testing/selftests/drivers/net/netconsole/netcons_fragmented_msg.sh b/tools/testing/selftests/drivers/net/netconsole/netcons_fragmented_msg.sh index 0dc7280c3080..fc3db40c1df5 100755 --- a/tools/testing/selftests/drivers/net/netconsole/netcons_fragmented_msg.sh +++ b/tools/testing/selftests/drivers/net/netconsole/netcons_fragmented_msg.sh @@ -104,7 +104,7 @@ wait_local_port_listen "${NAMESPACE}" "${PORT}" udp # Send the message echo "${MSG}: ${TARGET}" > /dev/kmsg # Wait until socat saves the file to disk -busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" +busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" || true # Check if the message was not corrupted validate_fragmented_result "${OUTPUT_FILE}" @@ -117,6 +117,6 @@ disable_release_append listen_port_and_save_to "${OUTPUT_FILE}" & wait_local_port_listen "${NAMESPACE}" "${PORT}" udp echo "${MSG}: ${TARGET}" > /dev/kmsg -busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" +busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" || true validate_fragmented_result "${OUTPUT_FILE}" exit "${ksft_pass}" diff --git a/tools/testing/selftests/drivers/net/netconsole/netcons_resume.sh b/tools/testing/selftests/drivers/net/netconsole/netcons_resume.sh index d9111f2102bc..b379dff9087e 100755 --- a/tools/testing/selftests/drivers/net/netconsole/netcons_resume.sh +++ b/tools/testing/selftests/drivers/net/netconsole/netcons_resume.sh @@ -108,7 +108,7 @@ do # Send the message echo "${MSG}: ${TARGET}" > /dev/kmsg # Wait until socat saves the file to disk - busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" + busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" || true # Make sure the message was received in the dst part # and exit validate_msg "${OUTPUT_FILE}" diff --git a/tools/testing/selftests/drivers/net/netconsole/netcons_sysdata.sh b/tools/testing/selftests/drivers/net/netconsole/netcons_sysdata.sh index 3fb8c4afe3d2..7089f7bd1e34 100755 --- a/tools/testing/selftests/drivers/net/netconsole/netcons_sysdata.sh +++ b/tools/testing/selftests/drivers/net/netconsole/netcons_sysdata.sh @@ -197,7 +197,7 @@ function runtest { # Send the message taskset -c "${CPU}" echo "${MSG}: ${TARGET}" > /dev/kmsg # Wait until socat saves the file to disk - busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" + busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" || true } # ========== # diff --git a/tools/testing/selftests/drivers/net/netconsole/netcons_userdata.sh b/tools/testing/selftests/drivers/net/netconsole/netcons_userdata.sh new file mode 100755 index 000000000000..113903f4ce1c --- /dev/null +++ b/tools/testing/selftests/drivers/net/netconsole/netcons_userdata.sh @@ -0,0 +1,229 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-2.0 + +# Exercise the netconsole userdata payload. +# +# The first part checks that the payload the target transmits follows what +# configfs says: a value shows up in the next message, an update replaces the +# previous one, clearing the value drops the entry, and so does removing the +# key. +# +# The second part rewrites values, creates and deletes keys, and clears the +# payload entirely while messages are being sent, so the transmit path keeps +# picking up payloads that are being replaced underneath it. It runs twice, +# once with a payload small enough to fit in a single packet and once large +# enough to be fragmented. +# +# Author: Breno Leitao <leitao@debian.org> + +set -euo pipefail + +SCRIPTDIR=$(dirname "$(readlink -e "${BASH_SOURCE[0]}")") + +source "${SCRIPTDIR}"/../lib/sh/lib_netcons.sh + +# Number of times each torture worker loops +ITERATIONS=${1:-200} + +# Keys owned by each torture worker. Workers do not share keys, so a failing +# configfs operation means a real problem and not a lost race. +CHURN_KEY="churnkey" +TRANSIENT_KEY="transientkey" +# Number of keys used to push a message past MAX_PRINT_CHUNK +BULK_KEYS=8 + +USERDATA_DIR="${NETCONS_PATH}/userdata" +# Values are capped at MAX_EXTRADATA_VALUE_LEN(200) bytes, so ${BULK_KEYS} +# entries of this size are enough to force fragmentation +LONG_VALUE=$(printf -- 'v%.0s' {1..190}) + +function write_key() { + local KEY="${1}" + local VALUE="${2}" + + mkdir -p "${USERDATA_DIR}/${KEY}" + echo "${VALUE}" > "${USERDATA_DIR}/${KEY}/value" +} + +# Send a single message and capture it on the destination interface +function send_and_capture() { + rm -f "${OUTPUT_FILE}" + + listen_port_and_save_to "${OUTPUT_FILE}" & + wait_for_port "${NAMESPACE}" "${PORT}" "${IP_VERSION}" + echo "${MSG}: ${TARGET}" > /dev/kmsg + busywait "${BUSYWAIT_TIMEOUT}" test -s "${OUTPUT_FILE}" || true + pkill_socat + validate_msg "${OUTPUT_FILE}" +} + +function expect_in_msg() { + local WANTED="${1}" + + if ! grep -q -- "${WANTED}" "${OUTPUT_FILE}"; then + echo "FAIL: '${WANTED}' not found in ${OUTPUT_FILE}" >&2 + cat "${OUTPUT_FILE}" >&2 + exit "${ksft_fail}" + fi +} + +function expect_not_in_msg() { + local UNWANTED="${1}" + + if grep -q -- "${UNWANTED}" "${OUTPUT_FILE}"; then + echo "FAIL: '${UNWANTED}' found in ${OUTPUT_FILE}" >&2 + cat "${OUTPUT_FILE}" >&2 + exit "${ksft_fail}" + fi +} + +# Every write publishes a new payload and frees the previous one. An empty +# value is skipped when the payload is formatted, so this also drives the +# target through having no payload at all. +function churn_value() { + local i + + for i in $(seq "${ITERATIONS}") + do + echo "value${i}" > "${USERDATA_DIR}/${CHURN_KEY}/value" + echo > "${USERDATA_DIR}/${CHURN_KEY}/value" + done +} + +# Create and delete a key underneath the sender +function churn_key() { + local i + + for i in $(seq "${ITERATIONS}") + do + mkdir "${USERDATA_DIR}/${TRANSIENT_KEY}" + echo "transient${i}" > "${USERDATA_DIR}/${TRANSIENT_KEY}/value" + rmdir "${USERDATA_DIR}/${TRANSIENT_KEY}" + done +} + +# Keep the transmit path busy while the payload is being replaced +function send_messages() { + local i + + for i in $(seq "${ITERATIONS}") + do + echo "${MSG}: ${TARGET} ${i}" > /dev/kmsg + done +} + +# Run the workers concurrently and fail if any of them hits an error +function run_workers() { + local PIDS=() + local WORKER + local RET=0 + local PID + + for WORKER in "$@" + do + "${WORKER}" & + PIDS+=("$!") + done + + # Reap every worker before reporting a failure, otherwise a surviving + # worker keeps writing to configfs while the exit trap cleans it up. + for PID in "${PIDS[@]}" + do + wait "${PID}" || RET=1 + done + + if [[ "${RET}" -ne 0 ]] + then + echo "FAIL: userdata torture worker failed" >&2 + exit "${ksft_fail}" + fi +} + +function create_bulk_keys() { + local i + + for i in $(seq "${BULK_KEYS}") + do + write_key "bulk${i}" "${LONG_VALUE}" + done +} + +function delete_bulk_keys() { + local i + + for i in $(seq "${BULK_KEYS}") + do + rmdir "${USERDATA_DIR}/bulk${i}" + done +} + +# ========== # +# Start here # +# ========== # + +modprobe netdevsim 2> /dev/null || true +modprobe netconsole 2> /dev/null || true + +IP_VERSION="ipv4" +# The content of kmsg will be saved to the following file +OUTPUT_FILE="/tmp/${TARGET}" + +# Check for basic system dependency and exit if not found +check_for_dependencies +# Set current loglevel to KERN_INFO(6), and default to KERN_NOTICE(5) +echo "6 5" > /proc/sys/kernel/printk +# Remove the namespace, interfaces and netconsole target on exit +trap cleanup EXIT +# Create one namespace and two interfaces +set_network "${IP_VERSION}" +# Create a dynamic target for netconsole +create_dynamic_target + +# =================================================== +# TEST #1 +# A value written to configfs reaches the destination +# =================================================== +write_key "${USERDATA_KEY}" "first" +send_and_capture +expect_in_msg "${USERDATA_KEY}=first" + +# =================================================== +# TEST #2 +# Updating the value replaces the previous payload +# =================================================== +write_key "${USERDATA_KEY}" "second" +send_and_capture +expect_in_msg "${USERDATA_KEY}=second" +expect_not_in_msg "${USERDATA_KEY}=first" + +# =================================================== +# TEST #3 +# Clearing the value drops the entry +# =================================================== +echo > "${USERDATA_DIR}/${USERDATA_KEY}/value" +send_and_capture +expect_not_in_msg "${USERDATA_KEY}=" + +# =================================================== +# TEST #4 +# Removing the key drops the entry +# =================================================== +write_key "${USERDATA_KEY}" "third" +rmdir "${USERDATA_DIR}/${USERDATA_KEY}" +send_and_capture +expect_not_in_msg "${USERDATA_KEY}=" +rm "${OUTPUT_FILE}" + +# =================================================== +# TEST #5 +# Torture the payload while messages are being sent, +# first unfragmented and then fragmented +# =================================================== +write_key "${CHURN_KEY}" "${USERDATA_VALUE}" +run_workers churn_value churn_key send_messages + +create_bulk_keys +run_workers churn_value churn_key send_messages +delete_bulk_keys + +exit "${ksft_pass}" diff --git a/tools/testing/selftests/drivers/net/netdevsim/ethtool-coalesce.sh b/tools/testing/selftests/drivers/net/netdevsim/ethtool-coalesce.sh index 9adfba8f87e6..b9fcafad4258 100755 --- a/tools/testing/selftests/drivers/net/netdevsim/ethtool-coalesce.sh +++ b/tools/testing/selftests/drivers/net/netdevsim/ethtool-coalesce.sh @@ -116,12 +116,14 @@ done # bool settings which ethtool displays on the same line ethtool -C $NSIM_NETDEV adaptive-rx on -s=$(ethtool -c $NSIM_NETDEV | grep -q "Adaptive RX: on TX: off") -check $? "$s" "" +s=$(ethtool -c $NSIM_NETDEV) +echo "$s" | grep -q "Adaptive RX: on TX: off" +check $? "" "" ethtool -C $NSIM_NETDEV adaptive-tx on -s=$(ethtool -c $NSIM_NETDEV | grep -q "Adaptive RX: on TX: on") -check $? "$s" "" +s=$(ethtool -c $NSIM_NETDEV) +echo "$s" | grep -q "Adaptive RX: on TX: on" +check $? "" "" if [ $num_errors -eq 0 ]; then echo "PASSED all $((num_passes)) checks" diff --git a/tools/testing/selftests/drivers/net/psp.py b/tools/testing/selftests/drivers/net/psp.py index 315648a770d0..a5b1e14f120f 100755 --- a/tools/testing/selftests/drivers/net/psp.py +++ b/tools/testing/selftests/drivers/net/psp.py @@ -23,6 +23,8 @@ from lib.py import NetNSEnter from lib.py import bkg, rand_port, wait_port_listen from lib.py import ip +TCP_ULP = 31 + def _get_outq(s): one = b'\0' * 4 @@ -333,6 +335,50 @@ def assoc_version_mismatch(cfg): ksft_eq(the_exception.nl_msg.error, -errno.EINVAL) +def _require_tls_ulp(): + with socket.create_server(("localhost", 0)) as srv, \ + socket.create_connection(srv.getsockname()) as s: + try: + s.setsockopt(socket.SOL_TCP, TCP_ULP, b"tls") + except OSError as exc: + raise KsftSkipEx("kTLS not available") from exc + + +def assoc_psp_ulp_exclusive(cfg): + """ Test that a TCP ULP cannot be attached to a PSP socket """ + _init_psp_dev(cfg) + _require_tls_ulp() + + with _make_clr_conn(cfg) as s: + try: + cfg.pspnl.rx_assoc({"version": 0, + "dev-id": cfg.psp_dev_id, + "sock-fd": s.fileno()}) + with ksft_raises(OSError) as cm: + s.setsockopt(socket.SOL_TCP, TCP_ULP, b"tls") + ksft_eq(cm.exception.errno, errno.EINVAL) + finally: + _close_conn(cfg, s) + + +def assoc_ulp_psp_exclusive(cfg): + """ Test that a PSP assoc cannot be added to a socket with a TCP ULP """ + _init_psp_dev(cfg) + _require_tls_ulp() + + with _make_clr_conn(cfg) as s: + try: + s.setsockopt(socket.SOL_TCP, TCP_ULP, b"tls") + with ksft_raises(NlError) as cm: + cfg.pspnl.rx_assoc({"version": 0, + "dev-id": cfg.psp_dev_id, + "sock-fd": s.fileno()}) + ksft_eq(cm.exception.nl_msg.error, -errno.EINVAL) + ksft_eq(cm.exception.nl_msg.extack['bad-attr'], ".sock-fd") + finally: + _close_conn(cfg, s) + + def assoc_twice(cfg): """ Test reusing Tx assoc for two sockets """ _init_psp_dev(cfg) diff --git a/tools/testing/selftests/drivers/net/ring_reconfig.py b/tools/testing/selftests/drivers/net/ring_reconfig.py index f9530a8b0856..2bc329b77134 100755 --- a/tools/testing/selftests/drivers/net/ring_reconfig.py +++ b/tools/testing/selftests/drivers/net/ring_reconfig.py @@ -5,10 +5,25 @@ Test channel and ring size configuration via ethtool (-L / -G). """ +import socket +import struct +import time + from lib.py import ksft_run, ksft_exit, ksft_pr from lib.py import ksft_eq +from lib.py import KsftSkipEx, KsftXfailEx from lib.py import NetDrvEpEnv, EthtoolFamily, GenerateTraffic -from lib.py import defer, NlError +from lib.py import cmd, defer, rand_port, tc, NlError + +# Added in Python 3.13; fallback to 61 for x86/ARM/MIPS +SO_TXTIME = getattr(socket, "SO_TXTIME", 61) + +# Not always exported by the socket module; asm-generic value (x86/ARM/MIPS). +SO_SNDBUFFORCE = getattr(socket, "SO_SNDBUFFORCE", 32) + +# TX ring size the test shrinks to so the ring fills quickly. +MIN_TX_RING = 32 +MAX_TX_RING = 1024 def channels(cfg) -> None: @@ -151,14 +166,248 @@ def ringparam(cfg) -> None: GenerateTraffic(cfg).wait_pkts_and_stop(10000) +def _write_file(path, val): + """Write val to a file.""" + with open(path, "w", encoding="utf-8") as fp: + fp.write(str(val)) + + +def _write_sysfs(path, val): + """Write val to a sysfs file, restoring the original value on exit.""" + with open(path, "r", encoding="utf-8") as fp: + orig_val = fp.read().strip() + if str(val) == orig_val: + return + _write_file(path, val) + defer(_write_file, path, orig_val) + + +def _get_qdisc_backlog(cfg, mq_handle, queue): + """Return the qdisc backlog (bytes) for the given TX queue's leaf.""" + target_parent = f"{mq_handle}{queue + 1:x}" + for q in tc(f"-s qdisc show dev {cfg.ifname}", json=True): + if q.get("parent", "") == target_parent: + return q.get("backlog") or 0 + return 0 + + +def _setup_fq_qdisc(cfg, port, target_queue, other_queue, flow_limit): + """Put an fq qdisc on target_queue's leaf and return the mq handle in use. + + We must not disturb the device's existing TX/RX qdisc policy. On a real + NIC the root mq already has an addressable handle, so we leave the root + and every other queue alone and only swap this one leaf, restoring its + original qdisc afterwards. + + @flow_limit raises fq's per-flow packet limit (default 100) so a single + flow can back up more packets than the Tx ring holds and thus overflow it. + """ + qdiscs = tc(f"qdisc show dev {cfg.ifname}", json=True) + root = next((q for q in qdiscs if q.get("root")), None) + + if root and root["kind"] == "mq" and root["handle"] != "0:": + # Addressable mq (previously-configured): touch only the target queue's + # leaf and restore its original qdisc afterwards. + mq_handle = root["handle"] + parent = f"{mq_handle}{target_queue + 1:x}" + orig = next((q for q in qdiscs if q.get("parent") == parent), None) + orig_kind = orig["kind"] if orig else \ + cmd("sysctl -n net.core.default_qdisc").stdout.strip() + defer(tc, f"qdisc replace dev {cfg.ifname} parent {parent} {orig_kind}") + elif root is None or root["kind"] in ("mq", "noqueue"): + # The auto-attached root mq has handle 0: on any device (real or sim), + # which the kernel rejects as a qdisc parent. A 0: handle means the mq + # is the untouched kernel default - no custom child qdiscs can hang off + # an unaddressable parent - so installing a real handle and restoring + # the default mq on exit preserves the device's effective policy. + mq_handle = "1:" + tc(f"qdisc replace dev {cfg.ifname} root handle {mq_handle} mq") + defer(tc, f"qdisc replace dev {cfg.ifname} root mq") + parent = f"{mq_handle}{target_queue + 1:x}" + else: + raise KsftSkipEx(f"root qdisc '{root['kind']}' is not mq; " + "refusing to disturb existing qdisc policy") + + try: + tc(f"qdisc replace dev {cfg.ifname} parent {parent} fq " + f"flow_limit {flow_limit} limit {flow_limit * 2}") + except Exception as exc: + raise KsftSkipEx( + f"fq not available (CONFIG_NET_SCH_FQ): {exc}") from exc + + qdisc_j = tc(f"qdisc show dev {cfg.ifname}", json=True) + has_clsact = any(q['kind'] == 'clsact' for q in qdisc_j) + if not has_clsact: + tc(f"qdisc add dev {cfg.ifname} clsact") + defer(tc, f"qdisc del dev {cfg.ifname} clsact") + + proto = "ipv6" if int(cfg.addr_ipver) == 6 else "ip" + try: + tc(f"filter add dev {cfg.ifname} egress protocol {proto} " + f"pref 1 flower ip_proto udp dst_port {port} " + f"action skbedit queue_mapping {target_queue}") + except Exception as exc: + raise KsftSkipEx("tc flower/act_skbedit not available") from exc + defer(tc, f"filter del dev {cfg.ifname} egress pref 1") + + tc(f"filter add dev {cfg.ifname} egress pref 101 " + f"matchall action skbedit queue_mapping {other_queue}") + defer(tc, f"filter del dev {cfg.ifname} egress pref 101") + + return mq_handle + + +def _create_sotxtime_socket(cfg, sndbuf): + """Create a UDP socket with SO_TXTIME enabled, bound to the test device.""" + sock = socket.socket(socket.AF_INET6 if cfg.addr_ipver == "6" + else socket.AF_INET, socket.SOCK_DGRAM) + try: + sock.setsockopt(socket.SOL_SOCKET, SO_TXTIME, struct.pack("Ii", 1, 0)) + except OSError as exc: + sock.close() + raise KsftSkipEx("SO_TXTIME not supported") from exc + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, + cfg.ifname.encode()) + # Deferred completions keep every in-flight skb charged to the socket, so + # size the send buffer to hold the whole burst. SO_SNDBUFFORCE bypasses + # net.core.wmem_max (the test runs as root). + try: + sock.setsockopt(socket.SOL_SOCKET, SO_SNDBUFFORCE, sndbuf) + except OSError: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, sndbuf) + return sock + + +def _send_sotxtime_burst(cfg, sock, port, count, delay_ns, pkt_size): + """Send count UDP packets scheduled delay_ns ahead using SO_TXTIME.""" + payload = b'\x00' * pkt_size + txtime_ns = time.clock_gettime_ns(time.CLOCK_MONOTONIC) + delay_ns + + ancdata = [(socket.SOL_SOCKET, SO_TXTIME, struct.pack("Q", txtime_ns))] + if int(cfg.addr_ipver) == 6: + dest = (cfg.remote_addr, port, 0, 0) + else: + dest = (cfg.remote_addr, port) + for _ in range(count): + sock.sendmsg([payload], ancdata, 0, dest) + + +def _set_small_tx_ring(cfg, ehdr): + """Set the Tx ring to the smallest size the driver accepts. + + Start at 32 so the ring fills quickly, then grow exponentially (64, + 128, 256, ...) up to 1024. Some drivers enforce a minimum well above 32 + (e.g. bnxt needs a large ring for software UDP segmentation), so raise + the lower bound until the driver accepts it, giving up past 1024. + """ + size = MIN_TX_RING + while size <= MAX_TX_RING: + try: + cfg.eth.rings_set(ehdr | {'tx': size}) + return size + except NlError: + size = size * 2 + continue + raise KsftSkipEx("driver rejects all tx ring sizes up to 1024") + + +def reconfig_tx_stall(cfg) -> None: + """Test that qdisc backlog drains after ring reconfiguration.""" + target_queue = 1 + other_queue = 0 + + ehdr = {'header': {'dev-index': cfg.ifindex}} + chans = cfg.eth.channels_get(ehdr) + + if "combined-max" not in chans: + raise KsftSkipEx("device does not support combined channels") + if chans.get("combined-max", 0) < 2: + raise KsftSkipEx("device does not support 2+ combined channels") + if chans["combined-count"] < 2: + defer(cfg.eth.channels_set, + ehdr | {"combined-count": chans["combined-count"]}) + cfg.eth.channels_set(ehdr | {"combined-count": 2}) + + rings = cfg.eth.rings_get(ehdr) + if 'rx' not in rings or 'tx' not in rings: + raise KsftSkipEx("device does not expose rx/tx ring params") + tx_cur = rings['tx'] + if tx_cur <= MIN_TX_RING: + raise KsftSkipEx("tx ring size already at minimum") + defer(cfg.eth.rings_set, ehdr | {'tx': tx_cur}) + + # Use the smallest Tx ring the driver accepts (32, growing to 1024). + tx_ring = _set_small_tx_ring(cfg, ehdr) + + # Slow completions so the ring stays full after FQ releases packets + napi_defer = f"/sys/class/net/{cfg.ifname}/napi_defer_hard_irqs" + gro_timeout = f"/sys/class/net/{cfg.ifname}/gro_flush_timeout" + _write_sysfs(napi_defer, 100) + _write_sysfs(gro_timeout, 1000000000) + + port = rand_port() + # A single flow must overflow the ring, so send twice the ring depth and + # let fq hold that many packets for the flow. + pkt_count = tx_ring * 2 + mq_handle = _setup_fq_qdisc(cfg, port, target_queue, other_queue, + tx_ring * 2) + + # Size each packet to one MTU (less L3/L4 headers to avoid fragmentation). + pkt_size = cfg.dev['mtu'] - (48 if int(cfg.addr_ipver) == 6 else 28) + + # Each queued skb charges the socket its truesize (~2x the payload), so + # budget the send buffer for the whole in-flight burst. + sock = _create_sotxtime_socket(cfg, pkt_count * pkt_size * 2) + defer(sock.close) + + for delay_ms in [100, 200, 500]: + _send_sotxtime_burst(cfg, sock, port, pkt_count, + delay_ms * 1_000_000, pkt_size) + ksft_pr(f"Sent {pkt_count} SO_TXTIME packets (+{delay_ms}ms)") + time.sleep(delay_ms / 1000 + 0.3) + + backlog = _get_qdisc_backlog(cfg, mq_handle, target_queue) + if backlog: + break + else: + # A device that completes Tx synchronously (e.g. a software/virtual + # driver like netdevsim) never keeps the ring full long enough for a + # backlog to form, so the wake-vs-start behavior can't be exercised. + # Treat that as an expected failure rather than a hard failure. + raise KsftXfailEx("could not build qdisc backlog") + + ksft_pr(f"Backlog before reconfig: {backlog} bytes") + + # Trigger ring reconfig — driver should call wake, not just start. + # Grow back to the original size so the driver actually switches channels + # (setting the current size is a no-op the driver short-circuits). + cfg.eth.rings_set(ehdr | {'tx': tx_cur}) + + # Let completions proceed normally + _write_sysfs(napi_defer, 0) + _write_sysfs(gro_timeout, 0) + + # Poll for backlog to drain + for _ in range(100): + backlog = _get_qdisc_backlog(cfg, mq_handle, target_queue) + if not backlog: + break + time.sleep(0.1) + + ksft_eq(0, backlog, + comment=f"qdisc backlog stuck on queue {target_queue} " + f"after ring reconfig") + + def main() -> None: """ Ksft boiler plate main """ - with NetDrvEpEnv(__file__) as cfg: + with NetDrvEpEnv(__file__, queue_count=2) as cfg: cfg.eth = EthtoolFamily() ksft_run([channels, - ringparam], + ringparam, + reconfig_tx_stall], args=(cfg, )) ksft_exit() diff --git a/tools/testing/selftests/drivers/net/settings b/tools/testing/selftests/drivers/net/settings new file mode 100644 index 000000000000..eef533824a3c --- /dev/null +++ b/tools/testing/selftests/drivers/net/settings @@ -0,0 +1 @@ +timeout=360 diff --git a/tools/testing/selftests/drivers/net/shaper.py b/tools/testing/selftests/drivers/net/shaper.py index e39d270e688d..a53316726f69 100755 --- a/tools/testing/selftests/drivers/net/shaper.py +++ b/tools/testing/selftests/drivers/net/shaper.py @@ -1,14 +1,54 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0 +# pylint: disable=too-many-lines import errno +import glob from lib.py import ksft_run, ksft_exit -from lib.py import ksft_eq, ksft_raises, ksft_true, KsftSkipEx +from lib.py import ksft_eq, ksft_true, ksft_raises, KsftSkipEx from lib.py import EthtoolFamily, NetshaperFamily from lib.py import NetDrvEnv from lib.py import NlError -from lib.py import cmd +from lib.py import cmd, defer + +def _delete_shaper(cfg, nl_shaper, handle) -> None: + """ Delete the shaper identified by handle, ignoring a missing-shaper error. """ + try: + nl_shaper.delete({'ifindex': cfg.ifindex, + 'handle': handle}) + except NlError as e: + if e.error != errno.ENOENT: + raise + +def _require_queues(cfg, count): + """ Return the netdev TX queue count, skipping the test if fewer than count exist. """ + qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/tx-*")) + if qcnt < count: + raise KsftSkipEx(f"netdev has {qcnt} queues, {count} required") + return qcnt + +def _cap_get(cfg, nl_shaper, scope): + """ Return the shaper capabilities for the given scope, caching them on cfg. """ + if not hasattr(cfg, 'cap_cache'): + cfg.cap_cache = {} + if scope not in cfg.cap_cache: + cfg.cap_cache[scope] = nl_shaper.cap_get({'ifindex': cfg.ifindex, + 'scope': scope}) + + return cfg.cap_cache[scope] + +def _require_caps(cfg, nl_shaper, scope, caps, msg) -> None: + """ Skip the test unless the given scope advertises all the required caps. """ + try: + supported = _cap_get(cfg, nl_shaper, scope) + except NlError as e: + if e.error == errno.EOPNOTSUPP: + raise KsftSkipEx(f"{scope} scope shapers not supported by the device") + raise + + if not set(caps).issubset(supported): + raise KsftSkipEx(msg) def get_shapers(cfg, nl_shaper) -> None: try: @@ -44,17 +84,8 @@ def set_qshapers(cfg, nl_shaper) -> None: if not 'support-bw-max' in caps or not 'support-metric-bps' in caps: raise KsftSkipEx("device does not support queue scope shapers with bw_max and metric bps") - cfg.queues = True; - netnl = EthtoolFamily() - channels = netnl.channels_get({'header': {'dev-index': cfg.ifindex}}) - if channels['combined-count'] == 0: - cfg.rx_type = 'rx' - cfg.nr_queues = channels['rx-count'] - else: - cfg.rx_type = 'combined' - cfg.nr_queues = channels['combined-count'] - if cfg.nr_queues < 3: - raise KsftSkipEx(f"device does not support enough queues min 3 found {cfg.nr_queues}") + _require_queues(cfg, 3) + cfg.queues = True nl_shaper.set({'ifindex': cfg.ifindex, 'handle': {'scope': 'queue', 'id': 1}, @@ -137,77 +168,370 @@ def del_nshapers(cfg, nl_shaper) -> None: shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(len(shapers), 0) -def basic_groups(cfg, nl_shaper) -> None: - if not cfg.netdev: - raise KsftSkipEx("netdev shaper not supported by the device") - if cfg.nr_queues < 3: - raise KsftSkipEx(f"netdev does not have enough queues min 3 reported {cfg.nr_queues}") +def set_all_supported_attrs(cfg, nl_shaper) -> None: + """ Set every queue-scope attribute the device advertises and verify the read-back. """ + _require_queues(cfg, 1) - try: - caps = nl_shaper.cap_get({'ifindex': cfg.ifindex, - 'scope':'queue'}) - except NlError as e: - if e.error == 95: - raise KsftSkipEx("shapers not supported by the device") - raise - if not 'support-weight' in caps: - raise KsftSkipEx("device does not support queue scope shapers with weight") + _require_caps(cfg, nl_shaper, 'queue', [], + "queue scope shapers not supported by the device") + caps = _cap_get(cfg, nl_shaper, 'queue') + + attrs = {'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}} + expected = {'ifindex': cfg.ifindex, + 'parent': {'scope': 'netdev'}, + 'handle': {'scope': 'queue', 'id': 0}} + + rate_attrs = {'support-bw-min': ('bw-min', 10000, 100), + 'support-bw-max': ('bw-max', 20000, 200), + 'support-burst': ('burst', 3000, 30)} + rate_attr_supported = any(cap in caps for cap in rate_attrs) + bps_supported = 'support-metric-bps' in caps + pps_supported = 'support-metric-pps' in caps + + def add_rate_attrs(metric, value_idx) -> None: + attrs['metric'] = metric + expected['metric'] = metric + for cap, (attr, bps_value, pps_value) in rate_attrs.items(): + if cap not in caps: + continue + + value = bps_value if value_idx == 0 else pps_value + attrs[attr] = value + expected[attr] = value + + if rate_attr_supported: + if bps_supported: + add_rate_attrs('bps', 0) + elif pps_supported: + add_rate_attrs('pps', 1) + + if 'support-priority' in caps: + attrs['priority'] = 1 + expected['priority'] = 1 + if 'support-weight' in caps: + attrs['weight'] = 2 + expected['weight'] = 2 + + if len(attrs) == 2: + raise KsftSkipEx("device does not advertise any supported queue shaper attributes") + + nl_shaper.set(attrs) + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper, expected) + + if rate_attr_supported and bps_supported and pps_supported: + add_rate_attrs('pps', 1) + nl_shaper.set(attrs) + + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper, expected) + + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + +def invalid_set_preserves_state(cfg, nl_shaper) -> None: + """ Verify a rejected .set leaves the existing shaper configuration unchanged. """ + nq = _require_queues(cfg, 1) + _require_caps(cfg, nl_shaper, 'queue', + ['support-bw-max', 'support-metric-bps'], + "device does not support queue scope bw_max with bps metric") + + initial = {'ifindex': cfg.ifindex, + 'parent': {'scope': 'netdev'}, + 'handle': {'scope': 'queue', 'id': 0}, + 'metric': 'bps', + 'bw-max': 10000} + nl_shaper.set({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}, + 'metric': 'bps', + 'bw-max': 10000}) + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + + with ksft_raises(NlError): + nl_shaper.set({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': 0}, + 'metric': 'bps', + 'bw-max': 20000}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper, initial) + + with ksft_raises(NlError): + nl_shaper.set({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': nq}, + 'metric': 'bps', + 'bw-max': 20000}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper, initial) + + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + +def mixed_parent_group_requires_parent(cfg, nl_shaper) -> None: + r"""Grouping leaves from different nodes requires an explicit parent. + + netdev netdev + / \ parent=netdev + N1 N2 group N + | | {Q0,Q1} / \ + Q0 Q1 -------> Q0 Q1 + + Without an explicit parent the group is rejected; parent=netdev + collapses the leaves into one new node. + """ + _require_queues(cfg, 2) + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps'], + "device does not support node scope shapers with bw_max and metric bps") + _require_caps(cfg, nl_shaper, 'queue', + ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + n1_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 10000}) + n1_id = n1_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + + n2_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 2}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 20000}) + n2_id = n2_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 1}) + + with ksft_raises(NlError): + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 3}, + {'handle': {'scope': 'queue', 'id': 1}, + 'weight': 4}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 30000}) + + shaper_q0 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper_q0, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n1_id}, + 'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}) + shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 1}}) + ksft_eq(shaper_q1, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n2_id}, + 'handle': {'scope': 'queue', 'id': 1}, + 'weight': 2}) node_handle = nl_shaper.group({ - 'ifindex': cfg.ifindex, - 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, - 'weight': 1}, - {'handle': {'scope': 'queue', 'id': 2}, - 'weight': 2}], - 'handle': {'scope':'netdev'}, - 'metric': 'bps', - 'bw-max': 10000}) + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 3}, + {'handle': {'scope': 'queue', 'id': 1}, + 'weight': 4}], + 'handle': {'scope':'node'}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': 30000}) + node_id = node_handle['handle']['id'] + + for old_id in (n1_id, n2_id): + with ksft_raises(NlError): + nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': old_id}}) + + shaper_q0 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper_q0, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': 0}, + 'weight': 3}) + shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 1}}) + ksft_eq(shaper_q1, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': 1}, + 'weight': 4}) + + for i in range(2): + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + +def recursive_empty_node_cleanup(cfg, nl_shaper) -> None: + r"""Deleting the last leaf recursively removes the emptied ancestors. + + netdev netdev + | del Q0 + N1 ------> (N1 and N2 removed too) + | + N2 + | + Q0 + """ + _require_queues(cfg, 1) + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support nested node scope shapers") + _require_caps(cfg, nl_shaper, 'queue', + ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + n1_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 10000}) + n1_id = n1_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + + n2_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'parent': {'scope': 'node', 'id': n1_id}, + 'metric': 'bps', + 'bw-max': 5000}) + n2_id = n2_handle['handle']['id'] + + shaper_q0 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + ksft_eq(shaper_q0, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n2_id}, + 'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}) + + nl_shaper.delete({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 0}}) + + for handle in ({'scope': 'queue', 'id': 0}, + {'scope': 'node', 'id': n2_id}, + {'scope': 'node', 'id': n1_id}): + with ksft_raises(NlError): + nl_shaper.get({'ifindex': cfg.ifindex, 'handle': handle}) + + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + +def _group_under_netdev(cfg, nl_shaper, bw_max=None): + r"""Group queues under a netdev-scope node; caller owns node teardown. + + netdev netdev + / \ del Q1,Q2 + Q1 Q2 -------> (netdev node persists) + """ + group_args = { + 'ifindex': cfg.ifindex, + 'leaves': [{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 2}, + 'weight': 2}], + 'handle': {'scope': 'netdev'}} + if bw_max: + group_args['metric'] = 'bps' + group_args['bw-max'] = bw_max + + node_handle = nl_shaper.group(group_args) ksft_eq(node_handle, {'ifindex': cfg.ifindex, 'handle': {'scope': 'netdev'}}) + del_node = defer(_delete_shaper, cfg, nl_shaper, {'scope': 'netdev'}) + del_queues = [defer(_delete_shaper, cfg, nl_shaper, + {'scope': 'queue', 'id': qid}) + for qid in (1, 2)] + shaper = nl_shaper.get({'ifindex': cfg.ifindex, 'handle': {'scope': 'queue', 'id': 1}}) ksft_eq(shaper, {'ifindex': cfg.ifindex, 'parent': {'scope': 'netdev'}, 'handle': {'scope': 'queue', 'id': 1}, - 'weight': 1 }) + 'weight': 1}) + for dq in del_queues: + dq.exec() - nl_shaper.delete({'ifindex': cfg.ifindex, - 'handle': {'scope': 'queue', 'id': 2}}) - nl_shaper.delete({'ifindex': cfg.ifindex, - 'handle': {'scope': 'queue', 'id': 1}}) + # Caller owns the node teardown so it can verify the netdev-scope node + # survives leaf deletion before removing it. + return del_node + +def basic_groups(cfg, nl_shaper) -> None: + r"""Group queues under a netdev-scope node, then tear it down. + + netdev + / \ + Q1 Q2 + """ + _require_queues(cfg, 3) + + _require_caps(cfg, nl_shaper, 'netdev', [], "netdev scope not supported by the device") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "queue scope not supported with nesting and weight") + + del_node = _group_under_netdev(cfg, nl_shaper) + + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(shapers, [{'ifindex': cfg.ifindex, + 'handle': {'scope': 'netdev'}}]) + + del_node.exec() + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + +def basic_groups_with_rate(cfg, nl_shaper) -> None: + r"""Rate-limited netdev-scope node outlives deletion of its leaves. + + netdev[10kbps] netdev[10kbps] + / \ del Q1,Q2 + Q1 Q2 -------> (node persists) + """ + bw_max = 10000 + + _require_queues(cfg, 3) + + _require_caps(cfg, nl_shaper, 'netdev', ['support-bw-max', 'support-metric-bps'], + "device does not support netdev scope rate limiting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support queue scope shapers with nesting and weight") + + del_node = _group_under_netdev(cfg, nl_shaper, bw_max=bw_max) # Deleting all the leaves shaper does not affect the node one # when the latter has 'netdev' scope. shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) - ksft_eq(len(shapers), 1) + ksft_eq(shapers, [{'ifindex': cfg.ifindex, + 'handle': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': bw_max}]) - nl_shaper.delete({'ifindex': cfg.ifindex, - 'handle': {'scope': 'netdev'}}) + del_node.exec() def qgroups(cfg, nl_shaper) -> None: - if cfg.nr_queues < 4: - raise KsftSkipEx(f"netdev does not have enough queues min 4 reported {cfg.nr_queues}") - try: - caps = nl_shaper.cap_get({'ifindex': cfg.ifindex, - 'scope':'node'}) - except NlError as e: - if e.error == 95: - raise KsftSkipEx("shapers not supported by the device") - raise - if not 'support-bw-max' in caps or not 'support-metric-bps' in caps: - raise KsftSkipEx("device does not support node scope shapers with bw_max and metric bps") - try: - caps = nl_shaper.cap_get({'ifindex': cfg.ifindex, - 'scope':'queue'}) - except NlError as e: - if e.error == 95: - raise KsftSkipEx("shapers not supported by the device") - raise - if not 'support-nesting' in caps or not 'support-weight' in caps or not 'support-metric-bps' in caps: - raise KsftSkipEx("device does not support nested queue scope shapers with weight") + _require_queues(cfg, 4) + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps'], + "device does not support node scope shapers with bw_max and metric bps") + _require_caps(cfg, nl_shaper, 'queue', + ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") - cfg.groups = True; node_handle = nl_shaper.group({ 'ifindex': cfg.ifindex, 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, @@ -284,18 +608,116 @@ def qgroups(cfg, nl_shaper) -> None: shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(len(shapers), 0) +def set_node_shaper(cfg, nl_shaper) -> None: + """ Verify a node-scope shaper rate can be updated via .set. """ + _require_queues(cfg, 2) + _require_caps(cfg, nl_shaper, 'node', ['support-bw-max', 'support-metric-bps'], + "device does not support node scope shapers with bw_max and metric bps") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + node_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 10000}) + node_id = node_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 1}) + + # Update the node's rate via .set + nl_shaper.set({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}, + 'metric': 'bps', + 'bw-max': 20000}) + + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': 20000}) + + # Cleanup + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 1}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + +def group_update_rate(cfg, nl_shaper) -> None: + """ Verify re-grouping a node updates its rate while leaving the leaves untouched. """ + _require_queues(cfg, 3) + _require_caps(cfg, nl_shaper, 'node', ['support-bw-max', 'support-metric-bps'], + "device does not support node scope shapers with bw_max and metric bps") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + # Create node with Q1, Q2 at bw_max=10000 + node_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': 10000}) + node_id = node_handle['handle']['id'] + for i in range(1, 3): + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': i}) + + # Update rate via .group on the same node + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}], + 'handle': {'scope':'node', 'id': node_id}, + 'metric': 'bps', + 'bw-max': 50000}) + + # Verify rate updated + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': 50000}) + + # Verify leaves unchanged + shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 1}}) + ksft_eq(shaper_q1, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}) + shaper_q2 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 2}}) + ksft_eq(shaper_q2, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}) + + # Make sure we only have 3 shapers including 2 queues and the node + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 3) + + # Cleanup + for i in range(1, 3): + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + def delegation(cfg, nl_shaper) -> None: - if not cfg.groups: - raise KsftSkipEx("device does not support node scope") - try: - caps = nl_shaper.cap_get({'ifindex': cfg.ifindex, - 'scope':'node'}) - except NlError as e: - if e.error == 95: - raise KsftSkipEx("node scope shapers not supported by the device") - raise - if not 'support-nesting' in caps: - raise KsftSkipEx("device does not support node scope shapers nesting") + _require_queues(cfg, 4) + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support node scope shapers with bw_max, metric bps and nesting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") node_handle = nl_shaper.group({ 'ifindex': cfg.ifindex, @@ -375,20 +797,466 @@ def delegation(cfg, nl_shaper) -> None: shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(len(shapers), 0) +def nested_depth_limit(cfg, nl_shaper) -> None: + r"""Nest nodes as deep as the device allows to find the max depth. + + netdev + | + N1 -- Q1 + | + N2 -- Q2 + | + N3 -- Q3 + : (deepen until the driver rejects) + """ + bw_max = 10000 + + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support node scope shapers with bw_max, metric bps and nesting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + nq = _require_queues(cfg, 3) + + node_ids = [] + cleanups = [] + queue_id = 1 + max_depth = 0 + limit_err = None + + # Create initial node with a queue leaf + node_id = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves': [{'handle': {'scope': 'queue', 'id': queue_id}, + 'weight': 1}], + 'handle': {'scope': 'node'}, + 'metric': 'bps', + 'bw-max': bw_max})['handle']['id'] + node_ids.append(node_id) + cleanups.append(defer(_delete_shaper, cfg, nl_shaper, + {'scope': 'node', 'id': node_id})) + cleanups.append(defer(_delete_shaper, cfg, nl_shaper, + {'scope': 'queue', 'id': queue_id})) + max_depth = 1 + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': bw_max}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': queue_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': queue_id}, + 'weight': 1}) + queue_id += 1 + + # Keep nesting deeper until the driver rejects or queues run out. + while queue_id < nq: + parent_id = node_ids[-1] + try: + node_id = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves': [{'handle': {'scope': 'queue', + 'id': queue_id}, + 'weight': 1}], + 'handle': {'scope': 'node'}, + 'parent': {'scope': 'node', + 'id': parent_id}, + 'metric': 'bps', + 'bw-max': bw_max})['handle']['id'] + except NlError as e: + # Only treat "cannot nest deeper" errors as the depth limit; + # drivers report it differently (EOPNOTSUPP/ENOSPC/E2BIG/EINVAL). + # Anything else (ENOMEM, EIO, EPERM, driver bug) is a real failure. + if e.error not in (errno.EOPNOTSUPP, errno.ENOSPC, + errno.E2BIG, errno.EINVAL): + raise + limit_err = e + break + + node_ids.append(node_id) + cleanups.append(defer(_delete_shaper, cfg, nl_shaper, + {'scope': 'node', 'id': node_id})) + cleanups.append(defer(_delete_shaper, cfg, nl_shaper, + {'scope': 'queue', 'id': queue_id})) + max_depth += 1 + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node_id}, + 'parent': {'scope': 'node', 'id': parent_id}, + 'metric': 'bps', + 'bw-max': bw_max}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', + 'id': queue_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node_id}, + 'handle': {'scope': 'queue', 'id': queue_id}, + 'weight': 1}) + queue_id += 1 + + if limit_err: + print(f"# max nesting depth supported: {max_depth} (errno {limit_err.error})") + else: + print(f"# max nesting depth tested: {max_depth}") + ksft_true(max_depth >= 2, + f"max nesting depth: {max_depth}") + + # Cleanup: exec the deferred deletes in reverse creation order, so each + # queue leaf and deeper node is removed before its parent node. + for cleanup in reversed(cleanups): + cleanup.exec() + ksft_eq(len(nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)), 0) + +def delete_child_reparent(cfg, nl_shaper) -> None: + r"""Deleting a child node reparents its queue leaf to the parent. + + netdev netdev + | | + N1 del N2 N1 + / | \ -----> / | \ + Q1 Q2 N2 Q1 Q2 Q3 + | + Q3 + """ + n1_bw_max = 10000 + n2_bw_max = 5000 + + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support node scope shapers with bw_max, metric bps and nesting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + _require_queues(cfg, 4) + + # Create parent node N1 with Q1, Q2 + n1_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': n1_bw_max}) + n1_id = n1_handle['handle']['id'] + for i in range(1, 3): + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': i}) + + # Create child node N2 under N1 with Q3 + n2_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'parent': {'scope': 'node', 'id': n1_id}, + 'metric': 'bps', + 'bw-max': n2_bw_max}) + n2_id = n2_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 3}) + + # Delete child N2 - Q3 should reparent to N1 + nl_shaper.delete({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n2_id}}) + + with ksft_raises(NlError): + nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n2_id}}) + + shaper_n1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n1_id}}) + ksft_eq(shaper_n1, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n1_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': n1_bw_max}) + shaper_q3 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 3}}) + ksft_eq(shaper_q3, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n1_id}, + 'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}) + + # Cleanup + for i in range(1, 4): + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + +def move_queue_between_nodes(cfg, nl_shaper) -> None: + r"""Move a queue between nodes by re-grouping the destination node. + + netdev netdev + / \ .group N2 / \ + N1 N2 {Q1,Q3} N1 N2 + / \ | -------> | / \ + Q1 Q2 Q3 Q2 Q1 Q3 + """ + n1_bw_max = 10000 + n2_bw_max = 20000 + + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support node scope shapers with bw_max, metric bps and nesting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + _require_queues(cfg, 4) + + # Create N1 with Q1, Q2 + n1_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': n1_bw_max}) + n1_id = n1_handle['handle']['id'] + for i in range(1, 3): + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': i}) + + # Create N2 with Q3 + n2_handle = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': n2_bw_max}) + n2_id = n2_handle['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 3}) + + # Move Q1 from N1 to N2 by re-grouping N2 with Q1, Q3 + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 2}, + {'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}], + 'handle': {'scope':'node', 'id': n2_id}, + 'metric': 'bps', + 'bw-max': n2_bw_max}) + + shaper_n1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n1_id}}) + ksft_eq(shaper_n1, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n1_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': n1_bw_max}) + shaper_n2 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n2_id}}) + ksft_eq(shaper_n2, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': n2_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': n2_bw_max}) + + # Verify Q1 moved to N2 + shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 1}}) + ksft_eq(shaper_q1, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n2_id}, + 'handle': {'scope': 'queue', 'id': 1}, + 'weight': 2}) + + # Verify Q2 still under N1 + shaper_q2 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 2}}) + ksft_eq(shaper_q2, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n1_id}, + 'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}) + + # Verify Q3 remained under N2 + shaper_q3 = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 3}}) + ksft_eq(shaper_q3, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': n2_id}, + 'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}) + + # Cleanup + for i in range(1, 4): + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + +def reject_reparenting(cfg, nl_shaper) -> None: + r"""Reject reparenting an existing node; the hierarchy stays intact. + + netdev + / \ rejected: N3 -> netdev + N1 N2 rejected: N1 -> N2 + / \ | (both EOPNOTSUPP) + Q1 N3 Q2 + | + Q3 + """ + node1_bw_max = 10000 + node2_bw_max = 5000 + node3_bw_max = 20000 + + _require_caps(cfg, nl_shaper, 'node', + ['support-bw-max', 'support-metric-bps', 'support-nesting'], + "device does not support node scope shapers with bw_max, metric bps and nesting") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + + _require_queues(cfg, 4) + + # Create Node1 under netdev with Q1. + node1_id = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': node1_bw_max})['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 1}) + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'node', 'id': node1_id}) + + # Create Node2 under netdev with Q2. + node2_id = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 2}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': node2_bw_max})['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 2}) + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'node', 'id': node2_id}) + + # Create Node3 nested under Node1 with Q3. + node3_id = nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}], + 'handle': {'scope':'node'}, + 'metric': 'bps', + 'bw-max': node3_bw_max, + 'parent': {'scope': 'node', 'id': node1_id}})['handle']['id'] + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 3}) + defer(_delete_shaper, cfg, nl_shaper, {'scope': 'node', 'id': node3_id}) + + # Reparenting a nested node up to netdev must fail. + with ksft_raises(NlError) as cm: + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}], + 'handle': {'scope':'node', 'id': node3_id}, + 'parent': {'scope': 'netdev'}}) + if cm.exception: + ksft_eq(cm.exception.error, errno.EOPNOTSUPP) + + # Reparenting a node under another node must fail as well. + with ksft_raises(NlError) as cm: + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 1}], + 'handle': {'scope':'node', 'id': node1_id}, + 'parent': {'scope': 'node', 'id': node2_id}}) + if cm.exception: + ksft_eq(cm.exception.error, errno.EOPNOTSUPP) + + # Updating a node with the same parent must succeed. + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, + 'weight': 5}], + 'handle': {'scope':'node', 'id': node1_id}, + 'parent': {'scope': 'netdev'}}) + + # Updating a node without specifying the parent must succeed. + nl_shaper.group({ + 'ifindex': cfg.ifindex, + 'leaves':[{'handle': {'scope': 'queue', 'id': 2}, + 'weight': 7}], + 'handle': {'scope':'node', 'id': node2_id}}) + + # The rejected reparents must have left the hierarchy intact. + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node1_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node1_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': node1_bw_max}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node2_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node2_id}, + 'parent': {'scope': 'netdev'}, + 'metric': 'bps', + 'bw-max': node2_bw_max}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node3_id}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'handle': {'scope': 'node', 'id': node3_id}, + 'parent': {'scope': 'node', 'id': node1_id}, + 'metric': 'bps', + 'bw-max': node3_bw_max}) + + # Verify the leaf weights were updated and parents unchanged. + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 1}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node1_id}, + 'handle': {'scope': 'queue', 'id': 1}, + 'weight': 5}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 2}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node2_id}, + 'handle': {'scope': 'queue', 'id': 2}, + 'weight': 7}) + shaper = nl_shaper.get({'ifindex': cfg.ifindex, + 'handle': {'scope': 'queue', 'id': 3}}) + ksft_eq(shaper, {'ifindex': cfg.ifindex, + 'parent': {'scope': 'node', 'id': node3_id}, + 'handle': {'scope': 'queue', 'id': 3}, + 'weight': 1}) + + # Cleanup. Delete the nodes explicitly instead of relying on the + # empty-node auto-delete: a kernel that wrongly accepts a reparent may + # mishandle the leaf accounting and leave a node behind. Removing them + # by handle keeps a failing run from leaking state into later tests. + for i in range(1, 4): + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) + for nid in (node1_id, node2_id, node3_id): + _delete_shaper(cfg, nl_shaper, {'scope': 'node', 'id': nid}) + shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) + ksft_eq(len(shapers), 0) + def queue_update(cfg, nl_shaper) -> None: - if cfg.nr_queues < 4: - raise KsftSkipEx(f"netdev does not have enough queues min 4 reported {cfg.nr_queues}") + nq = _require_queues(cfg, 4) if not cfg.queues: raise KsftSkipEx("device does not support queue scope") + netnl = EthtoolFamily() + channels = netnl.channels_get({'header': {'dev-index': cfg.ifindex}}) + ch_type = 'combined' if channels['combined-count'] else 'tx' + for i in range(3): nl_shaper.set({'ifindex': cfg.ifindex, 'handle': {'scope': 'queue', 'id': i}, 'metric': 'bps', 'bw-max': (i + 1) * 1000}) + defer(cmd, f"ethtool -L {cfg.dev['ifname']} {ch_type} {nq}") + # Delete a channel, with no shapers configured on top of the related # queue: no changes expected - cmd(f"ethtool -L {cfg.dev['ifname']} {cfg.rx_type} 3", timeout=10) + cmd(f"ethtool -L {cfg.dev['ifname']} {ch_type} 3") shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(shapers, [{'ifindex': cfg.ifindex, 'parent': {'scope': 'netdev'}, @@ -408,7 +1276,7 @@ def queue_update(cfg, nl_shaper) -> None: # Delete a channel, with a shaper configured on top of the related # queue: the shaper must be deleted, too - cmd(f"ethtool -L {cfg.dev['ifname']} {cfg.rx_type} 2", timeout=10) + cmd(f"ethtool -L {cfg.dev['ifname']} {ch_type} 2") shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(shapers, [{'ifindex': cfg.ifindex, @@ -423,7 +1291,7 @@ def queue_update(cfg, nl_shaper) -> None: 'bw-max': 2000}]) # Restore the original channels number, no expected changes - cmd(f"ethtool -L {cfg.dev['ifname']} {cfg.rx_type} {cfg.nr_queues}", timeout=10) + cmd(f"ethtool -L {cfg.dev['ifname']} {ch_type} {nq}") shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) ksft_eq(shapers, [{'ifindex': cfg.ifindex, 'parent': {'scope': 'netdev'}, @@ -443,34 +1311,57 @@ def queue_update(cfg, nl_shaper) -> None: def dup_leaves(cfg, nl_shaper) -> None: """ Ensure that the kernel rejects duplicate leaves. """ - if not cfg.groups: - raise KsftSkipEx("device does not support node scope") + _require_caps(cfg, nl_shaper, 'node', ['support-bw-max', 'support-metric-bps'], + "device does not support node scope shapers with bw_max and metric bps") + _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], + "device does not support nested queue scope shapers with weight") + node_handle = None with ksft_raises(NlError) as cm: - nl_shaper.group({ + node_handle = nl_shaper.group({ 'ifindex': cfg.ifindex, - 'leaves':[{'handle': {'scope': 'queue', 'id': 0}}, - {'handle': {'scope': 'queue', 'id': 0}}], + 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, + 'weight': 1}, + {'handle': {'scope': 'queue', 'id': 0}, + 'weight': 2}], 'handle': {'scope':'node'}, 'metric': 'bps', 'bw-max': 10000}) + + # Clean up in case the kernel wrongly accepted the request. + if node_handle: + _delete_shaper(cfg, nl_shaper, node_handle['handle']) + _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 0}) + + # ksft_raises() has already recorded the failure if nothing was raised. + if cm.exception is None: + return ksft_eq(cm.exception.error, errno.EINVAL) def main() -> None: with NetDrvEnv(__file__, queue_count=4) as cfg: cfg.queues = False cfg.netdev = False - cfg.groups = False - cfg.nr_queues = 0 ksft_run([get_shapers, get_caps, set_qshapers, del_qshapers, set_nshapers, del_nshapers, + set_all_supported_attrs, + invalid_set_preserves_state, + mixed_parent_group_requires_parent, + recursive_empty_node_cleanup, basic_groups, + basic_groups_with_rate, qgroups, + set_node_shaper, + group_update_rate, delegation, + nested_depth_limit, + delete_child_reparent, + move_queue_between_nodes, + reject_reparenting, dup_leaves, queue_update], args=(cfg, NetshaperFamily())) diff --git a/tools/testing/selftests/drivers/net/so_txtime.py b/tools/testing/selftests/drivers/net/so_txtime.py index adf6c848d6d8..a097fae0b335 100755 --- a/tools/testing/selftests/drivers/net/so_txtime.py +++ b/tools/testing/selftests/drivers/net/so_txtime.py @@ -12,6 +12,7 @@ import time from lib.py import ksft_exit, ksft_run, ksft_variants from lib.py import KsftNamedVariant, KsftSkipEx from lib.py import NetDrvEpEnv, bkg, cmd, defer, tc +from lib.py import CmdExitFailure def test_so_txtime(cfg, clockid, ipver, args_tx, args_rx, expect_success): @@ -27,7 +28,7 @@ def test_so_txtime(cfg, clockid, ipver, args_tx, args_rx, expect_success): cmd_addr = f"-S {cfg.addr_v[ipver]} -D {cfg.remote_addr_v[ipver]}" cmd_args = f"-{ipver} -c {clockid} -t {tstart} {cmd_addr}" cmd_rx = f"{cfg.bin_remote} {cmd_args} {args_rx} -r" - cmd_tx = f"{cfg.bin_local} {cmd_args} {args_tx}" + cmd_tx = f"{cfg.bin_local} -m 100 {cmd_args} {args_tx}" expect_fail = not expect_success if slow_machine: @@ -45,7 +46,11 @@ def _qdisc_setup(ifname, qdisc, optargs=""): """ orig = tc(f"qdisc show dev {ifname} root", json=True)[0].get("kind", None) defer(tc, f"qdisc replace dev {ifname} root {orig}") - tc(f"qdisc replace dev {ifname} root {qdisc} {optargs}") + try: + tc(f"qdisc del dev {ifname} root") + except CmdExitFailure: + pass + tc(f"qdisc replace dev {ifname} root handle 1: {qdisc} {optargs}") def _test_variants_fq(): @@ -96,11 +101,21 @@ def _test_variants_etf(): def test_so_txtime_etf(cfg, ipver, args_tx, args_rx, expect_fail): """Run all variants of etf tests.""" cfg.require_ipver(ipver) + + # root qdisc for background traffic (e.g., bkg()) + _qdisc_setup(cfg.ifname, "prio") + + # leaf ETF qdisc only for intended packets try: - _qdisc_setup(cfg.ifname, "etf", "clockid CLOCK_TAI delta 400000") + etf_args = "clockid CLOCK_TAI delta 400000" + tc(f"qdisc add dev {cfg.ifname} parent 1:1 handle 10: etf {etf_args}") except Exception as e: raise KsftSkipEx("tc does not support qdisc etf. skipping") from e + # redirect mark 100 to leaf + filter_args = "protocol all handle 100 fw flowid 1:1" + tc(f"filter add dev {cfg.ifname} parent 1: {filter_args}") + test_so_txtime(cfg, "tai", ipver, args_tx, args_rx, expect_fail) diff --git a/tools/testing/selftests/drivers/net/xdp.py b/tools/testing/selftests/drivers/net/xdp.py index 2ad5932299e8..0369929f3c51 100755 --- a/tools/testing/selftests/drivers/net/xdp.py +++ b/tools/testing/selftests/drivers/net/xdp.py @@ -172,25 +172,45 @@ def _test_pass(cfg, bpf_info, msg_sz): ksft_eq(stats[XDPStats.RX.value], stats[XDPStats.PASS.value], "RX and PASS stats mismatch") -def test_xdp_native_pass_sb(cfg): +_ipvers = [ + KsftNamedVariant("ipv4", "4"), + KsftNamedVariant("ipv6", "6"), +] + + +def _set_ipver_defer_restore(cfg, ipver): + old_ipver = cfg.addr_ipver + cfg.set_ipver(ipver) + defer(cfg.set_ipver, old_ipver) + + +@ksft_variants(_ipvers) +def test_xdp_native_pass_sb(cfg, ipver): """ Tests the XDP_PASS action for single buffer case. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). """ + _set_ipver_defer_restore(cfg, ipver) + bpf_info = BPFProgInfo("xdp_prog", "xdp_native.bpf.o", "xdp", 1500) _test_pass(cfg, bpf_info, 256) -def test_xdp_native_pass_mb(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_pass_mb(cfg, ipver): """ Tests the XDP_PASS action for a multi-buff size. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). """ + _set_ipver_defer_restore(cfg, ipver) + bpf_info = BPFProgInfo("xdp_prog_frags", "xdp_native.bpf.o", "xdp.frags", 9000) _test_pass(cfg, bpf_info, 8000) @@ -219,25 +239,33 @@ def _test_drop(cfg, bpf_info, msg_sz): ksft_eq(stats[XDPStats.RX.value], stats[XDPStats.DROP.value], "RX and DROP stats mismatch") -def test_xdp_native_drop_sb(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_drop_sb(cfg, ipver): """ Tests the XDP_DROP action for a signle-buff case. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). """ + _set_ipver_defer_restore(cfg, ipver) + bpf_info = BPFProgInfo("xdp_prog", "xdp_native.bpf.o", "xdp", 1500) _test_drop(cfg, bpf_info, 256) -def test_xdp_native_drop_mb(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_drop_mb(cfg, ipver): """ Tests the XDP_DROP action for a multi-buff case. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). """ + _set_ipver_defer_restore(cfg, ipver) + bpf_info = BPFProgInfo("xdp_prog_frags", "xdp_native.bpf.o", "xdp.frags", 9000) _test_drop(cfg, bpf_info, 8000) @@ -287,13 +315,17 @@ def _test_xdp_native_tx(cfg, bpf_info, payload_lens): ksft_eq(stats[XDPStats.TX.value], expected_pkts, "TX stats mismatch") -def test_xdp_native_tx_sb(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_tx_sb(cfg, ipver): """ Tests the XDP_TX action for a single-buff case. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). """ + _set_ipver_defer_restore(cfg, ipver) + bpf_info = BPFProgInfo("xdp_prog", "xdp_native.bpf.o", "xdp", 1500) # Ensure there's enough room for an ETH / IP / UDP header @@ -302,13 +334,17 @@ def test_xdp_native_tx_sb(cfg): _test_xdp_native_tx(cfg, bpf_info, [0, 1500 // 2, 1500 - pkt_hdr_len]) -def test_xdp_native_tx_mb(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_tx_mb(cfg, ipver): """ Tests the XDP_TX action for a multi-buff case. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). """ + _set_ipver_defer_restore(cfg, ipver) + bpf_info = BPFProgInfo("xdp_prog_frags", "xdp_native.bpf.o", "xdp.frags", 9000) # The first packet ensures we exercise the fragmented code path. @@ -447,13 +483,17 @@ def _test_xdp_native_tail_adjst(cfg, pkt_sz_lst, offset_lst): return {"status": "pass"} -def test_xdp_native_adjst_tail_grow_data(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_adjst_tail_grow_data(cfg, ipver): """ Tests the XDP tail adjustment by growing packet data. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). """ + _set_ipver_defer_restore(cfg, ipver) + pkt_sz_lst = [512, 1024, 2048] offset_lst = [1, 16, 32, 64, 128, 256] res = _test_xdp_native_tail_adjst( @@ -465,13 +505,17 @@ def test_xdp_native_adjst_tail_grow_data(cfg): _validate_res(res, offset_lst, pkt_sz_lst) -def test_xdp_native_adjst_tail_shrnk_data(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_adjst_tail_shrnk_data(cfg, ipver): """ Tests the XDP tail adjustment by shrinking packet data. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). """ + _set_ipver_defer_restore(cfg, ipver) + pkt_sz_lst = [512, 1024, 2048] offset_lst = [-16, -32, -64, -128, -256] res = _test_xdp_native_tail_adjst( @@ -535,7 +579,7 @@ def _test_xdp_native_head_adjst(cfg, prog, pkt_sz_lst, offset_lst): # after we eat into it. We send large-enough packets, but if HDS # is enabled head will only contain headers. Don't try to eat # more than 28 bytes (UDPv4 + eth hdr left: (14 + 20 + 8) - 14) - l2_cut_off = 28 if cfg.addr_ipver == 4 else 48 + l2_cut_off = 28 if cfg.addr_ipver == "4" else 48 if pkt_sz > hds_thresh and offset > l2_cut_off: ksft_pr( f"Failed run: pkt_sz ({pkt_sz}) > HDS threshold ({hds_thresh}) and " @@ -579,18 +623,22 @@ def _test_xdp_native_head_adjst(cfg, prog, pkt_sz_lst, offset_lst): return {"status": "pass"} -def test_xdp_native_adjst_head_grow_data(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_adjst_head_grow_data(cfg, ipver): """ Tests the XDP headroom growth support. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). This function sets up the packet size and offset lists, then calls the _test_xdp_native_head_adjst_mb function to perform the actual test. The test is passed if the headroom is successfully extended for given packet sizes and offsets. """ + _set_ipver_defer_restore(cfg, ipver) + pkt_sz_lst = [512, 1024, 2048] # Negative values result in headroom shrinking, resulting in growing of payload @@ -600,18 +648,22 @@ def test_xdp_native_adjst_head_grow_data(cfg): _validate_res(res, offset_lst, pkt_sz_lst) -def test_xdp_native_adjst_head_shrnk_data(cfg): +@ksft_variants(_ipvers) +def test_xdp_native_adjst_head_shrnk_data(cfg, ipver): """ Tests the XDP headroom shrinking support. Args: cfg: Configuration object containing network settings. + ipver: IP version to use ("4" or "6"). This function sets up the packet size and offset lists, then calls the _test_xdp_native_head_adjst_mb function to perform the actual test. The test is passed if the headroom is successfully shrunk for given packet sizes and offsets. """ + _set_ipver_defer_restore(cfg, ipver) + pkt_sz_lst = [512, 1024, 2048] # Positive values result in headroom growing, resulting in shrinking of payload @@ -621,12 +673,19 @@ def test_xdp_native_adjst_head_shrnk_data(cfg): _validate_res(res, offset_lst, pkt_sz_lst) -@ksft_variants([ - KsftNamedVariant("pass", XDPAction.PASS), - KsftNamedVariant("drop", XDPAction.DROP), - KsftNamedVariant("tx", XDPAction.TX), -]) -def test_xdp_native_qstats(cfg, act): +def _qstats_variants(): + actions = [ + ("pass", XDPAction.PASS), + ("drop", XDPAction.DROP), + ("tx", XDPAction.TX), + ] + for ipver in ["4", "6"]: + for name, act in actions: + yield KsftNamedVariant(f"{name}_ipv{ipver}", act, ipver) + + +@ksft_variants(_qstats_variants()) +def test_xdp_native_qstats(cfg, act, ipver): """ Send 1000 messages. Expect XDP action specified in @act. Make sure the packets were counted to interface level qstats @@ -634,6 +693,7 @@ def test_xdp_native_qstats(cfg, act): """ cfg.require_cmd("socat") + _set_ipver_defer_restore(cfg, ipver) bpf_info = BPFProgInfo("xdp_prog", "xdp_native.bpf.o", "xdp", 1500) prog_info = _load_xdp_prog(cfg, bpf_info) diff --git a/tools/testing/selftests/drivers/ntsync/config b/tools/testing/selftests/drivers/ntsync/config index 60539c826d06..0aa68de147af 100644 --- a/tools/testing/selftests/drivers/ntsync/config +++ b/tools/testing/selftests/drivers/ntsync/config @@ -1 +1 @@ -CONFIG_WINESYNC=y +CONFIG_NTSYNC=y diff --git a/tools/testing/selftests/drivers/ntsync/ntsync.c b/tools/testing/selftests/drivers/ntsync/ntsync.c index e6a37214aa46..1f0dc43bb4c0 100644 --- a/tools/testing/selftests/drivers/ntsync/ntsync.c +++ b/tools/testing/selftests/drivers/ntsync/ntsync.c @@ -8,12 +8,18 @@ #define _GNU_SOURCE #include <sys/ioctl.h> #include <sys/stat.h> +#include <sys/wait.h> #include <fcntl.h> +#include <sched.h> #include <time.h> #include <pthread.h> #include <linux/ntsync.h> #include "kselftest_harness.h" +#ifndef CLONE_NEWTIME +#define CLONE_NEWTIME 0x00000080 +#endif + static int read_sem_state(int sem, __u32 *count, __u32 *max) { struct ntsync_sem_args args; @@ -968,7 +974,7 @@ TEST(wake_all) auto_event_args.manual = false; auto_event_args.signaled = true; objs[3] = ioctl(fd, NTSYNC_IOC_CREATE_EVENT, &auto_event_args); - EXPECT_EQ(0, objs[3]); + EXPECT_LE(0, objs[3]); wait_args.timeout = get_abs_timeout(1000); wait_args.objs = (uintptr_t)objs; @@ -1340,4 +1346,129 @@ TEST(stress_wait) close(stress_device); } +TEST(wait_args_validation) +{ + struct ntsync_sem_args sem_args = { .count = 1, .max = 1 }; + struct ntsync_wait_args wait_args = {0}; + struct timespec timeout; + int fd, fd2, sem, ret; + __u32 index; + + fd = open("/dev/ntsync", O_CLOEXEC | O_RDONLY); + ASSERT_GE(fd, 0); + + fd2 = open("/dev/ntsync", O_CLOEXEC | O_RDONLY); + ASSERT_GE(fd2, 0); + + sem = ioctl(fd, NTSYNC_IOC_CREATE_SEM, &sem_args); + EXPECT_GE(sem, 0); + + ret = wait_any(fd, 1, &sem, 0, &index); + EXPECT_EQ(-1, ret); + EXPECT_EQ(EINVAL, errno); + + ret = wait_all(fd, 1, &sem, 0, &index); + EXPECT_EQ(-1, ret); + EXPECT_EQ(EINVAL, errno); + + clock_gettime(CLOCK_MONOTONIC, &timeout); + wait_args.timeout = timeout.tv_sec * 1000000000ULL + timeout.tv_nsec; + wait_args.count = 0; + wait_args.objs = 0; + wait_args.owner = 123; + wait_args.pad = 1; + ret = ioctl(fd, NTSYNC_IOC_WAIT_ANY, &wait_args); + EXPECT_EQ(-1, ret); + EXPECT_EQ(EINVAL, errno); + + ret = wait_any(fd2, 1, &sem, 123, &index); + EXPECT_EQ(-1, ret); + EXPECT_EQ(EINVAL, errno); + + close(sem); + close(fd2); + close(fd); +} + +/* + * Absolute MONOTONIC timeouts must honour the caller's time namespace. + * With a negative monotonic offset, a 100 ms wait must still take ~100 ms + * of namespace time (not return immediately against the host clock). + */ +TEST(wait_any_monotonic_timens) +{ + struct ntsync_sem_args sem_args = {0}; + struct ntsync_wait_args wait_args = {0}; + struct timespec start, end; + char buf[64]; + __u64 elapsed_ns; + int fd, offset_fd, sem, ret, status, len; + pid_t pid; + + if (access("/proc/self/ns/time", F_OK)) + SKIP(return, "Time namespaces are not supported"); + + fd = open("/dev/ntsync", O_CLOEXEC | O_RDONLY); + if (fd < 0) + SKIP(return, "/dev/ntsync is not available"); + + ret = unshare(CLONE_NEWTIME); + if (ret) { + close(fd); + if (errno == EPERM) + SKIP(return, "need CAP_SYS_ADMIN for CLONE_NEWTIME"); + ASSERT_EQ(0, ret); + } + + len = snprintf(buf, sizeof(buf), "%d %d 0", CLOCK_MONOTONIC, -10); + offset_fd = open("/proc/self/timens_offsets", O_WRONLY); + ASSERT_LE(0, offset_fd); + ASSERT_EQ(len, write(offset_fd, buf, len)); + close(offset_fd); + + pid = fork(); + ASSERT_LE(0, pid); + if (!pid) { + int obj; + + sem_args.count = 0; + sem_args.max = 1; + sem = ioctl(fd, NTSYNC_IOC_CREATE_SEM, &sem_args); + if (sem < 0) + _exit(1); + + obj = sem; + wait_args.timeout = get_abs_timeout(100); + wait_args.objs = (uintptr_t)&obj; + wait_args.count = 1; + wait_args.owner = 123; + wait_args.index = 0xdeadbeef; + + if (clock_gettime(CLOCK_MONOTONIC, &start)) + _exit(2); + ret = ioctl(fd, NTSYNC_IOC_WAIT_ANY, &wait_args); + if (clock_gettime(CLOCK_MONOTONIC, &end)) + _exit(2); + + if (ret != -1 || errno != ETIMEDOUT) + _exit(3); + + elapsed_ns = (end.tv_sec - start.tv_sec) * 1000000000ULL + + (end.tv_nsec - start.tv_nsec); + /* Without timens conversion this returns in ~0 ms. */ + if (elapsed_ns < 50 * 1000000ULL) + _exit(4); + if (elapsed_ns > 1000 * 1000000ULL) + _exit(5); + + _exit(0); + } + + ASSERT_EQ(pid, waitpid(pid, &status, 0)); + EXPECT_TRUE(WIFEXITED(status)); + EXPECT_EQ(0, WEXITSTATUS(status)); + + close(fd); +} + TEST_HARNESS_MAIN |
