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

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

_NVMET_TRTYPES_is_valid() {
	local type

	for type in $NVMET_TRTYPES; do
		case $type in
		loop | rdma | tcp | fc)
			;;
		*)
			SKIP_REASONS+=("Invalid NVMET_TRTYPE value: $type")
			return 1
			;;
		esac
	done
	return 0
}

_set_nvme_trtype() {
	local index=$1
	local -a types

	read -r -a types <<< "$NVMET_TRTYPES"

	if [[ -z $index ]]; then
		echo ${#types[@]}
		return
	fi

	nvme_trtype=${types[index]}
	COND_DESC="tr=${nvme_trtype}"
}

_set_nvmet_blkdev_type() {
	local index=$1
	local -a types

	read -r -a types <<< "$NVMET_BLKDEV_TYPES"

	if [[ -z $index ]]; then
		echo ${#types[@]}
		return
	fi

	nvmet_blkdev_type=${types[index]}
	COND_DESC="bd=${nvmet_blkdev_type}"
}

_nvme_requires() {
	_have_program nvme
	_require_nvme_test_img_size 4m
	case ${nvme_trtype} in
	loop)
		_have_driver nvme-loop
		_have_configfs
		;;
	pci)
		_have_driver nvme
		;;
	tcp)
		_have_driver nvme-tcp
		_have_driver nvmet-tcp
		_have_configfs
		;;
	rdma)
		_have_driver nvme-rdma
		_have_driver nvmet-rdma
		_have_configfs
		_have_program rdma
		if [ -n "$USE_RXE" ]; then
			_have_driver rdma_rxe
		else
			_have_driver siw
		fi
		;;
	fc)
		_have_driver nvme-fc
		_have_driver nvme-fcloop
		_have_configfs
		def_adrfam="fc"
		;;
	esac

	if [[ -n ${nvme_adrfam} ]]; then
		case ${nvme_adrfam} in
		ipv6)
			def_traddr="::1"
			def_adrfam="ipv6"
			;;
		ipv4)
			;; # was already set
		fc)
			def_adrfam="fc"
			;;
		*)
			# ignore for non ip transports
			if [[ "${nvme_trtype}" == "tcp" ||
			      "${nvme_trtype}" == "rdma" ]]; then
				SKIP_REASONS+=("unsupported nvme_adrfam=${nvme_adrfam}")
				return 1
			fi
		esac
	fi

	return 0
}

group_setup() {
	if [[ -n "${nvme_target_control}" ]]; then
		NVMET_TRTYPES="$(${nvme_target_control} config --show-trtype)"
		NVMET_BLKDEV_TYPES="$(${nvme_target_control} config --show-blkdev-type)"
	fi
}

group_requires() {
	_have_root
	_NVMET_TRTYPES_is_valid
}

group_device_requires() {
	_require_test_dev_is_nvme
}

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

_require_test_dev_is_nvme_pci() {
	if [[ ! "$(readlink -f "$TEST_DEV_SYSFS/device")" =~ devices/pci ]]; then
		SKIP_REASONS+=("$TEST_DEV is not a PCI NVMe device")
		return 1
	fi
	return 0
}

_require_test_dev_is_not_nvme_multipath() {
	if [[ "$(readlink -f "$TEST_DEV_SYSFS/device")" =~ /nvme-subsystem/ ]]; then
		SKIP_REASONS+=("$TEST_DEV is a NVMe multipath device")
		return 1
	fi
	return 0
}

_require_nvme_test_img_size() {
	local require_sz_mb
	local nvme_img_size_mb

	require_sz_mb="$(convert_to_mb "$1")"
	nvme_img_size_mb="$(convert_to_mb "${NVME_IMG_SIZE}")"

	if ((nvme_img_size_mb < require_sz_mb)); then
		SKIP_REASONS+=("NVME_IMG_SIZE must be at least ${require_sz_mb}m")
		return 1
	fi
	return 0
}

_require_nvme_cli_auth() {
	if ! nvme gen-dhchap-key --nqn nvmf-test-subsys > /dev/null 2>&1 ; then
		SKIP_REASONS+=("nvme gen-dhchap-key command missing")
		return 1
	fi
	return 0
}

_require_nvme_cli_tls() {
	if ! nvme gen-tls-key --subsysnqn nvmf-test-subsys > /dev/null 2>&1; then
		SKIP_REASON+=("nvme gen-tls-key command missing")
		return 1
	fi
	return 0
}

_require_kernel_nvme_fabrics_feature() {
	local feature="$1"

	_have_driver nvme-fabrics || return 1

	if ! [[ -r /dev/nvme-fabrics ]]; then
		SKIP_REASONS+=("/dev/nvme-fabrics not available")
		return 1;
	fi
	if ! grep -qe "${feature}" /dev/nvme-fabrics; then
		SKIP_REASONS+=("nvme-fabrics does not support ${feature}")
		return 1;
	fi
	return 0
}

