Path: blob/master/tools/testing/selftests/drivers/net/netpoll_basic.py
26292 views
#!/usr/bin/env python31# SPDX-License-Identifier: GPL-2.02# Author: Breno Leitao <[email protected]>3"""4This test aims to evaluate the netpoll polling mechanism (as in5netpoll_poll_dev()). It presents a complex scenario where the network6attempts to send a packet but fails, prompting it to poll the NIC from within7the netpoll TX side.89This has been a crucial path in netpoll that was previously untested. Jakub10suggested using a single RX/TX queue, pushing traffic to the NIC, and then11sending netpoll messages (via netconsole) to trigger the poll.1213In parallel, bpftrace is used to detect if netpoll_poll_dev() was called. If14so, the test passes, otherwise it will be skipped. This test is very dependent on15the driver and environment, given we are trying to trigger a tricky scenario.16"""1718import errno19import logging20import os21import random22import string23import threading24import time25from typing import Optional2627from lib.py import (28bpftrace,29CmdExitFailure,30defer,31ethtool,32GenerateTraffic,33ksft_exit,34ksft_pr,35ksft_run,36KsftFailEx,37KsftSkipEx,38NetDrvEpEnv,39KsftXfailEx,40)4142# Configure logging43logging.basicConfig(44level=logging.INFO,45format="%(asctime)s - %(levelname)s - %(message)s",46)4748NETCONSOLE_CONFIGFS_PATH: str = "/sys/kernel/config/netconsole"49NETCONS_REMOTE_PORT: int = 666650NETCONS_LOCAL_PORT: int = 15145152# Max number of netcons messages to send. Each iteration will setup53# netconsole and send MAX_WRITES messages54ITERATIONS: int = 2055# Number of writes to /dev/kmsg per iteration56MAX_WRITES: int = 4057# MAPS contains the information coming from bpftrace it will have only one58# key: "hits", which tells the number of times netpoll_poll_dev() was called59MAPS: dict[str, int] = {}60# Thread to run bpftrace in parallel61BPF_THREAD: Optional[threading.Thread] = None62# Time bpftrace will be running in parallel.63BPFTRACE_TIMEOUT: int = 10646566def ethtool_get_ringsize(interface_name: str) -> tuple[int, int]:67"""68Read the ringsize using ethtool. This will be used to restore it after the test69"""70try:71ethtool_result = ethtool(f"-g {interface_name}", json=True)[0]72rxs = ethtool_result["rx"]73txs = ethtool_result["tx"]74except (KeyError, IndexError) as exception:75raise KsftSkipEx(76f"Failed to read RX/TX ringsize: {exception}. Not going to mess with them."77) from exception7879return rxs, txs808182def ethtool_set_ringsize(interface_name: str, ring_size: tuple[int, int]) -> bool:83"""Try to the number of RX and TX ringsize."""84rxs = ring_size[0]85txs = ring_size[1]8687logging.debug("Setting ring size to %d/%d", rxs, txs)88try:89ethtool(f"-G {interface_name} rx {rxs} tx {txs}")90except CmdExitFailure:91# This might fail on real device, retry with a higher value,92# worst case, keep it as it is.93return False9495return True969798def ethtool_get_queues_cnt(interface_name: str) -> tuple[int, int, int]:99"""Read the number of RX, TX and combined queues using ethtool"""100101try:102ethtool_result = ethtool(f"-l {interface_name}", json=True)[0]103rxq = ethtool_result.get("rx", -1)104txq = ethtool_result.get("tx", -1)105combined = ethtool_result.get("combined", -1)106107except IndexError as exception:108raise KsftSkipEx(109f"Failed to read queues numbers: {exception}. Not going to mess with them."110) from exception111112return rxq, txq, combined113114115def ethtool_set_queues_cnt(interface_name: str, queues: tuple[int, int, int]) -> None:116"""Set the number of RX, TX and combined queues using ethtool"""117rxq, txq, combined = queues118119cmdline = f"-L {interface_name}"120121if rxq != -1:122cmdline += f" rx {rxq}"123if txq != -1:124cmdline += f" tx {txq}"125if combined != -1:126cmdline += f" combined {combined}"127128logging.debug("calling: ethtool %s", cmdline)129130try:131ethtool(cmdline)132except CmdExitFailure as exception:133raise KsftSkipEx(134f"Failed to configure RX/TX queues: {exception}. Ethtool not available?"135) from exception136137138def netcons_generate_random_target_name() -> str:139"""Generate a random target name starting with 'netcons'"""140random_suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8))141return f"netcons_{random_suffix}"142143144def netcons_create_target(145config_data: dict[str, str],146target_name: str,147) -> None:148"""Create a netconsole dynamic target against the interfaces"""149logging.debug("Using netconsole name: %s", target_name)150try:151os.makedirs(f"{NETCONSOLE_CONFIGFS_PATH}/{target_name}", exist_ok=True)152logging.debug(153"Created target directory: %s/%s", NETCONSOLE_CONFIGFS_PATH, target_name154)155except OSError as exception:156if exception.errno != errno.EEXIST:157raise KsftFailEx(158f"Failed to create netconsole target directory: {exception}"159) from exception160161try:162for key, value in config_data.items():163path = f"{NETCONSOLE_CONFIGFS_PATH}/{target_name}/{key}"164logging.debug("Writing %s to %s", key, path)165with open(path, "w", encoding="utf-8") as file:166# Always convert to string to write to file167file.write(str(value))168169# Read all configuration values for debugging purposes170for debug_key in config_data.keys():171with open(172f"{NETCONSOLE_CONFIGFS_PATH}/{target_name}/{debug_key}",173"r",174encoding="utf-8",175) as file:176content = file.read()177logging.debug(178"%s/%s/%s : %s",179NETCONSOLE_CONFIGFS_PATH,180target_name,181debug_key,182content.strip(),183)184185except Exception as exception:186raise KsftFailEx(187f"Failed to configure netconsole target: {exception}"188) from exception189190191def netcons_configure_target(192cfg: NetDrvEpEnv, interface_name: str, target_name: str193) -> None:194"""Configure netconsole on the interface with the given target name"""195config_data = {196"extended": "1",197"dev_name": interface_name,198"local_port": NETCONS_LOCAL_PORT,199"remote_port": NETCONS_REMOTE_PORT,200"local_ip": cfg.addr,201"remote_ip": cfg.remote_addr,202"remote_mac": "00:00:00:00:00:00", # Not important for this test203"enabled": "1",204}205206netcons_create_target(config_data, target_name)207logging.debug(208"Created netconsole target: %s on interface %s", target_name, interface_name209)210211212def netcons_delete_target(name: str) -> None:213"""Delete a netconsole dynamic target"""214target_path = f"{NETCONSOLE_CONFIGFS_PATH}/{name}"215try:216if os.path.exists(target_path):217os.rmdir(target_path)218except OSError as exception:219raise KsftFailEx(220f"Failed to delete netconsole target: {exception}"221) from exception222223224def netcons_load_module() -> None:225"""Try to load the netconsole module"""226os.system("modprobe netconsole")227228229def bpftrace_call() -> None:230"""Call bpftrace to find how many times netpoll_poll_dev() is called.231Output is saved in the global variable `maps`"""232233# This is going to update the global variable, that will be seen by the234# main function235global MAPS # pylint: disable=W0603236237# This will be passed to bpftrace as in bpftrace -e "expr"238expr = "kprobe:netpoll_poll_dev { @hits = count(); }"239240MAPS = bpftrace(expr, timeout=BPFTRACE_TIMEOUT, json=True)241logging.debug("BPFtrace output: %s", MAPS)242243244def bpftrace_start():245"""Start a thread to call `call_bpf` in a parallel thread"""246global BPF_THREAD # pylint: disable=W0603247248BPF_THREAD = threading.Thread(target=bpftrace_call)249BPF_THREAD.start()250if not BPF_THREAD.is_alive():251raise KsftSkipEx("BPFtrace thread is not alive. Skipping test")252253254def bpftrace_stop() -> None:255"""Stop the bpftrace thread"""256if BPF_THREAD:257BPF_THREAD.join()258259260def bpftrace_any_hit(join: bool) -> bool:261"""Check if netpoll_poll_dev() was called by checking the global variable `maps`"""262if not BPF_THREAD:263raise KsftFailEx("BPFtrace didn't start")264265if BPF_THREAD.is_alive():266if join:267# Wait for bpftrace to finish268BPF_THREAD.join()269else:270# bpftrace is still running, so, we will not check the result yet271return False272273logging.debug("MAPS coming from bpftrace = %s", MAPS)274if "hits" not in MAPS.keys():275raise KsftFailEx(f"bpftrace failed to run!?: {MAPS}")276277logging.debug("Got a total of %d hits", MAPS["hits"])278return MAPS["hits"] > 0279280281def do_netpoll_flush_monitored(cfg: NetDrvEpEnv, ifname: str, target_name: str) -> None:282"""Print messages to the console, trying to trigger a netpoll poll"""283# Start bpftrace in parallel, so, it is watching284# netpoll_poll_dev() while we are sending netconsole messages285bpftrace_start()286defer(bpftrace_stop)287288do_netpoll_flush(cfg, ifname, target_name)289290if bpftrace_any_hit(join=True):291ksft_pr("netpoll_poll_dev() was called. Success")292return293294raise KsftXfailEx("netpoll_poll_dev() was not called during the test...")295296297def do_netpoll_flush(cfg: NetDrvEpEnv, ifname: str, target_name: str) -> None:298"""Print messages to the console, trying to trigger a netpoll poll"""299netcons_configure_target(cfg, ifname, target_name)300retry = 0301302for i in range(int(ITERATIONS)):303if not BPF_THREAD.is_alive() or bpftrace_any_hit(join=False):304# bpftrace is done, stop sending messages305break306307msg = f"netcons test #{i}"308with open("/dev/kmsg", "w", encoding="utf-8") as kmsg:309for j in range(MAX_WRITES):310try:311kmsg.write(f"{msg}-{j}\n")312except OSError as exception:313# in some cases, kmsg can be busy, so, we will retry314time.sleep(1)315retry += 1316if retry < 5:317logging.info("Failed to write to kmsg. Retrying")318# Just retry a few times319continue320raise KsftFailEx(321f"Failed to write to kmsg: {exception}"322) from exception323324netcons_delete_target(target_name)325netcons_configure_target(cfg, ifname, target_name)326# If we sleep here, we will have a better chance of triggering327# This number is based on a few tests I ran while developing this test328time.sleep(0.4)329330331def configure_network(ifname: str) -> None:332"""Configure ring size and queue numbers"""333334# Set defined queues to 1 to force congestion335prev_queues = ethtool_get_queues_cnt(ifname)336logging.debug("RX/TX/combined queues: %s", prev_queues)337# Only set the queues to 1 if they exists in the device. I.e, they are > 0338ethtool_set_queues_cnt(ifname, tuple(1 if x > 0 else x for x in prev_queues))339defer(ethtool_set_queues_cnt, ifname, prev_queues)340341# Try to set the ring size to some low value.342# Do not fail if the hardware do not accepted desired values343prev_ring_size = ethtool_get_ringsize(ifname)344for size in [(1, 1), (128, 128), (256, 256)]:345if ethtool_set_ringsize(ifname, size):346# hardware accepted the desired ringsize347logging.debug("Set RX/TX ringsize to: %s from %s", size, prev_ring_size)348break349defer(ethtool_set_ringsize, ifname, prev_ring_size)350351352def test_netpoll(cfg: NetDrvEpEnv) -> None:353"""354Test netpoll by sending traffic to the interface and then sending355netconsole messages to trigger a poll356"""357358ifname = cfg.ifname359configure_network(ifname)360target_name = netcons_generate_random_target_name()361traffic = None362363try:364traffic = GenerateTraffic(cfg)365do_netpoll_flush_monitored(cfg, ifname, target_name)366finally:367if traffic:368traffic.stop()369370# Revert RX/TX queues371netcons_delete_target(target_name)372373374def test_check_dependencies() -> None:375"""Check if the dependencies are met"""376if not os.path.exists(NETCONSOLE_CONFIGFS_PATH):377raise KsftSkipEx(378f"Directory {NETCONSOLE_CONFIGFS_PATH} does not exist. CONFIG_NETCONSOLE_DYNAMIC might not be set." # pylint: disable=C0301379)380381382def main() -> None:383"""Main function to run the test"""384netcons_load_module()385test_check_dependencies()386with NetDrvEpEnv(__file__) as cfg:387ksft_run(388[test_netpoll],389args=(cfg,),390)391ksft_exit()392393394if __name__ == "__main__":395main()396397398