#!/bin/bash
#
# Takes a list of modules and unloads them and all dependent modules.
# If a module cannot be unloaded (e.g. it's in use), an error is
# returned.
###############################################################################

SCRIPT_NAME="$(basename "$0")"
LCTL=${LCTL:-lctl}
DEBUG='false'

# Print help message
print_usage() {
	echo "$SCRIPT_NAME -h|--help"
	echo "$SCRIPT_NAME [-d|--debug-kernel] [modulesname...]"
	echo
	echo -e "\t-h, --help\t\tDisplay this help message"
	echo -e "\t-d, --debug-kernel\tDisplay lustre kernel debug messages"
	echo -e "\tmodulesname\t\tList of lustre modules to unload. By default"
	echo -e "\t\t\t\tldiskfs and libcfs (and their dependencies) are"
	echo -e "\t\t\t\tselected."
}

# Print kernel debug message for lustre modules
print_debug() {
	$LCTL mark "$SCRIPT_NAME : Stop debug"
	$LCTL debug_kernel
	DEBUG='false'
}

# Unload all modules dependent on $1 (exclude removal of $1)
unload_dep_modules_exclusive() {
	local MODULE=$1

	local DEPS="$(lsmod | awk '($1 == "'$MODULE'") { print $4 }')"
	for SUBMOD in $(echo $DEPS | tr ',' ' '); do
		unload_dep_modules_inclusive $SUBMOD || return 1
	done
	return 0
}

# Unload all modules dependent on $1 (include removal of $1)
unload_dep_modules_inclusive() {
	local MODULE=$1

	# if $MODULE not loaded, return 0
	lsmod | egrep -q "^\<$MODULE\>" || return 0
	unload_dep_modules_exclusive $MODULE || return 1

	if $DEBUG; then
		if [ "$MODULE" = 'libcfs' ]; then
			print_debug
		fi
		$LCTL mark "$SCRIPT_NAME : Unload $MODULE"
	fi

	rmmod $MODULE || return 1
	return 0
}

declare -a modules
while [ $# -gt 0 ]; do
	case "$1" in
		-h|--help)
			print_usage >&2
			exit 0
			;;
		-d|--debug-kernel)
			if lsmod | egrep -q '^libcfs'; then
				DEBUG='true'
			else
				echo "Debug unavailable: libcfs is not loaded" >&2
			fi
			;;
		-*)
			echo "Error invalid option" >&2
			print_usage >&2
			exit 2
			;;
		*)
			modules+=("$1")
			;;
	esac
	shift
done

# To maintain backwards compatibility, ldiskfs and libcfs must be
# unloaded if no parameters are given, or if only the ldiskfs parameter
# is given. It's ugly, but is needed to emulate the prior functionality
if [ "${#modules[@]}" -eq 0 ] || [ "${modules[*]}" = "ldiskfs" ]; then
	unload_all=true
	modules=('lnet_selftest' 'ldiskfs' 'libcfs')
else
	unload_all=false
fi

if [ -f /sys/kernel/debug/kmemleak ] ; then
	cat /proc/modules >/tmp/kmemleak-modules-list.txt
	echo scan > /sys/kernel/debug/kmemleak
	cat /sys/kernel/debug/kmemleak > /tmp/kmemleak-before-unload.txt
	test -s /tmp/kmemleak-before-unload.txt && logger -t leak-pre -f /tmp/kmemleak-before-unload.txt
	rm /tmp/kmemleak-before-unload.txt
	# Clear everything here so that only new leaks show up
	# after module unload
	echo clear > /sys/kernel/debug/kmemleak
fi

# Manage debug
if $DEBUG; then
	echo "Lustre debug parameters:" >&2
	$LCTL get_param debug >&2
	$LCTL get_param debug_mb >&2

	$LCTL mark "$SCRIPT_NAME : Start debug"
fi

if $unload_all; then
	unload_dep_modules_inclusive 'ptlrpc' || exit 1
	# LNet may have an internal ref which can prevent LND modules from
	# unloading. Try to drop it before unloading modules.
	# NB: we squelch stderr because lnetctl/lctl may complain about
	# LNet being "busy", but this is normal. We're making a best effort
	# here.
	# Prefer lnetctl if it is present
	if [ -n "$(which lnetctl 2>/dev/null)" ]; then
		lnetctl lnet unconfigure 2>/dev/null
	elif [ -n "$(which lctl 2>/dev/null)" ]; then
		lctl net down 2>/dev/null
	fi
fi

for mod in ${modules[*]}; do
	unload_dep_modules_inclusive $mod || exit 1
done

if $DEBUG; then
	print_debug
fi

if [ -f /sys/kernel/debug/kmemleak ] ; then
	echo scan > /sys/kernel/debug/kmemleak
	cat /sys/kernel/debug/kmemleak > /tmp/kmemleak-after-unload.txt
	test -s /tmp/kmemleak-after-unload.txt && logger -t leak-mods -f /tmp/kmemleak-modules-list.txt && logger -t leak-post -f /tmp/kmemleak-after-unload.txt
	rm -f /tmp/kmemleak-after-unload.txt /tmp/kmemleak-modules-list.txt
fi

exit 0
