#!/bin/bash
# SPDX-License-Identifier: GPL-3.0+
# Copyright (C) 2022 Oracle and/or its affiliates
#
# Test nvme error logging by injecting errors. Kernel must have FAULT_INJECTION
# and FAULT_INJECTION_DEBUG_FS configured to use error injector. Tests can be
# run with or without NVME_VERBOSE_ERRORS configured.
#
# Test for commit bd83fe6f2cd2 ("nvme: add verbose error logging").

. tests/nvme/rc
DESCRIPTION="test error logging"
QUICK=1

requires() {
	_have_program nvme
	_have_kernel_option FAULT_INJECTION
	_have_kernel_option FAULT_INJECTION_DEBUG_FS
}

device_requires() {
	_require_test_dev_is_not_nvme_multipath
}

# Get the last dmesg lines as many as specified. Exclude the lines to indicate
# suppression by rate limit.
last_dmesg()
{
	local nr_lines=$1

	dmesg -t | grep -v "callbacks suppressed" | tail "-$nr_lines" \
		| sed 's/\[.*\] //'
}

inject_unrec_read_on_read()
{
	# Inject a 'Unrecovered Read Error' (0x281) status error on a READ
	_nvme_enable_err_inject "$1" 0 100 1 0x281 1

	dd if=/dev/"$1" of=/dev/null bs="${LB_SZ}" count=1 iflag=direct \
	    2> /dev/null 1>&2

	_nvme_disable_err_inject "$1"

	if ${nvme_verbose_errors}; then
		last_dmesg 2 | grep "Unrecovered Read Error (" | \
		    sed 's/nvme.*://g'
	else
		last_dmesg 2 | grep "Cmd(" | sed 's/I\/O Cmd/Read/g' | \
		    sed 's/I\/O Error/Unrecovered Read Error/g' | \
		    sed 's/nvme.*://g'
	fi
}

inject_invalid_status_on_read()
{
	# Inject an invalid status (0x375) on a READ
	_nvme_enable_err_inject "$1" 0 100 1 0x375 1

	dd if=/dev/"$1" of=/dev/null bs="${LB_SZ}" count=1 iflag=direct \
	    2> /dev/null 1>&2

	_nvme_disable_err_inject "$1"

	if ${nvme_verbose_errors}; then
		last_dmesg 2 | grep "Unknown (" | \
		    sed 's/nvme.*://g'
	else
		last_dmesg 2 | grep "Cmd(" | sed 's/I\/O Cmd/Read/g' | \
		    sed 's/I\/O Error/Unknown/g' | \
		    sed 's/nvme.*://g'
	fi
}

inject_write_fault_on_write()
{
	# Inject a 'Write Fault' 0x280 status error on a WRITE
	_nvme_enable_err_inject "$1" 0 100 1 0x280 1

	dd if=/dev/zero of=/dev/"$1" bs="${LB_SZ}" count=1 oflag=direct \
	    2> /dev/null 1>&2

	_nvme_disable_err_inject "$1"

	if ${nvme_verbose_errors}; then
		last_dmesg 2 | grep "Write Fault (" | \
		    sed 's/nvme.*://g'
	else
		last_dmesg 2 | grep "Cmd(" | sed 's/I\/O Cmd/Write/g' | \
		    sed 's/I\/O Error/Write Fault/g' | \
		    sed 's/nvme.*://g'
	fi
}

inject_access_denied_on_identify()
{
	# Inject a 'Access Denied' (0x286) status error on an
	# Identify admin command
	_nvme_enable_err_inject "$1" 0 100 1 0x286 1

	nvme admin-passthru /dev/"$1" --opcode=0x06 --data-len=4096 \
	    --cdw10=1 -r 2> /dev/null 1>&2

	_nvme_disable_err_inject "$1"

	if ${nvme_verbose_errors}; then
		last_dmesg 1 | grep "Access Denied (" | \
		    sed 's/nvme.*://g'
	else
		last_dmesg 1 | grep "Admin Cmd(" | \
		    sed 's/Admin Cmd/Identify/g' | \
		    sed 's/I\/O Error/Access Denied/g' | \
		    sed 's/nvme.*://g'
	fi
}

