Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
@@ -0,0 +1,37 @@
# SPDX-License-Identifier: GPL-2.0
"""
Python selftest helpers for netdev.
"""
from .consts import KSRC
from .ksft import KsftFailEx, KsftSkipEx, KsftXfailEx, ksft_pr, ksft_eq, \
ksft_ne, ksft_true, ksft_not_none, ksft_in, ksft_not_in, ksft_is, \
ksft_ge, ksft_gt, ksft_lt, ksft_raises, ksft_busy_wait, \
ktap_result, ksft_disruptive, ksft_setup, ksft_run, ksft_exit, \
ksft_variants, KsftNamedVariant
from .netns import NetNS, NetNSEnter, UserNetNS
from .nsim import NetdevSim, NetdevSimDev
from .utils import CmdExitFailure, fd_read_timeout, cmd, bkg, defer, \
bpftool, ip, ethtool, bpftrace, rand_port, rand_ports, wait_port_listen, \
wait_file, tool, tc
from .bpf import bpf_map_set, bpf_map_dump, bpf_prog_map_ids
from .ynl import NlError, NlctrlFamily, YnlFamily, \
EthtoolFamily, NetdevFamily, RtnlFamily, RtnlAddrFamily
from .ynl import NetshaperFamily, DevlinkFamily, PSPFamily, Netlink
__all__ = ["KSRC",
"KsftFailEx", "KsftSkipEx", "KsftXfailEx", "ksft_pr", "ksft_eq",
"ksft_ne", "ksft_true", "ksft_not_none", "ksft_in", "ksft_not_in",
"ksft_is", "ksft_ge", "ksft_gt", "ksft_lt", "ksft_raises",
"ksft_busy_wait", "ktap_result", "ksft_disruptive", "ksft_setup",
"ksft_run", "ksft_exit", "ksft_variants", "KsftNamedVariant",
"NetNS", "NetNSEnter", "UserNetNS",
"CmdExitFailure", "fd_read_timeout", "cmd", "bkg", "defer",
"bpftool", "ip", "ethtool", "bpftrace", "rand_port", "rand_ports",
"wait_port_listen", "wait_file", "tool", "tc",
"bpf_map_set", "bpf_map_dump", "bpf_prog_map_ids",
"NetdevSim", "NetdevSimDev",
"NetshaperFamily", "DevlinkFamily", "PSPFamily", "NlError",
"YnlFamily", "EthtoolFamily", "NetdevFamily", "RtnlFamily",
"NlctrlFamily", "RtnlAddrFamily", "Netlink"]
@@ -0,0 +1,68 @@
# SPDX-License-Identifier: GPL-2.0
"""
BPF helper utilities for kernel selftests.
Provides common operations for interacting with BPF maps and programs
via bpftool, used by XDP and other BPF-based test files.
"""
from .utils import bpftool
def _format_hex_bytes(value):
"""
Helper function that converts an integer into a formatted hexadecimal byte string.
Args:
value: An integer representing the number to be converted.
Returns:
A string representing hexadecimal equivalent of value, with bytes separated by spaces.
"""
hex_str = value.to_bytes(4, byteorder='little', signed=True)
return ' '.join(f'{byte:02x}' for byte in hex_str)
def bpf_map_set(map_name, key, value):
"""
Updates an XDP map with a given key-value pair using bpftool.
Args:
map_name: The name of the XDP map to update.
key: The key to update in the map, formatted as a hexadecimal string.
value: The value to associate with the key, formatted as a hexadecimal string.
"""
key_formatted = _format_hex_bytes(key)
value_formatted = _format_hex_bytes(value)
bpftool(
f"map update name {map_name} key hex {key_formatted} value hex {value_formatted}"
)
def bpf_map_dump(map_id):
"""Dump all entries of a BPF array map.
Args:
map_id: Numeric map ID (as returned by bpftool prog show).
Returns:
A dict mapping formatted key (int) to formatted value (int).
"""
raw = bpftool(f"map dump id {map_id}", json=True)
return {e["formatted"]["key"]: e["formatted"]["value"] for e in raw}
def bpf_prog_map_ids(prog_id):
"""Get the map name-to-ID mapping for a loaded BPF program.
Args:
prog_id: Numeric program ID.
Returns:
A dict mapping map name (str) to map ID (int).
"""
map_ids = bpftool(f"prog show id {prog_id}", json=True)["map_ids"]
maps = {}
for mid in map_ids:
name = bpftool(f"map show id {mid}", json=True)["name"]
maps[name] = mid
return maps
@@ -0,0 +1,9 @@
# SPDX-License-Identifier: GPL-2.0
import sys
from pathlib import Path
KSFT_DIR = (Path(__file__).parent / "../../..").resolve()
KSRC = (Path(__file__).parent / "../../../../../..").resolve()
KSFT_MAIN_NAME = Path(sys.argv[0]).with_suffix("").name
@@ -0,0 +1,468 @@
# SPDX-License-Identifier: GPL-2.0
import fnmatch
import functools
import getopt
import inspect
import os
import signal
import sys
import time
import traceback
from collections import namedtuple
from .consts import KSFT_MAIN_NAME
from . import utils
KSFT_RESULT = None
KSFT_RESULT_ALL = True
KSFT_DISRUPTIVE = True
class KsftFailEx(Exception):
pass
class KsftSkipEx(Exception):
pass
class KsftXfailEx(Exception):
pass
class KsftTerminate(KeyboardInterrupt):
pass
class _KsftArgs:
def __init__(self):
self.list_tests = False
self.filters = []
try:
opts, _ = getopt.getopt(sys.argv[1:], 'hlt:T:')
except getopt.GetoptError as e:
print(e, file=sys.stderr)
sys.exit(1)
for opt, val in opts:
if opt == '-h':
print(f"Usage: {sys.argv[0]} [-h|-l] [-t|-T name]\n"
f"\t-h print help\n"
f"\t-l list tests (filtered, if filters were specified)\n"
f"\t-t name include test\n"
f"\t-T name exclude test",
file=sys.stderr)
sys.exit(0)
elif opt == '-l':
self.list_tests = True
elif opt == '-t':
self.filters.append((True, val))
elif opt == '-T':
self.filters.append((False, val))
@functools.lru_cache()
def _ksft_supports_color():
if os.environ.get("NO_COLOR") is not None:
return False
if not hasattr(sys.stdout, "isatty") or not sys.stdout.isatty():
return False
if os.environ.get("TERM") == "dumb":
return False
return True
def ksft_pr(*objs, **kwargs):
"""
Print logs to stdout.
Behaves like print() but log lines will be prefixed
with # to prevent breaking the TAP output formatting.
Extra arguments (on top of what print() supports):
line_pfx - add extra string before each line
"""
sep = kwargs.pop("sep", " ")
pfx = kwargs.pop("line_pfx", "")
pfx = "#" + (" " + pfx if pfx else "")
kwargs["flush"] = True
text = sep.join(str(obj) for obj in objs)
prefixed = f"\n{pfx} ".join(text.split('\n'))
print(pfx, prefixed, **kwargs)
def _fail(*args):
global KSFT_RESULT
KSFT_RESULT = False
stack = inspect.stack()
started = False
for frame in reversed(stack[2:]):
# Start printing from the test case function
if not started:
if frame.function == 'ksft_run':
started = True
continue
ksft_pr("Check| At " + frame.filename + ", line " + str(frame.lineno) +
", in " + frame.function + ":")
ksft_pr("Check| " + frame.code_context[0].strip())
ksft_pr(*args)
def ksft_eq(a, b, comment=""):
global KSFT_RESULT
if a != b:
_fail("Check failed", a, "!=", b, comment)
def ksft_ne(a, b, comment=""):
global KSFT_RESULT
if a == b:
_fail("Check failed", a, "==", b, comment)
def ksft_true(a, comment=""):
if not a:
_fail("Check failed", a, "does not eval to True", comment)
def ksft_not_none(a, comment=""):
if a is None:
_fail("Check failed", a, "is None", comment)
def ksft_in(a, b, comment=""):
if a not in b:
_fail("Check failed", a, "not in", b, comment)
def ksft_not_in(a, b, comment=""):
if a in b:
_fail("Check failed", a, "in", b, comment)
def ksft_is(a, b, comment=""):
if a is not b:
_fail("Check failed", a, "is not", b, comment)
def ksft_ge(a, b, comment=""):
if a < b:
_fail("Check failed", a, "<", b, comment)
def ksft_gt(a, b, comment=""):
if a <= b:
_fail("Check failed", a, "<=", b, comment)
def ksft_lt(a, b, comment=""):
if a >= b:
_fail("Check failed", a, ">=", b, comment)
class ksft_raises:
def __init__(self, expected_type):
self.exception = None
self.expected_type = expected_type
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
_fail(f"Expected exception {str(self.expected_type.__name__)}, none raised")
elif self.expected_type != exc_type:
_fail(f"Expected exception {str(self.expected_type.__name__)}, raised {str(exc_type.__name__)}")
self.exception = exc_val
# Suppress the exception if its the expected one
return self.expected_type == exc_type
def ksft_busy_wait(cond, sleep=0.005, deadline=1, comment=""):
end = time.monotonic() + deadline
while True:
if cond():
return
if time.monotonic() > end:
_fail("Waiting for condition timed out", comment)
return
time.sleep(sleep)
def ktap_result(ok, cnt=1, case_name="", comment=""):
global KSFT_RESULT_ALL
KSFT_RESULT_ALL = KSFT_RESULT_ALL and ok
res = ""
if not ok:
res += "not "
res += "ok "
res += str(cnt) + " "
res += KSFT_MAIN_NAME
if case_name:
res += "." + case_name
if comment:
res += " # " + comment
if _ksft_supports_color():
if comment.startswith(("SKIP", "XFAIL")):
color = "\033[33m"
elif ok:
color = "\033[32m"
else:
color = "\033[31m"
res = color + res + "\033[0m"
print(res, flush=True)
def _ksft_defer_arm(state):
""" Allow or disallow the use of defer() """
utils.GLOBAL_DEFER_ARMED = state
def ksft_flush_defer():
global KSFT_RESULT
i = 0
qlen_start = len(utils.GLOBAL_DEFER_QUEUE)
while utils.GLOBAL_DEFER_QUEUE:
i += 1
entry = utils.GLOBAL_DEFER_QUEUE.pop()
try:
entry.exec_only()
except Exception:
ksft_pr(f"Exception while handling defer / cleanup (callback {i} of {qlen_start})!")
ksft_pr(traceback.format_exc(), line_pfx="Defer Exception|")
KSFT_RESULT = False
KsftCaseFunction = namedtuple("KsftCaseFunction",
['name', 'original_func', 'variants'])
def ksft_disruptive(func):
"""
Decorator that marks the test as disruptive (e.g. the test
that can down the interface). Disruptive tests can be skipped
by passing DISRUPTIVE=False environment variable.
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
if not KSFT_DISRUPTIVE:
raise KsftSkipEx("marked as disruptive")
return func(*args, **kwargs)
return wrapper
class KsftNamedVariant:
""" Named string name + argument list tuple for @ksft_variants """
def __init__(self, name, *params):
self.params = params
self.name = name or "_".join([str(x) for x in self.params])
def ksft_variants(params):
"""
Decorator defining the sets of inputs for a test.
The parameters will be included in the name of the resulting sub-case.
Parameters can be either single object, tuple or a KsftNamedVariant.
The argument can be a list or a generator.
Example:
@ksft_variants([
(1, "a"),
(2, "b"),
KsftNamedVariant("three", 3, "c"),
])
def my_case(cfg, a, b):
pass # ...
ksft_run(cases=[my_case], args=(cfg, ))
Will generate cases:
my_case.1_a
my_case.2_b
my_case.three
"""
return lambda func: KsftCaseFunction(func.__name__, func, params)
def ksft_setup(env):
"""
Setup test framework global state from the environment.
"""
def get_bool(env, name):
value = env.get(name, "").lower()
if value in ["yes", "true"]:
return True
if value in ["no", "false"]:
return False
try:
return bool(int(value))
except Exception:
raise Exception(f"failed to parse {name}")
if "DISRUPTIVE" in env:
global KSFT_DISRUPTIVE
KSFT_DISRUPTIVE = get_bool(env, "DISRUPTIVE")
return env
def _ksft_intr(signum, frame):
# ksft runner.sh sends 2 SIGTERMs in a row on a timeout
# if we don't ignore the second one it will stop us from handling cleanup
global term_cnt
term_cnt += 1
if term_cnt == 1:
raise KsftTerminate()
else:
ksft_pr(f"Ignoring SIGTERM (cnt: {term_cnt}), already exiting...")
def _ksft_name_matches(name, pattern):
if '*' in pattern or '?' in pattern or '[' in pattern:
return fnmatch.fnmatchcase(name, pattern)
return name == pattern
def _ksft_test_enabled(name, filters):
has_positive = False
for include, pattern in filters:
has_positive |= include
if _ksft_name_matches(name, pattern):
return include
return not has_positive
def _ksft_generate_test_cases(cases, globs, case_pfx, args, cli_args):
"""Generate a filtered list of (func, args, name) tuples.
If -l is given, prints matching test names and exits.
"""
cases = cases or []
test_cases = []
# If using the globs method find all relevant functions
if globs and case_pfx:
for key, value in globs.items():
if not callable(value):
continue
for prefix in case_pfx:
if key.startswith(prefix):
cases.append(value)
break
for func in cases:
if isinstance(func, KsftCaseFunction):
# Parametrized test - create case for each param
for param in func.variants:
if not isinstance(param, KsftNamedVariant):
if not isinstance(param, tuple):
param = (param, )
param = KsftNamedVariant(None, *param)
test_cases.append((func.original_func,
(*args, *param.params),
func.name + "." + param.name))
else:
test_cases.append((func, args, func.__name__))
if cli_args.filters:
test_cases = [tc for tc in test_cases
if _ksft_test_enabled(tc[2], cli_args.filters)]
if cli_args.list_tests:
for _, _, name in test_cases:
print(name)
sys.exit(0)
return test_cases
def ksft_run(cases=None, globs=None, case_pfx=None, args=()):
cli_args = _KsftArgs()
test_cases = _ksft_generate_test_cases(cases, globs, case_pfx, args,
cli_args)
global term_cnt
term_cnt = 0
prev_sigterm = signal.signal(signal.SIGTERM, _ksft_intr)
totals = {"pass": 0, "fail": 0, "skip": 0, "xfail": 0}
global KSFT_RESULT
if KSFT_RESULT is not None:
raise RuntimeError("ksft_run() can't be called multiple times.")
print("TAP version 13", flush=True)
print("1.." + str(len(test_cases)), flush=True)
cnt = 0
stop = False
for func, args, name in test_cases:
KSFT_RESULT = True
cnt += 1
comment = ""
cnt_key = ""
_ksft_defer_arm(True)
try:
func(*args)
except KsftSkipEx as e:
comment = "SKIP " + str(e)
cnt_key = 'skip'
except KsftXfailEx as e:
comment = "XFAIL " + str(e)
cnt_key = 'xfail'
except BaseException as e:
stop |= isinstance(e, KeyboardInterrupt)
ksft_pr(traceback.format_exc(), line_pfx="Exception|")
if stop:
ksft_pr(f"Stopping tests due to {type(e).__name__}.")
KSFT_RESULT = False
cnt_key = 'fail'
_ksft_defer_arm(False)
try:
ksft_flush_defer()
except BaseException as e:
ksft_pr(traceback.format_exc(), line_pfx="Exception|")
if isinstance(e, KeyboardInterrupt):
ksft_pr()
ksft_pr("WARN: defer() interrupted, cleanup may be incomplete.")
ksft_pr(" Attempting to finish cleanup before exiting.")
ksft_pr(" Interrupt again to exit immediately.")
ksft_pr()
stop = True
# Flush was interrupted, try to finish the job best we can
ksft_flush_defer()
if not cnt_key:
cnt_key = 'pass' if KSFT_RESULT else 'fail'
ktap_result(KSFT_RESULT, cnt, name, comment=comment)
totals[cnt_key] += 1
if stop:
break
signal.signal(signal.SIGTERM, prev_sigterm)
print(
f"# Totals: pass:{totals['pass']} fail:{totals['fail']} xfail:{totals['xfail']} xpass:0 skip:{totals['skip']} error:0"
)
def ksft_exit():
global KSFT_RESULT_ALL
sys.exit(0 if KSFT_RESULT_ALL else 1)
@@ -0,0 +1,122 @@
# SPDX-License-Identifier: GPL-2.0
import ctypes
import os
import random
import string
import subprocess
import time
from pathlib import Path
from .utils import ip
libc = ctypes.cdll.LoadLibrary('libc.so.6')
class NetNS:
def __init__(self, name=None):
if name:
self.name = name
else:
self.name = ''.join(random.choice(string.ascii_lowercase) for _ in range(8))
ip('netns add ' + self.name)
def __del__(self):
if self.name:
ip('netns del ' + self.name)
self.name = None
def __enter__(self):
return self
def __exit__(self, ex_type, ex_value, ex_tb):
self.__del__()
def __str__(self):
return self.name
def __repr__(self):
return f"NetNS({self.name})"
class UserNetNS:
"""Network namespace owned by a non-init user namespace."""
def __init__(self):
self.name = ''.join(
random.choice(string.ascii_lowercase) for _ in range(8))
self.user_ns_path = f"/run/userns/{self.name}"
self.net_ns_path = f"/run/netns/{self.name}"
self._user_mounted = False
self._net_mounted = False
os.makedirs("/run/userns", exist_ok=True)
os.makedirs("/run/netns", exist_ok=True)
Path(self.user_ns_path).touch()
Path(self.net_ns_path).touch()
with subprocess.Popen(
["unshare", "--user", "--net", "--map-root-user",
"sleep", "infinity"]) as proc:
try:
pid = proc.pid
init_user = os.readlink("/proc/self/ns/user")
for _ in range(200):
try:
if os.readlink(f"/proc/{pid}/ns/user") != init_user:
break
except OSError:
pass
time.sleep(0.01)
else:
raise RuntimeError("unshare child did not create userns")
subprocess.run(["mount", "--bind", f"/proc/{pid}/ns/user",
self.user_ns_path], check=True)
self._user_mounted = True
subprocess.run(["mount", "--bind", f"/proc/{pid}/ns/net",
self.net_ns_path], check=True)
self._net_mounted = True
finally:
proc.kill()
def __del__(self):
if self._net_mounted:
subprocess.run(["umount", self.net_ns_path], check=False)
self._net_mounted = False
if self._user_mounted:
subprocess.run(["umount", self.user_ns_path], check=False)
self._user_mounted = False
for path in (self.net_ns_path, self.user_ns_path):
try:
os.unlink(path)
except OSError:
pass
def __enter__(self):
return self
def __exit__(self, ex_type, ex_value, ex_tb):
self.__del__()
def __str__(self):
return self.name
def __repr__(self):
return f"UserNetNS({self.name})"
class NetNSEnter:
def __init__(self, ns_name):
self.ns_path = f"/run/netns/{ns_name}"
def __enter__(self):
self.saved = open("/proc/thread-self/ns/net")
with open(self.ns_path) as ns_file:
libc.setns(ns_file.fileno(), 0)
return self
def __exit__(self, exc_type, exc_value, traceback):
libc.setns(self.saved.fileno(), 0)
self.saved.close()
@@ -0,0 +1,135 @@
# SPDX-License-Identifier: GPL-2.0
import errno
import json
import os
import random
import re
import time
from .utils import cmd, ip
class NetdevSim:
"""
Class for netdevsim netdevice and its attributes.
"""
def __init__(self, nsimdev, port_index, ifname, ns=None):
# In case udev renamed the netdev to according to new schema,
# check if the name matches the port_index.
nsimnamere = re.compile(r"eni\d+np(\d+)")
match = nsimnamere.match(ifname)
if match and int(match.groups()[0]) != port_index + 1:
raise Exception("netdevice name mismatches the expected one")
self.ifname = ifname
self.nsimdev = nsimdev
self.port_index = port_index
self.ns = ns
self.dfs_dir = "%s/ports/%u/" % (nsimdev.dfs_dir, port_index)
ret = ip("-d -j link show dev %s" % ifname, ns=ns)
self.dev = json.loads(ret.stdout)[0]
self.ifindex = self.dev["ifindex"]
def dfs_write(self, path, val):
self.nsimdev.dfs_write(f'ports/{self.port_index}/' + path, val)
class NetdevSimDev:
"""
Class for netdevsim bus device and its attributes.
"""
@staticmethod
def ctrl_write(path, val):
fullpath = os.path.join("/sys/bus/netdevsim/", path)
with open(fullpath, "w") as f:
f.write(val)
def dfs_write(self, path, val):
fullpath = os.path.join(f"/sys/kernel/debug/netdevsim/netdevsim{self.addr}/", path)
with open(fullpath, "w") as f:
f.write(val)
def __init__(self, port_count=1, queue_count=1, ns=None):
# nsim will spawn in init_net, we'll set to actual ns once we switch it there
self.ns = None
if not os.path.exists("/sys/bus/netdevsim"):
cmd("modprobe netdevsim")
addr = random.randrange(1 << 15)
while True:
try:
self.ctrl_write("new_device", "%u %u %u" % (addr, port_count, queue_count))
except OSError as e:
if e.errno == errno.ENOSPC:
addr = random.randrange(1 << 15)
continue
raise e
break
self.addr = addr
# As probe of netdevsim device might happen from a workqueue,
# so wait here until all netdevs appear.
self.wait_for_netdevs(port_count)
if ns:
cmd(f"devlink dev reload netdevsim/netdevsim{addr} netns {ns.name}")
self.ns = ns
cmd("udevadm settle", ns=self.ns)
ifnames = self.get_ifnames()
self.dfs_dir = "/sys/kernel/debug/netdevsim/netdevsim%u/" % addr
self.nsims = []
for port_index in range(port_count):
self.nsims.append(self._make_port(port_index, ifnames[port_index]))
self.removed = False
def __enter__(self):
return self
def __exit__(self, ex_type, ex_value, ex_tb):
"""
__exit__ gets called at the end of a "with" block.
"""
self.remove()
def _make_port(self, port_index, ifname):
return NetdevSim(self, port_index, ifname, self.ns)
def get_ifnames(self):
ifnames = []
listdir = cmd(f"ls /sys/bus/netdevsim/devices/netdevsim{self.addr}/net/",
ns=self.ns).stdout.split()
for ifname in listdir:
ifnames.append(ifname)
ifnames.sort()
return ifnames
def wait_for_netdevs(self, port_count):
timeout = 5
timeout_start = time.time()
while True:
try:
ifnames = self.get_ifnames()
except FileNotFoundError as e:
ifnames = []
if len(ifnames) == port_count:
break
if time.time() < timeout_start + timeout:
continue
raise Exception("netdevices did not appear within timeout")
def remove(self):
if not self.removed:
self.ctrl_write("del_device", "%u" % (self.addr, ))
self.removed = True
def remove_nsim(self, nsim):
self.nsims.remove(nsim)
self.ctrl_write("devices/netdevsim%u/del_port" % (self.addr, ),
"%u" % (nsim.port_index, ))
@@ -0,0 +1,375 @@
# 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)
@@ -0,0 +1,82 @@
# SPDX-License-Identifier: GPL-2.0
import sys
from pathlib import Path
from .consts import KSRC, KSFT_DIR
from .ksft import ksft_pr, ktap_result
# Resolve paths
try:
if (KSFT_DIR / "kselftest-list.txt").exists():
# Running in "installed" selftests
tools_full_path = KSFT_DIR
SPEC_PATH = KSFT_DIR / "net/lib/specs"
sys.path.append(tools_full_path.as_posix())
from net.lib.ynl.pyynl.lib import YnlFamily, NlError, NlPolicy, Netlink
else:
# Running in tree
tools_full_path = KSRC / "tools"
SPEC_PATH = KSRC / "Documentation/netlink/specs"
sys.path.append(tools_full_path.as_posix())
from net.ynl.pyynl.lib import YnlFamily, NlError, NlPolicy, Netlink
except ModuleNotFoundError as e:
ksft_pr("Failed importing `ynl` library from kernel sources")
ksft_pr(str(e))
ktap_result(True, comment="SKIP")
sys.exit(4)
__all__ = [
"NlError", "NlPolicy", "Netlink", "YnlFamily", "SPEC_PATH",
"EthtoolFamily", "RtnlFamily", "RtnlAddrFamily",
"NetdevFamily", "NetshaperFamily", "NlctrlFamily", "DevlinkFamily",
"PSPFamily",
]
#
# Wrapper classes, loading the right specs
# Set schema='' to avoid jsonschema validation, it's slow
#
class EthtoolFamily(YnlFamily):
def __init__(self, recv_size=0):
super().__init__((SPEC_PATH / Path('ethtool.yaml')).as_posix(),
schema='', recv_size=recv_size)
class RtnlFamily(YnlFamily):
def __init__(self, recv_size=0):
super().__init__((SPEC_PATH / Path('rt-link.yaml')).as_posix(),
schema='', recv_size=recv_size)
class RtnlAddrFamily(YnlFamily):
def __init__(self, recv_size=0):
super().__init__((SPEC_PATH / Path('rt-addr.yaml')).as_posix(),
schema='', recv_size=recv_size)
class NetdevFamily(YnlFamily):
def __init__(self, recv_size=0):
super().__init__((SPEC_PATH / Path('netdev.yaml')).as_posix(),
schema='', recv_size=recv_size)
class NetshaperFamily(YnlFamily):
def __init__(self, recv_size=0):
super().__init__((SPEC_PATH / Path('net_shaper.yaml')).as_posix(),
schema='', recv_size=recv_size)
class NlctrlFamily(YnlFamily):
def __init__(self, recv_size=0):
super().__init__((SPEC_PATH / Path('nlctrl.yaml')).as_posix(),
schema='', recv_size=recv_size)
class DevlinkFamily(YnlFamily):
def __init__(self, recv_size=0):
super().__init__((SPEC_PATH / Path('devlink.yaml')).as_posix(),
schema='', recv_size=recv_size)
class PSPFamily(YnlFamily):
def __init__(self, recv_size=0):
super().__init__((SPEC_PATH / Path('psp.yaml')).as_posix(),
schema='', recv_size=recv_size)