Files
linux-stable-mirror/tools/testing/selftests/net/lib/py/utils.py
T
Jakub Kicinski 57bb59ab6f selftests: net: bump default cmd() timeout to 20 seconds
We always used 5 sec as the default command timeout. But soon after
it was introduced, David effectively made us ignore the timeout
(it was passed to process.communicate() as the wrong argument).
Gal recently fixed that, but turns out the 5 sec is not enough
for a lot of tests and setups. The fix caused regressions.

In particular running reconfig commands (e.g. XDP attach) on mlx5
with 32 rings and 9k MTU, on a heavily-debug-enabled kernel takes
more than 5 sec. The XDP installation command will time out after
5 sec but since the sleeps in the kernel are non interruptible
the command finishes anyway, leaving the XDP program attached,
but with non-zero exit code. defer()ed cleanups are not installed,
breaking the environment for subsequent tests.

Since "install XDP" is a pretty normal command a "point fix"
does not seem appropriate. 32 rings is a fairly reasonable
config, too, so we should just increase the timeout to 20 sec.

There's no real reason behind the value of 20.

Fixes: 1cf2704242 ("net: selftest: add test for netdev netlink queue-get API")
Fixes: f0bd193166 ("selftests: net: fix timeout passed as positional argument to communicate()")
Reviewed-by: Pavan Chebbi <pavan.chebbi@broadcom.com>
Acked-by: Breno Leitao <leitao@debian.org>
Reviewed-by: Nimrod Oren <noren@nvidia.com>
Link: https://patch.msgid.link/20260629233348.2145841-1-kuba@kernel.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
2026-06-30 16:54:37 -07:00

376 lines
12 KiB
Python

