#!/bin/bash
#
# lszfcp - Tool to display information about zfcp devices (adapters/ports/units)
#
# Copyright IBM Corp. 2006, 2023
#
# s390-tools is free software; you can redistribute it and/or modify
# it under the terms of the MIT license. See LICENSE for details.
#

SCRIPTNAME='lszfcp'
SYSFS=`cat /proc/mounts | grep -m1 sysfs | awk '{ print $2 }'`
FC_CLASS=false

# Command line parameters
VERBOSITY=0
SHOW_MODPARMS=0
SHOW_HOSTS=0
SHOW_PORTS=0
SHOW_DEVICES=0
SHOW_ATTRIBUTES=false
SHOW_MORE_ATTRS=0
SHOW_EXTENDED=false
unset PAR_BUSID PAR_WWPN PAR_LUN


##############################################################################

check_sysfs()
{
	if [ -z $SYSFS -o ! -d $SYSFS -o ! -r $SYSFS ]; then
		echo "Error: sysfs not available."
		exit 1
	fi
}

check_zfcp_support()
{
	if [ ! -e $SYSFS/bus/ccw/drivers/zfcp ]; then
		echo "Error: No zfcp support available."
		echo "Load the zfcp module or compile"
		echo "the kernel with zfcp support."
		exit 1
	fi
}

