#!/bin/bash

function usage()
{
	cat  >&2 <<-__END
	Usage:
	kdumptool [--configfile f] calibrate [-s | --shrink] [-d]
	    Outputs possible and suggested memory reservation values.
	    Options:
	        --configfile f    use f as alternative configfile
	        -d                turn on debugging
	        -s or --shrink    shrink the current reservation to the calculated value
	kdumptool commandline [-c] [-u] [-d]
	    Output the expected kernel command line options based on the
	    values of KDUMP_FADUMP and KDUMP_CRASHKERNEL and/or the calibrate result
	    Options:
	        -c    check if the current options found in /proc/cmdline are equal to the
	              expected values; print warnings if not
	        -u    call pbl to update the options to the expected values
	              (when used together with -c only update when needed)
	        -U    same as -u but only if KDUMP_UPDATE_BOOTLOADER is true
	        -d    call pbl to delete kdump-related kernel command line options
	        -D    same as -d but only if KDUMP_UPDATE_BOOTLOADER is true
	__END
	exit 1
}

function do_calibrate()
{
	. /usr/lib/kdump/calibrate.conf

	# find possible LUKS memory requirement
	# and export it in KDUMP_LUKS_MEMORY
	KDUMP_LUKS_MEMORY=0
	if [[ "${KDUMP_PROTO}" == "file" ]]; then
		KDUMP_SAVEDIR_REALPATH=$(realpath -m "${KDUMP_SAVEDIR#*://}")
		mkdir -p "$KDUMP_SAVEDIR_REALPATH"
		MOUNT_SOURCE=$(findmnt -nvr -o SOURCE --target "${KDUMP_SAVEDIR_REALPATH}")


		# find which device is the encrypted device for MOUNT_SOURCE
		CRYPTO_SOURCE=""
		while read SOURCE FSTYPE; do
			[[ "${FSTYPE}" == crypto_LUKS ]] && CRYPTO_SOURCE="${SOURCE}"
		done < <(lsblk -n -l -s -o PATH,FSTYPE "${MOUNT_SOURCE}")

		if [[ -n "${CRYPTO_SOURCE}" ]]; then
			while read KEY VALUE; do
				[[ "$KEY" == "Memory:" ]] && KDUMP_LUKS_MEMORY=$((KDUMP_LUKS_MEMORY + VALUE))
			done < <(cryptsetup luksDump "${CRYPTO_SOURCE}")
		fi
	fi
	[[ -f /var/lib/kdump/kernel-version ]] && read KDUMP_KERNEL_VERSION < /var/lib/kdump/kernel-version
	# skip over the "calibrate" argument and pass the rest to the binary
	shift
	/usr/lib/kdump/calibrate "$@"
	RET=$?
	# exit code 2 means bad arguments
	[[ $RET -eq 2 ]] && usage
	return $RET
}

