#!/bin/bash
# SPDX-License-Identifier: GPL-3.0+
# Copyright (C) 2018 Johannes Thumshirn
#
# Test specific to NVMe devices

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

def_traddr="127.0.0.1"
def_adrfam="ipv4"
def_trsvcid="4420"
nvme_trtype=${nvme_trtype:-"loop"}

_nvme_requires() {
	_have_program nvme
	case ${nvme_trtype} in
	loop)
		_have_modules nvmet nvme-core nvme-loop
		_have_configfs
		;;
	pci)
		_have_modules nvme nvme-core
		;;
	tcp)
		_have_modules nvmet nvme-core nvme-tcp nvmet-tcp
		_have_configfs
		;;
	rdma)
		_have_modules nvmet nvme-core nvme-rdma nvmet-rdma
		_have_configfs
		_have_program rdma
		_have_modules rdma_rxe || _have_modules siw
		;;
	*)
		SKIP_REASON="unsupported nvme_trtype=${nvme_trtype}"
		return 1
	esac
	return 0
}

group_requires() {
	_have_root
}

group_device_requires() {
	_require_test_dev_is_nvme
}

NVMET_CFS="/sys/kernel/config/nvmet/"

_require_test_dev_is_nvme() {
	if ! readlink -f "$TEST_DEV_SYSFS/device" | grep -q nvme; then
		SKIP_REASON="$TEST_DEV is not a NVMe device"
		return 1
	fi
	return 0
}

_require_nvme_trtype_is_loop() {
	if [[ "${nvme_trtype}" != "loop" ]]; then
		SKIP_REASON="nvme_trtype=${nvme_trtype} is not supported in this test"
		return 1
	fi
	return 0
}

_require_nvme_trtype_is_fabrics() {
	if [[ "${nvme_trtype}" == "pci" ]]; then
		SKIP_REASON="nvme_trtype=${nvme_trtype} is not supported in this test"
		return 1
	fi
	return 0
}