check_fcp_devs()
{
	local found=0

	if $FC_CLASS; then
		# theoretically this includes "defunct" (and thus online) devs
		ignore=`ls $SYSFS/class/fc_host/host* 2>&1`
	else
		ignore=`ls $SYSFS/devices/css0/[0-9]*/[0-9]*/host[0-9]* 2>&1`
	fi
	[ $? -eq 0 ] && found=1

	if [ $found -eq 0 ] && $SHOW_EXTENDED; then
		# nothing found yet so search for "defunct" FCP devices
		ignore=$(ls -dX $SYSFS/devices/css[0-9]*/defunct/[0-9]*/host[0-9]* 2>&1)
		[ $? -eq 0 ] && found=1
	fi
	if [ $found -eq 0 ] && $SHOW_EXTENDED; then
		local mypath mylist=""
		# nothing found yet so search for FCP devices never been online
		for mypath in $SYSFS/bus/ccw/drivers/zfcp/*.*.*; do
			if [ "$mypath" = "$SYSFS/bus/ccw/drivers/zfcp/*.*.*" ];
			then
				break # glob did not match anything
			fi
			[ -d $mypath/host[0-9]* ] && continue # is/was online
			mylist="$mylist $(readlink -e $mypath)/-"
		done
		[ -n "$mylist" ] && found=1
	fi

	if [ $found -eq 0 ]; then
		echo "Error: No fcp devices found."
		exit 1
	fi
}

check_fc_class()
{
	if [ -d "$SYSFS/class/fc_host" ]; then
		FC_CLASS=true
	fi
}

print_version()
{
	cat <<EOF
$SCRIPTNAME: version %S390_TOOLS_VERSION%
Copyright IBM Corp. 2006, 2019
EOF
}

print_help()
{
	cat <<EOF
Usage: $SCRIPTNAME [OPTIONS]

Provide information contained in sysfs about zfcp adapters, ports and
units that are online.

Mandatory arguments to long options are mandatory for short options too.

OPTIONS:
    -H, --hosts		show host information (default)
    -P, --ports		show remote port information
    -D, --devices	show SCSI device information
    -Z, --modparms	show zfcp (module) parameters
    -b, --busid=BUSID	select specific busid
    -p, --wwpn=WWPN	select specific port name
    -l, --lun=LUN	select specific LUN
    -a, --attributes	show attributes
    -m, --moreattrs	show more attributes
			(host: css; port: zfcp_port; SCSI device: zfcp_unit)
			specify twice to show more attributes for SCSI devices
			(scsi_disk,block,integrity,queue,iosched)
    -V, --verbose	show sysfs paths of associated class
			and bus devices
    -e, --extended	extended output format
    -s, --sysfs=PATH	use path as sysfs (for dbginfo archives)
    -h, --help		print this help
    -v, --version	print version information

EXAMPLE:
    List for all zfcp adapters, ports and units the names of their
    associated SCSI hosts, FC remote ports and SCSI devices.

    #> lszfcp -P -H -D
    0.0.3d0c host0
    0.0.3d0c/0x500507630300c562 rport-0:0-0
    0.0.3d0c/0x500507630300c562/0x4010403300000000 0:0:0:0

The default is to list bus_ids of all zfcp adapters and corresponding
SCSI host names (equals "lszfcp -H").
EOF
}


show_attributes()
{
	if [ -z $1 -o ! -d $1 -o ! -r $1 ]; then
		return 1
	fi

	for FILE in `ls $1`; do
		read 2>/dev/null CONTENT < $1/$FILE

		# Read fails for directories and
		# files with permissions 0200.
		if [ $? -ne 0 ]; then
			continue
		fi

		printf "    %-19s = \"%s\"\n" "$FILE" "$CONTENT"
	done
}


show_modparms()
{
	local modparmdir=$SYSFS/module/zfcp/parameters

	[ -d "$modparmdir" ] || return

	if [ $VERBOSITY -ne 0 ]; then
		echo "$modparmdir"
	fi

	echo 'Module = "zfcp"'
	show_attributes "$modparmdir"
	echo
}


show_hosts()
{
	HOST_LIST=`ls -dX $SYSFS/devices/css0/[0-9]*/[0-9]*/host[0-9]* \
			2> /dev/null`

	if $SHOW_EXTENDED; then
		# add all "defunct" FCP devices
		HOST_LIST="$HOST_LIST
$(ls -dX $SYSFS/devices/css[0-9]*/defunct/[0-9]*/host[0-9]* 2>/dev/null)"
		# add all FCP devices that have never been online
		for HOST_PATH in $SYSFS/bus/ccw/drivers/zfcp/*.*.*; do
			[ -d $HOST_PATH/host[0-9]* ] && continue # is/was online
			HOST_LIST="$HOST_LIST
$(readlink -e $HOST_PATH)/-"
		done
	fi

	for HOST_PATH in $HOST_LIST; do
		SCSI_HOST=`basename $HOST_PATH`
		ADAPTER_PATH=`dirname $HOST_PATH`
		ADAPTER=`basename $ADAPTER_PATH`

		[ $ADAPTER != ${PAR_BUSID:-$ADAPTER} ] && continue

		read ONLINE < $ADAPTER_PATH/online
		read AVAILABILITY < $ADAPTER_PATH/availability
		[ -r "$SYSFS/class/fc_host/$SCSI_HOST/port_state" ] && read PORT_STATE < "$SYSFS/class/fc_host/$SCSI_HOST/port_state"
		[ -r $ADAPTER_PATH/failed ] && read FAILED < $ADAPTER_PATH/failed
		DEFUNCT=${ADAPTER_PATH%/*} # strip devbusid
		DEFUNCT=${DEFUNCT##*/}     # basename
		HOSTMARKER=""
		if $SHOW_EXTENDED; then
			if [ "$DEFUNCT" = "defunct" ]; then
				HOSTMARKER=" defunct"
			elif [ "$ONLINE" = "0" ]; then
				HOSTMARKER=" offline"
			elif [ "$PORT_STATE" = "Linkdown" ]; then
				HOSTMARKER=" linkdown"
			elif [ "$FAILED" = "1" ]; then
				HOSTMARKER=" failed"
			elif [ "$AVAILABILITY" != "good" ]; then
				HOSTMARKER=" NotAvailable"
			elif [ "$PORT_STATE" != "Online" ]; then
				HOSTMARKER=" NotOnline"
			fi
		fi
		if [ $VERBOSITY -eq 0 ]; then
			echo $ADAPTER $SCSI_HOST$HOSTMARKER
		else
			echo $ADAPTER_PATH$HOSTMARKER
			if [ "$SCSI_HOST" != "-" ]; then
				$FC_CLASS && echo "$SYSFS/class/fc_host/$SCSI_HOST"
				echo "$SYSFS/class/scsi_host/$SCSI_HOST"
			else
				echo "-"
				echo "-"
			fi
		fi

		if $SHOW_ATTRIBUTES; then
			if [ $SHOW_MORE_ATTRS -ge 1 ]; then
				echo 'Bus = "css"'
				if [ "$DEFUNCT" = "defunct" ]; then
					printf "    %-19s\n" "defunct"
				else
					show_attributes ${ADAPTER_PATH%/*}
				fi
			fi

			echo 'Bus = "ccw"'
			show_attributes $ADAPTER_PATH

			if [ "$SCSI_HOST" = "-" ]; then
				# skip output of non-existent fc_host & scsi_host
				echo
				continue
			fi

			if $FC_CLASS; then
				echo 'Class = "fc_host"'
				show_attributes \
					"$SYSFS/class/fc_host/$SCSI_HOST"
			fi

			echo 'Class = "scsi_host"'
			show_attributes "$SYSFS/class/scsi_host/$SCSI_HOST"
			echo
		fi
	done
}


show_ports()
{
	# Without fc_remote_class there is far less information to display.
	if ! $FC_CLASS; then
		PORT_LIST=`ls -d $SYSFS/devices/css0/*/*/0x*`

		for PORT_PATH in $PORT_LIST; do
			WWPN=`basename $PORT_PATH`
			ADAPTER=`basename \`dirname $PORT_PATH\``

			[ $WWPN != ${PAR_WWPN:-$WWPN} ] && continue
			[ $ADAPTER != ${PAR_BUSID:-$ADAPTER} ] && continue

			if [ $VERBOSITY -eq 0 ]; then
				echo "$ADAPTER/$WWPN"
			else
				echo $PORT_PATH
			fi
		done
		return
	fi


	if [ -e $SYSFS/class/fc_remote_ports/ ]; then
		PORT_LIST=`ls -d $SYSFS/class/fc_remote_ports/* 2>/dev/null`
	fi;

	for FC_PORT_PATH in $PORT_LIST; do
		PORT=`basename $FC_PORT_PATH`
		read PORT_STATE < $FC_PORT_PATH/port_state
		if [ "$PORT_STATE" == "Online" ] || $SHOW_EXTENDED; then
			read WWPN < $FC_PORT_PATH/port_name
		else
			continue
		fi
		PORTSTATEMARKER=""
		$SHOW_EXTENDED && [ "$PORT_STATE" != "Online" ] \
			&& PORTSTATEMARKER=" NotOnline"

		[ $WWPN != ${PAR_WWPN:-$WWPN} ] && continue

		local sysreal=$(readlink -e "$FC_PORT_PATH")
		local ADAPTER=""
		while [ -n "$sysreal" ]; do
			# ascend to parent: strip last path part
			sysreal=${sysreal%/*}
			[ -h $sysreal/subsystem ] || continue
			local subsystem=$(readlink -e $sysreal/subsystem)
			if [ "${subsystem##*/}" = "ccw" ]; then
				ADAPTER=${sysreal##*/}
				break
			fi
		done
		[ -z "$ADAPTER" ] && continue # skip not zfcp-attached ones
		if [ -d $SYSFS/devices/css[0-9]*/[0-9d]*/[0-9]*/$WWPN/../host[0-9]*/$PORT ];
		then
			ADAPTER_PORT_PATH=$SYSFS/devices/css[0-9]*/[0-9d]*/$ADAPTER/$WWPN
		else
			ADAPTER_PORT_PATH="-"
		fi

		[ $ADAPTER != ${PAR_BUSID:-$ADAPTER} ] && continue

		if [ $VERBOSITY -eq 0 ]; then
			echo "$ADAPTER/$WWPN $PORT$PORTSTATEMARKER"
		else
			if [ "$ADAPTER_PORT_PATH" != "-" ]; then
				echo $ADAPTER_PORT_PATH
			else
				echo "- (NoMoreZfcpPort:$ADAPTER/$WWPN)"
			fi
			echo $FC_PORT_PATH$PORTSTATEMARKER
		fi

		if $SHOW_ATTRIBUTES; then
			if [ $SHOW_MORE_ATTRS -ge 1 ] \
				&& [ "$ADAPTER_PORT_PATH" != "-" ] \
				&& [ -d $ADAPTER_PORT_PATH ];
			then
				# fc_rport can exist without zfcp_port
				# (e.g. after port_remove)
				echo 'Class = "zfcp_port"'
				show_attributes "$ADAPTER_PORT_PATH"
			fi
			echo 'Class = "fc_remote_ports"'
			show_attributes "$FC_PORT_PATH"
			echo
		fi
	done
}


show_devices()
{
	# Differentiate old and new sysfs layout
	if $FC_CLASS; then
		SCSI_DEVICE_LIST=`ls -d \
			$SYSFS/bus/ccw/drivers/zfcp/*/host*/rport*/target*/*:*:*:* \
			2>/dev/null`
	else
		SCSI_DEVICE_LIST=`ls -d $SYSFS/devices/css0/*/*/host[0-9]*/*/`
	fi

	declare -A ZFCP_UNIT_DICT
	if $SHOW_EXTENDED; then
		ZFCP_UNITS=false
		for UNIT_PATH in "$SYSFS"/devices/css[0-9]*/[0-9d]*/[0-9]*/0x*/0x*; do
			if [ "$UNIT_PATH" = "$SYSFS/devices/css[0-9]*/[0-9d]*/[0-9]*/0x*/0x*" ]; then
				break # no match (as seen without nullglob)
			else
				ZFCP_UNITS=true
			fi
			STRIPPED_PATH=${UNIT_PATH}
			L=${UNIT_PATH##*/}
			STRIPPED_PATH=${STRIPPED_PATH%/*}
			W=${STRIPPED_PATH##*/}
			STRIPPED_PATH=${STRIPPED_PATH%/*}
			A=${STRIPPED_PATH##*/}
			ZFCP_UNIT_DICT["$A/$W/$L"]=${UNIT_PATH}
		done
	else
		ZFCP_UNITS=false
	fi

	if [ -z "$SCSI_DEVICE_LIST" ] && ! "$ZFCP_UNITS"; then
		if $SHOW_EXTENDED; then
			echo "Error: No zfcp-attached SCSI devices found."
		else
			echo "Error: No fcp devices found."
		fi
	fi

	for SCSI_DEVICE_PATH in $SCSI_DEVICE_LIST; do
		read ADAPTER < $SCSI_DEVICE_PATH/hba_id
		read WWPN < $SCSI_DEVICE_PATH/wwpn
		read LUN < $SCSI_DEVICE_PATH/fcp_lun

		ZFCP_UNIT_PATH="${ZFCP_UNIT_DICT[$ADAPTER/$WWPN/$LUN]}"
		# remove from ZFCP_UNIT_DICT if SCSI device exists
		unset "ZFCP_UNIT_DICT[$ADAPTER/$WWPN/$LUN]"

		[ $LUN != ${PAR_LUN:-$LUN} ] && continue
		[ $WWPN != ${PAR_WWPN:-$WWPN} ] && continue
		[ $ADAPTER != ${PAR_BUSID:-$ADAPTER} ] && continue

		SDEVMARKER=""
		if $SHOW_EXTENDED; then
			[ -n "$ZFCP_UNIT_PATH" ] || SDEVMARKER="$SDEVMARKER auto"
			read SDEVSTATE < $SCSI_DEVICE_PATH/state
			[ "$SDEVSTATE" != "running" ] \
				&& SDEVMARKER="$SDEVMARKER NotRunning"
		else
			# build manually because ZFCP_UNIT_DICT is not used
			ZFCP_UNIT_PATH=$SYSFS/devices/css[0-9]*/[0-9d]*/$ADAPTER/$WWPN/$LUN
		fi

		if [ $VERBOSITY -eq 0 ]; then
			echo "$ADAPTER/$WWPN/$LUN ${SCSI_DEVICE_PATH##*/}$SDEVMARKER"
		else
			echo "`ls -d $SYSFS/devices/css0/[0-9d]*/$ADAPTER`/$WWPN/$LUN"
			echo ${SCSI_DEVICE_PATH%*/}$SDEVMARKER # without trailing slash

			# On live systems, there are links to the block and
			# generic devices. In a dbginfo archive, these links
			# are not present.  Therefore, fall back to reading
			# the runtime.out log file.
			if [ `ls $SCSI_DEVICE_PATH | grep -c block:` -eq 1 ]
			then
				BLOCK_DEV=`ls $SCSI_DEVICE_PATH | grep block:`
				GEN_DEV=`ls $SCSI_DEVICE_PATH |\
					grep scsi_generic:`
				echo -n "$SYSFS/block/${BLOCK_DEV#*:} "
				echo "$SYSFS/class/scsi_generic/${GEN_DEV#*:}"

			elif [ -d $SCSI_DEVICE_PATH/block ] && $SHOW_EXTENDED
			then
				# case without CONFIG_SYSFS_DEPRECATED
				BLOCK_DEV=$(echo $SCSI_DEVICE_PATH/block/*)
				BLOCK_DEV=${BLOCK_DEV##*/}
				echo -n "$SYSFS/block/$BLOCK_DEV "
				local SGPATH=$SCSI_DEVICE_PATH/scsi_generic
				if [ -d $SGPATH ]; then
					GEN_DEV=$(echo $SGPATH/*)
					GEN_DEV=${GEN_DEV##*/}
					echo "$SYSFS/class/scsi_generic/$GEN_DEV"
				else
					echo "-"
				fi

			# FIXME Find a way to assign the generic devices.
			elif [ -r $SYSFS/../runtime.out ]; then
				SCSI_DEV=`basename $SCSI_DEVICE_PATH`
				echo "$SYSFS/block/"`grep -r "\[$SCSI_DEV\]"\
					$SYSFS/../runtime.out |\
					awk -F "/dev/" '{print $2}'`
			fi
		fi

		if $SHOW_ATTRIBUTES && [ $SHOW_MORE_ATTRS -ge 1 ]; then
			# auto scan LUNs not necessarily have a zfcp_unit
			if [ -n "$ZFCP_UNIT_PATH" ] && [ -d $ZFCP_UNIT_PATH ]; then
				echo 'Class = "zfcp_unit"'
				show_attributes "$ZFCP_UNIT_PATH"
			fi
		fi
		if $SHOW_ATTRIBUTES; then
			echo 'Class = "scsi_device"'
			show_attributes "$SCSI_DEVICE_PATH"
		fi
		if $SHOW_ATTRIBUTES && [ $SHOW_MORE_ATTRS -ge 2 ]; then
			if [ -d $SCSI_DEVICE_PATH/scsi_disk ]; then
				echo 'Class = "scsi_disk"'
				show_attributes "$SCSI_DEVICE_PATH/scsi_disk/$(basename $SCSI_DEVICE_PATH)"
			fi
			if [ -d $SCSI_DEVICE_PATH/block ]; then
				echo 'Class = "block"'
				show_attributes "$SCSI_DEVICE_PATH/block/*/"
			fi
			if [ -d $SCSI_DEVICE_PATH/block/*/integrity ]; then
				echo 'Class = "block_integrity"'
				show_attributes "$SCSI_DEVICE_PATH/block/*/integrity"
			fi
			if [ -d $SCSI_DEVICE_PATH/block/*/queue ]; then
				echo 'Class = "block_queue"'
				show_attributes "$SCSI_DEVICE_PATH/block/*/queue"
			fi
			if [ -d $SCSI_DEVICE_PATH/block/*/queue/iosched ]; then
				echo 'Class = "block_queue_iosched"'
				show_attributes "$SCSI_DEVICE_PATH/block/*/queue/iosched"
			fi
		fi
		if $SHOW_ATTRIBUTES; then
			echo
		fi
	done

	# what's left in ZFCP_UNIT_DICT are now units without SCSI device
	for UNIT_PATH in "${ZFCP_UNIT_DICT[@]}"; do
		STRIPPED_PATH=$UNIT_PATH
		LUN=${UNIT_PATH##*/}
		STRIPPED_PATH=${STRIPPED_PATH%/*}
		WWPN=${STRIPPED_PATH##*/}
		STRIPPED_PATH=${STRIPPED_PATH%/*}
		ADAPTER=${STRIPPED_PATH##*/}

		[ $LUN != ${PAR_LUN:-$LUN} ] && continue
		[ $WWPN != ${PAR_WWPN:-$WWPN} ] && continue
		[ $ADAPTER != ${PAR_BUSID:-$ADAPTER} ] && continue

		if [ $VERBOSITY -eq 0 ]; then
			echo "$ADAPTER/$WWPN/$LUN - failed"
		else
			echo "$UNIT_PATH failed"
			echo "-"
			echo "- -"
		fi

		if $SHOW_ATTRIBUTES && [ $SHOW_MORE_ATTRS -ge 1 ]; then
			echo 'Class = "zfcp_unit"'
			show_attributes "$UNIT_PATH"
		fi

		if $SHOW_ATTRIBUTES; then
			echo
		fi
	done
}


##############################################################################

ARGS=`getopt --options ahvHPDVb:p:l:s:emZ --longoptions \
attributes,help,version,hosts,ports,devices,verbose,busid:,wwpn:,lun:,sysfs:,extended,moreattrs,modparms \
-n "$SCRIPTNAME" -- "$@"`

if [ $? -ne 0 ]; then
	echo
	print_help
	exit
fi

eval set -- "$ARGS"

for ARG; do
	case "$ARG" in
		-a|--attributes) SHOW_ATTRIBUTES=true; 	shift 1;;
		-m|--moreattrs)	 ((SHOW_MORE_ATTRS++));	shift 1;;
		-e|--extended)	 SHOW_EXTENDED=true;	shift 1;;
		-b|--busid) 	 PAR_BUSID=$2;		shift 2;;
		-h|--help) 	 print_help;		exit 0;;
		-l|--lun) 	 PAR_LUN=$2;		shift 2;;
		-p|--wwpn) 	 PAR_WWPN=$2;		shift 2;;
		-v|--version) 	 print_version;		exit 0;;
		-H|--hosts) 	 SHOW_HOSTS=1;		shift 1;;
		-D|--devices) 	 SHOW_DEVICES=1;	shift 1;;
		-P|--ports) 	 SHOW_PORTS=1;		shift 1;;
		-V|--verbose) 	 VERBOSITY=1;		shift 1;;
		-Z|--modparms) 	 SHOW_MODPARMS=1;	shift 1;;
		-s|--sysfs)	 SYSFS=$2;		shift 2;;
		--) shift; break;;
	esac
done

check_sysfs
check_zfcp_support
check_fc_class
check_fcp_devs

default=1
if [ $SHOW_MODPARMS -eq 1 ]; then
	default=0; show_modparms
fi

if [ $SHOW_HOSTS -eq 1 ]; then
	default=0; show_hosts
elif [ $SHOW_PORTS -eq 0 -a $SHOW_DEVICES -eq 0 -a -n "$PAR_BUSID" ]; then
	default=0; show_hosts
fi

if [ $SHOW_PORTS -eq 1 ]; then
	default=0; show_ports
elif [ $SHOW_HOSTS -eq 0 -a $SHOW_DEVICES -eq 0 -a -n "$PAR_WWPN" ]; then
	default=0; show_ports
fi

if [ $SHOW_DEVICES -eq 1 ]; then
	default=0; show_devices
elif [ $SHOW_HOSTS -eq 0 -a $SHOW_PORTS -eq 0 -a -n "$PAR_LUN" ]; then
	default=0; show_devices
fi

if [ $default == 1 ]; then
	show_hosts
fi
