#!/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;
}

# currently requires bash version 3.0 or later

. /lib/s390-tools/znetcontrolunits

# 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"
    "LCS OSA"
    "OSX"
    "OSM"
)

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

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

readonly -a CHPIDTYPES=(
    [0x10]=OSE
    [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
    3088/60
    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

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

# Searches for a match of argument 1 on the array $CU_TCPIP.
# 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
            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 $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
