#!/bin/bash
#
# cio_ignore - Tool to query and modify the cio blacklist
#
# Copyright IBM Corp. 2009, 2017
#
# s390-tools is free software; you can redistribute it and/or modify
# it under the terms of the MIT license. See LICENSE for details.
#

VERSION="%S390_TOOLS_VERSION%"
BLACKLIST="/proc/cio_ignore"
CIO_SETTLE="/proc/cio_settle"
WAIT_FOR_CIO=0
SYSINFO="/proc/sysinfo"
CONSDRV="/sys/bus/ccw/drivers/3215"
MAXCSSID=0
MAXSSID=3
MAXDEVNO=65535
LSCSS="lscss"
TOOLNAME="${0##*/}"

# print_help()
# Print help text.
function print_help()
{
	cat << EOF
Usage: ${TOOLNAME} COMMANDS

Query or modify the CIO device driver blacklist. This blacklist determines if
the CIO device driver ignores a newly discovered device. Ignored devices are
not accessible and do not use resources.

COMMANDS:
 -h, --help                  Print this help text
 -v, --version               Print version information
 -a, --add DEVID             Add a device ID to the blacklist
 -A, --add-all               Add all device IDs to the blacklist
 -r, --remove DEVID          Remove a device ID from the blacklist
 -R, --remove-all            Remove all device IDs from the blacklist
 -l, --list                  List device IDs on the blacklist
 -L, --list-not-blacklisted  List device IDs not on the blacklist
 -i, --is-ignored DEVID      Check if specified device ID is on the blacklist
 -k, --kernel-param          List blacklist in cio_ignore kernel param format
 -u, --unused                Create blacklist including all unused devices
 -p, --purge                 Unregister all unused devices on the blacklist
EOF
}

# print_version()
# Print version information.
function print_version()
{
	echo "${TOOLNAME}: version ${VERSION}"
	echo "Copyright IBM Corp. 2009, 2017"
}

# print_usage_tip()
# Print usage tip text.
function print_usage_tip()
{
	echo "Use '${TOOLNAME} --help' to get usage information." >&2
}

# warn(msg)
# Print msg to stderr in warning text format.
function warn()
{
	local MSG=$1

	echo "${TOOLNAME}: $MSG" >&2
}

# error(msg)
# Print msg to stderr in error text format and exit with non-zero exit code.
function error()
{
	local MSG=$1

	warn "$MSG"
	exit 1
}

# blacklist_write(msg)
# Check for write access and write msg to blacklist
function blacklist_write()
{
	local MSG="$*"

	if [ ! -w "$BLACKLIST" ] ; then
		error "Error: missing write permission for $BLACKLIST"
	fi

	echo "$MSG" > "$BLACKLIST" 2>/dev/null || return 1

	return 0
}

# split_range(range, &bus_id1, &bus_id2)
# Set bus_id1 and bus_id2 to the bus IDs as specified in range.
function split_range()
{
	local RANGE=$1
	local LOCAL_BUSID1="${RANGE%%-*}"
	local LOCAL_BUSID2="${RANGE##*-}"

	eval "$2=$LOCAL_BUSID1"
	eval "$3=$LOCAL_BUSID2"
}

# split_bus_id(bus_id, &cssid, &ssid, &devno)
# Set cssid, ssid and devno to the respective values as specified in bus_id.
function split_bus_id()
{
	local BUSID=$1
	local DEVNO="${BUSID##*.}"
	local BUS="${BUSID%.*}"
	local CSSID="${BUS%%.*}"
	local SSID="${BUS##*.}"

	# Set cssid and ssid to zero if not specified
	if [ "${BUSID#*.}" == "$BUSID" ] ; then
		CSSID=0
		SSID=0
	fi
	eval "let $2=0x$CSSID"
	eval "let $3=0x$SSID"
	eval "let $4=0x$DEVNO"
}

# count_char(string, char, &count)
# Count the number of times that char occurs in string.
function count_char()
{
	local STRING=$1
	local CHAR=$2
	local COUNT=$3
	local I

	I=0
	while [ "${STRING#*$CHAR}" != "$STRING" ] ; do
		let I=$I+1
		STRING="${STRING#*$CHAR}"
	done
	eval "$COUNT=$I"
}