inject_invalid_admin_cmd()
{
	# Inject a 'Invalid Command Opcode' (0x1) on an invalid command (0x96)
	 _nvme_enable_err_inject "$1" 0 100 1 0x1 1

	nvme admin-passthru /dev/"$1" --opcode=0x96 --data-len="${LB_SZ}" \
	    --cdw10=1 -r 2> /dev/null 1>&2

	_nvme_disable_err_inject "$1"

	if ${nvme_verbose_errors}; then
		dmesg -t | tail -1 | grep "Invalid Command Opcode (" | \
		    sed 's/nvme.*://g'
	else
		dmesg -t | tail -1 | grep "Admin Cmd(" | \
		    sed 's/Admin Cmd/Unknown/g' | \
		    sed 's/I\/O Error/Invalid Command Opcode/g' | \
		    sed 's/nvme.*://g'
	fi
}

inject_invalid_io_cmd_passthru()
{
	local ns

	ns=$(echo "$1" |  cut -d "n" -f3)

	# Inject a 'Invalid Command Opcode' (0x1) on a read (0x02)
	_nvme_enable_err_inject "$ns_dev" 0 100 1 0x1 1

	nvme io-passthru /dev/"$1" --opcode=0x02 --namespace-id="$ns" \
		--data-len="${LB_SZ}" --read --cdw10=0 --cdw11=0 --cdw12="$2" 2> /dev/null 1>&2

	_nvme_disable_err_inject "$1"
	if ${nvme_verbose_errors}; then
		last_dmesg 2 | grep "Invalid Command Opcode (" | \
		    sed 's/nvme.*://g'
	else
		last_dmesg 2 | grep "Cmd(0x2" | sed 's/I\/O Cmd/Read/g' | \
		    sed 's/I\/O Error/Invalid Command Opcode/g' | \
		    sed 's/nvme.*://g'
	fi
}

test_device() {
	echo "Running ${TEST_NAME}"

	local nvme_verbose_errors
	local ns_dev
	local ctrl_dev

	if _check_kernel_option NVME_VERBOSE_ERRORS; then
		nvme_verbose_errors=true
	else
		nvme_verbose_errors=false
	fi

	ns_dev=${TEST_DEV##*/}
	ctrl_dev=${ns_dev%n*}

	LB_SZ=$(blockdev --getss "${TEST_DEV}")

	_nvme_err_inject_setup "${ns_dev}" "${ctrl_dev}"

	# wait DEFAULT_RATELIMIT_INTERVAL=5 seconds to ensure errors are printed
	sleep 5

	inject_unrec_read_on_read "${ns_dev}"
	inject_invalid_status_on_read "${ns_dev}"
	inject_write_fault_on_write "${ns_dev}"

	if [ -e "$TEST_DEV_SYSFS/passthru_err_log_enabled" ]; then
		_nvme_passthru_logging_setup "${ns_dev}" "${ctrl_dev}"

		# Test Pass Thru Admin Logging
		_nvme_disable_passthru_admin_error_logging "${ctrl_dev}"
		inject_invalid_admin_cmd "${ctrl_dev}"
		_nvme_enable_passthru_admin_error_logging "${ctrl_dev}"
		inject_access_denied_on_identify "${ctrl_dev}"

		# Test Pass Thru IO Logging
		_nvme_disable_passthru_io_error_logging "${ns_dev}" "${ctrl_dev}"
		inject_invalid_io_cmd_passthru "${ns_dev}" 0
		_nvme_enable_passthru_io_error_logging "${ns_dev}" "${ctrl_dev}"
		inject_invalid_io_cmd_passthru "${ns_dev}" 1

		_nvme_passthru_logging_cleanup "${ns_dev}" "${ctrl_dev}"
	else
		echo " Identify(0x6), Access Denied (sct 0x2 / sc 0x86) DNR cdw10=0x1 cdw11=0x0 cdw12=0x0 cdw13=0x0 cdw14=0x0 cdw15=0x0"
		echo " Read(0x2), Invalid Command Opcode (sct 0x0 / sc 0x1) DNR cdw10=0x0 cdw11=0x0 cdw12=0x1 cdw13=0x0 cdw14=0x0 cdw15=0x0"
	fi
	_nvme_err_inject_cleanup "${ns_dev}" "${ctrl_dev}"

	echo "Test complete"
}
