#!/bin/bash
# SPDX-License-Identifier: GPL-3.0+
# Copyright (C) 2024 Western Digital Corporation or its affiliates.
#
# Test DM zone resource limits stacking (max open zones and max active zones
# limits). The limits shall follow these rules:
# 1) For a target mapping an entire zoned block device, the limits for the
#    target are set to the limits of the device.
# 2) For a target partially mapping a zoned block device, the number of
#    mapped sequential zones is used to determine the limits: if the
#    target maps more sequential write required zones than the device
#    limits, then the limits of the device are used as-is. If the number
#    of mapped sequential zones is lower than the limits, then we assume
#    that the target has no limits (limits set to 0).
# As this evaluation is done for each target, the zone resource limits
# for the mapped device are evaluated as the non-zero minimum of the
# limits of all the targets.

. tests/zbd/rc

DESCRIPTION="DM zone resource limits stacking"
QUICK=1

requires() {
	_have_kver 6 11
	_have_driver dm-mod
	_have_driver dm-crypt
	_have_program dmsetup
	_have_program cryptsetup
}

# setup_dm: <map_type> <zoned device> <start zone> [nr zones]>
# Create a DM device using dm-linear, dm-error or dm-crypt.
# If <nr_zones> is omitted, then all zones from <start zone> are mapped.
setup_dm() {
	local type devpath dname nrz mapzstart mapnrz zsz maplen mapoffset
	local keyfile="$TMPDIR/keyfile"

	type="$1"
	devpath="$2"
	dname="${devpath##*/}"
	nrz=$(<"/sys/block/${dname}/queue/nr_zones")

	mapzstart="$3"
	if [ $# == 3 ]; then
		mapnrz=$((nrz - mapzstart))
	else
		mapnrz="$4"
		if ((mapnrz == 0)) || (( mapzstart + mapnrz > nrz )); then
			mapnrz=$((nrz - mapzstart))
		fi
	fi

	zsz=$(<"/sys/block/${dname}/queue/chunk_sectors")
	maplen=$((mapnrz * zsz))
	mapoffset=$((mapzstart * zsz))

	case "$type" in
	linear|error)
		if echo "0 ${maplen} ${type} ${devpath} ${mapoffset}" | \
				dmsetup create "zbd_011-${type}"; then
			echo "zbd_011-${type}"
		fi
		;;
	crypt)
		# Generate a key (4096-bits)
		dd if=/dev/random of="$keyfile" bs=1 count=512 &> /dev/null
		if cryptsetup open --type plain --cipher null --key-size 512 \
			      --key-file "$keyfile" --size ${maplen} \
			      --offset ${mapoffset} "${devpath}" \
			      "zbd_011-crypt"; then
			echo "zbd_011-crypt"
		fi
		;;
	esac
}

# setup_concat: <zoned dev0> <start_zone0> <nr zones0> <zoned dev1> <start_zone1> <nr zones1>
# Use dm-linear to concatenate 2 sets of zones into a zoned block device.
# If <nr_zonesX> is 0, then all zones from <start_zoneX> are mapped.
setup_concat() {
	local dev0 dname0 nrz0 mapzstart0 mapnrz0
	local dev1 dname1 nrz1 mapzstart1 mapnrz1

	dev0="$(realpath "$1")"
	dname0="$(basename "${dev0}")"

	dev1="$(realpath "$4")"
	dname1="$(basename "${dev1}")"

	nrz0=$(< "/sys/block/${dname0}/queue/nr_zones")
	mapzstart0=$2
	if ((mapzstart0 >= nrz0)); then
		echo "Invalid start zone ${mapzstart0} / ${nrz0}"
		exit 1
	fi

	mapnrz0=$3
	if ((mapnrz0 == 0 || mapzstart0 + mapnrz0 > nrz0)); then
		mapnrz0=$((nrz0 - mapzstart0))
	fi

	nrz1=$(< "/sys/block/${dname1}/queue/nr_zones")
	mapzstart1=$5
	if ((mapzstart1 >= nrz1)); then
		echo "Invalid start zone ${mapzstart1} / ${nrz1}"
		return 1
	fi

	mapnrz1=$6
	if ((mapnrz1 == 0 || mapzstart1 + mapnrz1 > nrz1)); then
		mapnrz1=$((nrz1 - mapzstart1))
	fi

	zsz=$(< "/sys/block/${dname0}/queue/chunk_sectors")
	maplen0=$((mapnrz0 * zsz))
	mapofst0=$((mapzstart0 * zsz))
	maplen1=$((mapnrz1 * zsz))
	mapofst1=$((mapzstart1 * zsz))

	# Linear table entries: "start length linear device offset"
	#  start: starting block in virtual device
	#  length: length of this segment
	#  device: block device, referenced by the device name or by major:minor
	#  offset: starting offset of the mapping on the device

	if echo -e "0 ${maplen0} linear /dev/${dname0} ${mapofst0}\n" \
		"${maplen0} ${maplen1} linear /dev/${dname1} ${mapofst1}" | \
			dmsetup create "zbd_011-concat"; then
		echo "zbd_011-concat"
	fi
}

