#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Exercise RDMA device name handling across network namespaces.

source "$(dirname "$0")/../kselftest/ktap_helpers.sh"

NAME_PREFIX="rxe_netns_names_$$"
NETDEV_PREFIX="rxn$$"
NS1="${NAME_PREFIX}ns1"
NS2="${NAME_PREFIX}ns2"
RXE_A="${NAME_PREFIX}rxe_a"
RXE_B="${NAME_PREFIX}rxe_b"
RXE_SAME="${NAME_PREFIX}rxe_same"
RXE_NEW="${NAME_PREFIX}rxe_new"
DUMMY_A="${NETDEV_PREFIX}a"
DUMMY_B="${NETDEV_PREFIX}b"
OLD_MODE=""
MODE_CHANGED=0
MODS=("dummy" "rdma_rxe")
TEST_SAME_NAMES="same RDMA device name can exist in two net namespaces"
TEST_MOVE_CONFLICT="move without rename fails on destination name conflict"
TEST_MOVE_RENAME="move then rename succeeds"
TEST_COMBINED_MOVE_RENAME="move with requested destination name succeeds"
TEST_SAME_NETNS_DUP_RENAME="same-netns rename rejects duplicate name"
TEST_TEARDOWN_RETURN="netns delete returns device to init_net and renames on conflict"

ksft_skip()
{
	ktap_skip_all "$*"
	exit "$KSFT_SKIP"
}

fail()
{
	ktap_exit_fail_msg "$*"
}

need_cmd()
{
	command -v "$1" >/dev/null 2>&1 || ksft_skip "missing command: $1"
}

rdma_ns()
{
	local ns=$1

	shift
	ip netns exec "$ns" rdma "$@"
}

rdma_dev_exists()
{
	local ns=$1
	local dev=$2

	if [ -n "$ns" ]; then
		rdma_ns "$ns" dev show "$dev" >/dev/null 2>&1
	else
		rdma dev show "$dev" >/dev/null 2>&1
	fi
}

add_dummy()
{
	local netdev=$1

	ip link add "$netdev" type dummy || return 1
	ip link set "$netdev" up || return 1
}

add_rxe()
{
	local dev=$1
	local netdev=$2

	rdma link add "$dev" type rxe netdev "$netdev"
}

rdma_dev_on_netdev()
{
	local netdev=$1

	rdma link show 2>/dev/null | awk -v want="$netdev" '
		{
			for (i = 1; i < NF; i++)
				if ($i == "netdev" && $(i + 1) == want) {
					dev = $2
					sub(/\/.*/, "", dev)
					print dev
					exit
				}
		}'
}

wait_rdma_dev_on_netdev()
{
	local netdev=$1
	local dev
	local i

	for i in $(seq 1 50); do
		dev=$(rdma_dev_on_netdev "$netdev")
		if [ -n "$dev" ]; then
			echo "$dev"
			return 0
		fi
		sleep 0.1
	done

	return 1
}

# ip link del returns after NETDEV_UNREGISTER, but rxe tears the RDMA device
# down asynchronously via ib_unregister_device_queued(). Wait until our names
# are gone.
wait_rdma_devs_gone()
{
	local i name ns
	local names=("$RXE_A" "$RXE_B" "$RXE_SAME" "$RXE_NEW")

	for i in $(seq 1 50); do
		local found=0

		for name in "${names[@]}"; do
			if rdma_dev_exists "" "$name"; then
				found=1
				break
			fi
			for ns in "$NS1" "$NS2"; do
				ip netns exec "$ns" true 2>/dev/null || continue
				if rdma_dev_exists "$ns" "$name"; then
					found=1
					break 2
				fi
			done
		done

		[ "$found" -eq 0 ] && return 0
		sleep 0.1
	done

	return 1
}

setup_devs()
{
	cleanup_devs || return 1

	add_dummy "$DUMMY_A" || return 1
	add_dummy "$DUMMY_B" || return 1

	add_rxe "$RXE_A" "$DUMMY_A" || return 1
	add_rxe "$RXE_B" "$DUMMY_B" || return 1
}

cleanup_devs()
{
	ip link del "$DUMMY_A" 2>/dev/null
	ip link del "$DUMMY_B" 2>/dev/null
	wait_rdma_devs_gone
}

setup()
{
	OLD_MODE=$(rdma system show 2>/dev/null |
		   sed -n 's/.*netns \([^ ]*\).*/\1/p')
	[ -n "$OLD_MODE" ] || ksft_skip "failed to read RDMA netns mode"

	rdma system set netns exclusive >/dev/null 2>&1 ||
		ksft_skip "rdma netns exclusive mode is not supported"
	MODE_CHANGED=1

	ip netns add "$NS1" || return 1
	ip netns add "$NS2" || return 1
}

# ip netns del returns before rdma_dev_exit_net() removes the net from
# rdma_nets. rdma_compatdev_set() returns -EBUSY until that completes, so
# retry the mode restore instead of leaving the system in exclusive mode.
restore_netns_mode()
{
	local i

	[ "$MODE_CHANGED" -eq 1 ] || return 0

	for i in $(seq 1 50); do
		if rdma system set netns "$OLD_MODE" >/dev/null 2>&1; then
			MODE_CHANGED=0
			return 0
		fi
		sleep 0.1
	done

	echo "warning: failed to restore RDMA netns mode to $OLD_MODE" >&2
	return 1
}

cleanup()
{
	cleanup_devs

	ip netns del "$NS1" 2>/dev/null
	ip netns del "$NS2" 2>/dev/null

	restore_netns_mode

	for m in "${MODS[@]}"; do
		modprobe -r "$m" 2>/dev/null
	done
}