_require_kernel_nvme_target() {
	if [[ -n "${nvme_target_control}" ]]; then
		SKIP_REASONS+=("Linux kernel soft target not available")
		return 1;
	fi
	return 0
}

_require_remote_nvme_target() {
	if [ -z "${nvme_target_control}" ]; then
		SKIP_REASONS+=("Remote target required but NVME_TARGET_CONTROL is not set")
		return 1
	fi
	return 0
}

_test_dev_nvme_ctrl() {
	echo "/dev/char/$(cat "${TEST_DEV_SYSFS}/device/dev")"
}

_test_dev_nvme_nsid() {
	cat "${TEST_DEV_SYSFS}/nsid"
}

_nvme_get_ctrl_list() {
	local subsys
	local c

	subsys=$(readlink  "${TEST_DEV_SYSFS}/device/subsystem")
	case $subsys in
		*/nvme)
			readlink -f "${TEST_DEV_SYSFS}/device"
			;;
		*/nvme-subsystem)
			for c in "${TEST_DEV_SYSFS}"/device/nvme*; do
				[[ -L "$c" ]] || continue
				[[ -f "$c/dev" ]] && readlink -f "$c"
			done
			;;
	esac
}

_nvme_calc_rand_io_size() {
	local img_size_mb
	local io_size_kb

	img_size_mb="$(convert_to_mb "$1")"
	io_size_kb="$(((img_size_mb * 1024) / $(nproc)))"

	echo "${io_size_kb}k"
}

_nvme_discover() {
	local trtype="$1"
	local port="$2"
	local traddr="$def_traddr"
	local trsvcid="$def_trsvcid"

	ARGS=(--transport "${trtype}")
	ARGS+=(--hostnqn="${def_hostnqn}")
	ARGS+=(--hostid="${def_hostid}")
	if [[ "${trtype}" = "fc" ]]; then
		ARGS+=(--traddr "$(_fc_traddr "$port")")
		ARGS+=(--host-traddr "$(_fc_host_traddr "$port")")
	elif [[ "${trtype}" != "loop" ]]; then
		ARGS+=(--traddr "${traddr}" --trsvcid "${trsvcid}")
	fi
	nvme discover "${ARGS[@]}"
}

_remove_nvmet_allow_hosts() {
	local nvmet_subsystem="$1"
	local nvmet_hostnqn="$2"
	local cfs_path="${NVMET_CFS}/subsystems/${nvmet_subsystem}"

	rm "${cfs_path}/allowed_hosts/${nvmet_hostnqn}"
}

_create_nvmet_passthru() {
	local nvmet_subsystem="$1"
	local subsys_path="${NVMET_CFS}/subsystems/${nvmet_subsystem}"
	local passthru_path="${subsys_path}/passthru"

	mkdir -p "${subsys_path}"
	echo 0 > "${subsys_path}/attr_allow_any_host"

	_test_dev_nvme_ctrl > "${passthru_path}/device_path"
	echo 1 > "${passthru_path}/enable"
	if [[ -f "${passthru_path}/clear_ids" ]]; then
		echo 1 > "${passthru_path}/clear_ids"
	fi
}

