316 lines · 10.5 KB · Python
RawDownload
# SPDX-License-Identifier: GPL-2.0# pylint: disable=invalid-name,too-many-arguments"""Shared helpers for devmem TCP selftests.""" import osimport re from lib.py import (bkg, cmd, defer, ethtool, rand_port, wait_port_listen,                    ksft_eq, KsftSkipEx, NetNSEnter, EthtoolFamily,                    NetdevFamily)  RX_PAGE_SIZE_DEFAULT = 0RX_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  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     cfg.require_ipver('6')    ethnl = EthtoolFamily()     channels = ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})    channels = channels['combined-count']    if channels < 2:        raise KsftSkipEx(            'Test requires NETIF with at least 2 combined channels'        )     rings = ethnl.rings_get({'header': {'dev-index': cfg.ifindex}})    orig_rx_rings = rings['rx']    orig_hds_thresh = rings.get('hds-thresh', 0)     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': 'unknown',                            'hds-thresh': orig_hds_thresh,                            'rx': orig_rx_rings})     cfg.src_queue = channels - 1    ethtool(f"-X {cfg.ifname} equal {cfg.src_queue}")    defer(ethtool, f"-X {cfg.ifname} default")     if not hasattr(cfg, 'nk_queue'):        with NetNSEnter(str(cfg.netns)):            netdevnl = NetdevFamily()            lease_result = netdevnl.queue_create({                "ifindex": cfg.nk_guest_ifindex,                "type": "rx",                "lease": {                    "ifindex": cfg.ifindex,                    "queue": {"id": cfg.src_queue, "type": "rx"},                    "netns-id": 0,                },            })            cfg.nk_queue = lease_result['id']  def set_flow_rule(cfg, port):    """Install a flow rule steering to src_queue and return the flow rule ID."""    output = ethtool(        f"-N {cfg.ifname} flow-type tcp6 dst-port {port}"        f" action {cfg.src_queue}"    ).stdout    return int(re.search(r'ID (\d+)', output).group(1))  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)        defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")         ifname = cfg.nk_guest_ifname        addr = cfg.nk_guest_ipv6        extras = [f"-t {cfg.nk_queue}", "-q 1", "-n"]    else:        ifname = cfg.ifname        addr = cfg.addr        extras = []        if flow_steer:            extras.append(f"-c {cfg.remote_addr}")     if verify:        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]    return " ".join(parts)  def ncdevmem_tx(cfg, port, chunk_size=0):    """Build the ncdevmem TX send command."""    if hasattr(cfg, 'netns'):        ifname = cfg.nk_guest_ifname        addr = cfg.remote_addr_v['6']        extras = ["-t 0", "-q 1", "-n"]    else:        ifname = cfg.ifname        addr = cfg.remote_addr        extras = []     if chunk_size:        extras.append(f"-z {chunk_size}")     parts = [cfg.bin_local, f"-f {ifname}", f"-s {addr}",             f"-p {port}", *extras]    return " ".join(parts)  def socat_send(cfg, port, buf_size=0):    """Socat command for sending to the devmem listener.     When buf_size > 0, force one TCP segment per write of exactly that size by    setting socat's buffer (-b) and disabling Nagle (TCP_NODELAY).    """    proto = f"TCP{cfg.addr_ipver}"     if hasattr(cfg, 'netns'):        addr = f"[{cfg.nk_guest_ipv6}]"    else:        addr = cfg.baddr     suffix = f",bind={cfg.remote_baddr}:{port}"     buf = ""    if buf_size:        buf = f"-b {buf_size}"        suffix += ",nodelay"     return f"socat {buf} -u - {proto}:{addr}:{port}{suffix}"  def socat_listen(cfg, port):    """Socat listen command for TX tests."""    return f"socat -U - TCP{cfg.addr_ipver}-LISTEN:{port}"  def setup_test(cfg, bin_local):    """Stash the local ncdevmem path on cfg and deploy it to the remote."""    cfg.bin_local = bin_local    cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)  def run_rx(cfg):    """Run the devmem RX test."""    require_devmem(cfg)    configure_nic(cfg)    port = rand_port()    socat = socat_send(cfg, port)    data_pipe = (f"yes $(echo -e \x01\x02\x03\x04\x05\x06) | head -c 1K"                 f" | {socat}")    netns = getattr(cfg, "netns", None)     listen_cmd = ncdevmem_rx(cfg, port, flow_steer=not hasattr(cfg, 'netns'))    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)  def run_tx(cfg):    """Run the devmem TX test."""    require_devmem(cfg)    configure_nic(cfg)    netns = getattr(cfg, "netns", None)    port = rand_port()    tx_cmd = ncdevmem_tx(cfg, port)    listen_cmd = socat_listen(cfg, port)     with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as socat:        wait_port_listen(port, host=cfg.remote)        cmd(f"bash -c 'echo -e \"hello\\nworld\" | {tx_cmd}'", ns=netns, shell=True)    ksft_eq(socat.stdout.strip(), "hello\nworld")  def run_tx_chunks(cfg):    """Run the devmem TX chunking test."""    require_devmem(cfg)    configure_nic(cfg)    netns = getattr(cfg, "netns", None)    port = rand_port()    tx_cmd = ncdevmem_tx(cfg, port, chunk_size=3)    listen_cmd = socat_listen(cfg, port)     with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as socat:        wait_port_listen(port, host=cfg.remote)        cmd(f"bash -c 'echo -e \"hello\\nworld\" | {tx_cmd}'", ns=netns, shell=True)    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)    configure_nic(cfg)    netns = getattr(cfg, "netns", None)     for size in [1, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192]:        port = rand_port()         listen_cmd = ncdevmem_rx(cfg, port, verify=False,                                 fail_on_linear=True)        socat = socat_send(cfg, port, buf_size=size)         with bkg(listen_cmd, exit_wait=True, ns=netns) as ncdevmem:            wait_port_listen(port, proto="tcp", ns=netns)            cmd(f"dd if=/dev/zero bs={size} count=1 2>/dev/null | "                f"{socat}", host=cfg.remote, shell=True)        ksft_eq(ncdevmem.ret, 0, f"HDS failed for payload size {size}")