rdma_supports_combined_move_rename()
{
	rdma dev help 2>&1 | grep -Eq 'netns .*name|name .*netns'
}

[ "$(id -u)" -eq 0 ] || ksft_skip "must be run as root"
need_cmd ip
need_cmd rdma
need_cmd modprobe

trap cleanup EXIT

for m in "${MODS[@]}"; do
	modinfo "$m" >/dev/null 2>&1 || ksft_skip "module $m not found"
	modprobe "$m" || fail "failed to load $m"
done

setup || fail "failed to create net namespaces"

ktap_print_header
ktap_set_plan 6

if setup_devs &&
   rdma dev set "$RXE_A" netns "$NS1" &&
   rdma_ns "$NS1" dev set "$RXE_A" name "$RXE_SAME" &&
   rdma dev set "$RXE_B" netns "$NS2" &&
   rdma_ns "$NS2" dev set "$RXE_B" name "$RXE_SAME" &&
   rdma_dev_exists "$NS1" "$RXE_SAME" &&
   rdma_dev_exists "$NS2" "$RXE_SAME"; then
	ktap_test_pass "$TEST_SAME_NAMES"
else
	ktap_test_fail "$TEST_SAME_NAMES"
fi
cleanup_devs

if ! setup_devs ||
   ! rdma dev set "$RXE_A" netns "$NS1" ||
   ! rdma_ns "$NS1" dev set "$RXE_A" name "$RXE_SAME" ||
   ! rdma dev set "$RXE_B" netns "$NS2" ||
   ! rdma_ns "$NS2" dev set "$RXE_B" name "$RXE_SAME"; then
	ktap_test_fail "$TEST_MOVE_CONFLICT"
elif rdma_ns "$NS1" dev set "$RXE_SAME" netns "$NS2" >/dev/null 2>&1; then
	ktap_test_fail "$TEST_MOVE_CONFLICT"
elif rdma_dev_exists "$NS1" "$RXE_SAME" &&
     rdma_dev_exists "$NS2" "$RXE_SAME"; then
	ktap_test_pass "$TEST_MOVE_CONFLICT"
else
	ktap_test_fail "$TEST_MOVE_CONFLICT"
fi
cleanup_devs

if ! setup_devs; then
	ktap_test_fail "$TEST_MOVE_RENAME"
elif rdma dev set "$RXE_A" netns "$NS2" &&
     rdma_ns "$NS2" dev set "$RXE_A" name "$RXE_NEW"; then
	if rdma_dev_exists "$NS2" "$RXE_NEW" &&
	   ! rdma_dev_exists "" "$RXE_A"; then
		ktap_test_pass "$TEST_MOVE_RENAME"
	else
		ktap_test_fail "$TEST_MOVE_RENAME"
	fi
else
	ktap_test_fail "$TEST_MOVE_RENAME"
fi
cleanup_devs

if ! rdma_supports_combined_move_rename; then
	ktap_test_skip "$TEST_COMBINED_MOVE_RENAME"
elif ! setup_devs; then
	ktap_test_fail "$TEST_COMBINED_MOVE_RENAME"
elif rdma dev set "$RXE_A" netns "$NS2" name "$RXE_NEW"; then
	if rdma_dev_exists "$NS2" "$RXE_NEW" &&
	   ! rdma_dev_exists "" "$RXE_A"; then
		ktap_test_pass "$TEST_COMBINED_MOVE_RENAME"
	else
		ktap_test_fail "$TEST_COMBINED_MOVE_RENAME"
	fi
else
	ktap_test_fail "$TEST_COMBINED_MOVE_RENAME"
fi
cleanup_devs

if ! setup_devs; then
	ktap_test_fail "$TEST_SAME_NETNS_DUP_RENAME"
elif rdma dev set "$RXE_A" name "$RXE_SAME" &&
     rdma dev set "$RXE_B" name "$RXE_NEW"; then
	if rdma dev set "$RXE_SAME" name "$RXE_NEW" >/dev/null 2>&1; then
		ktap_test_fail "$TEST_SAME_NETNS_DUP_RENAME"
	elif rdma_dev_exists "" "$RXE_SAME" &&
	     rdma_dev_exists "" "$RXE_NEW"; then
		ktap_test_pass "$TEST_SAME_NETNS_DUP_RENAME"
	else
		ktap_test_fail "$TEST_SAME_NETNS_DUP_RENAME"
	fi
else
	ktap_test_fail "$TEST_SAME_NETNS_DUP_RENAME"
fi
cleanup_devs

if ! setup_devs; then
	ktap_test_fail "$TEST_TEARDOWN_RETURN"
elif ! rdma dev set "$RXE_A" name "$RXE_SAME" ||
     ! rdma dev set "$RXE_B" netns "$NS2" ||
     ! rdma_ns "$NS2" dev set "$RXE_B" name "$RXE_SAME" ||
     ! rdma_dev_exists "$NS2" "$RXE_SAME"; then
	ktap_test_fail "$TEST_TEARDOWN_RETURN"
else
	ip netns del "$NS2"
	returned=$(wait_rdma_dev_on_netdev "$DUMMY_B")
	ktap_print_msg "device returned to init_net as '${returned:-<missing>}'"
	if rdma_dev_exists "" "$RXE_SAME" &&
	   [ -n "$returned" ] &&
	   [ "$returned" != "$RXE_SAME" ] &&
	   [ "${returned#ibdev}" != "$returned" ]; then
		ktap_test_pass "$TEST_TEARDOWN_RETURN"
	else
		ktap_test_fail "$TEST_TEARDOWN_RETURN"
	fi
fi
cleanup_devs

ktap_finished