# exit codes:
# 2 - pbl failed
# 1 - config problem
function do_commandline()
{
	DELETE=false
	UPDATE=false
	CHECK=false
	shift
	while getopts "cuUdD" name ; do
		case $name in
			c)
				CHECK=true
				;;
			u)
				UPDATE=true
				;;
			U)
				$KDUMP_UPDATE_BOOTLOADER && UPDATE=true
				;;
			d)
				DELETE=true
				;;
			D)
				$KDUMP_UPDATE_BOOTLOADER && DELETE=true
				;;
			*)
				usage
				;;
		esac
	done
	shift $((OPTIND-1))
	[[ -n $1 ]] && usage

	if $DELETE; then
		if $UPDATE || $CHECK; then
			echo "Options -d / -D are mutually exclusive with options -c and -u / -U" >&2
			return 1
		fi
		pbl --del-option "crashkernel" --del-option "fadump" --config || return 2
		return 0
	fi
	
	
	TOLERANCE_UP=0
	TOLERANCE_DOWN=0

	# sanitize the value in KDUMP_CRASHKERNEL
	read -ra KDUMP_CRASHKERNEL_ITEMS <<<"$KDUMP_CRASHKERNEL"

	declare -a FADUMP=()
	declare -a CRASHKERNEL=()
	if [[ "${KDUMP_CRASHKERNEL_ITEMS[*]}" == "auto" ]]; then
		CALIBRATE_LOW=
		CALIBRATE_HIGH=
		CALIBRATE_FADUMP=
		while read -r KEY VALUE; do
			case "${KEY}" in
				"Low:") CALIBRATE_LOW=${VALUE};;
				"High:") CALIBRATE_HIGH=${VALUE};;
				"Fadump:") CALIBRATE_FADUMP=${VALUE};;
			esac
		done <<<"$(do_calibrate)"

		if ${KDUMP_FADUMP}; then
			if [[ -z ${CALIBRATE_FADUMP} ]]; then
				echo "KDUMP_FADUMP set to true but Fadump is not supported on this machine" >&2
				return 1
			fi
			CRASHKERNEL=("crashkernel=${CALIBRATE_FADUMP}M")
		else
			if [[ -z ${CALIBRATE_LOW} ]] || [[ -z ${CALIBRATE_HIGH} ]]; then
				echo "Error parsing calibrate output" >&2
				return 1
			fi

			if [[ ${CALIBRATE_HIGH} -gt 0 ]]; then
				if [[ ${CALIBRATE_LOW} -gt 0 ]]; then
					CRASHKERNEL=("crashkernel=${CALIBRATE_LOW}M,low" "crashkernel=${CALIBRATE_HIGH}M,high")
				else
					CRASHKERNEL=("crashkernel=${CALIBRATE_HIGH}M,high")
				fi
			else
				CRASHKERNEL=("crashkernel=${CALIBRATE_LOW}M")
			fi
		fi
		CRASHKERNEL_SOURCE="the output of 'kdumptool calibrate'"

		# kdumptool calibrate is not stable
		# avoid unnecessary minor adjustments to the bootloader
		TOLERANCE_UP=16
		TOLERANCE_DOWN=4
	else
		for i in "${KDUMP_CRASHKERNEL_ITEMS[@]}"; do
			case "${i}" in
				crashkernel=*)
					CRASHKERNEL+=("$i")
					;;
				*)
					echo "Ignoring unsupported \"$i\" found in KDUMP_CRASHKERNEL" >&2
					;;
			esac
		done
		if [[ -z "${CRASHKERNEL[*]}" ]]; then
			echo "Invalid value of KDUMP_CRASHKERNEL"
			return 1
		fi
		CRASHKERNEL_SOURCE="the value of KDUMP_CRASHKERNEL"
	fi

	if $KDUMP_FADUMP; then
		FADUMP=(fadump=on)
		# if makedumpfile not configured to filter out user pages (dump level), 
		# or makedumpfile is not used (raw format),
		# set fadump=nocma
		[[ $((KDUMP_DUMPLEVEL & 8)) -eq 0 ]] && FADUMP=(fadump=nocma)
		[[ "${KDUMP_DUMPFORMAT}" = "raw" ]] && FADUMP=(fadump=nocma)
	fi

	if ! $CHECK && ! $UPDATE; then
		[[ -n "${CRASHKERNEL[*]}" ]] && echo -n "${CRASHKERNEL[*]}"
		[[ -n "${FADUMP[*]}" ]] && echo -n " ${FADUMP[*]}"
		echo
		return 0
	fi

	if $CHECK; then
		# get the running kernel's kdump arguments
		declare -a CMDLINE_CRASHKERNEL=()
		declare -a CMDLINE_FADUMP=()
		# shellcheck disable=SC2046
		read -ra CMDLINE < /proc/cmdline
		for x in "${CMDLINE[@]}"; do
			case "$x" in
				crashkernel=*)
					CMDLINE_CRASHKERNEL+=("$x")
				;;
				fadump=*)
					CMDLINE_FADUMP+=("$x")
				;;
			esac
		done

		#compare to expected command line
		CHECK_FAILED=false

		if [[ "${CRASHKERNEL[*]}" != "${CMDLINE_CRASHKERNEL[*]}" ]]; then
			echo "Kdump expects these crashkernel= values on the kernel command line:"
			echo "    ${CRASHKERNEL[*]}"
			echo "(based on ${CRASHKERNEL_SOURCE})"

			if [[ -z "${CMDLINE_CRASHKERNEL[*]}" ]]; then
				echo "No crashkernel= found in /proc/cmdline. Kdump can't be started."
				CHECK_FAILED=true
			else
				echo "/proc/cmdline contains:"
				echo "    ${CMDLINE_CRASHKERNEL[*]}"

				# check if differences are within tolerance
				if [[ $TOLERANCE_UP -eq 0 ]] && [[ $TOLERANCE_DOWN -eq 0 ]]; then
					# no tolerance allowed
					CHECK_FAILED=true
				elif [[ "${#CRASHKERNEL[*]}" -ne "${#CMDLINE_CRASHKERNEL[*]}" ]]; then
					# different number of crashkernel= parameters
					CHECK_FAILED=true
				else
					# check individual elements
					for ((i=0; i<${#CRASHKERNEL[*]}; ++i)); do
						A=${CRASHKERNEL[i]}
						B=${CMDLINE_CRASHKERNEL[i]}

						# extract numeric values
						AN=${A//[^0-9]/}
						BN=${B//[^0-9]/}

						# no numeric value found
						[[ -z $AN ]] && CHECK_FAILED=true
						[[ -z $BN ]] && CHECK_FAILED=true
						$CHECK_FAILED && break

						# A with B's numeric value differs from B
						[[ "${A/$AN/$BN}" != "$B" ]] && CHECK_FAILED=true
						$CHECK_FAILED && break

						# value not within tolerance
						[[ $((AN)) -gt $((BN + TOLERANCE_DOWN)) ]] && CHECK_FAILED=true
						[[ $((AN)) -lt $((BN - TOLERANCE_UP)) ]] && CHECK_FAILED=true
						$CHECK_FAILED && break
					done
				fi

				if $CHECK_FAILED; then
					echo "Kdump may not work correctly."
				else
					echo "Ignoring minor differencies in crashkernel sizes."
				fi
			fi
		fi

		if [[ "${FADUMP[*]}" != "${CMDLINE_FADUMP[*]}" ]]; then
			CHECK_FAILED=true
			if [[ -n "${FADUMP[*]}" ]]; then
				echo "Kdump expects this fadump= value on the kernel command line:"
				echo "    ${FADUMP[*]}"

				if [[ -z "${CMDLINE_FADUMP[*]}" ]]; then
					echo "No fadump= found in /proc/cmdline. Kdump can't be started."
				else
					echo "/proc/cmdline contains:"
					echo "    ${CMDLINE_FADUMP[*]}"
					echo "Kdump may not work correctly"
				fi
			else
				echo "Kdump does not expect fadump= on the kernel command line"
				echo "/proc/cmdline contains:"
				echo "    ${CMDLINE_FADUMP[*]}"
			fi
		fi

		if ! $UPDATE; then
			$CHECK_FAILED && return 1
			return 0
		fi
	fi

	if $UPDATE; then
		$CHECK && ! $CHECK_FAILED && return 0
		if [[ -n "${FADUMP[*]}" ]]; then
			pbl --add-option "${CRASHKERNEL[*]}" --add-option "${FADUMP[*]}" --config || return 2
		else
			pbl --add-option "${CRASHKERNEL[*]}" --del-option "fadump"  --config || return 2
		fi

		if $CHECK; then
			echo "Updated the command line options in the bootloader."
			echo "Changes will take effect on next boot."
		fi
		return 0
	fi
}


if [[ "$1" == "--configfile" ]]; then
	export KDUMP_CONF="$2"
	shift 2
fi

# export all config values to the binary
set -a
. /usr/lib/kdump/kdump-read-config.sh

case $1 in
	calibrate)
		do_calibrate "$@"
		exit
		;;
	commandline)
		do_commandline "$@"
		exit
		;;
	*)
		usage
		;;
esac