# check_hex_number(number, mindigits, maxdigits, max, &errmsg)
# Check hex number for validity. Return 0 when valid, 1 otherwise.
function check_hex_number()
{
	local NUMBER=$1
	local MINDIGITS=$2
	local MAXDIGITS=$3
	local MAX=$4
	local ERRHEX=$5
	local VAL

	if [ -z "$NUMBER" ] ; then
		eval "$ERRHEX='is empty'"
		return 1
	fi
	let VAL="0x$NUMBER" 2>/dev/null
	if [ -z "$VAL" ] ; then
		eval "$ERRHEX='is not valid'"
		return 1
	fi
	if [ $VAL -gt $MAX ] ; then
		eval "$ERRHEX='is too large'"
		return 1
	fi
	if [ ${#NUMBER} -lt $MINDIGITS ] ; then
		eval "$ERRHEX='is too short'"
		return 1
	fi
	if [ ${#NUMBER} -gt $MAXDIGITS ] ; then
		eval "$ERRHEX='is too long'"
		return 1
	fi
	return 0
}

# check_dev_id(devid, &errmsg)
# Check device ID for validity. Return 0 when ID is valid, 1 otherwise.
function check_dev_id()
{
	local DEVID=$1
	local ERRDEV=$2
	local ERRTXT
	local CSSID
	local SSID
	local DEVNO
	local DOTCOUNT
	local IFS

	# Check for empty device ID
	if [ -z "$DEVID" ] ; then
		eval "$ERRDEV='device ID is empty'"
		return 1
	fi
	# Check for spaces in device id
	IFS=' '
	set - $DEVID
	if [ $# -ne 1 -o "$DEVID" != "$1" ] ; then
		eval "$ERRDEV='device ID contains spaces'"
		return 1
	fi
	# Convert bus id to positional parameters
	count_char "$DEVID" '.' DOTCOUNT
	IFS='.'
	set - $DEVID
	unset IFS
	# Check number of components
	if [ $DOTCOUNT -eq 0 ] ; then
		# Old style device number
		DEVNO="${1#0x}"
		if [ -z "$DEVNO" ] ; then
			eval "$ERRDEV='device number is incomplete'"
			return 1
		fi
		# Check for valid device number
		if ! check_hex_number "$DEVNO" 1 65535 $MAXDEVNO ERRTXT; then
			eval "$ERRDEV='device number $ERRTXT'"
			return 1
		fi
		return 0
	fi
	# Check for bus id format
	if [ $DOTCOUNT -ne 2 ] ; then
		eval "$ERRDEV='unrecognized format'"
		return 1
	fi
	CSSID=$1
	SSID=$2
	DEVNO=$3
	# Check cssid
	if ! check_hex_number "$CSSID" 1 2 $MAXCSSID ERRTXT; then
		eval "$ERRDEV='CSSID $ERRTXT'"
		return 1
	fi
	# Check ssid
	if ! check_hex_number "$SSID" 1 1 $MAXSSID ERRTXT; then
		eval "$ERRDEV='SSID $ERRTXT'"
		return 1
	fi
	# Check devno
	if ! check_hex_number "$DEVNO" 4 4 $MAXDEVNO ERRTXT; then
		eval "$ERRDEV='device number $ERRTXT'"
		return 1
	fi

	return 0
}

# check_range(range, &errmsg)
# Check a device ID range for validity. Return 0 if valid, 1 otherwise.
function check_range()
{
	local RANGE=$1
	local ERRRNG=$2
	local ERRTXTRNG
	local MINUSCOUNT
	local BUSID1
	local BUSID2
	local CSSID1
	local CSSID2
	local SSID1
	local SSID2
	local DEVNO1
	local DEVNO2
	local IFS

	count_char "$RANGE" '-' MINUSCOUNT
	IFS='-'
	set - $RANGE
	unset IFS
	# Check number of device IDs
	if [ $MINUSCOUNT -gt 1 ] ; then
		eval "$ERRRNG='unrecognized format'"
		return 1
	fi
	# Check first device ID
	if ! check_dev_id "$1" ERRTXTRNG ; then
		eval "$ERRRNG='$ERRTXTRNG'"
		return 1
	fi
	if [ $MINUSCOUNT -eq 0 ] ; then
		return 0
	fi
	# Check second device ID
	if ! check_dev_id "$2" ERRTXTRNG ; then
		eval "$ERRRNG='$ERRTXTRNG'"
		return 1
	fi
	# Check actual ID
	split_range "$RANGE" BUSID1 BUSID2
	split_bus_id "$BUSID1" CSSID1 SSID1 DEVNO1
	split_bus_id "$BUSID2" CSSID2 SSID2 DEVNO2
	if [ "$CSSID1" -ne "$CSSID2" ] ; then
		eval "$ERRRNG='CSSIDs do not match'"
		return 1
	fi
	if [ "$SSID1" -ne "$SSID2" ] ; then
		eval "$ERRRNG='SSIDs do not match'"
		return 1
	fi
	if [ "$DEVNO1" -gt "$DEVNO2" ] ; then
		eval "$ERRRNG='reversed device ID order'"
		return 1
	fi
	return 0
}

# add_device(list)
# Add a list of devices to blacklist
function add_device()
{
	local DEVID_LIST=$1
	local RANGE
	local ERRMSG
	local MINUSCOUNT
	local IFS=','

	if [ -z "$DEVID_LIST" ] ; then
		error "--add requires an argument"
	fi
	for RANGE in $DEVID_LIST ; do
		if blacklist_write "add $RANGE" ; then
			continue
		fi
		# Try to determine why blacklist operation failed
		if ! check_range "$RANGE" ERRMSG ; then
			count_char "$RANGE" '-' MINUSCOUNT
			if [ "$MINUSCOUNT" -eq 0 ] ; then
				error "Error: device ID '$RANGE': $ERRMSG"
			else
				error "Error: device ID range '$RANGE': $ERRMSG"
			fi
		else
			error "Error: could not add '$RANGE' to blacklist"
		fi
	done
}

# add_all_devices()
# Add all devices to the blacklist.
function add_all_devices()
{
	blacklist_write 'add all' || \
		error "Error: add-all function not accepted by kernel"
}

# remove_device(list)
# Remove a list of devices from blacklist.
function remove_device()
{
	local DEVID_LIST=$1
	local RANGE
	local IFS=','

	if [ -z "$DEVID_LIST" ] ; then
		error "--remove requires an argument"
	fi
	for RANGE in $DEVID_LIST ; do
		if blacklist_write "free $RANGE" ; then
			continue
		fi
		# Try to determine why blacklist operation failed
		if ! check_range "$RANGE" ERRMSG ; then
			count_char "$RANGE" '-' MINUSCOUNT
			if [ "$MINUSCOUNT" -eq 0 ] ; then
				error "Error: device ID '$RANGE': $ERRMSG"
			else
				error "Error: device ID range '$RANGE': $ERRMSG"
			fi
		else
			error "Error: could not remove '$RANGE' from blacklist"
		fi
	done
}

# remove_all_devices()
# Remove all devices from blacklist.
function remove_all_devices()
{
	blacklist_write 'free all' || \
		error "Error: remove-all function not accepted by kernel"
}

# list_blacklisted(showheader)
# Print list of devices on blacklist. Precede output by header text
# if showheader is 1
function list_blacklisted()
{
	local SHOWHEADER=$1
	local LIST
	local ENTRY

	if [ $SHOWHEADER -eq 1 ] ; then
		echo 'Ignored devices:'
		echo '================='
	fi
	LIST=$(cat "$BLACKLIST" 2>/dev/null) || \
		error "Error: could not read $BLACKLIST"
	# Parse each blacklist entry
	for ENTRY in $LIST ; do
		echo "$ENTRY"
	done
}

# advance_ssid(cssid, ssid, &newcssid, &newssid)
# Set newcssid and newssid to the next ssid after cssid and ssid. Return
# 0 if there was another ssid, 1 if all ssids have been processed.
function advance_ssid()
{
	local LOCAL_CSSID=$1
	local LOCAL_SSID=$2
	let LOCAL_SSID=$LOCAL_SSID+1

	if [ $LOCAL_SSID -gt $MAXSSID ] ; then
		LOCAL_SSID=0
		let LOCAL_CSSID=$LOCAL_CSSID+1
		if [ $LOCAL_CSSID -gt $MAXCSSID ] ; then
			return 1
		fi
	fi
	eval "$3=$LOCAL_CSSID"
	eval "$4=$LOCAL_SSID"
	return 0
}

# print_range(cssid1, ssid1, devno1, cssid2, ssid2, devno2)
# Print range for given device ID.
function print_range()
{
	local CSSID1
	local SSID1
	local DEVNO1
	local CSSID2
	local SSID2
	local DEVNO2

	let CSSID1=$1
	let SSID1=$2
	let DEVNO1=$3
	let CSSID2=$4
	let SSID2=$5
	let DEVNO2=$6

	if [ $CSSID1 -eq $CSSID2 -a $SSID1 -eq $SSID2 -a \
	     $DEVNO1 -eq $DEVNO2 ] ; then
		printf '%x.%x.%04x\n' $CSSID1 $SSID1 $DEVNO1
	else
		printf '%x.%x.%04x-%x.%x.%04x\n' $CSSID1 $SSID1 $DEVNO1 \
						 $CSSID2 $SSID2 $DEVNO2

	fi
}

# list_not_blacklisted(showheader)
# Print list of devices not on blacklist. Precede output by header text
# if showheader is 1.
function list_not_blacklisted()
{
	local SHOWHEADER=$1
	local CSSID=0
	local SSID=0
	local DEVNO=0
	local ENTRY
	local LIST

	if [ $SHOWHEADER -eq 1 ] ; then
		echo 'Devices that are not ignored:'
		echo '============================='
	fi
	LIST=$(cat "$BLACKLIST" 2>/dev/null) || \
		error "Error: could not read $BLACKLIST"
	# Parse each blacklist entry
	for ENTRY in $LIST ; do
		local BLBUSID1
		local BLBUSID2
		local BLCSSID
		local BLSSID
		local BLDEVNO1
		local BLDEVNO2

		# Prepare variables containing bus id information
		split_range $ENTRY BLBUSID1 BLBUSID2
		split_bus_id $BLBUSID1 BLCSSID BLSSID BLDEVNO1
		split_bus_id $BLBUSID2 BLCSSID BLSSID BLDEVNO2

		# Print ranges in ssids before this entry
		while [ $CSSID -ne $BLCSSID -o $SSID -ne $BLSSID ] ; do
			print_range $CSSID $SSID $DEVNO $CSSID $SSID $MAXDEVNO
			DEVNO=0
			if ! advance_ssid $CSSID $SSID CSSID SSID ; then
				return
			fi
		done
		# Print range before this entry in the same ssid
		if [ $BLDEVNO1 -gt 0 ] ; then
			print_range $CSSID $SSID $DEVNO \
				    $CSSID $SSID $BLDEVNO1-1
		fi
		# Advance current id pointer to after the end of this entry
		let DEVNO=$BLDEVNO2+1
		if [ $DEVNO -gt $MAXDEVNO ] ; then
			DEVNO=0
			if ! advance_ssid $CSSID $SSID CSSID SSID ; then
				return
			fi
		fi
	done

	# Print ranges in ssids after the final entry
	while [ $CSSID -le $MAXCSSID -o $SSID -le $MAXSSID ] ; do
		print_range $CSSID $SSID $DEVNO $CSSID $SSID $MAXDEVNO
		DEVNO=0
		if ! advance_ssid $CSSID $SSID CSSID SSID ; then
			return
		fi
	done
}

# simplify_range(range, &dest_range)
# Remove 0.0. from bus ids in range.
function simplify_range()
{
	local LOCAL_RANGE=$1
	local BUSID1
	local BUSID2

	split_range $LOCAL_RANGE BUSID1 BUSID2
	BUSID1=${BUSID1##0.0.}
	BUSID2=${BUSID2##0.0.}
	if [ $BUSID1 == $BUSID2 ] ; then
		eval "$2=$BUSID1"
	else
		eval "$2=$BUSID1-$BUSID2"
	fi
}


# to_param(invert, list)
# Print list in comma-separated format, preceding each range with ! if
# invert is 1.
function to_param()
{
	local INVERT=$1
	local LIST=$2
	local RANGE
	local SEP
	local PREFIX

	if [ $INVERT -eq 1 ] ; then
		echo -n 'all'
		PREFIX='!'
		SEP=','
	fi

	for RANGE in $LIST ; do
		simplify_range $RANGE RANGE
		echo -n "$SEP$PREFIX$RANGE"
		SEP=','
	done
}

# is_blacklisted(bus_id)
# Check if device is on the blacklist. Return 0 when on blacklist, 1 otherwise.
function is_blacklisted()
{
	local ISBUSID=$1
	local ISCSSID
	local ISSSID
	local ISDEVNO
	local BUSID1
	local BUSID2
	local CSSID1
	local CSSID2
	local SSID1
	local SSID2
	local DEVNO1
	local DEVNO2
	local LIST
	local RANGE

	split_bus_id $ISBUSID ISCSSID ISSSID ISDEVNO
	LIST=$(cat "$BLACKLIST" 2>/dev/null) || \
		error "Error: could not read $BLACKLIST"
	# Parse each blacklist entry
	for RANGE in $LIST ; do
		split_range "$RANGE" BUSID1 BUSID2
		split_bus_id "$BUSID1" CSSID1 SSID1 DEVNO1
		split_bus_id "$BUSID2" CSSID2 SSID2 DEVNO2

		if [ $ISCSSID -ne $CSSID1 -o $ISSSID -ne $SSID1 ] ; then
			continue
		fi
		if [ $ISDEVNO -ge $DEVNO1 -a $ISDEVNO -le $DEVNO2 ] ; then
			return 0
		fi
	done
	return 1
}

# is_blacklisted_opt(bus_id)
# Check if specified busid is on blacklist. Print result and exit with code 0
# if on blacklist, 2 otherwise.
function is_blacklisted_opt()
{
	local BUSID=$1
	local ERRBLO
	local COMMACOUNT
	local MINUSCOUNT

	if [ -z "$BUSID" ] ; then
		error "--is-ignored requires an argument"
	fi
	count_char "$BUSID" ',' COMMACOUNT
	count_char "$BUSID" '-' MINUSCOUNT
	if [ $COMMACOUNT -gt 0 -o $MINUSCOUNT -gt 0 ] ; then
		error "Error: --is-ignored accepts only a single device ID"
	fi
	if ! check_dev_id "$BUSID" ERRBLO ; then
		error "Error: device ID '$BUSID': $ERRBLO"
	fi
	if is_blacklisted $BUSID ; then
		echo "Device $BUSID is ignored"
		exit 0
	else
		echo "Device $BUSID is not ignored"
		exit 2
	fi
}

# is_vm()
# Check if Linux is running on z/VM. Return 0 when on VM, 1 otherwise.
function is_vm()
{
	if grep 'z/VM' < "$SYSINFO" -q ; then
		return 0
	fi
	return 1
}

# get_console_busid(&busid)
# Return busid for VM console.
function get_console_busid()
{
	local DEVID

	if [ ! -d "$CONSDRV" ] ; then
		return
	fi
	DEVID=$(echo $CONSDRV/*.*.*)
	DEVID="${DEVID##*/}"
	if [ "$DEVID" != "*.*.*" ] ; then
		eval "$1=$DEVID"
	fi
}

# check_vm_console()
# Print a warning if VM console is on blacklist.
function check_vm_console()
{
	local CONSBUSID

	if ! is_vm ; then
		return
	fi
	get_console_busid CONSBUSID
	if [ -z "$CONSBUSID" ] ; then
		return
	fi
	if is_blacklisted $CONSBUSID ; then
		warn "Warning: reboot might fail due to blacklisted console $CONSBUSID"
	fi
}

# kernel_param()
# Print blacklist in kernel parameter format.
function kernel_param()
{
	local BLACKLISTED=$(list_blacklisted 0)
	local NOTBLACKLISTED=$(list_not_blacklisted 0)

	echo -n 'cio_ignore='
	# Determine shorter list
	if [ ${#BLACKLISTED} -le ${#NOTBLACKLISTED} ] ; then
		# Use blacklist
		to_param 0 "$BLACKLISTED"
	else
		# Use negative blacklist
		to_param 1 "$NOTBLACKLISTED"
	fi
	echo
	check_vm_console
}

# create_from_unused()
# Create blacklist from unused devices as determined by lscss.
function create_from_unused()
{
	local DEVID
	local UNUSED

	add_all_devices
	$LSCSS 2>/dev/null | grep yes | while read DEVID UNUSED ; do
		remove_device $DEVID
	done
}

# purge()
# Perform blacklist purge function.
function purge()
{
	blacklist_write 'purge' || \
		error "Error: purge function not accepted by kernel"
}

# Check for blacklist
if [ ! -f "$BLACKLIST" ] ; then
	error "Error: file $BLACKLIST not found"
fi

# Check for zero options
if [ $# -eq 0 ] ; then
	warn 'Need one of options -a, -A, -r, -R, -l, -L, -i, -k, -u or -p'
	print_usage_tip
	exit 1
fi

# Parse command line options
while [ $# -gt 0 ] ; do
	case $1 in
	-h|--help)
		print_help
		exit 0
		;;
	-v|--version)
		print_version
		exit 0
		;;
	-a|--add)
		shift
		add_device $1
		;;
	-A|--add-all)
		add_all_devices
		;;
	-r|--remove)
		shift
		remove_device $1
		WAIT_FOR_CIO=1
		;;
	-R|--remove-all)
		remove_all_devices
		WAIT_FOR_CIO=1
		;;
	-l|--list)
		list_blacklisted 1
		;;
	-L|--list-not-blacklisted)
		list_not_blacklisted 1
		;;
	-i|--is-ignored)
		shift
		is_blacklisted_opt $1
		;;
	-k|--kernel-param)
		kernel_param
		;;
	-u|--unused)
		create_from_unused
		;;
	-p|--purge)
		purge
		WAIT_FOR_CIO=1
		;;
	*)
		warn "invalid option '$1'"
		print_usage_tip
		exit 1
		;;
	esac
	shift
done

if [ \( -w $CIO_SETTLE \) -a $WAIT_FOR_CIO = 1 ] ; then
	echo 1 > $CIO_SETTLE
fi

exit 0