_cleanup_nvmet() {
	local dev
	local port
	local subsys
	local transport
	local name

	if [[ ! -d "${NVMET_CFS}" ]]; then
		return 0
	fi

	# Don't let successive Ctrl-Cs interrupt the cleanup processes
	trap '' SIGINT

	shopt -s nullglob

	for dev in /sys/class/nvme/nvme*; do
		dev="$(basename "$dev")"
		transport="$(cat "/sys/class/nvme/${dev}/transport")"
		if [[ "$transport" == "${nvme_trtype}" ]]; then
			echo "WARNING: Test did not clean up ${nvme_trtype} device: ${dev}"
			_nvme_disconnect_ctrl "${dev}"
		fi
	done

	for port in "${NVMET_CFS}"/ports/*; do
		name=$(basename "${port}")
		echo "WARNING: Test did not clean up port: ${name}"
		rm -f "${port}"/subsystems/*
		rmdir "${port}"
	done

	for subsys in "${NVMET_CFS}"/subsystems/*; do
		name=$(basename "${subsys}")
		echo "WARNING: Test did not clean up subsystem: ${name}"
		for ns in "${subsys}"/namespaces/*; do
			rmdir "${ns}"
		done
		rmdir "${subsys}"
	done

	shopt -u nullglob
	trap SIGINT

	modprobe -r nvme-"${nvme_trtype}" 2>/dev/null
	if [[ "${nvme_trtype}" != "loop" ]]; then
		modprobe -r nvmet-"${nvme_trtype}" 2>/dev/null
	fi
	modprobe -r nvmet 2>/dev/null
	if [[ "${nvme_trtype}" == "rdma" ]]; then
		stop_soft_rdma
	fi
}

_setup_nvmet() {
	_register_test_cleanup _cleanup_nvmet
	modprobe nvmet
	if [[ "${nvme_trtype}" != "loop" ]]; then
		modprobe nvmet-"${nvme_trtype}"
	fi
	modprobe nvme-"${nvme_trtype}"
	if [[ "${nvme_trtype}" == "rdma" ]]; then
		start_soft_rdma
		for i in $(rdma_network_interfaces)
		do
			ipv4_addr=$(get_ipv4_addr "$i")
			if [ -n "${ipv4_addr}" ]; then
				def_traddr=${ipv4_addr}
			fi
		done
	fi
}

_nvme_disconnect_ctrl() {
	local ctrl="$1"

	nvme disconnect -d "${ctrl}"
}

_nvme_disconnect_subsys() {
	local subsysnqn="$1"

	nvme disconnect -n "${subsysnqn}"
}

_nvme_connect_subsys() {
	local trtype="$1"
	local subsysnqn="$2"
	local traddr="${3:-$def_traddr}"
	local trsvcid="${4:-$def_trsvcid}"

	ARGS=(-t "${trtype}" -n "${subsysnqn}")
	if [[ "${trtype}" != "loop" ]]; then
		ARGS+=(-a "${traddr}" -s "${trsvcid}")
	fi
	nvme connect "${ARGS[@]}"
}

_nvme_discover() {
	local trtype="$1"
	local traddr="${2:-$def_traddr}"
	local trsvcid="${3:-$def_trsvcid}"

	ARGS=(-t "${trtype}")
	if [[ "${trtype}" != "loop" ]]; then
		ARGS+=(-a "${traddr}" -s "${trsvcid}")
	fi
	nvme discover "${ARGS[@]}"
}

_create_nvmet_port() {
	local trtype="$1"
	local traddr="${2:-$def_traddr}"
	local adrfam="${3:-$def_adrfam}"
	local trsvcid="${4:-$def_trsvcid}"

	local port
	for ((port = 0; ; port++)); do
		if [[ ! -e "${NVMET_CFS}/ports/${port}" ]]; then
			break
		fi
	done

	mkdir "${NVMET_CFS}/ports/${port}"
	echo "${trtype}" > "${NVMET_CFS}/ports/${port}/addr_trtype"
	echo "${traddr}" > "${NVMET_CFS}/ports/${port}/addr_traddr"
	echo "${adrfam}" > "${NVMET_CFS}/ports/${port}/addr_adrfam"
	echo "${trsvcid}" > "${NVMET_CFS}/ports/${port}/addr_trsvcid"

	echo "${port}"
}

_remove_nvmet_port() {
	local port="$1"
	rmdir "${NVMET_CFS}/ports/${port}"
}

_create_nvmet_ns() {
	local nvmet_subsystem="$1"
	local nsid="$2"
	local blkdev="$3"
	local uuid="00000000-0000-0000-0000-000000000000"
	local subsys_path="${NVMET_CFS}/subsystems/${nvmet_subsystem}"
	local ns_path="${subsys_path}/namespaces/${nsid}"

	if [[ $# -eq 4 ]]; then
		uuid="$4"
	fi

	mkdir "${ns_path}"
	printf "%s" "${blkdev}" > "${ns_path}/device_path"
	printf "%s" "${uuid}" > "${ns_path}/device_uuid"
	printf 1 > "${ns_path}/enable"
}

_create_nvmet_subsystem() {
	local nvmet_subsystem="$1"
	local blkdev="$2"
	local uuid=$3
	local cfs_path="${NVMET_CFS}/subsystems/${nvmet_subsystem}"

	mkdir -p "${cfs_path}"
	echo 1 > "${cfs_path}/attr_allow_any_host"
	_create_nvmet_ns "${nvmet_subsystem}" "1" "${blkdev}" "${uuid}"
}

_remove_nvmet_ns() {
	local nvmet_subsystem="$1"
	local nsid=$2
	local subsys_path="${NVMET_CFS}/subsystems/${nvmet_subsystem}"
	local nvmet_ns_path="${subsys_path}/namespaces/${nsid}"

	echo 0 > "${nvmet_ns_path}/enable"
	rmdir "${nvmet_ns_path}"
}

_remove_nvmet_subsystem() {
	local nvmet_subsystem="$1"
	local subsys_path="${NVMET_CFS}/subsystems/${nvmet_subsystem}"

	_remove_nvmet_ns "${nvmet_subsystem}" "1"
	rmdir "${subsys_path}"
}

_add_nvmet_subsys_to_port() {
	local port="$1"
	local nvmet_subsystem="$2"

	ln -s "${NVMET_CFS}/subsystems/${nvmet_subsystem}" \
		"${NVMET_CFS}/ports/${port}/subsystems/${nvmet_subsystem}"
}

_remove_nvmet_subsystem_from_port() {
	local port="$1"
	local nvmet_subsystem="$2"

	rm "${NVMET_CFS}/ports/${port}/subsystems/${nvmet_subsystem}"
}

_find_nvme_dev() {
	local dev
	local transport
	for dev in /sys/class/nvme/nvme*; do
		dev="$(basename "$dev")"
		transport="$(cat "/sys/class/nvme/${dev}/transport")"
		if [[ "$transport" == "${nvme_trtype}" ]]; then
			echo "$dev"
			for ((i = 0; i < 10; i++)); do
				if [[ -e /sys/block/$dev/uuid &&
					-e /sys/block/$dev/wwid ]]; then
					return
				fi
				sleep .1
			done
		fi
	done
}

_filter_discovery() {
	sed -n -r -e "s/Generation counter [0-9]+/Generation counter X/" \
		  -e '/Discovery Log Number|Log Entry|trtype|subnqn/p'

}

_discovery_genctr() {
	_nvme_discover "${nvme_trtype}" |
		sed -n -e 's/^.*Generation counter \([0-9]\+\).*$/\1/p'
}
