diff options
Diffstat (limited to 'lib/testing/test_runner.sh')
-rwxr-xr-x | lib/testing/test_runner.sh | 591 |
1 files changed, 591 insertions, 0 deletions
diff --git a/lib/testing/test_runner.sh b/lib/testing/test_runner.sh new file mode 100755 index 0000000..726b902 --- /dev/null +++ b/lib/testing/test_runner.sh @@ -0,0 +1,591 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Script to setup and manage tests for xdp-tools. +# Based on the test-env script from xdp-tutorial. +# +# Author: Toke Høiland-Jørgensen (toke@redhat.com) +# Date: 26 May 2020 +# Copyright (c) 2020 Red Hat + +set -o errexit +set -o nounset +umask 077 + +TEST_PROG_DIR="${TEST_PROG_DIR:-$(dirname "${BASH_SOURCE[0]}")}" +SETUP_SCRIPT="$TEST_PROG_DIR/setup-netns-env.sh" +TEST_CONFIG="$TEST_PROG_DIR/test_config.sh" +IP6_SUBNET=fc42:dead:cafe # must have exactly three :-separated elements +IP6_PREFIX_SIZE=64 # Size of assigned prefixes +IP6_FULL_PREFIX_SIZE=48 # Size of IP6_SUBNET +IP4_SUBNET=10.11 +IP4_PREFIX_SIZE=24 # Size of assigned prefixes +IP4_FULL_PREFIX_SIZE=16 # Size of IP4_SUBNET +VLAN_IDS=(1 2) +GENERATED_NAME_PREFIX="xdptest" +ALL_TESTS="" +VERBOSE_TESTS=${V:-0} + +NEEDED_TOOLS="capinfos ethtool ip ping sed tc tcpdump timeout nc tshark" +MAX_NAMELEN=15 + +if [ -f "$TEST_CONFIG" ]; then + source "$TEST_CONFIG" +fi + +if command -v ping6 >/dev/null 2>&1; then + PING6=ping6 +else + PING6=ping +fi + +# Odd return value for skipping, as only 0-255 is valid. +SKIPPED_TEST=249 + +# Global state variables that will be set by options etc below +GENERATE_NEW=0 +CLEANUP_FUNC= +STATEDIR= +STATEFILE= +CMD= +NS= +LEGACY_IP=1 +USE_VLAN=0 +RUN_ON_INNER=0 + +# State variables that are written to and read from statefile +STATEVARS=(IP6_PREFIX IP4_PREFIX + INSIDE_IP6 INSIDE_IP4 INSIDE_MAC + OUTSIDE_IP6 OUTSIDE_IP4 OUTSIDE_MAC + ENABLE_IPV4 ENABLE_VLAN) +IP6_PREFIX= +IP4_PREFIX= +INSIDE_IP6= +INSIDE_IP4= +INSIDE_MAC= +OUTSIDE_IP6= +OUTSIDE_IP4= +OUTSIDE_MAC= +ENABLE_IPV4=0 +ENABLE_VLAN=0 + +is_trace_attach_supported() +{ + if [[ -z "${TRACE_ATTACH_SUPPORT:-}" ]]; then + [ -f "$STATEDIR/trace_attach_support" ] && \ + TRACE_ATTACH_SUPPORT=$(< "$STATEDIR/trace_attach_support") + + if [[ -z "${TRACE_ATTACH_SUPPORT:-}" ]]; then + RESULT=$($XDP_LOADER load -v "$NS" "$TEST_PROG_DIR/xdp_pass.o" 2>&1) + PID=$(start_background "$XDPDUMP -i $NS") + RESULT=$(stop_background "$PID") + if [[ "$RESULT" == *"The kernel does not support fentry function attach"* ]]; then + TRACE_ATTACH_SUPPORT="false" + else + TRACE_ATTACH_SUPPORT="true" + fi + echo "$TRACE_ATTACH_SUPPORT" > "$STATEDIR/trace_attach_support" + $XDP_LOADER unload "$NS" --all + fi + fi + + if [[ "$TRACE_ATTACH_SUPPORT" == "true" ]]; then + return 0 + else + return 1 + fi +} + +is_multiprog_supported() +{ + if [[ -z "${MULTIPROG_SUPPORT:-}" ]]; then + RESULT=$($XDP_LOADER load -v "$NS" "$TEST_PROG_DIR/xdp_pass.o" 2>&1) + if [[ "$RESULT" == *"Compatibility check for dispatcher program failed"* ]]; then + MULTIPROG_SUPPORT="false" + else + MULTIPROG_SUPPORT="true" + fi + $XDP_LOADER unload "$NS" --all + fi + + if [[ "$MULTIPROG_SUPPORT" == "true" ]]; then + return 0 + else + return 1 + fi +} + +is_progmap_supported() +{ + if [[ -z "${PROGMAP_SUPPORT:-}" ]]; then + RESULT=$(timeout -s INT 1 $XDP_BENCH redirect-cpu "$NS" -c 0 -r drop -vv 2>&1) + if [[ "$RESULT" == *"Create CPU entry failed: Cannot allocate memory"* ]]; then + PROGMAP_SUPPORT="false" + else + PROGMAP_SUPPORT="true" + fi + fi + + if [[ "$PROGMAP_SUPPORT" == "true" ]]; then + return 0 + else + return 1 + fi +} + +skip_if_missing_veth_rxq() +{ + if ! ethtool -l $NS >/dev/null 2>&1; then + exit "$SKIPPED_TEST" + fi +} + +skip_if_missing_cpumap_attach() +{ + if ! $TEST_PROG_DIR/test-tool probe cpumap-prog; then + exit "$SKIPPED_TEST" + fi +} + +skip_if_missing_kernel_symbol() +{ + if ! grep -q "$1" /proc/kallsyms; then + exit "$SKIPPED_TEST" + fi +} + +skip_if_legacy_fallback() +{ + if ! is_multiprog_supported; then + exit "$SKIPPED_TEST" + fi +} + +skip_if_missing_trace_attach() +{ + if ! is_trace_attach_supported; then + exit "$SKIPPED_TEST" + fi +} + +die() +{ + echo "$1" >&2 + exit 1 +} + +start_background() +{ + local TMP_FILE="${STATEDIR}/tmp_proc_$$_$RANDOM" + setsid bash -c "$*" &> ${TMP_FILE} & + local PID=$! + sleep 2 # Wait to make sure the command is executed in the background + + mv "$TMP_FILE" "${STATEDIR}/proc/${PID}" >& /dev/null + + echo "$PID" +} + +start_background_no_stderr() +{ + local TMP_FILE="${STATEDIR}/tmp_proc_$$_$RANDOM" + setsid bash -c "$*" 1> ${TMP_FILE} 2>/dev/null & + local PID=$! + sleep 2 # Wait to make sure the command is executed in the background + + mv "$TMP_FILE" "${STATEDIR}/proc/${PID}" >& /dev/null + + echo "$PID" +} + +start_background_ns_devnull() +{ + get_nsname && ensure_nsname "$NS" + + local TMP_FILE="${STATEDIR}/tmp_proc_$$_$RANDOM" + setsid ip netns exec "$NS" env TESTENV_NAME="$NS" "$SETUP_SCRIPT" bash -c "$*" 1>/dev/null 2>${TMP_FILE} & + local PID=$! + sleep 2 # Wait to make sure the command is executed in the background + + mv "$TMP_FILE" "${STATEDIR}/proc/${PID}" >& /dev/null + echo $PID +} + +stop_background() +{ + local PID=$1 + + local OUTPUT_FILE="${STATEDIR}/proc/${PID}" + if kill -SIGINT "-$PID" 2>/dev/null; then + sleep 2 # Wait to make sure the buffer is flushed after the shutdown + kill -SIGTERM "-$PID" 2>/dev/null && sleep 1 # just in case SIGINT was not enough + fi + + if [ -f "$OUTPUT_FILE" ]; then + cat "$OUTPUT_FILE" + rm "$OUTPUT_FILE" >& /dev/null + fi +} + +check_prereq() +{ + local max_locked_mem=$(ulimit -l) + + for t in $NEEDED_TOOLS; do + command -v "$t" > /dev/null || die "Missing required tool: $t" + done + + if [ "$EUID" -ne "0" ]; then + die "This script needs root permissions to run." + fi + + STATEDIR="$(mktemp -d --tmpdir=${TMPDIR:-/tmp} --suffix=.xdptest)" + if [ $? -ne 0 ]; then + die "Unable to create state dir in $TMPDIR" + fi + mkdir ${STATEDIR}/proc + + if [ "$max_locked_mem" != "unlimited" ]; then + ulimit -l unlimited || die "Unable to set ulimit" + fi +} + +get_nsname() +{ + local GENERATE=${1:-1} + + if [ -z "$NS" ]; then + [ -f "$STATEDIR/current" ] && NS=$(< "$STATEDIR/current") + + if [ "$GENERATE" -eq "1" ] && [ -z "$NS" ] || [ "$GENERATE_NEW" -eq "1" ]; then + NS="$GENERATED_NAME_PREFIX" + while [ -e "$STATEDIR/${NS}.state" ]; do + NS=$(printf "%s-%04x" "$GENERATED_NAME_PREFIX" $RANDOM) + done + fi + fi + + if [ "${#NS}" -gt "$MAX_NAMELEN" ]; then + die "Environment name '$NS' is too long (max $MAX_NAMELEN)" + fi + + STATEFILE="$STATEDIR/${NS}.state" +} + +ensure_nsname() +{ + [ -z "$NS" ] && die "No environment selected; use --name to select one or 'setup' to create one" + [ -e "$STATEFILE" ] || die "Environment for $NS doesn't seem to exist" + + echo "$NS" > "$STATEDIR/current" + + read_statefile +} + +get_num() +{ + local num=1 + if [ -f "$STATEDIR/highest_num" ]; then + num=$(( 1 + $(< "$STATEDIR/highest_num" ))) + fi + + echo $num > "$STATEDIR/highest_num" + printf "%x" $num +} + +write_statefile() +{ + [ -z "$STATEFILE" ] && return 1 + echo > "$STATEFILE" + for var in "${STATEVARS[@]}"; do + echo "${var}='$(eval echo '$'$var)'" >> "$STATEFILE" + done +} + +read_statefile() +{ + local value + for var in "${STATEVARS[@]}"; do + value=$(source "$STATEFILE"; eval echo '$'$var) + eval "$var=\"$value\"" + done +} + +cleanup_setup() +{ + echo "Error during setup, removing partially-configured environment '$NS'" >&2 + set +o errexit + ip netns del "$NS" 2>/dev/null + ip link del dev "$NS" 2>/dev/null + rm -f "$STATEFILE" +} + +cleanup_teardown() +{ + echo "Warning: Errors during teardown, partial environment may be left" >&2 +} + + +cleanup() +{ + [ -n "$CLEANUP_FUNC" ] && $CLEANUP_FUNC + + local statefiles=("$STATEDIR"/*.state) + + if [ "${#statefiles[*]}" -eq 1 ] && [ ! -e "${statefiles[0]}" ]; then + rm -f "${STATEDIR}/highest_num" "${STATEDIR}/current" \ + "${STATEDIR}/trace_attach_support" + rmdir "$STATEDIR" + fi +} + +iface_macaddr() +{ + local iface="$1" + ip -br link show dev "$iface" | awk '{print $3}' +} + +set_sysctls() +{ + local iface="$1" + local in_ns="${2:-}" + local nscmd= + + [ -n "$in_ns" ] && nscmd="ip netns exec $in_ns" + local sysctls=(accept_dad + accept_ra + mldv1_unsolicited_report_interval + mldv2_unsolicited_report_interval) + + for s in ${sysctls[*]}; do + $nscmd sysctl -w net.ipv6.conf.$iface.${s}=0 >/dev/null + done +} + +get_vlan_prefix() +{ + # Split the IPv6 prefix, and add the VLAN ID to the upper byte of the fourth + # element in the prefix. This will break if the global prefix config doesn't + # have exactly three elements in it. + local prefix="$1" + local vid="$2" + (IFS=:; set -- $prefix; printf "%s:%s:%s:%x::" "$1" "$2" "$3" $(($4 + $vid * 4096))) +} + +setup() +{ + get_nsname 1 + + [ -e "$STATEFILE" ] && die "Environment for '$NS' already exists" + + local NUM=$(get_num "$NS") + local PEERNAME="testl-ve-$NUM" + [ -z "$IP6_PREFIX" ] && IP6_PREFIX="${IP6_SUBNET}:${NUM}::" + [ -z "$IP4_PREFIX" ] && IP4_PREFIX="${IP4_SUBNET}.$((0x$NUM))." + + INSIDE_IP6="${IP6_PREFIX}2" + INSIDE_IP4="${IP4_PREFIX}2" + OUTSIDE_IP6="${IP6_PREFIX}1" + OUTSIDE_IP4="${IP4_PREFIX}1" + + CLEANUP_FUNC=cleanup_setup + + if ! mount | grep -q /sys/fs/bpf; then + mount -t bpf bpf /sys/fs/bpf/ + fi + + ip netns add "$NS" + ip link add dev "$NS" type veth peer name "$PEERNAME" + OUTSIDE_MAC=$(iface_macaddr "$NS") + INSIDE_MAC=$(iface_macaddr "$PEERNAME") + set_sysctls $NS + + ethtool -K "$NS" rxvlan off txvlan off + ethtool -K "$PEERNAME" rxvlan off txvlan off + ip link set dev "$PEERNAME" netns "$NS" + ip link set dev "$NS" up + ip addr add dev "$NS" "${OUTSIDE_IP6}/${IP6_PREFIX_SIZE}" + + ip -n "$NS" link set dev "$PEERNAME" name veth0 + ip -n "$NS" link set dev lo up + ip -n "$NS" link set dev veth0 up + set_sysctls veth0 "$NS" + ip -n "$NS" addr add dev veth0 "${INSIDE_IP6}/${IP6_PREFIX_SIZE}" + + # Prevent neighbour queries on the link + ip neigh add "$INSIDE_IP6" lladdr "$INSIDE_MAC" dev "$NS" nud permanent + ip -n "$NS" neigh add "$OUTSIDE_IP6" lladdr "$OUTSIDE_MAC" dev veth0 nud permanent + + # Add route for whole test subnet, to make it easier to communicate between + # namespaces + ip -n "$NS" route add "${IP6_SUBNET}::/$IP6_FULL_PREFIX_SIZE" via "$OUTSIDE_IP6" dev veth0 + + if [ "$LEGACY_IP" -eq "1" ]; then + ip addr add dev "$NS" "${OUTSIDE_IP4}/${IP4_PREFIX_SIZE}" + ip -n "$NS" addr add dev veth0 "${INSIDE_IP4}/${IP4_PREFIX_SIZE}" + ip neigh add "$INSIDE_IP4" lladdr "$INSIDE_MAC" dev "$NS" nud permanent + ip -n "$NS" neigh add "$OUTSIDE_IP4" lladdr "$OUTSIDE_MAC" dev veth0 nud permanent + ip -n "$NS" route add "${IP4_SUBNET}/${IP4_FULL_PREFIX_SIZE}" via "$OUTSIDE_IP4" dev veth0 + ENABLE_IPV4=1 + else + ENABLE_IPV4=0 + fi + + if [ "$USE_VLAN" -eq "1" ]; then + ENABLE_VLAN=1 + for vid in "${VLAN_IDS[@]}"; do + local vlpx="$(get_vlan_prefix "$IP6_PREFIX" "$vid")" + local inside_ip="${vlpx}2" + local outside_ip="${vlpx}1" + ip link add dev "${NS}.$vid" link "$NS" type vlan id "$vid" + ip link set dev "${NS}.$vid" up + ip addr add dev "${NS}.$vid" "${outside_ip}/${IP6_PREFIX_SIZE}" + ip neigh add "$inside_ip" lladdr "$INSIDE_MAC" dev "${NS}.$vid" nud permanent + set_sysctls "${NS}/$vid" + + ip -n "$NS" link add dev "veth0.$vid" link "veth0" type vlan id "$vid" + ip -n "$NS" link set dev "veth0.$vid" up + ip -n "$NS" addr add dev "veth0.$vid" "${inside_ip}/${IP6_PREFIX_SIZE}" + ip -n "$NS" neigh add "$outside_ip" lladdr "$OUTSIDE_MAC" dev "veth0.$vid" nud permanent + set_sysctls "veth0/$vid" "$NS" + done + else + ENABLE_VLAN=0 + fi + + write_statefile + + CLEANUP_FUNC= + + echo "$NS" > "$STATEDIR/current" +} + +teardown() +{ + get_nsname && ensure_nsname "$NS" + + CLEANUP_FUNC=cleanup_teardown + + ip link del dev "$NS" + ip netns del "$NS" + rm -f "$STATEFILE" + [ -d "/sys/fs/bpf/$NS" ] && rmdir "/sys/fs/bpf/$NS" || true + + if [ -f "$STATEDIR/current" ]; then + local CUR=$(< "$STATEDIR/current" ) + [[ "$CUR" == "$NS" ]] && rm -f "$STATEDIR/current" + fi + + for f in ${STATEDIR}/proc/*; do + if [ -f "$f" ]; then + local pid="${f/${STATEDIR}\/proc\//}" + stop_background "$pid" &> /dev/null || true + fi + done + + rm -rf "$STATEDIR" + + CLEANUP_FUNC= +} + +ns_exec() +{ + get_nsname && ensure_nsname "$NS" + + ip netns exec "$NS" env TESTENV_NAME="$NS" "$SETUP_SCRIPT" "$@" +} + +is_func() +{ + type "$1" 2>/dev/null | grep -q 'is a function' +} + +check_run() +{ + local ret + + "$@" + ret=$? + echo "Command '$@' exited with status $ret" + echo "" + if [ "$ret" -ne "0" ]; then + exit $ret + fi +} + +exec_test() +{ + local testn="$1" + local output + local ret + + printf " %-30s" "[$testn]" + if ! is_func "$testn"; then + echo "INVALID" + return 1 + fi + + output=$($testn 2>&1) + ret=$? + if [ "$ret" -eq "0" ]; then + echo "PASS" + elif [ "$ret" -eq "$SKIPPED_TEST" ]; then + echo "SKIPPED" + ret=0 + else + echo "FAIL" + fi + if [ "$ret" -ne "0" ] || [ "$VERBOSE_TESTS" -eq "1" ]; then + echo "$output" | sed 's/^/ /' + echo " Test $testn exited with return code: $ret" + fi + return $ret +} + +run_tests() +{ + local TESTS="$*" + local ret=0 + [ -z "$TESTS" ] && TESTS="$ALL_TESTS" + setup || return 1 + + echo " Running tests from $TEST_DEFINITIONS" + + for testn in $TESTS; do + exec_test $testn || ret=1 + if is_func cleanup_tests; then + cleanup_tests || true + fi + done + + return $ret +} + +usage() +{ + echo "Usage: $0 <test_definition_file> [test names]" >&2 + exit 1 +} + +if [ "$EUID" -ne "0" ]; then + if command -v sudo >/dev/null 2>&1; then + exec sudo env V=${VERBOSE_TESTS} "$0" "$@" + else + die "Tests should be run as root" + fi +fi + +export XDP_FILTER +export XDP_LOADER +export XDPDUMP + +TEST_DEFINITIONS="${1:-}" +[ -f "$TEST_DEFINITIONS" ] || usage +source "$TEST_DEFINITIONS" + +TOOL_TESTS_DIR="$(dirname "$TEST_DEFINITIONS")" + +shift +trap teardown EXIT +check_prereq +run_tests "$@" |