# check_limits: <dev> <zoned model> <number of zones> <max open limit> <max active limit>
# Check that the zoned model, number of zones and zone resource limits of a DM
# device match the values of the arguments passed.
check_limits() {
	local ret=0
	local devpath dname sysqueue model nrz moz maz

	devpath=$(realpath "$1")
	sysqueue="/sys/block/${devpath##*/}/queue"
	model="$(< "${sysqueue}/zoned")"

	if [[ "$model" != "$2" ]]; then
		echo "Invalid zoned model: ${model} should be $2"
		return 1
	fi

	nrz=$(< "${sysqueue}/nr_zones")
	if [[ ${nrz} -ne $3 ]]; then
		echo "Invalid number of zones: ${nrz} should be $3"
		ret=1
	fi

	# Non-zoned block devices do not have max_open_zones and
	# max_active_zones sysfs attributes.
	[[ "$2" == "none" ]] && return $ret

	moz=$(< "${sysqueue}/max_open_zones")
	if [[ ${moz} -ne $4 ]]; then
		echo "Invalid max open zones limit: ${moz} should be $4"
		ret=1
	fi

	maz=$(< "${sysqueue}/max_active_zones")
	if [[ ${maz} -ne $5 ]]; then
		echo "Invalid max active zones limit: ${maz} should be $5"
		ret=1
	fi

	return $ret
}

declare -a TEST_DESCRIPTIONS
declare -a SETUP_COMMANDS
declare -a EXPECTED_LIMITS

# Test 1
TEST_DESCRIPTIONS+=("Map all zones of the 1st nullb")
SETUP_COMMANDS+=("setup_dm linear /dev/nullb_zbd_011_1 0")
EXPECTED_LIMITS+=("host-managed 1152 64 64")

# Test 2
TEST_DESCRIPTIONS+=("Map all zones of the 2nd nullb")
SETUP_COMMANDS+=("setup_dm linear /dev/nullb_zbd_011_2 0")
EXPECTED_LIMITS+=("host-managed 512 48 0")

# Test 3
TEST_DESCRIPTIONS+=("Map all CNV zones of the 1st nullb")
SETUP_COMMANDS+=("setup_dm linear /dev/nullb_zbd_011_1 0 128")
EXPECTED_LIMITS+=("none 0 0 0")

# Test 4
TEST_DESCRIPTIONS+=("Map all SWR zones of the 1st nullb")
SETUP_COMMANDS+=("setup_dm linear /dev/nullb_zbd_011_1 128")
EXPECTED_LIMITS+=("host-managed 1024 64 64")

# Test 5
TEST_DESCRIPTIONS+=("Map 32 SWR zones of the 1st nullb")
SETUP_COMMANDS+=("setup_dm linear /dev/nullb_zbd_011_1 128 32")
EXPECTED_LIMITS+=("host-managed 32 0 0")

# Test 6
TEST_DESCRIPTIONS+=("Map 64 SWR zones of the 1st nullb")
SETUP_COMMANDS+=("setup_dm linear /dev/nullb_zbd_011_1 128 64")
EXPECTED_LIMITS+=("host-managed 64 0 0")

# Test 7
TEST_DESCRIPTIONS+=("Map 128 SWR zones of the 1st nullb")
SETUP_COMMANDS+=("setup_dm linear /dev/nullb_zbd_011_1 128 128")
EXPECTED_LIMITS+=("host-managed 128 64 64")

# Test 8
TEST_DESCRIPTIONS+=("Concatenate all zones of the 1st and 2nd nullb")
SETUP_COMMANDS+=("setup_concat /dev/nullb_zbd_011_1 0 0 /dev/nullb_zbd_011_2 0 0")
EXPECTED_LIMITS+=("host-managed 1664 48 64")

# Test 9
TEST_DESCRIPTIONS+=("Map 32 CNV zones of the 1st nullb and all SWR zones of the 2nd nullb")
SETUP_COMMANDS+=("setup_concat /dev/nullb_zbd_011_1 0 32 /dev/nullb_zbd_011_2 0 0")
EXPECTED_LIMITS+=("host-managed 544 48 0")

# Test 10
TEST_DESCRIPTIONS+=("Map all SWR zones of the 1st nullb and all SWR zones of the 2nd nullb")
SETUP_COMMANDS+=("setup_concat /dev/nullb_zbd_011_1 128 0 /dev/nullb_zbd_011_2 0 0")
EXPECTED_LIMITS+=("host-managed 1536 48 64")