_remove_nvmet_passhtru() {
	local nvmet_subsystem="$1"
	local subsys_path="${NVMET_CFS}/subsystems/${nvmet_subsystem}"
	local passthru_path="${subsys_path}/passthru"

	echo 0 > "${passthru_path}/enable"
	rm -f "${subsys_path}"/allowed_hosts/*
	rmdir "${subsys_path}"
}

_set_nvmet_hostkey() {
	local nvmet_hostnqn="$1"
	local nvmet_hostkey="$2"
	local cfs_path="${NVMET_CFS}/hosts/${nvmet_hostnqn}"

	echo "${nvmet_hostkey}" > \
	     "${cfs_path}/dhchap_key"
}

_set_nvmet_ctrlkey() {
	local nvmet_hostnqn="$1"
	local nvmet_ctrlkey="$2"
	local cfs_path="${NVMET_CFS}/hosts/${nvmet_hostnqn}"

	echo "${nvmet_ctrlkey}" > \
	     "${cfs_path}/dhchap_ctrl_key"
}

_set_nvmet_hash() {
	local nvmet_hostnqn="$1"
	local nvmet_hash="$2"
	local cfs_path="${NVMET_CFS}/hosts/${nvmet_hostnqn}"

	echo "${nvmet_hash}" > \
	     "${cfs_path}/dhchap_hash"
}

_set_nvmet_dhgroup() {
	local nvmet_hostnqn="$1"
	local nvmet_dhgroup="$2"
	local cfs_path="${NVMET_CFS}/hosts/${nvmet_hostnqn}"

	echo "${nvmet_dhgroup}" > \
	     "${cfs_path}/dhchap_dhgroup"
}

_enable_nvmet_ns() {
	local subsysnqn="${def_subsysnqn}"
	local nsid="${1:-1}"

	cfs_path="${NVMET_CFS}/subsystems/${subsysnqn}"
	ns_path="${cfs_path}/namespaces/${nsid}"

	echo 1 > "${ns_path}/enable"
}

_disable_nvmet_ns() {
	local subsysnqn="${def_subsysnqn}"
	local nsid="${1:-1}"

	cfs_path="${NVMET_CFS}/subsystems/${subsysnqn}"
	ns_path="${cfs_path}/namespaces/${nsid}"

	echo 0 > "${ns_path}/enable"
}

_set_nvmet_ns_uuid() {
	local subsysnqn="${def_subsysnqn}"
	local nsid="${1:-1}"
	local uuid="${2:-$(uuidgen)}"

	cfs_path="${NVMET_CFS}/subsystems/${subsysnqn}"
	ns_path="${cfs_path}/namespaces/${nsid}"

	printf "%s" "${uuid}" > "${ns_path}/device_uuid"
}

_find_nvme_passthru_loop_dev() {
	local subsys=$1
	local nsid
	local dev

	dev=$(_find_nvme_dev "${subsys}")
	nsid=$(_test_dev_nvme_nsid)
	echo "/dev/${dev}n${nsid}"
}

_nvmet_passthru_target_setup() {
	local subsysnqn="$def_subsysnqn"
	local port

	while [[ $# -gt 0 ]]; do
		case $1 in
			--subsysnqn)
				subsysnqn="$2"
				shift 2
				;;
			*)
				echo "WARNING: unknown argument: $1"
				shift
				;;
		esac
	done

	_create_nvmet_passthru "${subsysnqn}"
	port="$(_create_nvmet_port)"
	_add_nvmet_subsys_to_port "${port}" "${subsysnqn}"
	_create_nvmet_host "${subsysnqn}" "${def_hostnqn}"
}

_nvmet_passthru_target_connect() {
	local subsysnqn="$def_subsysnqn"
	local timeout="5"
	local count="0"

	while [[ $# -gt 0 ]]; do
		case $1 in
			--subsysnqn)
				subsysnqn="$2"
				shift 2
				;;
			*)
				echo "WARNING: unknown argument: $1"
				shift
				;;
		esac
	done

	_nvme_connect_subsys --subsysnqn "${subsysnqn}" --no-wait || return
	nsdev=$(_find_nvme_passthru_loop_dev "${subsysnqn}")

	# The following tests can race with the creation
	# of the device so ensure the block device exists
	# before continuing
	while [ ! -b "${nsdev}" ]; do
		sleep 1
		if ((++count >= timeout)); then
			return 1
		fi
	done

	echo "${nsdev}"
}

_nvmet_passthru_target_cleanup() {
	local subsysnqn="$def_subsysnqn"
	local ports
	local port

	while [[ $# -gt 0 ]]; do
		case $1 in
			--subsysnqn)
				subsysnqn="$2"
				shift 2
				;;
			*)
				echo "WARNING: unknown argument: $1"
				shift
				;;
		esac
	done

	if [[ -n "${nvme_target_control}" ]]; then
		eval "${nvme_target_control}" cleanup \
			--subsysnqn "${subsysnqn}" \
			> /dev/null
		return
	fi

	_get_nvmet_ports "${subsysnqn}" ports

	for port in "${ports[@]}"; do
		_remove_nvmet_subsystem_from_port "${port}" "${subsysnqn}"
		_remove_nvmet_port "${port}"
	done

	_remove_nvmet_passhtru "${subsysnqn}"
	_remove_nvmet_host "${def_hostnqn}"
}

_discovery_genctr() {
	local port=${1}

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

_check_genctr() {
	local last=$1
	local port=$2
	local msg=$3
	local genctr

	genctr=$(_discovery_genctr "$port")
	if (( "${genctr}" <= "${last}" )); then
		echo "Generation counter not incremented when ${msg} (${genctr} <= ${last})"
	fi

	echo "${genctr}"
}

_check_uuid() {
	local nvmedev=$1
	local nr_nsid=0

	for ns in "/sys/block/${nvmedev}n"* ; do
		[ -e "${ns}/wwid" ] || continue
		nr_nsid=$((nr_nsid + 1))
		[ -e "${ns}/uuid" ] || continue
		uuid=$(cat "${ns}/uuid")
		wwid=$(cat "${ns}/wwid")
		if [ "${uuid}" != "${wwid#uuid.}" ]; then
			echo "UUID ${uuid} mismatch (wwid ${wwid})"
			return 1
		elif [ "${uuid}" != "${def_subsys_uuid}" ]; then
			echo "UUID ${uuid} mismatch with ${def_subsys_uuid})"
			return 1
		fi
	done
	if [ $nr_nsid -eq 0 ] ; then
		echo "No namespaces found"
		return 1
	fi
}

declare -A NS_DEV_FAULT_INJECT_SAVE
declare -A CTRL_DEV_FAULT_INJECT_SAVE
ns_dev_passthru_logging=off
ctrl_dev_passthru_logging=off

_nvme_passthru_logging_setup()
{
	ctrl_dev_passthru_logging=$(cat /sys/class/nvme/"$2"/passthru_err_log_enabled)
	ns_dev_passthru_logging=$(cat /sys/class/nvme/"$2"/"$1"/passthru_err_log_enabled)

	_nvme_disable_passthru_admin_error_logging "$2"
	_nvme_disable_passthru_io_error_logging "$1" "$2"
}

_nvme_passthru_logging_cleanup()
{
	echo "$ctrl_dev_passthru_logging" > /sys/class/nvme/"$2"/passthru_err_log_enabled
	echo "$ns_dev_passthru_logging" > /sys/class/nvme/"$2"/"$1"/passthru_err_log_enabled
}

_nvme_err_inject_setup()
{
        local a

        for a in /sys/kernel/debug/"$1"/fault_inject/*; do
                NS_DEV_FAULT_INJECT_SAVE[${a}]=$(<"${a}")
        done

        for a in /sys/kernel/debug/"$2"/fault_inject/*; do
                CTRL_DEV_FAULT_INJECT_SAVE[${a}]=$(<"${a}")
        done
}

_nvme_err_inject_cleanup()
{
        local a

        for a in /sys/kernel/debug/"$1"/fault_inject/*; do
                echo "${NS_DEV_FAULT_INJECT_SAVE[${a}]}" > "${a}"
        done

        for a in /sys/kernel/debug/"$2"/fault_inject/*; do
                echo "${CTRL_DEV_FAULT_INJECT_SAVE[${a}]}" > "${a}"
        done
}

_nvme_enable_err_inject()
{
        echo "$2" > /sys/kernel/debug/"$1"/fault_inject/verbose
        echo "$3" > /sys/kernel/debug/"$1"/fault_inject/probability
        echo "$4" > /sys/kernel/debug/"$1"/fault_inject/dont_retry
        echo "$5" > /sys/kernel/debug/"$1"/fault_inject/status
        echo "$6" > /sys/kernel/debug/"$1"/fault_inject/times
}

_nvme_disable_err_inject()
{
        echo 0 > /sys/kernel/debug/"$1"/fault_inject/probability
        echo 0 > /sys/kernel/debug/"$1"/fault_inject/times
}

_nvme_enable_passthru_admin_error_logging()
{
	echo on > /sys/class/nvme/"$1"/passthru_err_log_enabled
}

_nvme_enable_passthru_io_error_logging()
{
	echo on > /sys/class/nvme/"$2"/"$1"/passthru_err_log_enabled
}

_nvme_disable_passthru_admin_error_logging()
{
	echo off > /sys/class/nvme/"$1"/passthru_err_log_enabled
}

_nvme_disable_passthru_io_error_logging()
{
	echo off > /sys/class/nvme/"$2"/"$1"/passthru_err_log_enabled
}

_nvme_reset_ctrl() {
	echo 1 > /sys/class/nvme/"$1"/reset_controller
}

_nvme_delete_ctrl() {
	echo 1 > /sys/class/nvme/"$1"/delete_controller
}

# Check whether the version of the fio is greater than or equal to $1.$2.$3
_have_tlshd_ver() {
	_have_program tlshd || return $?

	if _compare_three_version_numbers \
		   "$(tlshd --version |& sed 's/.*utils \([0-9^.]*\).*/\1/')" \
		   "$1" "$2" "$3"; then
		SKIP_REASONS+=("tlshd version is older than ${1}.${2}.${3}")
		return 1
	fi
	return 0
}

_have_systemd_tlshd_service() {
	_have_tlshd_ver 1 0 0
	if ! _have_systemctl_unit tlshd; then
		SKIP_REASONS+=("Install ktls-utils for tlshd")
	fi
}

_have_libnvme_ver() {
	local ver

	_have_program nvme || return $?
	ver="$(nvme --version | grep libnvme | cut --delimiter ' ' --fields 3 | sed 's/-.*//')"

	if _compare_three_version_numbers "$ver" "$1" "$2" "$3"; then
		SKIP_REASONS+=("libnvme version is older than ${1}.${2}.${3:-0}")
		return 1
	fi
	return 0
}

_nvme_ctrl_tls_key() {
	local ctrl="$1"

	cat /sys/class/nvme/"$ctrl"/tls_key 2>/dev/null
}
