#!/bin/bash
#
# lsznet.raw - Tool to list sensible network device hardware setups
#
# This script is not intended to be used as standalone tool, but should be
# used from other tools like a library function. E.g. znetconf is one exploiter
# of this script.
#
# Copyright IBM Corp. 2008, 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.
#

readonly SYSFS=/sys
# DEBUG=0 turns off debugging. >=1 means increasing debugging.
readonly DEBUG=0

# nothing to be changed below here

readonly CMD=${0##*/}

function error() {
	echo "$CMD: ERROR: $*" 1>&2
	exit 1
}

# The arrays (among other things) should be adapted, if any of those device
# drivers start supporting different CU types/models.

# $CU_CARDTYPE array is the only one which may contain entries with spaces
readonly -a CU_CARDTYPE=(
	"OSA (QDIO)"
	"HiperSockets"
	"CTC adapter"
	"escon channel"
	"ficon channel"
	"OSX"
	"OSM"
)

readonly -a CU_DEVNAME=(
	eth
	hsi
	ctc
	ctc
	ctc
	eth
	eth
)

readonly -a CU_DEVDRV=(
	qeth
	qeth
	ctcm
	ctcm
	ctcm
	qeth
	qeth
)

readonly -a CU_GROUPCHANNELS=(
	3
	3
	2
	2
	2
	3
	3
)

readonly -a CHPIDTYPES=(
	[0x11]=OSD
	[0x24]=IQD
	[0x30]=OSX
	[0x31]=OSM
)
#   [0x15]=OSN is no longer supported

# whitelist of network devices for TCP/IP stack, e.g. for Linux installers
readonly -a CU_TCPIP=(
	1731/01
	1731/05
	3088/08
	3088/1f
	3088/1e
	1731/02
	1731/02
)
#   1731/06 (OSN) is no longer supported

readonly PREFIXFORMAT=[[:xdigit:]]*
readonly SSIDFORMAT=[0-3]
readonly BUSIDFORMAT=[[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]]
readonly IDFORMAT=$PREFIXFORMAT.$SSIDFORMAT.$BUSIDFORMAT
readonly SUBCHANNEL_TYPE_IO=0
declare CU_IDX

function debug() {
	level=$1
	shift
	[ $DEBUG -ge $level ] && echo "$*" 1>&2
}

# Searches for a match of argument 1 on the array $CU_TCPIP and sets $CU_IDX to
# the matched array index on success.
# Returns 0 on success, 1 on failure.
function search_cu_tcpip() {
	local scu=$1
	local i
	for ((i = 0; i < ${#CU_TCPIP[@]}; i++)); do
		if [ "$scu" == "${CU_TCPIP[i]}" ]; then
			CU_IDX=$i
			return 0
		fi
	done
	return 1
}

# Returns symbolic name of CHPID type in $chpidtype_symbolic,
# if an entry in the array $CHPIDTYPES has been found at index of argument 1.
# Returns "?" otherwise.
# Always succeeds and returns 0.
function search_chpt() {
	local chpidtype_number=$1
	chpidtype_symbolic=${CHPIDTYPES[$((0x$chpidtype_number))]}
	if [ "$chpidtype_symbolic" == "" ]; then
		chpidtype_symbolic="?"
	fi
	return 0
}

# build_list:
#
# Prints list on standard output consisting of all subchannels and
# ccwdevices whose control unit type/model match supported network
# device types on s390.  Each matching entry is accompanied with
# (almost all) corresponding attributes.
#
function build_list() {
	# use /sys/devices/css*/ for startpath
	readonly STARTPATH=$SYSFS/devices
	# change to base directory so path globbing length with find is minimal
	cd $STARTPATH
	# fail out gracefully, if there is not expected sysfs environment
	# (could even fail out near the top, if $(uname -m) != s390x)
	csses=css$PREFIXFORMAT
	for d in $csses; do
		[ -d $d ] || exit
	done
	find $csses -name "$IDFORMAT" |
	while read dir; do
		debug 6 " examining sysfs directory $dir"
		# must not use $...FORMAT (file globs) here since this is a regex:
		EXPR="^css([[:xdigit:]]+)/([[:xdigit:]]+.[0-3].[[:xdigit:]]{4})/([[:xdigit:]]+.[0-3].[[:xdigit:]]{4})$"
		[[ "$dir" =~ $EXPR ]]
		case $? in
		0)
			# string matched the pattern
			debug 6 " ${BASH_REMATCH[@]}"
			prefix=${BASH_REMATCH[1]}
			subch=${BASH_REMATCH[2]}
			devbusid=${BASH_REMATCH[3]}
			subch_p=css$prefix/$subch
			dev_p=$subch_p/$devbusid
			debug 6 " $subch_p $dev_p"
			;;
		1)
			# string did not match the pattern
			continue
			;;
		2)
			error "syntax error in regex of match operator =~, code needs to be fixed"
			;;
		*)
			error "unexpected return code of regex match operator =~, code needs to be fixed"
			;;
		esac
		debug 5 " sysfs directory matched regex $dir"
		# skip non-I/O-subchannels, i.e. chsc and message subchannels
		if [ -f $subch_p/type ]; then
			read type <$subch_p/type
			if [ $type != $SUBCHANNEL_TYPE_IO ]; then
				debug 3 " skip non-I/O subchannel"
				continue
			fi
		fi
		# get subchannel information...
		# ATTENTION: hex values from sysfs are WITHOUT leading 0x prefix!
		read chpid_list <$subch_p/chpids
		read -a chpids <<<"$chpid_list"
		if [ ${#chpids[@]} -ne 8 ]; then
			error "sysfs reported ${#chpids[@]} CHPIDs instead of expected 8"
		fi
		read pim pam pom foo <$subch_p/pimpampom
		pimchpidZ=""
		local chp
		for ((chp = 0; chp < 8; chp++)); do
			mask=$((0x80 >> chp))
			if ((0x$pim & $mask)); then
				pimchpidZ=${pimchpidZ}${chpids[chp]}
			else
				pimchpidZ=${pimchpidZ}"ZZ"
			fi
		done
		# get device information...
		read cutype <$dev_p/cutype
		read active <$dev_p/online
		# skip already active subchannels and those that are already in a
		# ccwgroup and thus not available any more:
		[ $active == "1" ] && continue
		[ -h $dev_p/group_device ] && continue
		# get chpid information...
		pimchpids=${pimchpidZ//ZZ/}
		[ $pimchpids == "" ] && continue
		# Taking the first 2 hex digits as CHPID relies somewhat on the fact
		# that network adaptors don't use multipathing and only have one CHP.
		# Anyway it's OK since we're only interested in CHPID type and I guess
		# this should be equal for all possible multipaths to the same device.
		chpid=${pimchpids:0:2}
		chpid_p=css$prefix/chp$prefix.$chpid
		read chptype <$chpid_p/type
		# filter and output...
		if [ -z "$all" ] && ! search_cu_tcpip $cutype; then
			continue
		fi
		if search_cu_tcpip $cutype; then
			if [ "${CU_DEVDRV[$CU_IDX]}" == "ctcm" ]; then
				# assume CTC are mostly virtual and ignore chpid from sysfs
				chpidtype_symbolic="-"
			else
				search_chpt $chptype
			fi
			echo $pimchpids $devbusid $cutype $chpidtype_symbolic ${CU_DEVDRV[$CU_IDX]} ${CU_DEVNAME[$cu_idx]} ${CU_GROUPCHANNELS[$cu_idx]} ${CU_CARDTYPE[$cu_idx]}
		else
			debug 5 " skip non-network device $devbusid CU $cutype"
		fi
	done
}

# search_groups:
#
# Prints enumeration list on standard output consisting of possible
# hardware configurations (ccwgroups) for network devices on s390.
# Each configuration suggestion includes corresponding attributes
# that are of potential interest for the user and fit in a fixed column
# table on an 80 column screen.
#
# PRECONDITION: Standard input has to be stably sorted by device bus IDs and
# then by CHPIDs, i.e. grouped by CHPIDs.
#
function search_groups() {
	local w_prefix w_ssid w_devno
	local d_prefix d_ssid d_devno
	local prefix ssid devno x
	local chp devbusid cutype chpidtypename devdrv devname groupchs cardtype
	# remembered last state variables for possible ccwgroup:
	local r_prefix="Z"
	local r_ssid="Z"
	local r_devno="ZZZZ"
	local r_chp="ZZ"
	local r_cutype="ZZZZ/ZZ"
	local count=0
	local item=1
	local skipped=0
	while read chp devbusid cutype chpidtypename devdrv devname groupchs cardtype; do
		debug 1 " # $chp $devbusid $cutype $chpidtypename $devdrv $devname $groupchs $cardtype"
		IFS=.
		read prefix ssid devno x <<<"$devbusid"
		unset IFS
		if [ $r_chp != $chp \
			-o $r_prefix != $prefix \
			-o $r_ssid != $ssid \
			-o $r_cutype != $cutype ]; then
			# restart with new read channel info and remember it
			r_prefix=$prefix
			r_ssid=$ssid
			r_devno=$devno
			r_chp=$chp
			r_cutype=$cutype
			count=1
			debug 2 " INFO: restart on different CHPID or prefix or CUtype/model"
			continue
		fi
		count=$((count + 1))
		if [ $count -eq 2 ]; then
			# about to check if write channel is one above read channel
			if [ $((0x$devno)) -ne $((0x$r_devno + 1)) ]; then
				# start with new read channel info
				r_prefix=$prefix
				r_ssid=$ssid
				r_devno=$devno
				r_chp=$chp
				r_cutype=$cutype
				count=1
				skipped=$((skipped + 1))
				# unimplemented possible packed channel usage option:
				# remember unused channels for later use as data channel
				debug 2 " INFO: restart on unmatching read channel"
				continue
			fi
			w_prefix=$prefix
			w_ssid=$ssid
			w_devno=$devno
		elif [ $count -eq 3 ]; then
			# remember data channel info
			d_prefix=$prefix
			d_ssid=$ssid
			d_devno=$devno
		fi
		debug 2 " INFO: groupchs=$groupchs count=$count"
		if [ $count -ne $groupchs ]; then
			debug 2 " INFO: skip"
			continue
		fi
		# found possible ccwgroup
		case $count in
		2)
			chlist=$r_prefix.$r_ssid.$r_devno,$w_prefix.$w_ssid.$w_devno
			;;
		3)
			chlist=$r_prefix.$r_ssid.$r_devno,$w_prefix.$w_ssid.$w_devno,$d_prefix.$d_ssid.$d_devno
			;;
		*)
			error "unknown number of channels for group, code needs to be fixed"
			;;
		esac
		echo $item $cutype $chp $chpidtypename $devdrv $devname $chlist "$cardtype"
		item=$((item + 1))
		# restart after successful detection
		r_prefix="Z"
		count=0
	done
	debug 1 " STATISTIC: skipped $skipped devnos because of unmatching read channel"
}

if [ $# == 1 -a "$1" == "-a" ]; then
	all=1
fi

build_list |
# stable sort by device bus IDs and then by CHPIDs => grouped by CHPIDs
# (sorting only works since keys are fixed no. of digits with leading zeros!)
sort -s -k 1,1 -k 2,2 |
#cat ; exit # move at desired line and uncomment to see intermediate output
search_groups
