summaryrefslogtreecommitdiff
path: root/tools/testing/selftests/net/rds
diff options
context:
space:
mode:
Diffstat (limited to 'tools/testing/selftests/net/rds')
-rw-r--r--tools/testing/selftests/net/rds/.gitignore1
-rw-r--r--tools/testing/selftests/net/rds/Makefile17
-rw-r--r--tools/testing/selftests/net/rds/README.txt64
-rw-r--r--tools/testing/selftests/net/rds/config5
-rwxr-xr-xtools/testing/selftests/net/rds/config.sh49
-rw-r--r--tools/testing/selftests/net/rds/getsockopt.c208
-rwxr-xr-xtools/testing/selftests/net/rds/rds_run.sh319
-rwxr-xr-xtools/testing/selftests/net/rds/run.sh224
-rw-r--r--tools/testing/selftests/net/rds/settings1
-rwxr-xr-xtools/testing/selftests/net/rds/test.py713
10 files changed, 1132 insertions, 469 deletions
diff --git a/tools/testing/selftests/net/rds/.gitignore b/tools/testing/selftests/net/rds/.gitignore
index 1c6f04e2aa11..7ca4b1440f51 100644
--- a/tools/testing/selftests/net/rds/.gitignore
+++ b/tools/testing/selftests/net/rds/.gitignore
@@ -1 +1,2 @@
include.sh
+getsockopt
diff --git a/tools/testing/selftests/net/rds/Makefile b/tools/testing/selftests/net/rds/Makefile
index 612a7219990e..ab9e92399a6d 100644
--- a/tools/testing/selftests/net/rds/Makefile
+++ b/tools/testing/selftests/net/rds/Makefile
@@ -3,10 +3,21 @@
all:
@echo mk_build_dir="$(shell pwd)" > include.sh
-TEST_PROGS := run.sh
+TEST_PROGS := rds_run.sh
-TEST_FILES := include.sh test.py
+TEST_GEN_PROGS := getsockopt
-EXTRA_CLEAN := /tmp/rds_logs include.sh
+TEST_FILES := \
+ include.sh \
+ settings \
+ test.py \
+# end of TEST_FILES
+
+EXTRA_CLEAN := \
+ include.sh \
+ /tmp/rds_logs \
+# end of EXTRA_CLEAN
+
+CFLAGS += $(KHDR_INCLUDES)
include ../../lib.mk
diff --git a/tools/testing/selftests/net/rds/README.txt b/tools/testing/selftests/net/rds/README.txt
index cbde2951ab13..8aa41148b1b5 100644
--- a/tools/testing/selftests/net/rds/README.txt
+++ b/tools/testing/selftests/net/rds/README.txt
@@ -1,21 +1,27 @@
RDS self-tests
==============
-These scripts provide a coverage test for RDS-TCP by creating two
-network namespaces and running rds packets between them. A loopback
-network is provisioned with optional probability of packet loss or
-corruption. A workload of 50000 hashes, each 64 characters in size,
-are passed over an RDS socket on this test network. A passing test means
-the RDS-TCP stack was able to recover properly. The provided config.sh
-can be used to compile the kernel with the necessary gcov options. The
-kernel may optionally be configured to omit the coverage report as well.
+These scripts provide a coverage test for RDS-TCP and RDS-RDMA (over
+RoCE/RXE) by setting up two endpoints and running RDS packets between
+them. The TCP path creates two network namespaces; the RDMA path uses
+an RXE (soft RoCE) device backed by a veth pair. A workload of 50000
+hashes, each 64 characters in size, is passed over an RDS socket on
+this test network with an optional probability of packet loss or
+corruption. A passing test means the RDS stack was able to recover
+properly. The provided config.sh can be used to compile the kernel
+with the necessary gcov options; pass -r to also enable the kernel
+configs required for the RDMA transport. The kernel may optionally be
+configured to omit the coverage report as well.
USAGE:
- run.sh [-d logdir] [-l packet_loss] [-c packet_corruption]
- [-u packet_duplcate]
+ rds_run.sh [-d logdir] [-l packet_loss] [-c packet_corruption]
+ [-u packet_duplicate] [-t timeout]
+ [-T tcp|rdma|tcp,rdma]
OPTIONS:
- -d Log directory. Defaults to tools/testing/selftests/net/rds/rds_logs
+ -d Log directory. If set, logs will be stored in the
+ given dir, or skipped if unset. Log dir can also be
+ set through the RDS_LOG_DIR env variable
-l Simulates a percentage of packet loss
@@ -23,19 +29,49 @@ OPTIONS:
-u Simulates a percentage of packet duplication.
+ -t Test timeout. Defaults to tools/testing/selftests/net/rds/settings
+
+ -T Comma-separated list of transports to test. Accepts
+ "tcp", "rdma", or "tcp,rdma". Defaults to "tcp". Use
+ config.sh -r to enable required RDMA configs
+
+ENV VARIABLES:
+ RDS_LOG_DIR Log directory. If set, logs will be stored in
+ the given dir, or skipped if unset. Log dir
+ can also be set with the -d flag.
+
+ Use with --rwdir on the CI path to retain logs after
+ test compleation. Log dir end point must be within
+ the specified --rwdir path for logs to persist on
+ the host.
+
+ SUDO_USER The user name that should be used for tcpdump
+ --relinquish-privileges. Set this to a user
+ belonging to the sudoers group to avoid drop
+ privilege errors with the vng 9p filesystem
+ which may result in empty pcaps
+
EXAMPLE:
# Create a suitable gcov enabled .config
tools/testing/selftests/net/rds/config.sh -g
+ # Optionally add RDMA configs (CONFIG_RDS_RDMA, CONFIG_RDMA_RXE)
+ tools/testing/selftests/net/rds/config.sh -r
+
# Alternatly create a gcov disabled .config
tools/testing/selftests/net/rds/config.sh
+ # Config paths may also be specified with the -c flag
+ tools/testing/selftests/net/rds/config.sh -c .config.local
+
# build the kernel
- vng --build --config tools/testing/selftests/net/config
+ vng --build --config .config
# launch the tests in a VM
vng -v --rwdir ./ --run . --user root --cpus 4 -- \
- "export PYTHONPATH=tools/testing/selftests/net/; tools/testing/selftests/net/rds/run.sh"
+ "export PYTHONPATH=tools/testing/selftests/net/; \
+ export SUDO_USER=example_user; \
+ export RDS_LOG_DIR=tools/testing/selftests/net/rds/rds_logs; \
+ tools/testing/selftests/net/rds/rds_run.sh -T tcp,rdma"
-An HTML coverage report will be output in tools/testing/selftests/net/rds/rds_logs/coverage/.
diff --git a/tools/testing/selftests/net/rds/config b/tools/testing/selftests/net/rds/config
new file mode 100644
index 000000000000..97db7ecb892a
--- /dev/null
+++ b/tools/testing/selftests/net/rds/config
@@ -0,0 +1,5 @@
+CONFIG_NET_NS=y
+CONFIG_NET_SCH_NETEM=y
+CONFIG_RDS=y
+CONFIG_RDS_TCP=y
+CONFIG_VETH=y
diff --git a/tools/testing/selftests/net/rds/config.sh b/tools/testing/selftests/net/rds/config.sh
index 791c8dbe1095..2df2226310ef 100755
--- a/tools/testing/selftests/net/rds/config.sh
+++ b/tools/testing/selftests/net/rds/config.sh
@@ -6,15 +6,24 @@ set -u
set -x
unset KBUILD_OUTPUT
+CONF_FILE=""
+FLAGS=()
GENERATE_GCOV_REPORT=0
-while getopts "g" opt; do
+ENABLE_RDMA=0
+while getopts "gc:r" opt; do
case ${opt} in
g)
GENERATE_GCOV_REPORT=1
;;
+ c)
+ CONF_FILE=$OPTARG
+ ;;
+ r)
+ ENABLE_RDMA=1
+ ;;
:)
- echo "USAGE: config.sh [-g]"
+ echo "USAGE: config.sh [-g] [-c config] [-r]"
exit 1
;;
?)
@@ -24,30 +33,36 @@ while getopts "g" opt; do
esac
done
-CONF_FILE="tools/testing/selftests/net/config"
-
-# no modules
-scripts/config --file "$CONF_FILE" --disable CONFIG_MODULES
+if [[ "$CONF_FILE" != "" ]]; then
+ FLAGS=(--file "$CONF_FILE")
+fi
# enable RDS
-scripts/config --file "$CONF_FILE" --enable CONFIG_RDS
-scripts/config --file "$CONF_FILE" --enable CONFIG_RDS_TCP
+scripts/config "${FLAGS[@]}" --enable CONFIG_RDS
+scripts/config "${FLAGS[@]}" --enable CONFIG_RDS_TCP
if [ "$GENERATE_GCOV_REPORT" -eq 1 ]; then
# instrument RDS and only RDS
- scripts/config --file "$CONF_FILE" --enable CONFIG_GCOV_KERNEL
- scripts/config --file "$CONF_FILE" --disable GCOV_PROFILE_ALL
- scripts/config --file "$CONF_FILE" --enable GCOV_PROFILE_RDS
+ scripts/config "${FLAGS[@]}" --enable CONFIG_GCOV_KERNEL
+ scripts/config "${FLAGS[@]}" --disable GCOV_PROFILE_ALL
+ scripts/config "${FLAGS[@]}" --enable GCOV_PROFILE_RDS
else
- scripts/config --file "$CONF_FILE" --disable CONFIG_GCOV_KERNEL
- scripts/config --file "$CONF_FILE" --disable GCOV_PROFILE_ALL
- scripts/config --file "$CONF_FILE" --disable GCOV_PROFILE_RDS
+ scripts/config "${FLAGS[@]}" --disable CONFIG_GCOV_KERNEL
+ scripts/config "${FLAGS[@]}" --disable GCOV_PROFILE_ALL
+ scripts/config "${FLAGS[@]}" --disable GCOV_PROFILE_RDS
fi
# need network namespaces to run tests with veth network interfaces
-scripts/config --file "$CONF_FILE" --enable CONFIG_NET_NS
-scripts/config --file "$CONF_FILE" --enable CONFIG_VETH
+scripts/config "${FLAGS[@]}" --enable CONFIG_NET_NS
+scripts/config "${FLAGS[@]}" --enable CONFIG_VETH
# simulate packet loss
-scripts/config --file "$CONF_FILE" --enable CONFIG_NET_SCH_NETEM
+scripts/config "${FLAGS[@]}" --enable CONFIG_NET_SCH_NETEM
+if [ "$ENABLE_RDMA" -eq 1 ]; then
+ # enable RDS over InfiniBand / RDMA (rds_rdma test)
+ scripts/config "${FLAGS[@]}" --enable CONFIG_INFINIBAND
+ scripts/config "${FLAGS[@]}" --enable CONFIG_INFINIBAND_ADDR_TRANS
+ scripts/config "${FLAGS[@]}" --enable CONFIG_RDMA_RXE
+ scripts/config "${FLAGS[@]}" --enable CONFIG_RDS_RDMA
+fi
diff --git a/tools/testing/selftests/net/rds/getsockopt.c b/tools/testing/selftests/net/rds/getsockopt.c
new file mode 100644
index 000000000000..93ff252c69b8
--- /dev/null
+++ b/tools/testing/selftests/net/rds/getsockopt.c
@@ -0,0 +1,208 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Exercise the RDS getsockopt() paths that were converted to the
+ * getsockopt_iter() / sockopt_t callback.
+ *
+ * Three distinct paths are covered:
+ *
+ * - RDS_RECVERR and SO_RDS_TRANSPORT, which now return their int value
+ * through copy_to_iter() and report the written length in opt->optlen.
+ *
+ * - RDS_INFO_*, which pins the userspace buffer with
+ * iov_iter_extract_pages() (including a non-zero starting page offset)
+ * and lets the info producers memcpy the snapshot in under a spinlock.
+ *
+ * The kvec (in-kernel buffer) -> -EOPNOTSUPP path of rds_info_getsockopt()
+ * is not reachable from a userspace getsockopt() and so is not tested here.
+ */
+#include <errno.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/socket.h>
+#include <linux/rds.h>
+
+#include "../../kselftest_harness.h"
+
+#ifndef AF_RDS
+#define AF_RDS 21
+#endif
+
+FIXTURE(rds) {
+ int fd;
+};
+
+FIXTURE_SETUP(rds)
+{
+ self->fd = socket(AF_RDS, SOCK_SEQPACKET, 0);
+ if (self->fd < 0)
+ SKIP(return, "AF_RDS unavailable (errno %d) - load the rds module",
+ errno);
+}
+
+FIXTURE_TEARDOWN(rds)
+{
+ if (self->fd >= 0)
+ close(self->fd);
+}
+
+/* RDS_RECVERR defaults to 0 and is reported back as a 4-byte int. */
+TEST_F(rds, recverr_default)
+{
+ socklen_t len = sizeof(int);
+ int val = 0xdeadbeef;
+
+ ASSERT_EQ(0, getsockopt(self->fd, SOL_RDS, RDS_RECVERR, &val, &len));
+ EXPECT_EQ(sizeof(int), len);
+ EXPECT_EQ(0, val);
+}
+
+/* A value set via setsockopt() must be readable back unchanged. */
+TEST_F(rds, recverr_set_get)
+{
+ socklen_t len = sizeof(int);
+ int val = 1;
+
+ ASSERT_EQ(0, setsockopt(self->fd, SOL_RDS, RDS_RECVERR, &val, len));
+
+ val = 0;
+ ASSERT_EQ(0, getsockopt(self->fd, SOL_RDS, RDS_RECVERR, &val, &len));
+ EXPECT_EQ(sizeof(int), len);
+ EXPECT_EQ(1, val);
+}
+
+/* A buffer smaller than an int is rejected with EINVAL, not silently. */
+TEST_F(rds, recverr_short_buffer)
+{
+ socklen_t len = sizeof(int) - 1;
+ char buf[sizeof(int)];
+
+ EXPECT_EQ(-1, getsockopt(self->fd, SOL_RDS, RDS_RECVERR, buf, &len));
+ EXPECT_EQ(EINVAL, errno);
+}
+
+/* An unbound socket reports RDS_TRANS_NONE for SO_RDS_TRANSPORT. */
+TEST_F(rds, transport_unbound)
+{
+ socklen_t len = sizeof(int);
+ int val = 0;
+
+ ASSERT_EQ(0, getsockopt(self->fd, SOL_RDS, SO_RDS_TRANSPORT, &val,
+ &len));
+ EXPECT_EQ(sizeof(int), len);
+ EXPECT_EQ(RDS_TRANS_NONE, (unsigned int)val);
+}
+
+TEST_F(rds, transport_short_buffer)
+{
+ socklen_t len = sizeof(int) - 1;
+ char buf[sizeof(int)];
+
+ EXPECT_EQ(-1, getsockopt(self->fd, SOL_RDS, SO_RDS_TRANSPORT, buf,
+ &len));
+ EXPECT_EQ(EINVAL, errno);
+}
+
+/*
+ * RDS_INFO_COUNTERS with a zero-length buffer is the "probe" call: it must
+ * fail with ENOSPC and report the required snapshot size in optlen.
+ */
+TEST_F(rds, info_counters_probe)
+{
+ socklen_t len = 0;
+
+ EXPECT_EQ(-1, getsockopt(self->fd, SOL_RDS, RDS_INFO_COUNTERS, NULL,
+ &len));
+ EXPECT_EQ(ENOSPC, errno);
+ EXPECT_GT(len, 0);
+ /* The snapshot is an array of fixed-size counter records. */
+ EXPECT_EQ(0, len % (socklen_t)sizeof(struct rds_info_counter));
+}
+
+/*
+ * A real snapshot into an unaligned userspace buffer exercises the
+ * iov_iter_extract_pages() path, including the non-zero offset0 handling
+ * that the patch reworked. Place the buffer at a non-page-aligned address
+ * spanning into the next page to make sure multi-page pinning works too.
+ */
+TEST_F(rds, info_counters_snapshot)
+{
+ struct rds_info_counter *ctr;
+ socklen_t need = 0, len;
+ long pagesz = sysconf(_SC_PAGESIZE);
+ size_t offset, map_len;
+ unsigned int i, n;
+ char *region, *buf;
+ int ret;
+
+ /* Probe for the required size. */
+ getsockopt(self->fd, SOL_RDS, RDS_INFO_COUNTERS, NULL, &need);
+ ASSERT_GT(need, 0);
+
+ /*
+ * Place the buffer at a non-page-aligned offset that runs past the
+ * first page boundary, and size the mapping from the probed length so
+ * the test keeps working if the counter set grows.
+ */
+ offset = pagesz - 64;
+ map_len = ((offset + need + pagesz - 1) / pagesz) * pagesz;
+
+ region = mmap(NULL, map_len, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(MAP_FAILED, region);
+
+ buf = region + offset;
+
+ /*
+ * On success the RDS_INFO path returns the positive per-element size
+ * (lens.each) rather than 0, and writes the full snapshot length back
+ * into optlen.
+ */
+ len = need;
+ ret = getsockopt(self->fd, SOL_RDS, RDS_INFO_COUNTERS, buf, &len);
+ ASSERT_GE(ret, 0) {
+ TH_LOG("getsockopt snapshot failed: errno %d", errno);
+ }
+ EXPECT_EQ(sizeof(struct rds_info_counter), ret);
+ EXPECT_EQ(need, len);
+
+ /* The counter names must be NUL-terminated, non-empty strings. */
+ ctr = (struct rds_info_counter *)buf;
+ n = len / sizeof(*ctr);
+ ASSERT_GT(n, 0);
+ for (i = 0; i < n; i++) {
+ size_t namelen = strnlen((char *)ctr[i].name,
+ sizeof(ctr[i].name));
+
+ EXPECT_GT(namelen, 0);
+ EXPECT_LT(namelen, sizeof(ctr[i].name));
+ }
+
+ munmap(region, map_len);
+}
+
+/*
+ * A non-zero but too-small buffer must report ENOSPC and the full required
+ * length, without corrupting memory past the buffer.
+ */
+TEST_F(rds, info_counters_short_buffer)
+{
+ socklen_t need = 0, len;
+ char small[sizeof(struct rds_info_counter)];
+
+ getsockopt(self->fd, SOL_RDS, RDS_INFO_COUNTERS, NULL, &need);
+ ASSERT_GT(need, 0);
+
+ /* Ask with a buffer guaranteed smaller than the full snapshot. */
+ if (need <= (socklen_t)sizeof(small))
+ SKIP(return, "snapshot fits in one record; nothing to test");
+
+ len = 1; /* < sizeof(struct rds_info_counter) */
+ EXPECT_EQ(-1, getsockopt(self->fd, SOL_RDS, RDS_INFO_COUNTERS, small,
+ &len));
+ EXPECT_EQ(ENOSPC, errno);
+ EXPECT_EQ(need, len);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/net/rds/rds_run.sh b/tools/testing/selftests/net/rds/rds_run.sh
new file mode 100755
index 000000000000..cdf487ec97dc
--- /dev/null
+++ b/tools/testing/selftests/net/rds/rds_run.sh
@@ -0,0 +1,319 @@
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+set -e
+set -u
+
+unset KBUILD_OUTPUT
+
+current_dir="$(realpath "$(dirname "$0")")"
+build_dir="$current_dir"
+
+build_include="$current_dir/include.sh"
+if test -f "$build_include"; then
+ # this include will define "$mk_build_dir" as the location the test was
+ # built. We will need this if the tests are installed in a location
+ # other than the kernel source
+
+ source "$build_include"
+ build_dir="$mk_build_dir"
+fi
+
+# Source settings for timeout value (also used by ksft runner)
+source "$current_dir"/settings
+
+# This test requires kernel source and the *.gcda data therein
+# Locate the top level of the kernel source, and the net/rds
+# subfolder with the appropriate *.gcno object files
+ksrc_dir="$(realpath "$build_dir"/../../../../../)"
+kconfig="$ksrc_dir/.config"
+obj_dir="$ksrc_dir/net/rds"
+
+GCOV_CMD=gcov
+
+#check to see if the host has the required packages to generate a gcov report
+check_gcov_env()
+{
+ if ! which "$GCOV_CMD" > /dev/null 2>&1; then
+ echo "# Warning: Could not find gcov. "
+ GENERATE_GCOV_REPORT=0
+ return
+ fi
+
+ # the gcov version must match the gcc version
+ GCC_VER=$(gcc -dumpfullversion)
+ GCOV_VER=$($GCOV_CMD -v | grep gcov | awk '{print $3}'| awk 'BEGIN {FS="-"}{print $1}')
+ if [ "$GCOV_VER" != "$GCC_VER" ]; then
+ #attempt to find a matching gcov version
+ GCOV_CMD=gcov-$(gcc -dumpversion)
+
+ if ! which "$GCOV_CMD" > /dev/null 2>&1; then
+ echo "# Warning: Could not find an appropriate gcov installation. \
+ gcov version must match gcc version"
+ GENERATE_GCOV_REPORT=0
+ return
+ fi
+
+ #recheck version number of found gcov executable
+ GCOV_VER=$($GCOV_CMD -v | grep gcov | awk '{print $3}'| \
+ awk 'BEGIN {FS="-"}{print $1}')
+ if [ "$GCOV_VER" != "$GCC_VER" ]; then
+ echo "# Warning: Could not find an appropriate gcov installation. \
+ gcov version must match gcc version"
+ GENERATE_GCOV_REPORT=0
+ else
+ echo "# Warning: Mismatched gcc and gcov detected. Using $GCOV_CMD"
+ fi
+ fi
+}
+
+# Check to see if the kconfig has the required configs to generate a coverage report
+check_gcov_conf()
+{
+ if ! grep -x "CONFIG_GCOV_PROFILE_RDS=y" "$kconfig" > /dev/null 2>&1; then
+ echo "# INFO: CONFIG_GCOV_PROFILE_RDS should be enabled for coverage reports"
+ GENERATE_GCOV_REPORT=0
+ fi
+ if ! grep -x "CONFIG_GCOV_KERNEL=y" "$kconfig" > /dev/null 2>&1; then
+ echo "# INFO: CONFIG_GCOV_KERNEL should be enabled for coverage reports"
+ GENERATE_GCOV_REPORT=0
+ fi
+ if grep -x "CONFIG_GCOV_PROFILE_ALL=y" "$kconfig" > /dev/null 2>&1; then
+ echo "# INFO: CONFIG_GCOV_PROFILE_ALL should be disabled for coverage reports"
+ GENERATE_GCOV_REPORT=0
+ fi
+
+ if [ "$GENERATE_GCOV_REPORT" -eq 0 ]; then
+ echo "# To enable gcov reports, please run "\
+ "\"tools/testing/selftests/net/rds/config.sh -g\" and rebuild the kernel"
+ else
+ # if we have the required kernel configs, proceed to check the environment to
+ # ensure we have the required gcov packages
+ check_gcov_env
+ fi
+}
+
+# Checks if a kconfig is enabled (set to =y or =m)
+# $1: kconfig symbol to check
+# $2: (optional) module name backing $1
+# Ex: check_conf_enabled CONFIG_RDS_TCP rds_tcp
+# Modules for configs set to =m will be probed
+# If omitted, only a built-in (=y) config is accepted.
+# Returns on success. exits 4 on failure
+# Kselftest framework requirement - SKIP code is 4.
+check_conf_enabled() {
+ if grep -x "$1=y" "$kconfig" > /dev/null 2>&1; then
+ return
+ fi
+ if [ -n "${2:-}" ] && grep -x "$1=m" "$kconfig" > /dev/null 2>&1; then
+ probe_module "$2"
+ return
+ fi
+ echo "selftests: [SKIP] This test requires $1 enabled"
+ echo "Please run" \
+ "tools/testing/selftests/net/rds/config.sh and rebuild the kernel"
+ exit 4
+}
+
+check_rdma_conf_enabled() {
+ if grep -x "$1=y" "$kconfig" > /dev/null 2>&1; then
+ return
+ fi
+ if [ -n "${2:-}" ] && grep -x "$1=m" "$kconfig" > /dev/null 2>&1; then
+ probe_module "$2"
+ return
+ fi
+ echo "selftests: [XFAIL] rdma transport requires $1 enabled"
+ echo "To enable, run" \
+ "tools/testing/selftests/net/rds/config.sh -r and rebuild"
+ exit 2
+}
+
+# Load the module backing a config that is built as a loadable module
+# (=m). Built-in (=y) configs are already available and don't reach
+# here. Exits with the SKIP code if a required module cannot be loaded.
+probe_module() {
+ if ! modprobe -q "$1"; then
+ echo "selftests: [SKIP] could not load required module $1"
+ exit 4
+ fi
+}
+
+check_conf() {
+ check_conf_enabled CONFIG_NET_SCH_NETEM sch_netem
+ check_conf_enabled CONFIG_VETH veth
+ check_conf_enabled CONFIG_NET_NS
+ check_conf_enabled CONFIG_RDS_TCP rds_tcp
+ check_conf_enabled CONFIG_RDS rds
+}
+
+# Check kernel config and host environment for RDS-RDMA support.
+# Exits with XFAIL (2) if the user requested rdma but prerequisites
+# are not met.
+check_rdma_conf()
+{
+ case "$TRANSPORT" in
+ *rdma*) ;;
+ *) return ;;
+ esac
+
+ # Kconfig will enforce CONFIG_INFINIBAND_* as dependencies
+ # of CONFIG_RDMA_RXE
+ check_rdma_conf_enabled CONFIG_RDMA_RXE rdma_rxe
+ check_rdma_conf_enabled CONFIG_RDS_RDMA rds_rdma
+
+ if ! which rdma > /dev/null 2>&1; then
+ echo "selftests: [XFAIL] rdma transport requires the 'rdma'" \
+ "tool (iproute2)"
+ exit 2
+ fi
+}
+
+check_env()
+{
+ if ! test -d "$obj_dir"; then
+ echo "selftests: [SKIP] This test requires a kernel source tree"
+ exit 4
+ fi
+ if ! test -e "$kconfig"; then
+ echo "selftests: [SKIP] This test requires a configured kernel source tree"
+ exit 4
+ fi
+ if ! which strace > /dev/null 2>&1; then
+ echo "selftests: [SKIP] Could not run test without strace"
+ exit 4
+ fi
+ if ! which tcpdump > /dev/null 2>&1; then
+ echo "selftests: [SKIP] Could not run test without tcpdump"
+ exit 4
+ fi
+
+ if ! which python3 > /dev/null 2>&1; then
+ echo "selftests: [SKIP] Could not run test without python3"
+ exit 4
+ fi
+
+ python_major=$(python3 -c "import sys; print(sys.version_info[0])")
+ python_minor=$(python3 -c "import sys; print(sys.version_info[1])")
+ if [[ python_major -lt 3 || ( python_major -eq 3 && python_minor -lt 9 ) ]] ; then
+ echo "selftests: [SKIP] Could not run test without at least python3.9"
+ python3 -V
+ exit 4
+ fi
+}
+
+LOG_DIR="${RDS_LOG_DIR:-}"
+TIMEOUT=$timeout
+GENERATE_GCOV_REPORT=1
+TRANSPORT=tcp
+FLAGS=()
+
+while getopts "d:l:c:u:t:T:" opt; do
+ case ${opt} in
+ d)
+ LOG_DIR=${OPTARG}
+ ;;
+ l)
+ FLAGS+=("-l" "${OPTARG}")
+ ;;
+ c)
+ FLAGS+=("-c" "${OPTARG}")
+ ;;
+ t)
+ TIMEOUT=${OPTARG}
+ ;;
+ u)
+ FLAGS+=("-u" "${OPTARG}")
+ ;;
+ T)
+ TRANSPORT=${OPTARG}
+ ;;
+ :)
+ echo "USAGE: rds_run.sh [-d logdir] [-l packet_loss]" \
+ "[-c packet_corruption] [-u packet_duplicate] [-t timeout]" \
+ "[-T tcp|rdma|tcp,rdma]"
+ exit 1
+ ;;
+ ?)
+ echo "Invalid option: -${OPTARG}."
+ exit 1
+ ;;
+ esac
+done
+
+# Validate transport tokens
+IFS=',' read -ra transports <<< "$TRANSPORT"
+for t in "${transports[@]}"; do
+ if [ "$t" != "tcp" ] && [ "$t" != "rdma" ]; then
+ echo "rds_run.sh: unknown transport '$t' (expected tcp or rdma)"
+ exit 1
+ fi
+done
+
+FLAGS+=("--transport" "${TRANSPORT}")
+
+check_env
+check_conf
+check_gcov_conf
+check_rdma_conf
+
+TRACE_CMD=()
+if [[ -n "$LOG_DIR" ]]; then
+ FLAGS+=("-d" "$LOG_DIR")
+
+ TRACE_FILE="${LOG_DIR}/rds-strace.txt"
+ COVR_DIR="${LOG_DIR}/coverage/"
+ DMESG_FILE="${LOG_DIR}/rds-dmesg.out"
+
+ mkdir -p "$LOG_DIR"
+ mkdir -p "$COVR_DIR"
+
+ rm -f "$TRACE_FILE"
+ rm -f "$DMESG_FILE"
+ rm -f "$LOG_DIR"/rds-*.pcap
+ rm -f "$COVR_DIR"/gcovr*
+
+ echo "# Traces will be logged to ${TRACE_FILE}"
+ TRACE_CMD=(strace -T -tt -o "${TRACE_FILE}")
+fi
+
+set +e
+echo "# running RDS tests..."
+"${TRACE_CMD[@]}" python3 "$(dirname "$0")/test.py" "${FLAGS[@]}" -t "$TIMEOUT"
+
+test_rc=$?
+
+if [[ -n "$LOG_DIR" ]]; then
+ dmesg > "${DMESG_FILE}"
+fi
+
+if [[ -n "$LOG_DIR" ]] && [ "$GENERATE_GCOV_REPORT" -eq 1 ]; then
+ echo "# saving coverage data..."
+
+ # Ensure debugfs is mounted before reading gcov data.
+ if ! mountpoint -q /sys/kernel/debug 2>/dev/null; then
+ mount -t debugfs debugfs /sys/kernel/debug 2>/dev/null || true
+ fi
+
+ (set +x; cd /sys/kernel/debug/gcov; find ./* -name '*.gcda' | \
+ while read -r f
+ do
+ cat < "/sys/kernel/debug/gcov/$f" > "/$f"
+ done)
+
+ echo "# running gcovr..."
+ gcovr -s --html-details --gcov-executable "$GCOV_CMD" --gcov-ignore-parse-errors \
+ --root "${ksrc_dir}" -o "${COVR_DIR}/gcovr" "${ksrc_dir}/net/rds/" \
+ > "${LOG_DIR}/gcovr.log" 2>&1
+ echo "# gcovr log: ${LOG_DIR}/gcovr.log"
+else
+ echo "# Coverage report will be skipped"
+fi
+
+if [ "$test_rc" -eq 0 ]; then
+ echo "# PASS: Test completed successfully"
+else
+ echo "# FAIL: Test failed"
+fi
+
+exit "$test_rc"
diff --git a/tools/testing/selftests/net/rds/run.sh b/tools/testing/selftests/net/rds/run.sh
deleted file mode 100755
index 8aee244f582a..000000000000
--- a/tools/testing/selftests/net/rds/run.sh
+++ /dev/null
@@ -1,224 +0,0 @@
-#! /bin/bash
-# SPDX-License-Identifier: GPL-2.0
-
-set -e
-set -u
-
-unset KBUILD_OUTPUT
-
-current_dir="$(realpath "$(dirname "$0")")"
-build_dir="$current_dir"
-
-build_include="$current_dir/include.sh"
-if test -f "$build_include"; then
- # this include will define "$mk_build_dir" as the location the test was
- # built. We will need this if the tests are installed in a location
- # other than the kernel source
-
- source "$build_include"
- build_dir="$mk_build_dir"
-fi
-
-# This test requires kernel source and the *.gcda data therein
-# Locate the top level of the kernel source, and the net/rds
-# subfolder with the appropriate *.gcno object files
-ksrc_dir="$(realpath "$build_dir"/../../../../../)"
-kconfig="$ksrc_dir/.config"
-obj_dir="$ksrc_dir/net/rds"
-
-GCOV_CMD=gcov
-
-#check to see if the host has the required packages to generate a gcov report
-check_gcov_env()
-{
- if ! which "$GCOV_CMD" > /dev/null 2>&1; then
- echo "Warning: Could not find gcov. "
- GENERATE_GCOV_REPORT=0
- return
- fi
-
- # the gcov version must match the gcc version
- GCC_VER=$(gcc -dumpfullversion)
- GCOV_VER=$($GCOV_CMD -v | grep gcov | awk '{print $3}'| awk 'BEGIN {FS="-"}{print $1}')
- if [ "$GCOV_VER" != "$GCC_VER" ]; then
- #attempt to find a matching gcov version
- GCOV_CMD=gcov-$(gcc -dumpversion)
-
- if ! which "$GCOV_CMD" > /dev/null 2>&1; then
- echo "Warning: Could not find an appropriate gcov installation. \
- gcov version must match gcc version"
- GENERATE_GCOV_REPORT=0
- return
- fi
-
- #recheck version number of found gcov executable
- GCOV_VER=$($GCOV_CMD -v | grep gcov | awk '{print $3}'| \
- awk 'BEGIN {FS="-"}{print $1}')
- if [ "$GCOV_VER" != "$GCC_VER" ]; then
- echo "Warning: Could not find an appropriate gcov installation. \
- gcov version must match gcc version"
- GENERATE_GCOV_REPORT=0
- else
- echo "Warning: Mismatched gcc and gcov detected. Using $GCOV_CMD"
- fi
- fi
-}
-
-# Check to see if the kconfig has the required configs to generate a coverage report
-check_gcov_conf()
-{
- if ! grep -x "CONFIG_GCOV_PROFILE_RDS=y" "$kconfig" > /dev/null 2>&1; then
- echo "INFO: CONFIG_GCOV_PROFILE_RDS should be enabled for coverage reports"
- GENERATE_GCOV_REPORT=0
- fi
- if ! grep -x "CONFIG_GCOV_KERNEL=y" "$kconfig" > /dev/null 2>&1; then
- echo "INFO: CONFIG_GCOV_KERNEL should be enabled for coverage reports"
- GENERATE_GCOV_REPORT=0
- fi
- if grep -x "CONFIG_GCOV_PROFILE_ALL=y" "$kconfig" > /dev/null 2>&1; then
- echo "INFO: CONFIG_GCOV_PROFILE_ALL should be disabled for coverage reports"
- GENERATE_GCOV_REPORT=0
- fi
-
- if [ "$GENERATE_GCOV_REPORT" -eq 0 ]; then
- echo "To enable gcov reports, please run "\
- "\"tools/testing/selftests/net/rds/config.sh -g\" and rebuild the kernel"
- else
- # if we have the required kernel configs, proceed to check the environment to
- # ensure we have the required gcov packages
- check_gcov_env
- fi
-}
-
-# Kselftest framework requirement - SKIP code is 4.
-check_conf_enabled() {
- if ! grep -x "$1=y" "$kconfig" > /dev/null 2>&1; then
- echo "selftests: [SKIP] This test requires $1 enabled"
- echo "Please run tools/testing/selftests/net/rds/config.sh and rebuild the kernel"
- exit 4
- fi
-}
-check_conf_disabled() {
- if grep -x "$1=y" "$kconfig" > /dev/null 2>&1; then
- echo "selftests: [SKIP] This test requires $1 disabled"
- echo "Please run tools/testing/selftests/net/rds/config.sh and rebuild the kernel"
- exit 4
- fi
-}
-check_conf() {
- check_conf_enabled CONFIG_NET_SCH_NETEM
- check_conf_enabled CONFIG_VETH
- check_conf_enabled CONFIG_NET_NS
- check_conf_enabled CONFIG_RDS_TCP
- check_conf_enabled CONFIG_RDS
- check_conf_disabled CONFIG_MODULES
-}
-
-check_env()
-{
- if ! test -d "$obj_dir"; then
- echo "selftests: [SKIP] This test requires a kernel source tree"
- exit 4
- fi
- if ! test -e "$kconfig"; then
- echo "selftests: [SKIP] This test requires a configured kernel source tree"
- exit 4
- fi
- if ! which strace > /dev/null 2>&1; then
- echo "selftests: [SKIP] Could not run test without strace"
- exit 4
- fi
- if ! which tcpdump > /dev/null 2>&1; then
- echo "selftests: [SKIP] Could not run test without tcpdump"
- exit 4
- fi
-
- if ! which python3 > /dev/null 2>&1; then
- echo "selftests: [SKIP] Could not run test without python3"
- exit 4
- fi
-
- python_major=$(python3 -c "import sys; print(sys.version_info[0])")
- python_minor=$(python3 -c "import sys; print(sys.version_info[1])")
- if [[ python_major -lt 3 || ( python_major -eq 3 && python_minor -lt 9 ) ]] ; then
- echo "selftests: [SKIP] Could not run test without at least python3.9"
- python3 -V
- exit 4
- fi
-}
-
-LOG_DIR="$current_dir"/rds_logs
-PLOSS=0
-PCORRUPT=0
-PDUP=0
-GENERATE_GCOV_REPORT=1
-while getopts "d:l:c:u:" opt; do
- case ${opt} in
- d)
- LOG_DIR=${OPTARG}
- ;;
- l)
- PLOSS=${OPTARG}
- ;;
- c)
- PCORRUPT=${OPTARG}
- ;;
- u)
- PDUP=${OPTARG}
- ;;
- :)
- echo "USAGE: run.sh [-d logdir] [-l packet_loss] [-c packet_corruption]" \
- "[-u packet_duplcate] [-g]"
- exit 1
- ;;
- ?)
- echo "Invalid option: -${OPTARG}."
- exit 1
- ;;
- esac
-done
-
-
-check_env
-check_conf
-check_gcov_conf
-
-
-rm -fr "$LOG_DIR"
-TRACE_FILE="${LOG_DIR}/rds-strace.txt"
-COVR_DIR="${LOG_DIR}/coverage/"
-mkdir -p "$LOG_DIR"
-mkdir -p "$COVR_DIR"
-
-set +e
-echo running RDS tests...
-echo Traces will be logged to "$TRACE_FILE"
-rm -f "$TRACE_FILE"
-strace -T -tt -o "$TRACE_FILE" python3 "$(dirname "$0")/test.py" --timeout 400 -d "$LOG_DIR" \
- -l "$PLOSS" -c "$PCORRUPT" -u "$PDUP"
-
-test_rc=$?
-dmesg > "${LOG_DIR}/dmesg.out"
-
-if [ "$GENERATE_GCOV_REPORT" -eq 1 ]; then
- echo saving coverage data...
- (set +x; cd /sys/kernel/debug/gcov; find ./* -name '*.gcda' | \
- while read -r f
- do
- cat < "/sys/kernel/debug/gcov/$f" > "/$f"
- done)
-
- echo running gcovr...
- gcovr -s --html-details --gcov-executable "$GCOV_CMD" --gcov-ignore-parse-errors \
- -o "${COVR_DIR}/gcovr" "${ksrc_dir}/net/rds/"
-else
- echo "Coverage report will be skipped"
-fi
-
-if [ "$test_rc" -eq 0 ]; then
- echo "PASS: Test completed successfully"
-else
- echo "FAIL: Test failed"
-fi
-
-exit "$test_rc"
diff --git a/tools/testing/selftests/net/rds/settings b/tools/testing/selftests/net/rds/settings
new file mode 100644
index 000000000000..8cb41e6a83cc
--- /dev/null
+++ b/tools/testing/selftests/net/rds/settings
@@ -0,0 +1 @@
+timeout=800
diff --git a/tools/testing/selftests/net/rds/test.py b/tools/testing/selftests/net/rds/test.py
index 4a7178d11193..9e4df01cb0d4 100755
--- a/tools/testing/selftests/net/rds/test.py
+++ b/tools/testing/selftests/net/rds/test.py
@@ -1,77 +1,486 @@
#! /usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
+"""
+This module provides functional testing for the net/rds component.
+"""
import argparse
+import atexit
import ctypes
import errno
import hashlib
import os
import select
+import re
import signal
import socket
import subprocess
import sys
-import atexit
-from pwd import getpwuid
-from os import stat
+import time
# Allow utils module to be imported from different directory
this_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.append(os.path.join(this_dir, "../"))
-from lib.py.utils import ip
+# pylint: disable-next=wrong-import-position,import-error,no-name-in-module
+from lib.py.utils import ip, cmd # noqa: E402
+# pylint: disable-next=wrong-import-position,import-error,no-name-in-module
+from lib.py.ksft import ksft_pr # noqa: E402
libc = ctypes.cdll.LoadLibrary('libc.so.6')
setns = libc.setns
-net0 = 'net0'
-net1 = 'net1'
+NET0 = 'net0'
+NET1 = 'net1'
+
+VETH0 = 'veth0'
+VETH1 = 'veth1'
+
+tcpdump_procs = []
+tcp_addrs = [
+ # we technically don't need different port numbers, but this will
+ # help identify traffic in the network analyzer
+ ('10.0.0.1', 10000),
+ ('10.0.0.2', 20000),
+]
-veth0 = 'veth0'
-veth1 = 'veth1'
+# RDMA network configs
+RXE_DEV0 = 'rxe0'
+RXE_DEV1 = 'rxe1'
+
+VETH_RDMA0 = 'veth_rdma0'
+VETH_RDMA1 = 'veth_rdma1'
+
+rdma_addrs = [
+ ('10.0.0.3', 30000),
+ ('10.0.0.4', 30000),
+]
+
+# send_packets flag space
+OP_FLAG_TCP = 0x1
+OP_FLAG_RDMA = 0x2
+
+# from include/uapi/linux/rds.h: SO_RDS_TRANSPORT pins a socket to a
+# specific RDS transport so connection setup cannot silently fall back
+# to another (e.g. loopback) transport.
+SOL_RDS = 276
+SO_RDS_TRANSPORT = 8
+RDS_TRANS_TCP = 2
+RDS_TRANS_IB = 0
+
+signal_handler_label = ""
+
+tap_idx = 0
+nr_pass = 0
+nr_fail = 0
# Helper function for creating a socket inside a network namespace.
# We need this because otherwise RDS will detect that the two TCP
# sockets are on the same interface and use the loop transport instead
# of the TCP transport.
-def netns_socket(netns, *args):
+def netns_socket(netns, *sock_args):
+ """
+ Creates sockets inside of network namespace
+
+ :param netns: the name of the network namespace
+ :param sock_args: socket family and type
+ """
u0, u1 = socket.socketpair(socket.AF_UNIX, socket.SOCK_SEQPACKET)
child = os.fork()
if child == 0:
- # change network namespace
- with open(f'/var/run/netns/{netns}') as f:
- try:
- ret = setns(f.fileno(), 0)
- except IOError as e:
- print(e.errno)
- print(e)
-
- # create socket in target namespace
- s = socket.socket(*args)
+ try:
+ # change network namespace
+ with open(f'/var/run/netns/{netns}', encoding='utf-8') as f:
+ setns(f.fileno(), 0)
+ # create socket in target namespace
+ sock = socket.socket(*sock_args)
- # send resulting socket to parent
- socket.send_fds(u0, [], [s.fileno()])
+ # send resulting socket to parent
+ socket.send_fds(u0, [], [sock.fileno()])
- sys.exit(0)
+ os._exit(0)
+ except BaseException:
+ os._exit(1)
# receive socket from child
- _, s, _, _ = socket.recv_fds(u1, 0, 1)
- os.waitpid(child, 0)
+ _, fds, _, _ = socket.recv_fds(u1, 0, 1)
+ _, status = os.waitpid(child, 0)
u0.close()
u1.close()
- return socket.fromfd(s[0], *args)
+ if not os.WIFEXITED(status) or os.WEXITSTATUS(status) != 0:
+ raise RuntimeError(
+ f"netns_socket child failed in netns {netns} (status={status})")
+ return socket.fromfd(fds[0], *sock_args)
-def signal_handler(sig, frame):
- print('Test timed out')
+def send_burst(socks, ip_addrs, snd_hashes, nr_sent, nr_total):
+ """Send until blocked or nr_total reached. Return updated nr_sent."""
+
+ while nr_sent < nr_total:
+ data = hashlib.sha256(
+ f'packet {nr_sent}'.encode('utf-8')).hexdigest().encode('utf-8')
+ # pseudo-random send/receive pattern
+ snd_idx = nr_sent % 2
+ rcv_idx = 1 - (nr_sent % 3) % 2
+
+ snd = socks[snd_idx]
+ rcv = socks[rcv_idx]
+ try:
+ snd.sendto(data, ip_addrs[rcv_idx])
+ except BlockingIOError:
+ return nr_sent
+ except OSError as e:
+ if e.errno in (errno.ENOBUFS, errno.ECONNRESET, errno.EPIPE):
+ return nr_sent
+ raise
+ snd_hashes.setdefault((snd.fileno(), rcv.fileno()),
+ hashlib.sha256()).update(f'<{data}>'.encode('utf-8'))
+ nr_sent += 1
+ return nr_sent
+
+def recv_burst(epoll, socks, ip_addrs, rcv_hashes, nr_rcv):
+ """Drain whatever's readable from epoll. Return updated nr_recv."""
+ for filen, evntmask in epoll.poll():
+ if not evntmask & select.EPOLLRDNORM:
+ continue
+ rcv = next(s for s in socks if s.fileno() == filen)
+ while True:
+ try:
+ data, adr = rcv.recvfrom(1024)
+ except BlockingIOError:
+ break
+ snd_idx = ip_addrs.index(adr)
+ snd = socks[snd_idx]
+ rcv_hashes.setdefault((snd.fileno(), rcv.fileno()),
+ hashlib.sha256()).update(f'<{data}>'.encode('utf-8'))
+ nr_rcv += 1
+ return nr_rcv
+
+def check_info(socks):
+ """
+ Check all rds info pages for errors
+
+ :param socks: list of sockets to check
+ """
+
+ # the Python socket module doesn't know these
+ rds_info_first = 10000
+ rds_info_last = 10017
+
+ nr_success = 0
+ nr_error = 0
+
+ for sock in socks:
+ for optname in range(rds_info_first, rds_info_last + 1):
+ # Sigh, the Python socket module doesn't allow us to pass
+ # buffer lengths greater than 1024 for some reason. RDS
+ # wants multiple pages.
+ try:
+ sock.getsockopt(socket.SOL_RDS, optname, 1024)
+ nr_success = nr_success + 1
+ except OSError as e:
+ nr_error = nr_error + 1
+ if e.errno == errno.ENOSPC:
+ # ignore
+ pass
+
+ ksft_pr(f"getsockopt(): {nr_success}/{nr_error}")
+
+def verify_hashes(snd_hashes, rcv_hashes):
+ """Compare send/recv hashes per (sender, receiver) pair."""
+ for key, snd_hash in snd_hashes.items():
+ rcv_hash = rcv_hashes.get(key)
+ if rcv_hash is None:
+ ksft_pr("FAIL: No data received")
+ return 1
+ if snd_hash.hexdigest() != rcv_hash.hexdigest():
+ ksft_pr("FAIL: Send/recv mismatch")
+ ksft_pr("hash expected:", snd_hash.hexdigest())
+ ksft_pr("hash received:", rcv_hash.hexdigest())
+ return 1
+ ksft_pr(f"{key[0]}/{key[1]}: ok")
+ return 0
+
+def snd_rcv_packets(env):
+ """
+ Send packets on the given network interfaces
+
+ :param env: transport-environment dict for setup_tcp() / setup_rdma().
+ "addrs": list of (ip, port) tuples matching the sockets
+ "netns": list of netns names for TCP or None for RDMA
+ "flags": OP_FLAG_TCP or OP_FLAG_RDMA, selects sockets
+ """
+
+ addrs = env["addrs"]
+ netns_list = env["netns"]
+ flags = env.get("flags", 0)
+
+ if (flags & OP_FLAG_TCP) and (flags & OP_FLAG_RDMA):
+ raise RuntimeError(f"Invalid transport flag sets multiple transports: {flags}")
+
+ if flags & OP_FLAG_TCP:
+ sockets = [
+ netns_socket(netns_list[0], socket.AF_RDS, socket.SOCK_SEQPACKET),
+ netns_socket(netns_list[1], socket.AF_RDS, socket.SOCK_SEQPACKET),
+ ]
+
+ # Pin the sockets to the TCP transport so it doesn't fail over to a
+ # different transport during this test
+ for s in sockets:
+ s.setsockopt(SOL_RDS, SO_RDS_TRANSPORT, RDS_TRANS_TCP)
+ elif flags & OP_FLAG_RDMA:
+ sockets = [
+ socket.socket(socket.AF_RDS, socket.SOCK_SEQPACKET),
+ socket.socket(socket.AF_RDS, socket.SOCK_SEQPACKET),
+ ]
+
+ # Pin the sockets to the RDMA transport so it doesn't fail over to a
+ # different transport during this test
+ for s in sockets:
+ s.setsockopt(SOL_RDS, SO_RDS_TRANSPORT, RDS_TRANS_IB)
+ else:
+ raise RuntimeError(f"Invalid transport flag sets no transports: {flags}")
+
+ for s, addr in zip(sockets, addrs):
+ s.bind(addr)
+ s.setblocking(0)
+
+ send_hashes = {}
+ recv_hashes = {}
+
+ ep = select.epoll()
+
+ for s in sockets:
+ ep.register(s, select.EPOLLRDNORM)
+
+ num_packets = 50000
+ nr_send = 0
+ nr_recv = 0
+
+ while nr_send < num_packets:
+
+ # Send as much as we can without blocking
+ ksft_pr("sending...", nr_send, nr_recv)
+ nr_send = send_burst(sockets, addrs, send_hashes, nr_send, num_packets)
+
+ # Receive as much as we can without blocking
+ ksft_pr("receiving...", nr_send, nr_recv)
+ while nr_recv < nr_send:
+ nr_recv = recv_burst(ep, sockets, addrs, recv_hashes, nr_recv)
+
+ # exercise net/rds/tcp.c:rds_tcp_sysctl_reset()
+ if netns_list:
+ for net in netns_list:
+ ip(f"netns exec {net} /usr/sbin/sysctl net.rds.tcp.rds_tcp_rcvbuf=10000")
+ ip(f"netns exec {net} /usr/sbin/sysctl net.rds.tcp.rds_tcp_sndbuf=10000")
+
+ ksft_pr("done", nr_send, nr_recv)
+
+ check_info(sockets)
+
+ # We're done sending and receiving stuff, now let's check if what
+ # we received is what we sent.
+ rc = verify_hashes(send_hashes, recv_hashes)
+
+ ep.close()
+ for s in sockets:
+ s.close()
+
+ return rc
+
+def stop_pcaps():
+ """Stop tcpdump processes.
+
+ We use pop() here to drain the list in the event that the test
+ completes after the signal handler is fired. List will be empty
+ if logdir is not set
+ """
+
+ if not tcpdump_procs:
+ return
+
+ ksft_pr("Stopping network packet captures")
+ while tcpdump_procs:
+ proc = tcpdump_procs.pop()
+ proc.terminate()
+ try:
+ proc.wait(timeout=5)
+ except subprocess.TimeoutExpired:
+ proc.kill()
+ proc.wait()
+
+def signal_handler(_sig, _frame):
+ """
+ Test timed out signal handler
+ """
+ ksft_pr(f"Test timed out: {signal_handler_label}")
+ print(f"not ok {tap_idx} rds selftest {signal_handler_label}")
sys.exit(1)
+def setup_tcp():
+ """
+ Configure tcp network
+ """
+
+ # clean up any leftovers from a previously interrupted run
+ teardown_tcp()
+
+ ip(f"netns add {NET0}")
+ ip(f"netns add {NET1}")
+ ip("link add type veth")
+
+ # Move TCP interfaces into separate namespaces so they can no longer be
+ # bound directly; this prevents rds from switching over from the tcp
+ # transport to the loop transport.
+ ip(f"link set {VETH0} netns {NET0} up")
+ ip(f"link set {VETH1} netns {NET1} up")
+
+ # add addresses
+ ip(f"-n {NET0} addr add {tcp_addrs[0][0]}/32 dev {VETH0}")
+ ip(f"-n {NET1} addr add {tcp_addrs[1][0]}/32 dev {VETH1}")
+
+ # add routes
+ ip(f"-n {NET0} route add {tcp_addrs[1][0]}/32 dev {VETH0}")
+ ip(f"-n {NET1} route add {tcp_addrs[0][0]}/32 dev {VETH1}")
+
+ # sanity check that our two interfaces/addresses are correctly set up
+ # and communicating by doing a single ping
+ ip(f"netns exec {NET0} ping -c 1 {tcp_addrs[1][0]}")
+
+ # Start a packet capture on each network
+ if logdir is not None:
+ for netn in [NET0, NET1]:
+ pcap = logdir+'/rds-'+netn+'.pcap'
+
+ tcpdump_cmd = ['ip', 'netns', 'exec', netn, '/usr/sbin/tcpdump']
+ sudo_user = os.environ.get('SUDO_USER')
+ if sudo_user:
+ tcpdump_cmd.extend(['-Z', sudo_user])
+ tcpdump_cmd.extend(['-i', 'any', '-w', pcap])
+
+ # pylint: disable-next=consider-using-with
+ p = subprocess.Popen(tcpdump_cmd,
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ tcpdump_procs.append(p)
+
+ # simulate packet loss, duplication and corruption
+ for netn, iface in [(NET0, VETH0), (NET1, VETH1)]:
+ ip(f"netns exec {netn} /usr/sbin/tc qdisc add dev {iface} root netem \
+ corrupt {PACKET_CORRUPTION} loss {PACKET_LOSS} duplicate \
+ {PACKET_DUPLICATE}")
+
+def teardown_tcp():
+ """
+ Tear down the tcp network configured by setup_tcp().
+
+ Removing the namespaces also removes the veth pair, addresses,
+ routes, and netem qdisc that live inside them. fail=False so
+ this is safe to call in error paths after a partial or complete setup.
+ """
+ cmd(f"ip netns del {NET0}", fail=False)
+ cmd(f"ip netns del {NET1}", fail=False)
+
+def get_iface_mac(iface):
+ """Return the MAC address of a local network interface."""
+ out = subprocess.check_output(['ip', 'link', 'show', iface], text=True)
+ mac = re.search(r'link/ether\s+([0-9a-f:]+)', out)
+ if not mac:
+ raise RuntimeError(f"Cannot determine MAC address of {iface}")
+ return mac.group(1)
+
+def setup_rdma():
+ """
+ Configure rdma network
+ """
+
+ # remove links left over by previously interrupted run.
+ teardown_rdma()
+
+ # use call here since modprobe may fail if the rdma_rxe
+ # module is built-in
+ subprocess.call(['modprobe', 'rdma_rxe'],
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+
+ ip(f"link add {VETH_RDMA0} type veth peer name {VETH_RDMA1}")
+
+ ip(f"link set {VETH_RDMA0} up")
+ ip(f"link set {VETH_RDMA1} up")
+
+ # Since both addresses are in the same namespace, the source address
+ # is always local, so enable accept_local
+ cmd(f"/usr/sbin/sysctl -q net.ipv4.conf.{VETH_RDMA0}.accept_local=1")
+ cmd(f"/usr/sbin/sysctl -q net.ipv4.conf.{VETH_RDMA1}.accept_local=1")
+
+ # Reverse path filters must be disabled so that the local routes don't
+ # cause RPF failures.
+ cmd(f"/usr/sbin/sysctl -q net.ipv4.conf.{VETH_RDMA0}.rp_filter=0")
+ cmd(f"/usr/sbin/sysctl -q net.ipv4.conf.{VETH_RDMA1}.rp_filter=0")
+
+ # add addresses
+ ip(f"addr add {rdma_addrs[0][0]}/32 dev {VETH_RDMA0}")
+ ip(f"addr add {rdma_addrs[1][0]}/32 dev {VETH_RDMA1}")
+
+ # add routes
+ ip(f"route add {rdma_addrs[1][0]}/32 dev {VETH_RDMA0}")
+ ip(f"route add {rdma_addrs[0][0]}/32 dev {VETH_RDMA1}")
+
+ # ARP will not resolve neighbor IPs on /32 routes without a subnet.
+ # Avoid this by adding neighbors directly so RDMA CM can populate path
+ # records with correct mac addrs without waiting for the ARP.
+ mac0 = get_iface_mac(VETH_RDMA0)
+ mac1 = get_iface_mac(VETH_RDMA1)
+ ip(f"neigh add {rdma_addrs[1][0]} lladdr {mac1} dev {VETH_RDMA0} nud permanent")
+ ip(f"neigh add {rdma_addrs[0][0]} lladdr {mac0} dev {VETH_RDMA1} nud permanent")
+
+ cmd(f'rdma link add {RXE_DEV0} type rxe netdev {VETH_RDMA0}')
+ cmd(f'rdma link add {RXE_DEV1} type rxe netdev {VETH_RDMA1}')
+
+ time.sleep(1) # allow RXE devices to initialise
+
+ # Start a packet capture on each network
+ if logdir is not None:
+ for iface in [VETH_RDMA0, VETH_RDMA1]:
+ pcap = logdir+'/rds-roce-'+iface+'.pcap'
+
+ tcpdump_cmd = ['/usr/sbin/tcpdump']
+ sudo_user = os.environ.get('SUDO_USER')
+ if sudo_user:
+ tcpdump_cmd.extend(['-Z', sudo_user])
+ tcpdump_cmd.extend(['-i', iface, '-w', pcap])
+
+ # pylint: disable-next=consider-using-with
+ p = subprocess.Popen(tcpdump_cmd,
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ tcpdump_procs.append(p)
+
+ # simulate packet loss, duplication and corruption
+ for iface in [VETH_RDMA0, VETH_RDMA1]:
+ cmd(f"/usr/sbin/tc qdisc add dev {iface} root netem \
+ corrupt {PACKET_CORRUPTION} loss {PACKET_LOSS} duplicate \
+ {PACKET_DUPLICATE}")
+
+def teardown_rdma():
+ """
+ Tear down the rdma network configured by setup_rdma().
+ """
+
+ # remove links left over by previously interrupted run.
+ cmd(f'rdma link del {RXE_DEV0}', fail=False)
+ cmd(f'rdma link del {RXE_DEV1}', fail=False)
+ cmd(f'ip link del {VETH_RDMA0}', fail=False)
+
+
#Parse out command line arguments. We take an optional
# timeout parameter and an optional log output folder
parser = argparse.ArgumentParser(description="init script args",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-d", "--logdir", action="store",
- help="directory to store logs", default="/tmp")
-parser.add_argument('--timeout', help="timeout to terminate hung test",
+ help="directory to store logs", default=None)
+parser.add_argument("-T", "--transport", default="tcp",
+ help="Comma-separated list of transports to test: "
+ "tcp, rdma, or tcp,rdma. Each matching test "
+ "is run once per transport. "
+ "'rdma' requires CONFIG_RDS_RDMA and rdma_rxe.")
+parser.add_argument('-t', '--timeout', help="timeout to terminate hung test",
type=int, default=0)
parser.add_argument('-l', '--loss', help="Simulate tcp packet loss",
type=int, default=0)
@@ -81,185 +490,67 @@ parser.add_argument('-u', '--duplicate', help="Simulate tcp packet duplication",
type=int, default=0)
args = parser.parse_args()
logdir=args.logdir
-packet_loss=str(args.loss)+'%'
-packet_corruption=str(args.corruption)+'%'
-packet_duplicate=str(args.duplicate)+'%'
-
-ip(f"netns add {net0}")
-ip(f"netns add {net1}")
-ip(f"link add type veth")
-
-addrs = [
- # we technically don't need different port numbers, but this will
- # help identify traffic in the network analyzer
- ('10.0.0.1', 10000),
- ('10.0.0.2', 20000),
-]
-
-# move interfaces to separate namespaces so they can no longer be
-# bound directly; this prevents rds from switching over from the tcp
-# transport to the loop transport.
-ip(f"link set {veth0} netns {net0} up")
-ip(f"link set {veth1} netns {net1} up")
-
-
-
-# add addresses
-ip(f"-n {net0} addr add {addrs[0][0]}/32 dev {veth0}")
-ip(f"-n {net1} addr add {addrs[1][0]}/32 dev {veth1}")
-
-# add routes
-ip(f"-n {net0} route add {addrs[1][0]}/32 dev {veth0}")
-ip(f"-n {net1} route add {addrs[0][0]}/32 dev {veth1}")
-
-# sanity check that our two interfaces/addresses are correctly set up
-# and communicating by doing a single ping
-ip(f"netns exec {net0} ping -c 1 {addrs[1][0]}")
-
-# Start a packet capture on each network
-for net in [net0, net1]:
- tcpdump_pid = os.fork()
- if tcpdump_pid == 0:
- pcap = logdir+'/'+net+'.pcap'
- subprocess.check_call(['touch', pcap])
- user = getpwuid(stat(pcap).st_uid).pw_name
- ip(f"netns exec {net} /usr/sbin/tcpdump -Z {user} -i any -w {pcap}")
- sys.exit(0)
-
-# simulate packet loss, duplication and corruption
-for net, iface in [(net0, veth0), (net1, veth1)]:
- ip(f"netns exec {net} /usr/sbin/tc qdisc add dev {iface} root netem \
- corrupt {packet_corruption} loss {packet_loss} duplicate \
- {packet_duplicate}")
-
-# add a timeout
-if args.timeout > 0:
- signal.alarm(args.timeout)
- signal.signal(signal.SIGALRM, signal_handler)
-
-sockets = [
- netns_socket(net0, socket.AF_RDS, socket.SOCK_SEQPACKET),
- netns_socket(net1, socket.AF_RDS, socket.SOCK_SEQPACKET),
-]
-
-for s, addr in zip(sockets, addrs):
- s.bind(addr)
- s.setblocking(0)
-
-fileno_to_socket = {
- s.fileno(): s for s in sockets
-}
-
-addr_to_socket = {
- addr: s for addr, s in zip(addrs, sockets)
-}
-
-socket_to_addr = {
- s: addr for addr, s in zip(addrs, sockets)
-}
-
-send_hashes = {}
-recv_hashes = {}
-
-ep = select.epoll()
-
-for s in sockets:
- ep.register(s, select.EPOLLRDNORM)
-
-n = 50000
-nr_send = 0
-nr_recv = 0
-
-while nr_send < n:
- # Send as much as we can without blocking
- print("sending...", nr_send, nr_recv)
- while nr_send < n:
- send_data = hashlib.sha256(
- f'packet {nr_send}'.encode('utf-8')).hexdigest().encode('utf-8')
-
- # pseudo-random send/receive pattern
- sender = sockets[nr_send % 2]
- receiver = sockets[1 - (nr_send % 3) % 2]
-
- try:
- sender.sendto(send_data, socket_to_addr[receiver])
- send_hashes.setdefault((sender.fileno(), receiver.fileno()),
- hashlib.sha256()).update(f'<{send_data}>'.encode('utf-8'))
- nr_send = nr_send + 1
- except BlockingIOError as e:
- break
- except OSError as e:
- if e.errno in [errno.ENOBUFS, errno.ECONNRESET, errno.EPIPE]:
- break
- raise
-
- # Receive as much as we can without blocking
- print("receiving...", nr_send, nr_recv)
- while nr_recv < nr_send:
- for fileno, eventmask in ep.poll():
- receiver = fileno_to_socket[fileno]
-
- if eventmask & select.EPOLLRDNORM:
- while True:
- try:
- recv_data, address = receiver.recvfrom(1024)
- sender = addr_to_socket[address]
- recv_hashes.setdefault((sender.fileno(),
- receiver.fileno()), hashlib.sha256()).update(
- f'<{recv_data}>'.encode('utf-8'))
- nr_recv = nr_recv + 1
- except BlockingIOError as e:
- break
-
- # exercise net/rds/tcp.c:rds_tcp_sysctl_reset()
- for net in [net0, net1]:
- ip(f"netns exec {net} /usr/sbin/sysctl net.rds.tcp.rds_tcp_rcvbuf=10000")
- ip(f"netns exec {net} /usr/sbin/sysctl net.rds.tcp.rds_tcp_sndbuf=10000")
-
-print("done", nr_send, nr_recv)
-
-# the Python socket module doesn't know these
-RDS_INFO_FIRST = 10000
-RDS_INFO_LAST = 10017
-
-nr_success = 0
-nr_error = 0
-
-for s in sockets:
- for optname in range(RDS_INFO_FIRST, RDS_INFO_LAST + 1):
- # Sigh, the Python socket module doesn't allow us to pass
- # buffer lengths greater than 1024 for some reason. RDS
- # wants multiple pages.
- try:
- s.getsockopt(socket.SOL_RDS, optname, 1024)
- nr_success = nr_success + 1
- except OSError as e:
- nr_error = nr_error + 1
- if e.errno == errno.ENOSPC:
- # ignore
- pass
-
-print(f"getsockopt(): {nr_success}/{nr_error}")
-
-print("Stopping network packet captures")
-subprocess.check_call(['killall', '-q', 'tcpdump'])
-
-# We're done sending and receiving stuff, now let's check if what
-# we received is what we sent.
-for (sender, receiver), send_hash in send_hashes.items():
- recv_hash = recv_hashes.get((sender, receiver))
-
- if recv_hash is None:
- print("FAIL: No data received")
- sys.exit(1)
-
- if send_hash.hexdigest() != recv_hash.hexdigest():
- print("FAIL: Send/recv mismatch")
- print("hash expected:", send_hash.hexdigest())
- print("hash received:", recv_hash.hexdigest())
- sys.exit(1)
-
- print(f"{sender}/{receiver}: ok")
-
-print("Success")
-sys.exit(0)
+PACKET_LOSS=str(args.loss)+'%'
+PACKET_CORRUPTION=str(args.corruption)+'%'
+PACKET_DUPLICATE=str(args.duplicate)+'%'
+
+# check transport is either tcp or rdma
+transports = [t.strip() for t in args.transport.split(',')]
+for t in transports:
+ if t not in ('tcp', 'rdma'):
+ raise SystemExit(f"test.py: unknown transport: {t!r}")
+
+# Register stop_pcaps before any network setups so that any partially setup
+# tcpdumps are still cleaned up on error
+atexit.register(stop_pcaps)
+
+# Set up all requested transports upfront so network plumbing is
+# ready before any test runs.
+transport_envs = {}
+FLAGS = 0
+if 'tcp' in transports:
+ # Register cleanups before setups to handle partial setups that error'd out
+ atexit.register(teardown_tcp)
+ setup_tcp()
+ transport_envs['tcp'] = {
+ 'addrs': tcp_addrs,
+ 'netns': [NET0, NET1],
+ 'flags': FLAGS | OP_FLAG_TCP,
+ }
+
+if 'rdma' in transports:
+ atexit.register(teardown_rdma)
+ setup_rdma()
+ transport_envs['rdma'] = {
+ 'addrs': rdma_addrs,
+ 'netns': None,
+ 'flags': FLAGS | OP_FLAG_RDMA,
+ }
+
+print("TAP version 13")
+print(f"1..{len(transport_envs)}")
+
+for transport, tenv in transport_envs.items():
+ tap_idx += 1
+
+ # add a timeout
+ if args.timeout > 0:
+ signal_handler_label = transport
+ signal.alarm(args.timeout)
+ signal.signal(signal.SIGALRM, signal_handler)
+
+ ret = snd_rcv_packets(tenv)
+
+ # cancel timeout
+ signal.alarm(0)
+
+ if ret == 0:
+ ksft_pr("Success")
+ print(f"ok {tap_idx} rds selftest {transport}")
+ nr_pass += 1
+ else:
+ print(f"not ok {tap_idx} rds selftest {transport}")
+ nr_fail += 1
+
+ksft_pr(f"Totals: pass:{nr_pass} fail:{nr_fail} skip:0")
+sys.exit(1 if nr_fail else 0)