# SPDX-License-Identifier: GPL-2.0
import json as _json
import os
import re
import select
import socket
import subprocess
import time
class CmdInitFailure(Exception):
""" Command failed to start. Only raised by bkg(). """
def __init__(self, msg, cmd_obj):
super().__init__(msg + "\n" + repr(cmd_obj))
self.cmd = cmd_obj
class CmdExitFailure(Exception):
""" Command failed (returned non-zero exit code). """
def __init__(self, msg, cmd_obj):
super().__init__(msg + "\n" + repr(cmd_obj))
self.cmd = cmd_obj
class CmdExitZeroFailure(CmdExitFailure):
""" Command succeeded (returned zero exit code), but expected failure. """
def fd_read_timeout(fd, timeout):
rlist, _, _ = select.select([fd], [], [], timeout)
if rlist:
return os.read(fd, 1024)
raise TimeoutError("Timeout waiting for fd read")
class cmd:
"""
Execute a command on local or remote host.
@shell defaults to false, and class will try to split @comm into a list
if it's a string with spaces.
Use bkg() instead to run a command in the background.
"""
def __init__(self, comm, shell=None, fail=True, expect_fail=False, ns=None,
background=False, host=None, timeout=20, ksft_ready=None,
ksft_wait=None):
if ns:
if hasattr(ns, 'user_ns_path'):
comm = (f'nsenter --user={ns.user_ns_path} '
f'--net={ns.net_ns_path} --setuid=0 --setgid=0 -- '
+ comm)
else:
comm = f'ip netns exec {ns} ' + comm
self.stdout = None
self.stderr = None
self.ret = None
self.ksft_term_fd = None
self.host = host
self.comm = comm
if host:
self.proc = host.cmd(comm)
else:
# If user doesn't explicitly request shell try to avoid it.
if shell is None and isinstance(comm, str) and ' ' in comm:
comm = comm.split()
# ksft_wait lets us wait for the background process to fully start,
# we pass an FD to the child process, and wait for it to write back.
# Similarly term_fd tells child it's time to exit.
pass_fds = []
env = os.environ.copy()
if ksft_wait is not None:
wait_fd, self.ksft_term_fd = os.pipe()
pass_fds.append(wait_fd)
env["KSFT_WAIT_FD"] = str(wait_fd)
ksft_ready = True # ksft_wait implies ready
if ksft_ready is not None:
rfd, ready_fd = os.pipe()
pass_fds.append(ready_fd)
env["KSFT_READY_FD"] = str(ready_fd)
self.proc = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, pass_fds=pass_fds,
env=env)
if ksft_wait is not None:
os.close(wait_fd)
if ksft_ready is not None:
os.close(ready_fd)
msg = fd_read_timeout(rfd, ksft_wait)
os.close(rfd)
if not msg:
terminate = self.proc.poll() is None
self._process_terminate(terminate=terminate, timeout=1)
raise CmdInitFailure("Did not receive ready message", self)
if not background:
self.process(terminate=False, fail=fail, expect_fail=expect_fail,
timeout=timeout)
def _process_terminate(self, terminate, timeout):
if terminate:
self.proc.terminate()
stdout, stderr = self.proc.communicate(timeout=timeout)
self.stdout = stdout.decode("utf-8")
self.stderr = stderr.decode("utf-8")
self.proc.stdout.close()
self.proc.stderr.close()
self.ret = self.proc.returncode
return stdout, stderr
def process(self, terminate=True, fail=None, expect_fail=False, timeout=20):
if fail is None:
fail = not terminate
if self.ksft_term_fd:
os.write(self.ksft_term_fd, b"1")
stdout, stderr = self._process_terminate(terminate=terminate,
timeout=timeout)
# Fail on unexpected test failure if fail.
# Fail on unexpected test success if expect_fail.
# Fail on negative returncode if either:
# Set by subprocess on crash or signal, this is never expected failure.
if (self.proc.returncode != 0 and fail or
(self.proc.returncode < 0 and expect_fail)):
if len(stderr) > 0 and stderr[-1] == "\n":
stderr = stderr[:-1]
raise CmdExitFailure("Command failed", self)
elif self.proc.returncode == 0 and expect_fail:
raise CmdExitZeroFailure("Command succeeded (expected fail)", self)
def __repr__(self):
def str_fmt(name, s):
name += ': '
return (name + s.strip().replace('\n', '\n' + ' ' * len(name)))
ret = "CMD"
if self.host:
ret += "[remote]"
if self.ret is None:
ret += f" (unterminated): {self.comm}\n"
elif self.ret == 0:
ret += f" (success): {self.comm}\n"
else:
ret += f": {self.comm}\n"
ret += f" EXIT: {self.ret}\n"
if self.stdout:
ret += str_fmt(" STDOUT", self.stdout) + "\n"
if self.stderr:
ret += str_fmt(" STDERR", self.stderr) + "\n"
return ret.strip()
class bkg(cmd):
"""
Run a command in the background.
Examples usage:
Run a command on remote host, and wait for it to finish.
This is usually paired with wait_port_listen() to make sure
the command has initialized:
with bkg("socat ...", exit_wait=True, host=cfg.remote) as nc:
...
Run a command and expect it to let us know that it's ready
by writing to a special file descriptor passed via KSFT_READY_FD.
Command will be terminated when we exit the context manager:
with bkg("my_binary", ksft_wait=5):
"""
def __init__(self, comm, shell=None, fail=None, expect_fail=None,
ns=None, host=None, exit_wait=False, ksft_ready=None,
ksft_wait=None):
super().__init__(comm, background=True,
shell=shell, fail=fail, expect_fail=expect_fail,
ns=ns, host=host, ksft_ready=ksft_ready,
ksft_wait=ksft_wait)
self.terminate = not exit_wait and not ksft_wait
self._exit_wait = exit_wait
self.check_fail = fail
self.expect_fail = expect_fail
if shell and self.terminate:
print("# Warning: combining shell and terminate is risky!")
print("# SIGTERM may not reach the child on zsh/ksh!")
def __enter__(self):
return self
def __exit__(self, ex_type, ex_value, ex_tb):
terminate = self.terminate
# Force termination on exception, but only if bkg() didn't already exit
# since forcing termination silences failures with fail=None
if self.proc.poll() is None:
terminate = terminate or (self._exit_wait and ex_type is not None)
return self.process(terminate=terminate, fail=self.check_fail,
expect_fail=self.expect_fail)
GLOBAL_DEFER_QUEUE = []
GLOBAL_DEFER_ARMED = False
class defer:
def __init__(self, func, *args, **kwargs):
if not callable(func):
raise Exception("defer created with un-callable object, did you call the function instead of passing its name?")
self.func = func
self.args = args
self.kwargs = kwargs
if not GLOBAL_DEFER_ARMED:
raise Exception("defer queue not armed, did you use defer() outside of a test case?")
self._queue = GLOBAL_DEFER_QUEUE
self._queue.append(self)
def __enter__(self):
return self
def __exit__(self, ex_type, ex_value, ex_tb):
return self.exec()
def exec_only(self):
self.func(*self.args, **self.kwargs)
def cancel(self):
self._queue.remove(self)
def exec(self):
self.cancel()
self.exec_only()
def tool(name, args, json=None, ns=None, host=None):
cmd_str = name + ' '
if json:
if name == 'tc':
cmd_str += '-json '
else:
cmd_str += '--json '
cmd_str += args
cmd_obj = cmd(cmd_str, ns=ns, host=host)
if json:
return _json.loads(cmd_obj.stdout)
return cmd_obj
def bpftool(args, json=None, ns=None, host=None):
return tool('bpftool', args, json=json, ns=ns, host=host)
def ip(args, json=None, ns=None, host=None):
if ns:
args = f'-netns {ns} ' + args
return tool('ip', args, json=json, host=host)
def tc(args, json=None, ns=None, host=None):
""" Helper to call tc with standard set of optional args. """
if ns:
args = f'-netns {ns} ' + args
return tool('tc', args, json=json, host=host)
def ethtool(args, json=None, ns=None, host=None):
return tool('ethtool', args, json=json, ns=ns, host=host)
def bpftrace(expr, json=None, ns=None, host=None, timeout=None):
"""
Run bpftrace and return map data (if json=True).
The output of bpftrace is inconvenient, so the helper converts
to a dict indexed by map name, e.g.:
{
"@": { ... },
"@map2": { ... },
}
"""
cmd_arr = ['bpftrace']
# Throw in --quiet if json, otherwise the output has two objects
if json:
cmd_arr += ['-f', 'json', '-q']
if timeout:
expr += ' interval:s:' + str(timeout) + ' { exit(); }'
timeout += 20
cmd_arr += ['-e', expr]
cmd_obj = cmd(cmd_arr, ns=ns, host=host, shell=False, timeout=timeout)
if json:
# bpftrace prints objects as lines
ret = {}
for l in cmd_obj.stdout.split('\n'):
if not l.strip():
continue
one = _json.loads(l)
if one.get('type') != 'map':
continue
for k, v in one["data"].items():
if k.startswith('@'):
k = k.lstrip('@')
ret[k] = v
return ret
return cmd_obj
def rand_port(stype=socket.SOCK_STREAM):
"""
Get a random unprivileged port.
"""
return rand_ports(1, stype)[0]
def rand_ports(count, stype=socket.SOCK_STREAM):
"""
Get a unique set of random unprivileged ports.
"""
sockets = []
ports = []
try:
for _ in range(count):
s = socket.socket(socket.AF_INET6, stype)
sockets.append(s)
s.bind(("", 0))
ports.append(s.getsockname()[1])
finally:
for s in sockets:
s.close()
return ports
def wait_port_listen(port, proto="tcp", ns=None, host=None, sleep=0.005, deadline=5):
end = time.monotonic() + deadline
pattern = f":{port:04X} .* "
if proto == "tcp": # for tcp protocol additionally check the socket state
pattern += "0A"
pattern = re.compile(pattern)
while True:
data = cmd(f'cat /proc/net/{proto}*', ns=ns, host=host, shell=True).stdout
for row in data.split("\n"):
if pattern.search(row):
return
if time.monotonic() > end:
raise Exception("Waiting for port listen timed out")
time.sleep(sleep)
def wait_file(fname, test_fn, sleep=0.005, deadline=5, encoding='utf-8'):
"""
Wait for file contents on the local system to satisfy a condition.
test_fn() should take one argument (file contents) and return whether
condition is met.
"""
end = time.monotonic() + deadline
with open(fname, "r", encoding=encoding) as fp:
while True:
if test_fn(fp.read()):
break
fp.seek(0)
if time.monotonic() > end:
raise TimeoutError("Wait for file contents failed", fname)
time.sleep(sleep)