#!/bin/bash
# SPDX-License-Identifier: GPL-2.0+
# Copyright (c) 2018 Western Digital Corporation or its affiliates

. common/rc
. common/scsi_debug
. common/multipath-over-rdma

vdev_path=(/dev/nullb0 /dev/nullb1 scsi_debug_dev_path_will_be_set_later)
vdevs=(iblock_0/vdev0 iblock_1/vdev1 iblock_2/vdev2)
scsi_serial=(nullb0 nullb1 scsidbg)
elevator=none
scsi_timeout=1
srp_login_params=
srp_rdma_cm_port=5555

is_lio_configured() {
	(
		cd /sys/kernel/config/target >&/dev/null || return 1
		for e in target/* core/fileio* core/iblock* core/pscsi*; do
			if [ -d "$e" ] && [ "$e" != core ]; then
				return 0
			fi
		done
	)

	return 1
}

group_requires() {
	local m name p

	_have_configfs || return
	if is_lio_configured; then
		SKIP_REASONS+=("LIO must be unloaded before the SRP tests are run")
		return
	fi
	_have_driver sd_mod
	_have_driver sg
	_have_driver scsi_dh_alua
	_have_driver scsi_dh_emc
	_have_driver scsi_dh_rdac

	_have_module dm_multipath
	_have_module dm_queue_length
	_have_module dm_service_time
	_have_module ib_ipoib
	_have_module ib_srp
	_have_module ib_srpt
	_have_module ib_umad
	_have_module ib_uverbs
	_have_module null_blk
	_have_module rdma_cm
	_have_module rdma_rxe
	_have_module scsi_debug
	_have_module target_core_iblock
	_have_module target_core_mod

	for p in mkfs.ext4 mkfs.xfs multipath multipathd pidof rdma \
		 sg_reset fio; do
		_have_program "$p" || return
	done

	_multipathd_version_ge 0.7.0 || return

	_have_root || return

	_have_src_program discontiguous-io || return

	_have_kernel_option DM_UEVENT || return

	for name in srp_daemon multipathd; do
		if pidof "$name" >/dev/null; then
			SKIP_REASONS+=("$name must be stopped before the SRP tests are run")
			return
		fi
	done
	if [ -e /etc/multipath.conf ] &&
	    ! diff -q /etc/multipath.conf tests/srp/multipath.conf >&/dev/null
	then
		SKIP_REASONS+=("/etc/multipath.conf already exists")
		return
	fi
}

# Log out, set dm and SCSI use_blk_mq parameters and log in. $1: device mapper
# use_blk_mq mode; $2: SCSI use_blk_mq mode; $3..${$#}: SRP kernel module
# parameters.
use_blk_mq() {
	local dm_mode=$1 scsi_mode=$2 kmod_params

	shift
	shift
	kmod_params=("$@")

	(
		cd /sys/module/dm_mod/parameters || return $?
		if [ -e use_blk_mq ]; then
			echo "$dm_mode" >use_blk_mq || return 1
		fi
	)
	(
		cd /sys/module/scsi_mod/parameters || return $?
		if [ -e use_blk_mq ]; then
			echo "$scsi_mode" >use_blk_mq || return 1
		fi
	)

	log_out &&
		remove_mpath_devs &&
		stop_srp_ini &&
		start_srp_ini indirect_sg_entries=2048 "${kmod_params[@]}" &&
		log_in
}

# Write SRP login string $1 into SRP login sysfs attribute $2.
srp_single_login() {
	{
		echo "echo $1 > $2"
		echo "$1" >"$2"
	} &>>"$FULL"
}

# Tell the SRP initiator to log in to an SRP target using the IB/CM.
# Arguments: $1: SRP target IOC GUID; $2: IB device to log in to; $3: IB device
# port to log in to; $4: additional login parameters.
do_ib_cm_login() {
	local add_param gid ibdev ioc_guid p port

	ioc_guid=$1
	ibdev=$2
	port=$3
	add_param=$4
	gid=$(<"/sys/class/infiniband/$ibdev/ports/$port/gids/0")
	gid=${gid//:}
	for p in "/sys/class/infiniband_srp/srp-${2}-"*; do
		[ -e "$p" ] || continue
		srp_single_login "id_ext=$ioc_guid,ioc_guid=$ioc_guid,dgid=$gid,pkey=7fff,service_id=$ioc_guid,$add_param" "$p/add_target"
	done
}

rdma_dev_to_net_dev() {
	rdma link show "$1/1" | sed 's/.* netdev //;s/[[:blank:]]*$//'
}

# Tell the SRP initiator to log in to an SRP target using the RDMA/CM.
# Arguments: $1: SRP target IOC GUID; $2: IB device to log in to; $3: additional
# login parameters.
do_rdma_cm_login() {
	local a b c add_param d dest dests ibdev ioc_guid params

	ioc_guid=$1
	ibdev=$2
	add_param=$3
	if d=$(rdma_dev_to_net_dev "$ibdev"); then
		a=$(get_ipv4_addr "$(basename "$d")")
		b=$(get_ipv6_addr "$(basename "$d")")
	fi
	echo "Interface $d: IPv4 $a IPv6 $b" >>"$FULL"
	[ -n "$a$b" ] || return 1
	b=${b}%$(<"/sys/class/net/$d/ifindex")
	dests=()
	for c in $a; do
		dests+=("${c}:${srp_rdma_cm_port}")
	done
	for c in $b; do
		dests+=("[${c}]:${srp_rdma_cm_port}")
	done
	for dest in "${dests[@]}"; do
		src=${dest%:*}
		for p in "/sys/class/infiniband_srp/srp-${2}-"*; do
			[ -e "$p" ] || continue
			ibdev=$(<"$p/ibdev")
			port=$(<"$p/port")
			gid=$(<"/sys/class/infiniband/$ibdev/ports/$port/gids/0")
			gid=${gid//:}
			gid_pfx=${gid:0:16}
			params+=",id_ext=$ioc_guid,initiator_ext=$gid_pfx,ioc_guid=$ioc_guid,src=$src,dest=$dest,$add_param"
			srp_single_login "${params}" "$p/add_target"
		done
	done
}

show_srp_connections() {
	local d f h hd scsi_devs srp_luns p s

	for p in /sys/class/scsi_host/*/orig_dgid; do
		[ -e "$p" ] || continue
		hd=${p%/orig_dgid}
		h=${hd#/sys/class/scsi_host/host}
		{ s=$(<"$hd/sgid"); } 2>/dev/null
		s=${s#fe80:0000:0000:0000:}
		s=${s:-?}
		d=$(<"$p")
		d=${d#fe80:0000:0000:0000:}
		d=${d:-?}
		srp_luns=()
		for f in "/sys/class/scsi_device/${h}:"*; do
			[ -e "$f" ] || continue
			srp_luns+=("$f")
		done
		scsi_devs=()
		for dev in "/sys/class/scsi_device/${h}:"*/device/block/*; do
			[ -e "$dev" ] || continue
			scsi_devs+=("${dev/*\/}")
		done
		echo "SCSI host $h: src GUID $s dst GUID $d ${#srp_luns[@]} LUNS; scsi devs: ${scsi_devs[*]}"
	done
}

# Make the SRP initiator driver log in to each SRP target port that exists
# on the local host.
log_in() {
	local a add_param="${srp_login_params}" d dest gid ibdev ioc_guid port p

	ioc_guid=$(</sys/module/ib_srpt/parameters/srpt_service_guid)

	for ((i=0;i<10;i++)); do
		for p in /sys/class/infiniband/*/ports/*; do
			[ -e "$p" ] || continue
			port=$(basename "$p")
			ibdev=$(basename "$(dirname "$(dirname "$p")")")
			link_layer=$(<"$p/link_layer")
			case $link_layer in
				InfiniBand)
					do_ib_cm_login   "$ioc_guid" "$ibdev" "$port" "$add_param" ||
						do_rdma_cm_login "$ioc_guid" "$ibdev" "$add_param";;
				*)
					do_rdma_cm_login "$ioc_guid" "$ibdev" "$add_param";;
			esac
		done

		for p in /sys/class/scsi_host/*/orig_dgid; do
			if [ -e "$p" ]; then
				show_srp_connections &>>"$FULL"
				return 0
			fi
		done
		sleep .1
	done

	echo "SRP login failed"

	return 1
}

# Tell the SRP initiator driver to log out.
log_out() {
	local p

	if [ -e /sys/class/srp_remote_ports ]; then
		for p in /sys/class/srp_remote_ports/*; do
			[ -e "$p" ] && echo 1 >"$p/delete" &
		done
	fi
	wait
}

# Simulate network failures for device $1 during $2 seconds.
simulate_network_failure_loop() {
	local d dev="$1" duration="$2" deadline i rc=0 s

	[ -e "$dev" ] || return 1
	[ -n "$duration" ] || return 1
	deadline=$(($(_uptime_s) + duration))
	s=5
	while [ $rc = 0 ]; do
		sleep_until 5 ${deadline} || break
		log_out
		sleep_until $s ${deadline}
		rc=$?
		s=$(((((s + 5) & 0xff) ^ 0xa6) * scsi_timeout / 60))
		log_in
	done

	for ((i=0;i<5;i++)); do
		log_in && break
		sleep 1
	done
}

# Remove all mpath devices that refer to one or more SRP devices or that refer
# to an already deleted block device.
remove_mpath_devs() {
	local b d dm h p s

	{
		echo "Examining all SRP LUNs"
		for p in /sys/class/srp_remote_ports/*; do
			[ -e "$p" ] || continue
			h="${p##*/}"; h="${h#port-}"; h="${h%:1}"
			for d in "/sys/class/scsi_device/${h}:"*/device/block/*; do
				[ -e "$d" ] || continue
				s=$(dirname "$(dirname "$(dirname "$d")")")
				s=$(basename "$s")
				b=$(basename "$d")
				for h in "/sys/class/block/$b/holders/"*; do
					[ -e "$h" ] || continue
					dm=/dev/$(basename "$h")
					{
						echo -n "SRP LUN $s / $b: removing $dm: "
						if ! remove_mpath_dev "$dm"; then
							echo "failed"
							[ -z "$debug" ] || return 1
						fi
					}
				done
			done
		done

		remove_stale_mpath_devs
	} &>> "$FULL"
}

# Load the SRP initiator driver with kernel module parameters $1..$n.
start_srp_ini() {
	modprobe scsi_transport_srp || return $?
	modprobe ib_srp "$@" dyndbg=+pmf || return $?
}

# Unload the SRP initiator driver.
stop_srp_ini() {
	local i

	log_out
	for ((i=40;i>=0;i--)); do
		remove_mpath_devs || return $?
		_unload_module ib_srp >/dev/null 2>&1 && break
		sleep 1
	done
	if [ -e /sys/module/ib_srp ]; then
		echo "Error: unloading kernel module ib_srp failed"
		return 1
	fi
	_unload_module scsi_transport_srp || return $?
}

# Associate the LIO device with name $1/$2 with file $3 and SCSI serial $4.
configure_lio_vdev() {
	local dirname=$1 vdev=$2 path=$3 serial=$4

	(
		cd /sys/kernel/config/target/core &&
			mkdir "$dirname" &&
			cd "$dirname" &&
			mkdir "$vdev" &&
			cd "$vdev" &&
			if [ -b "$(readlink -f "$path")" ]; then
				echo "udev_path=$path," >control
			elif [ -e "$path" ]; then
				size=$(stat -c %s "${path}") &&
					[ "$size" -gt 0 ] &&
					echo "fd_dev_name=$path,fd_dev_size=$size," >control
			else
				{
					ls -l "$path"
					readlink -f "$path"
				} &>>"$FULL"
				false
			fi &&
			echo "${serial}" >wwn/vpd_unit_serial &&
			echo 1 > enable
	)
}

# Return the multipath ID of LIO device $1. $1 is an index in the $scsi_serial
# array.
lio_scsi_mpath_id() {
	local i=$1 hs

	is_number "$i" || return $?
	hs=$(echo -n "${scsi_serial[i]}" | od -v -tx1 -w99 |
		     { read -r offset bytes;
		       echo "${bytes// }";
		       echo "$offset" > /dev/null
		     })
	while [ ${#hs} -lt 25 ]; do
		hs="${hs}0"
	done
	# See also spc_emulate_evpd_83() in drivers/target/target_core_spc.c.
	echo "36001405$hs"
}

scsi_mpath_id() {
	lio_scsi_mpath_id "$@"
}

# Get a the initiator path for LIO target device $1. $1 is an index in the
# $scsi_serial array.
get_bdev_path() {
	local i=$1 uuid

	is_number "$i" || return $?
	uuid=$(scsi_mpath_id "$i") || return $?
	echo "/dev/disk/by-id/dm-uuid-mpath-$uuid"
}

# Get a /dev/... path that points at dm device number $1.
get_bdev() {
	get_bdev_n "$1" "$elevator" "$scsi_timeout"
}

# Configure zero or more target ports such that these accept connections from
# zero or more initiator ports. Target and initiator port lists are separated
# by "--".
configure_target_ports() {
	local i ini initiators target_port target_ports

	target_ports=()
	while [ $# -gt 0 ]; do
		if [ "$1" = "--" ]; then
			shift
			break
		fi
		target_ports+=("$1")
		shift
	done

	initiators=()
	while [ $# -gt 0 ]; do
		initiators+=("$1")
		shift
	done

	for target_port in "${target_ports[@]}"; do
		mkdir "$target_port" || return $?
		[ -e "$target_port" ] || continue
		#echo "$target_port"
		mkdir "$target_port/$target_port" || continue
		i=0
		for v in "${vdevs[@]}"; do
			mkdir "$target_port/$target_port/lun/lun_$i" || return $?
			(
				cd "$target_port/$target_port/lun/lun_$i" &&
					ln -s "../../../../../core/$v" .
			) || return $?
			i=$((i+1))
		done
		for ini in "${initiators[@]}"; do
			(
				cd "$target_port/$target_port/acls" &&
					mkdir "${ini}" &&
					cd "${ini}" &&
					for ((i = 0; i < ${#vdevs[@]}; i++)) do
					    (
						    mkdir "lun_$i" &&
							    cd "lun_$i" &&
							    ln -s "../../../lun/lun_$i" .
					    ) || return $?
					done
			) || return $?
		done
		echo 1 >"$target_port/$target_port/enable"
	done
}

# Load LIO and configure the SRP target driver and LUNs.
start_lio_srpt() {
	local b d gid i ini_ids=() opts p target_ids=()

	for gid in $(all_primary_gids); do
		if [ "${gid#fe8}" != "$gid" ]; then
			gid=${gid#fe80:0000:0000:0000:}
		else
			gid=0x${gid//:}
		fi
		target_ids+=("$gid")
	done
	echo "target_ids=${target_ids[*]}" >>"$FULL"
	for p in /sys/class/infiniband/*/ports/*; do
		[ -e "$p" ] || continue
		gid=$(<"$p/gids/0")
		if [ "${gid#fe8}" != "$gid" ]; then
			gid=${gid#fe80:0000:0000:0000:}
			ini_ids+=("$gid")
		else
			gid="0x${gid//:}"
			ini_ids+=("$gid")
		fi
		d=$(rdma_dev_to_net_dev "$(basename "$(dirname "$(dirname "$p")")")")
		for b in $(get_ipv4_addr "$d") \
				 $(get_ipv6_addr "$d"|expand_ipv6_addr); do
			ini_ids+=("$b")
		done
	done
	echo "ini_ids=${ini_ids[*]}" >>"$FULL"
	mount_configfs || return $?
	modprobe target_core_mod || return $?
	modprobe target_core_iblock || return $?
	opts=("srp_max_req_size=8260" "dyndbg=+pmf")
	if modinfo ib_srpt | grep -q '^parm:[[:blank:]]*rdma_cm_port:'; then
		opts+=("rdma_cm_port=${srp_rdma_cm_port}")
	fi
	_unload_module ib_srpt
	modprobe ib_srpt "${opts[@]}" || return $?
	i=0
	for r in "${vdev_path[@]}"; do
		if [ -b "$(readlink -f "$r")" ]; then
			oflag=("oflag=direct")
		else
			oflag=()
		fi
		echo -n "Zero-initializing $r ... " >>"$FULL"
		dd if=/dev/zero of="${r}" bs=1M count=$((ramdisk_size>>20)) "${oflag[@]}" >/dev/null 2>&1 || return $?
		echo "done" >>"$FULL"
		mkdir -p "$(mountpoint $i)" || return $?
		((i++))
	done
	for ((i=0; i < ${#vdevs[@]}; i++)); do
		d="$(dirname "${vdevs[i]}")"
		b="$(basename "${vdevs[i]}")"
		hs=$(lio_scsi_mpath_id "$i")
		hs=${hs#36001405}
		configure_lio_vdev "$d" "$b" "${vdev_path[i]}" "$hs" ||
			return $?
	done
	(
		cd /sys/kernel/config/target || return $?
		mkdir srpt || return $?
		cd srpt || return $?
		if [ -e discovery_auth/rdma_cm_port ]; then
			echo "${srp_rdma_cm_port}" > discovery_auth/rdma_cm_port ||
				return 1
		fi
		configure_target_ports "${target_ids[@]}" -- "${ini_ids[@]}"
	)
}

# Unload the LIO SRP target driver.
stop_lio_srpt() {
	local e m

	mkdir -p /etc/target
	for e in /sys/kernel/config/target/srpt/*/*/enable; do
		if [ -e "$e" ]; then
			echo 0 >"$e"
		fi
	done

	rmdir /sys/kernel/config/target/*/*/*/np/* >&/dev/null
	rmdir /sys/kernel/config/target/*/*/*/np >&/dev/null
	rm -f /sys/kernel/config/target/*/*/*/acls/*/*/* >&/dev/null
	rmdir /sys/kernel/config/target/*/*/*/acls/*/* >&/dev/null
	rmdir /sys/kernel/config/target/*/*/*/acls/* >&/dev/null
	rm -f /sys/kernel/config/target/*/*/*/lun/*/* >&/dev/null
	rmdir /sys/kernel/config/target/*/*/*/lun/* >&/dev/null
	rmdir /sys/kernel/config/target/*/*/*/*/* >&/dev/null
	rmdir /sys/kernel/config/target/*/*/* >&/dev/null
	rmdir /sys/kernel/config/target/*/* >&/dev/null
	rmdir /sys/kernel/config/target/* >&/dev/null

	for m in ib_srpt ib_isert iscsi_target_mod target_core_pscsi target_core_iblock \
			 target_core_file target_core_stgt target_core_user \
			 target_core_mod
	do
		_unload_module $m 10 || return $?
	done
}

# Load and configure the SRP target driver
start_srpt() {
	local bd i

	_init_scsi_debug delay=0 dev_size_mb=$((ramdisk_size>>20)) dif=3 dix=1\
		|| return $?
	vdev_path[2]=/dev/${SCSI_DEBUG_DEVICES[0]}
	modprobe ib_uverbs
	modprobe ib_umad
	modprobe rdma_cm
	start_lio_srpt || return $?
	echo "Configured SRP target driver"
}

# Unload the SRP target driver.
stop_srpt() {
	stop_lio_srpt || return $?
	_exit_scsi_debug
}

start_target() {
	start_srpt
}

stop_target() {
	stop_srpt
}

shutdown_client() {
	remove_mpath_devs &&
		log_out &&
		stop_srp_ini
}

# Set up test configuration
setup() {
	setup_test "$PWD/tests/srp/multipath.conf"
}