# Test 11
TEST_DESCRIPTIONS+=("Map 32 SWR zones of the 1st nullb and all SWR zones of the 2nd nullb")
SETUP_COMMANDS+=("setup_concat /dev/nullb_zbd_011_1 128 32 /dev/nullb_zbd_011_2 0 0")
EXPECTED_LIMITS+=("host-managed 544 48 0")

# Test 12
TEST_DESCRIPTIONS+=("Map 128 SWR zones of the 1st nullb and 16 SWR zones of the 2nd nullb")
SETUP_COMMANDS+=("setup_concat /dev/nullb_zbd_011_1 128 128 /dev/nullb_zbd_011_2 0 16")
EXPECTED_LIMITS+=("host-managed 144 64 64")

# Test 13
TEST_DESCRIPTIONS+=("Map 32 SWR zones of the 1st nullb and 16 SWR zones of the 2nd nullb")
SETUP_COMMANDS+=("setup_concat /dev/nullb_zbd_011_1 128 32 /dev/nullb_zbd_011_2 0 16")
EXPECTED_LIMITS+=("host-managed 48 0 0")

# Test 14
TEST_DESCRIPTIONS+=("Map 32 SWR zones of the 1st nullb and 48 SWR zones of the 2nd nullb")
SETUP_COMMANDS+=("setup_concat /dev/nullb_zbd_011_1 128 32 /dev/nullb_zbd_011_2 0 48")
EXPECTED_LIMITS+=("host-managed 80 0 0")

# Test 15
TEST_DESCRIPTIONS+=("Map 32 SWR zones of the 1st nullb and 64 SWR zones of the 2nd nullb")
SETUP_COMMANDS+=("setup_concat /dev/nullb_zbd_011_1 128 32 /dev/nullb_zbd_011_2 0 64")
EXPECTED_LIMITS+=("host-managed 96 48 0")

# Test 16
TEST_DESCRIPTIONS+=("Insert dm-error on the 1st nullb")
SETUP_COMMANDS+=("setup_dm error /dev/nullb_zbd_011_1 0")
EXPECTED_LIMITS+=("host-managed 1152 64 64")

# Test 17
TEST_DESCRIPTIONS+=("Map all zones of the 1st nullb with dm-crypt")
SETUP_COMMANDS+=("setup_dm crypt /dev/nullb_zbd_011_1 0")
EXPECTED_LIMITS+=("host-managed 1152 64 64")

# Test 18
TEST_DESCRIPTIONS+=("Map all CNV zones of the 1st nullb with dm-crypt")
SETUP_COMMANDS+=("setup_dm crypt /dev/nullb_zbd_011_1 0 128")
EXPECTED_LIMITS+=("none 0 0 0")

# Test 19
TEST_DESCRIPTIONS+=("Map all CNV zones and 128 SWR zones of the 1st nullb with dm-crypt")
SETUP_COMMANDS+=("setup_dm crypt /dev/nullb_zbd_011_1 0 256")
EXPECTED_LIMITS+=("host-managed 256 64 64")

# Test 20
TEST_DESCRIPTIONS+=("Map 64 SWR zones of the 1st nullb with dm-crypt")
SETUP_COMMANDS+=("setup_dm crypt /dev/nullb_zbd_011_1 128 64")
EXPECTED_LIMITS+=("host-managed 64 0 0")

# Test 21
TEST_DESCRIPTIONS+=("Map all SWR zones of the 2nd nullb with dm-crypt")
SETUP_COMMANDS+=("setup_dm crypt /dev/nullb_zbd_011_2 0")
EXPECTED_LIMITS+=("host-managed 512 48 0")

test() {
	local i dm_name check_cmd

	echo "Running ${TEST_NAME}"

	_configure_null_blk nullb_zbd_011_1 size=2304 zoned=1 \
			    zone_size=2 zone_nr_conv=128 \
			    zone_max_open=64 zone_max_active=64 power=1
	_configure_null_blk nullb_zbd_011_2 size=1024 zoned=1 \
			    zone_size=2 zone_nr_conv=0 \
			    zone_max_open=48 zone_max_active=0 power=1

	for ((i = 0; i < ${#TEST_DESCRIPTIONS[@]}; i++)); do
		dm_name=$(eval "${SETUP_COMMANDS[i]}")
		check_cmd="check_limits /dev/mapper/$dm_name"
		check_cmd+=" ${EXPECTED_LIMITS[i]}"
		if ! eval "$check_cmd"; then
			echo "Test $((i + 1)) failed: ${TEST_DESCRIPTIONS[i]}"
		fi
		dmsetup remove "$dm_name"
	done

	_exit_null_blk

	echo "Test complete"
}
