#!/bin/bash
#
# znetconf - Tool to list and configure network devices
#
# Tool for the automatic and semi-automatic configuration of network devices
# from ccw devices, including network device option handling and network
# device removal.
#
# 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.
#

# 9 Could not group devices
readonly RC_COULD_NOT_GROUP_DEVICES=9
# 10 Could not set device online
readonly RC_COULD_NOT_SET_DEV_ONLINE=10
# 11 Could not set device offline
readonly RC_COULD_NOT_SET_DEV_OFFLINE=11
# 12 Invalid attribute value pair
readonly RC_INVALID_ATTRIBUTE_VALUE_PAIR=12
# 13 Missing component (broken installation)
readonly RC_BROKEN_INSTALLATION=13
# 15 Invalid device ID format
readonly RC_INVALID_DEVICE_ID_FORMAT=15
# 17 Unknown driver
readonly RC_UNKNOWN_DRIVER=17
# 19 Invalid argument
readonly RC_INVALID_ARGUMENT=19
# 20 Too much arguments
readonly RC_TOO_MUCH_ARGUMENTS=20
# 21 No configuration found for device ID
readonly RC_NO_CONFIG_FOUND_FOR_DEV=21
# 22 Device is not configured
readonly RC_DEVICE_NOT_CONFIGURED=22
# 23 Could not ungroup device
readonly RC_COULD_NOT_UNGROUP_DEVICE=23
# 24 At least one option could not be configured
readonly RC_OPTION_NOT_CONFIGURED=24
# 25 Missing value for attribute
readonly RC_MISSING_VALUE=25
# 26 Device does not exist
readonly RC_DEVICE_DOES_NOT_EXIST=26
# 27 Device already in use
readonly RC_DEVICE_ALREADY_IN_USE=27
# 28 Net device did not come online
readonly RC_NET_DEVICE_NOT_ONLINE=28
# 29 Some devices could not be added or failed
readonly RC_SOME_DEVICES_FAILED=29
# 30 Syntax error on command line, e.g. missing argument to option
readonly RC_SYNTAX_ERROR=30
# 31 There are no ccwgroup devices. Nothing to do.
readonly RC_NO_CCWGROUP=31
# 99 internal error, this should never happen
readonly RC_INTERNAL_ERROR=99

#==============================================================================
# constants
#==============================================================================

CMD=$(basename $0)
LSZNET=/lib/s390-tools/lsznet.raw
LSZNET_ARGS=-a
LSZNET_CALL="$LSZNET $LSZNET_ARGS"
UDEVSETTLE=udevadm
UDEVSETTLE_CALL="$UDEVSETTLE settle --timeout=10"
SYSFSDIR=$(cat /proc/mounts|awk '$3=="sysfs"{print $2; exit}')
CCWGROUPBUS_DIR=$SYSFSDIR/bus/ccwgroup
CCWDEV_DIR=$SYSFSDIR/bus/ccw/devices
CCWGROUPBUS_DEVICEDIR=$CCWGROUPBUS_DIR/devices
CCWGROUPBUS_DRIVERDIR=$CCWGROUPBUS_DIR/drivers
DEVNOSEP=","
readonly ERRCODE_NO_LAYER_DETECTED=255

FORMAT_FULLDEVNO=^[[:xdigit:]]\\.[[:xdigit:]]\\.[[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]]$
FORMAT_PARTDEVNO=^[[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]]$
FORMAT_DEVLIST=^[[:xdigit:]]\\.[[:xdigit:]]\\.[[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]],[[:xdigit:]]\\.[[:xdigit:]]\\.[[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]]\(,[[:xdigit:]]\\.[[:xdigit:]]\\.[[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]]\)?$
FORMAT_SHORTDEVLIST=^[[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]],[[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]]\(,[[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]]\)?$

#==============================================================================
# global variables
#==============================================================================

ATTRIBUTE_COUNT=0
ATTRIBUTE_NAME=()
ATTRIBUTE_VALUE=()
IS_VM_ENV=0

#==============================================================================
# functions
#==============================================================================

function print_error()
{
	local ERRMSG="$@"
	echo "$CMD: Error: $ERRMSG" >&2
}

#==============================================================================

function print_warning()
{
	local ERRMSG="$@"
	echo "$CMD: Warning: $ERRMSG" >&2
}

#==============================================================================

function check_vm_env()
{
	if cat /proc/sysinfo | grep VM00 &> /dev/null ; then
		if vmcp q cplevel &> /dev/null ; then
			IS_VM_ENV=1
		else
			if modprobe vmcp 2> /dev/null ; then
				IS_VM_ENV=1
			else
				IS_VM_ENV=0
			fi
		fi
	else
		IS_VM_ENV=0
	fi
}

#==============================================================================

function print_usage()
{
	cat <<-EOD
Usage: $CMD COMMANDS [OPTIONS]

  znetconf -a <device_bus_id>
  znetconf -A [-o ATTR=VALUE]+ [-d DRIVER] [-e <device_bus_id>]+
  znetconf -r <device_bus_id> | -R [-e <device_bus_id>]+
  znetconf -u | -c

List and configure network devices.

COMMANDS:
  -u, --unconfigured		List possible network devices
  -c, --configured		List configured network devices
  -a, --add <device_bus_id>[,..]Configure network device with the device bus-IDs
  -A, --add-all			Configure all possible network devices
  -r, --remove <device_bus_id>	Remove network device with given <device_bus_id>
  -R, --remove-all		Remove all network devices
  -h, --help			Print this help, then exit
  -v, --version			Print version infromation, then exit

OPTIONS:
  -o, --option ATTR=VALUE	Configure device with specified option
  -d, --driver DRIVER		Configure device with specified driver
  -n, --non-interactive		Answer all confirmation questions with 'yes'
  -e, --except			Do not add or remove the specified device
	EOD
}

#==============================================================================

function print_version()
{
	echo "$CMD: version %S390_TOOLS_VERSION%"
	echo "Copyright IBM Corp. 2009, 2017"
}

#==============================================================================

function print_short_usage()
{
	echo "znetconf: Need one of the options -u, -c, -a, -A, -r or -R"
	echo "Use 'znetconf --help' to get usage information"
}

#==============================================================================

function print_scanning_4_nwdevices()
{
	printf "%s\n" "Scanning for network devices..."
}

#==============================================================================

function lookup_vswitch_layer()
{
	local SWITCHNAME="$1"
	local LAYER2=$ERRCODE_NO_LAYER_DETECTED

	LAYER2=$(vmcp "q vswitch $SWITCHNAME" 2> /dev/null |
	{
		local FIELD1
		local FIELD2
		local FIELD3
		local REST
		local LAYER2=$ERRCODE_NO_LAYER_DETECTED
		local LINENO=1
		while read FIELD1 FIELD2 FIELD3 REST
		do
			if [ $LINENO -eq 2 ]
			then
				if [ "$FIELD3" == "ETHERNET" ]
				then
					LAYER2=1
				elif [ "$FIELD3" == "NONROUTER" ] ||
					[ "$FIELD3" == "PRIROUTER" ] ||
					[ "$FIELD3" == "IP" ]
				then
					LAYER2=0
				fi
			fi
			let "LINENO++"
		done
		echo $LAYER2
	})

	return $LAYER2
}

#==============================================================================

function lookup_lan_layer()
{
	local NAME="$1"
	local OWNER="$2"
	if [ "$NAME" != "*" ]
	then
		local LAYER2=$(vmcp "q lan $NAME owner $OWNER" 2> /dev/null |
		{
			local FIELD1
			local FIELD2
			local FIELD3
			local REST
			local LAYER2=$ERRCODE_NO_LAYER_DETECTED
			local LINENO=1
			while read FIELD1 FIELD2 FIELD3 REST
			do
				if [ $LINENO -eq 2 ]
				then
					if [ "$FIELD3" == "ETHERNET" ]
					then
						LAYER2=1
					elif [ "$FIELD3" == "IP" ]
					then
						LAYER2=0
					fi
				fi
				let "LINENO++"
			done
			echo $LAYER2
		})
		return $LAYER2
	fi
	return $ERRCODE_NO_LAYER_DETECTED
}

#==============================================================================

function prepare_udevsettle_cmd()
{
	# is the command available in $PATH
	if ! [ -x "$(command -v $UDEVSETTLE)" ]
	then
		# check the well known locations.
		if [ -e "/sbin/udevadm" ]
		then
			UDEVSETTLE=/sbin/udevadm
			UDEVSETTLE_CALL="$UDEVSETTLE settle --timeout=10"
		elif [ -e "/usr/bin/udevadm" ]
		then
			UDEVSETTLE=/usr/bin/udevadm
			UDEVSETTLE_CALL="$UDEVSETTLE settle --timeout=10"
		elif [ -e "/sbin/udevsettle" ]
		then
			# Fallback to udevsettle
			UDEVSETTLE=/sbin/udevsettle
			UDEVSETTLE_CALL="$UDEVSETTLE --timeout=10"
		else
			UDEVSETTLE=""
			UDEVSETTLE_CALL=""
			echo "Failed to find any candidate for udevsettle"
		fi
	fi
}

#==============================================================================

function lookup_type_and_lan_or_vswitch_name()
{
	local DEVNO="$1"
	vmcp "q v nic $DEVNO" 2> /dev/null |
	{
		local FIELD1
		local FIELD2
		local FIELD3
		local FIELD4
		local REST
		local TYPE="ERROR"
		local TARGET="ERROR"
		local NAME="ERROR"
		local OWNER="ERROR"
		local LINENO=1
		while read FIELD1 FIELD2 FIELD3 FIELD4 FIELD5 REST
		do
			if [ $LINENO -eq 1 ]
			then
				TYPE="$FIELD4"
			elif [ $LINENO -eq 2 ]
			then
				if [ "$FIELD3" == "LAN:" ]
				then
					TARGET="LAN"
					OWNER="$FIELD4"
					NAME="$FIELD5"
				elif [ "$FIELD3" == "VSWITCH:" ]
				then
					TARGET="VSWITCH"
					OWNER="$FIELD4"
					NAME="$FIELD5"
				fi
			fi
			let "LINENO++"
		done
		echo "$TYPE $TARGET $OWNER $NAME"
	}
}

#==============================================================================

function lookup_layer()
{
	local DEVNO=$1
	local LAYER2=$ERRCODE_NO_LAYER_DETECTED
	local QRESULT
	local VSWITCH
	local LAN

	# query VM for the nic
	QRESULT=$(lookup_type_and_lan_or_vswitch_name "$DEVNO")

	# derive type of nic
	local TYPE=${QRESULT%% *}
	local TARGET_OWNER=${QRESULT#* }
	TARGET_OWNER=${TARGET_OWNER% *}
	local TARGET=${TARGET_OWNER% *}
	local OWNER=${TARGET_OWNER#* }
	local NAME=${QRESULT##* }
	if [ "$TYPE" == "HIPERS" ]
	then
		# HIPERSOCKET in z/VM always indicates layer 3
		LAYER2=0
	elif [ "$TYPE" == "QDIO" ]
	then
		if  [ "$TARGET" == "VSWITCH" ]
		then
			lookup_vswitch_layer "$NAME"
			LAYER2=$?
		elif [ "$TARGET" == "LAN" ]
		then
			lookup_lan_layer "$NAME" "$OWNER"
			return $?
		fi
	fi

	return $LAYER2
}

#==============================================================================

#
# group_device
# 	$1: ccwdevid[;ccwdevid][;ccwdevid]
#	$2: ccwgroupdevid
#	$3: driver
# returns
#	0 success
#	RC_COULD_NOT_GROUP_DEVICES
#
function group_device()
{
	local DEVICES=$1
	local TARGET_DEVID=$2
	local DRIVER=$3
	local GROUPFILE=$CCWGROUPBUS_DRIVERDIR/$DRIVER/group
	local CTCMGROUPFILE=$CCWGROUPBUS_DRIVERDIR/ctcm/group
	local CTCGROUPFILE=$CCWGROUPBUS_DRIVERDIR/ctc/group

	# check if group file exists
	if [ ! -e $GROUPFILE ]
	then
		# try to load driver
		if ! modprobe $DRIVER &> /dev/null
		then
			if [ $DRIVER = "ctc" ]
			then
				# check if ctcm driver exists
				if modprobe ctcm &> /dev/null
				then
					GROUPFILE=$CTCMGROUPFILE
				fi
			elif [ $DRIVER = "ctcm" ]
			then
				# check if ctc driver exists
				if modprobe ctc &> /dev/null
				then
					GROUPFILE=$CTCGROUPFILE
				fi
			fi
		fi
	fi
	if [ -e $GROUPFILE ]
	then
		echo "$DEVICES" >> $GROUPFILE 2> /dev/null
		case $? in
			0)
				;;
			*)
				print_error "Could not group devices" \
					"$DEVICES"
				return $RC_COULD_NOT_GROUP_DEVICES
				;;
		esac
	else
		print_error "$GROUPFILE does not exist"

		return $RC_COULD_NOT_GROUP_DEVICES
	fi
	return 0
}

#==============================================================================

#
# ungroup_device
#	$1: ccwgroupdevid
#	$2: network device name (just for display purposes)
# returns
#	0 success
#	RC_COULD_NOT_UNGROUP_DEVICE
#
function ungroup_device()
{
	local TARGET_DEVID="$1"
	local DEVNAME="$2"
	local DEVICE_DIRECTORY="$CCWGROUPBUS_DEVICEDIR/$TARGET_DEVID"
	local DEVICE_UNGROUPFILE="$DEVICE_DIRECTORY/ungroup"
	if [ -e $DEVICE_UNGROUPFILE ]
	then
		echo "1" >> $DEVICE_UNGROUPFILE 2> /dev/null
		case $? in
			0)
				echo -n "Successfully removed device" \
					"$TARGET_DEVID"
				if [ "$DEVNAME" == "" ]
				then
					echo
				else
					echo " ($DEVNAME)"
				fi
				;;
			*)
				print_error "Failed to ungroup $TARGET_DEVID"
				return $RC_COULD_NOT_UNGROUP_DEVICE
				;;
		esac
	else
		print_error "$DEVICE_UNGROUPFILE does not exist."
		return $RC_COULD_NOT_UNGROUP_DEVICE
	fi
	return 0
}

#==============================================================================

#
# try_read_netdevname
# 	$1: ccwgroupdevno
# returns
#	0 success
#	1 failed. giving up after retries
# stdout
#	in case of success: device name
#
function try_read_netdevname()
{
	local CCWGROUPDEVNO="$1"
	local IF_NAME_FILE="$CCWGROUPBUS_DEVICEDIR/$CCWGROUPDEVNO/if_name"
	local NET_SUBDIR="$CCWGROUPBUS_DEVICEDIR/$CCWGROUPDEVNO/net/"
	local IF_NAME=""
	local rc=1

	# check if interface file containing the name exists
	if [ -e "$IF_NAME_FILE" ]
	then
		read IF_NAME < $IF_NAME_FILE
		rc=0
	elif [ -d "$NET_SUBDIR" ]
	then
		IF_NAME=$(ls $NET_SUBDIR)
	else
		# if the file does not exist
		local LINKNAME=$(find $CCWGROUPBUS_DEVICEDIR/$CCWGROUPDEVNO/ -type l -name net*)
		if [[ ! -z $LINKNAME ]]
		then
			IF_NAME=$(readlink $LINKNAME)
			IF_NAME=${IF_NAME##*/}
			rc=0
		fi
	fi
	echo $IF_NAME
	return $rc
}

#==============================================================================

#
# wait_for_net_device
# 	$1: ccwgroupdevno
# returns
#	0 success
#	1 failed. giving up after retries
#
function wait_for_net_device()
{
	local CCWGROUPDEVNO="$1"
	local retries=0
	local MAX_RETRIES=10
	local IF_NAME_FILE="$CCWGROUPBUS_DEVICEDIR/$CCWGROUPDEVNO/if_name"
	local IF_NAME=""
	local CMD_FINDNETLINK="find $CCWGROUPBUS_DEVICEDIR/$CCWGROUPDEVNO/ -type l -name net*"
	local LINKNAME=""

	prepare_udevsettle_cmd
	# polling loop to wait for net device to become available
	if [ "$UDEVSETTLE" != "" ]
	then
		$UDEVSETTLE_CALL
	fi
	IF_NAME=$(try_read_netdevname $CCWGROUPDEVNO)
	if [[ -z "$IF_NAME" ]]
	then
		echo "Now waiting for device $CCWGROUPDEVNO to get available..."
	fi
	while [[ -z "$IF_NAME" ]] && [[ retries -lt MAX_RETRIES ]]
	do
		sleep 1
		retries=$retries+1
		IF_NAME=$(try_read_netdevname $CCWGROUPDEVNO)
	done
	if [ ! -z "$IF_NAME" ]
	then
		echo "Successfully configured device $CCWGROUPDEVNO ($IF_NAME)"
	else
		print_error "Failed to make $CCWGROUPDEVNO online."
		return 1
	fi
}

#==============================================================================

#
# switch_device
# 	$1: ccwgroupdevno
#	$2: 1|0 (online|offline)
# returns
#	0 success
#	1 command to make device online/offline returned error
#	2 online file does not exist
#
function switch_device()
{
	local CCWGROUPDEVNO="$1"
	local SWITCHVALUE="$2"
	local DEVICE_DIRECTORY="$CCWGROUPBUS_DEVICEDIR/$CCWGROUPDEVNO"
	local ONLINE_FILE="$DEVICE_DIRECTORY/online"
	local STATESTR="online"

	if [ $SWITCHVALUE -eq 0 ]
	then
		STATESTR="offline"
	fi

	if [ -f $ONLINE_FILE ]
	then
		if [ "`cat $ONLINE_FILE`" != "$SWITCHVALUE" ]
		then
			echo "$SWITCHVALUE" >> $ONLINE_FILE
			case $? in
				0)
					;;
				*)
					print_error "Failed to make $CCWGROUPDEVNO" \
						"$STATESTR"
					return 1
					;;
			esac
		fi
	else
		print_error "$ONLINE_FILE does not exist."
		return 2
	fi

	return 0
}

#==============================================================================

#
# configure_ccwgroupdev_option
#	$1: ccwgroupdevid
#	$2: option_name
#	$3: option_value
# returns
#	0 success
# 	1 option not allowed to be set (e.g. online)
#	2 unknown option
#	3 configuration failed
#
function configure_ccwgroupdev_option()
{
	local CCWGROUPDEVID="$1"
	local OPTION_NAME="$2"
	local OPTION_VALUE="$3"

	# filter some attributes away
	if [ "$OPTION_NAME" == "online" ]
	then
		print_error "Ignoring option $OPTION_NAME=$OPTION_VALUE"
		return 1
	fi

	# check if attribute exists
	local ATTRFILE="$CCWGROUPBUS_DEVICEDIR/$CCWGROUPDEVID/$OPTION_NAME"

	if [ "`cat $CCWGROUPBUS_DEVICEDIR/$CCWGROUPDEVID/online`" == "1" ]
	then
		# Could be done by udev rule or else
		logger "znetconf: Device $CCWGROUPDEVID is already online."
		if [ "$OPTION_VALUE" != "`cat $ATTRFILE`" ]
		then
			logger "znetconf: Device $CCWGROUPDEVID is set offline to set $OPTION_NAME=$OPTION_VALUE"
			echo "0" >> "$CCWGROUPBUS_DEVICEDIR/$CCWGROUPDEVID/online" 2> /dev/null
		else
			logger "znetconf: Device $CCWGROUPDEVID has $OPTION_NAME=`cat $ATTRFILE` already set, skipping...."
			return 0
		fi
	fi

	if [ -f $ATTRFILE ]
	then
		echo $OPTION_VALUE >> $ATTRFILE 2> /dev/null
		case $? in
			0)
				;;
			*)
				print_error "Failed to configure" \
					"$OPTION_NAME=$OPTION_VALUE"
				return 3
				;;
		esac
	else
		print_error "$ATTRFILE does not exist"
		return 2
	fi

	return 0
}

#==============================================================================

#
# add_net_device
# 	$1: ccwdevid[;ccwdevid][;ccwdevid]
#	$2: ccwgroupdevid
#	$3: driver
#	$4: opt_count
#	$5..$(5+opt_count): opt_name_array
#	$5+opt_count..5+2*opt_count: opt_value_array
# returns
#	0 success
#	RC_COULD_NOT_GROUP_DEVICES
#	RC_COULD_NOT_SET_DEV_ONLINE
#	RC_NET_DEVICE_NOT_ONLINE
#	RC_OPTION_NOT_CONFIGURED
#
function add_net_device()
{
	local DEVICES="$1"
	local CCWGROUPDEVID="$2"
	local DRIVER="$3"
	local OPT_COUNT=$4
	local RAW=( "$@" )
	local OPT_NAME_ARRAY=("${RAW[@]:4:$OPT_COUNT}")
	local OPT_VALUE_ARRAY=("${RAW[@]:$[4 + $OPT_COUNT]:$OPT_COUNT}")

	if ! group_device $DEVICES $CCWGROUPDEVID $DRIVER
	then
		return $?
	fi

	# To avoid conflict with unfinished uevents triggered potentially by creating groupe device
	prepare_udevsettle_cmd
	if [ "$UDEVSETTLE" != "" ]
	then
		$UDEVSETTLE_CALL
	fi

	local i=0
	local SOMEOPTION_FAILED=0
	local HAS_LAYER2_OPTION=0
	while [ $i -lt $OPT_COUNT ]
	do
		if ! configure_ccwgroupdev_option $CCWGROUPDEVID \
			 ${OPT_NAME_ARRAY[$i]} ${OPT_VALUE_ARRAY[$i]}
		then
			SOMEOPTION_FAILED=1
		fi
		if [ "${OPT_NAME_ARRAY[$i]}" == "layer2" ]
		then
			HAS_LAYER2_OPTION=1
		fi
		let "i++"
	done

	# in case of qeth and no given layer2 option try to determine layer
	# automatically
	if [ $IS_VM_ENV -eq 1 ] && [ $HAS_LAYER2_OPTION -eq 0 ] &&
		[ "$DRIVER" == "qeth" ]
	then
		lookup_layer ${CCWGROUPDEVID##*.}
		local LAYER2=$?
		if [ $LAYER2 -eq 0 ] || [ $LAYER2 -eq 1 ]
		then
			if ! configure_ccwgroupdev_option $CCWGROUPDEVID \
				"layer2" "$LAYER2"
			then
				SOMEOPTION_FAILED=1
			fi
		fi
	fi

	if ! switch_device $CCWGROUPDEVNO 1
	then
		return $RC_COULD_NOT_SET_DEV_ONLINE
	fi

	if [ $SOMEOPTION_FAILED -ne 0 ]
	then
		return $RC_OPTION_NOT_CONFIGURED
	fi

	if ! wait_for_net_device $CCWGROUPDEVNO
	then
		return $RC_NET_DEVICE_NOT_ONLINE
	fi

	return 0
}

#==============================================================================

#
# remove_net_device
#	$1: ccwgroupdevid
#	$2: network interface name (just for display purposes)
# returns
#	0 success
#	RC_COULD_NOT_SET_DEV_OFFLINE
#	RC_COULD_NOT_UNGROUP_DEVICE
#
function remove_net_device()
{
	local DEVICE_TO_REMOVE="$1"
	local DEVNAME="$2"

	if ! switch_device $DEVICE_TO_REMOVE 0
	then
		return $RC_COULD_NOT_SET_DEV_OFFLINE
	fi

	if ! ungroup_device $DEVICE_TO_REMOVE $DEVNAME
	then
		return $RC_COULD_NOT_UNGROUP_DEVICE
	fi
	return 0
}

#==============================================================================

#
# list_configured
#	$1 supress_header
# returns
#	0 success
#
function list_configured()
{
	supress_header=0
	if [ $# -ge 1 ]
	then
		supress_header=$1
	fi

	local LIST_FORMAT_STRING="%-26.26s %-7.7s %-14.14s %5.5s %-4.4s %-16.16s %-7.7s\n"
	if [ $supress_header -eq 0 ]
	then
		printf "$LIST_FORMAT_STRING" "Device IDs" "Type" \
			"Card Type" "CHPID" "Drv." "Name" "State"
		echo "-------------------------------------------------------------------------------------"
	fi

	NETWORK_DEVICES=$(find $CCWGROUPBUS_DEVICEDIR -type l)
	for d in $NETWORK_DEVICES
	do
		# determine dev no
		local DEVNO=${d##*/*/}
		DRIVER=""
		local DEVNAME=""

		# determine interface name, if there (only for qeth and if
		# device is online)
		if [ -f $d/if_name ]
		then
			read DEVNAME < $d/if_name
		elif [ -d $d/net ]
		then
			DEVNAME=$(ls $d/net/)
		fi

		# read all links and parse driver, device name and ccw device
		# bus-ids
		ls -l $d/ | grep '^l' |
		{

			while read line
			do
				local LINKNAME=${line// ->*/""}
				LINKNAME=${LINKNAME##* }
				if [ "$LINKNAME" = "driver" ]
				then
					DRIVER=${line##* -> */}
				elif [[ "$LINKNAME" =~ "cdev" ]]
				then
					CDEVNOS="$CDEVNOS$DEVNOSEP${line//*..\/*\//}"
				# the following is necessary if the driver is
				# not qeth
				elif [[ -z "$DEVNAME" ]] && [[ "$LINKNAME" =~ "net" ]]
				then
					DEVNAME=${line##* -> */}
				fi
			done

			local CUTYPE=""
			if [ -e $d/cdev0/cutype ]
			then
				read CUTYPE < $d/cdev0/cutype
			fi

			read ONLINE < $d/online
			if [ $ONLINE -eq 1 ]
			then
				ONLINE_STATE="online"
			else
				ONLINE_STATE="offline"
			fi
			local TYPEFILE="$d/card_type"
			if [ ! -f $TYPEFILE ]
			then
				TYPEFILE="$d/type"
			fi
			read CARDTYPE < $TYPEFILE
			local CHPID=""
			if [ -e $d/chpid ]
			then
				read CHPID < $d/chpid
			fi
			printf "$LIST_FORMAT_STRING" "${CDEVNOS#$DEVNOSEP}" \
				"$CUTYPE" "$CARDTYPE" \
				"$CHPID" "$DRIVER" "$DEVNAME" "$ONLINE_STATE"
		}
	done
	return 0
}

#==============================================================================

#
# print_list_unconf_header
# 	$1: format string
# returns
# 	0
function print_list_unconf_header()
{
	local LIST_FORMATSTR="$1"
	printf "$LIST_FORMATSTR" "Device IDs" "Type" "Card Type" "CHPID" "Drv."
	echo "------------------------------------------------------------"
	return 0
}

#==============================================================================

#
# list_unconfigured
# returns
#	0 success
#
function list_unconfigured()
{
	local PRINTED_HEADLINES=0
	local LIST_FORMATSTR="%-26.26s %-7.7s %-14.14s %5.5s %-4.4s \n"
	print_scanning_4_nwdevices

	$LSZNET_CALL |
	{
		while read no cutype chp devtype devdrv devname chlist cardtype
		do
			if [ $PRINTED_HEADLINES -eq 0 ]
			then
				print_list_unconf_header "$LIST_FORMATSTR"
				PRINTED_HEADLINES=1
			fi
			# print devtype instead of cardtype for cardtype==OSX/OSM
			if [ "$cardtype" == "OSX" ]
			then
				printf "$LIST_FORMATSTR" "$chlist" "$cutype" \
					"$devtype" "$chp" "$devdrv"
			else
				printf "$LIST_FORMATSTR" "$chlist" "$cutype" \
					"$cardtype" "$chp" "$devdrv"
			fi
		done
	}
}

#==============================================================================

#
# store_option
#	$1: attribute=value
# returns
#	0 success
#	1 attribute starts with a - / or is invalid
#	2 missing value
#
function store_option()
{
	local OPTIONSTRING="$1"

	# ensure that there is no option intepreted as an
	# attribute value pair
	[[ "$OPTIONSTRING" =~ ^- ]]
	case $? in
		0)
			print_error "$OPTIONSTRING is not a valid attribute" \
				"value pair"
			exit 1
			;;
		1)
			# option considered ok
			;;
		2)
			print_error "Internal error"
			exit 1
	esac

	# do the parsing
	local NAME=$(echo $OPTIONSTRING|cut -d= -f1)
	local VALUE=$(echo $OPTIONSTRING|cut -d= -f2)
	if [ -z "$NAME" ]
	then
		print_error "Attribute name missing for -o|--option"
		return 1
	fi
	if [ -z "$VALUE" ]
	then
		print_error "Missing value for attribute $NAME"
		return 2
	fi
	ATTRIBUTE_COUNT=$[ $ATTRIBUTE_COUNT + 1 ]
	ATTRIBUTE_NAME[$ATTRIBUTE_COUNT]="$NAME"
	ATTRIBUTE_VALUE[$ATTRIBUTE_COUNT]="$VALUE"
	return 0
}

#==============================================================================

#
# is_complete_ccwdevbusid
#	$1: possibly correct ccw device bus id
# returns
#	0 if the given string is a correctly formatted ccw device bus id
#	1 else
#
function is_complete_ccwdevbusid()
{
	local DEV="$1"
	[[ "$DEV" =~ $FORMAT_FULLDEVNO ]]
	case $? in
		0)
			return 0
			;;
		1)
			return 1
			;;
		2)
			print_error "Internal error"
			exit 1
			;;
	esac
	return 0
}

#==============================================================================

#
# is_partial_ccwdevbusid
#	$1: possibly correct partial ccw device bus id
# returns
#	0 if the given string is a correctly formatted partial ccw device
#	  bus id
#	1 else
#
function is_partial_ccwdevbusid()
{
	local DEV="$1"
	[[ "$DEV" =~ $FORMAT_PARTDEVNO ]]
	case $? in
		0)
			return 0
			;;
		1)
			return 1
			;;
		2)
			print_error "Internal error"
			exit 1
			;;
	esac
}

#==============================================================================

#
# is_ccwdevbusid_list
#	$1: possibly correct list of ccw device bus ids
# returns
#	0 if the given string is a correctly formatted list of ccw device
# 	  bus ids
#	1 else
#
function is_ccwdevbusid_list()
{
	local DEVLIST="$1"
	[[ "$DEVLIST" =~ $FORMAT_DEVLIST ]]
	case $? in
		0)
			return 0
			;;
		1)
			return 1
			;;
		2)
			print_error "Internal error"
			exit 1
			;;
	esac
}


#==============================================================================

#
# is_shortccwdevbusid_list
#	$1: possibly correct list of short ccw device bus ids
# returns
#	0 if the given string is a correctly formatted list of short ccw device
# 	  bus ids
#	1 else
#
function is_shortccwdevbusid_list()
{
	local DEVLIST="$1"
	[[ "$DEVLIST" =~ $FORMAT_SHORTDEVLIST ]]
	case $? in
		0)
			return 0
			;;
		1)
			return 1
			;;
		2)
			print_error "Internal error"
			exit 1
			;;
	esac
}

#==============================================================================

#
# is_supported_driver
#	$1: possibly supported driver
# returns
#	0 if the given string denotes a supported driver
#	1 else
#
function is_supported_driver()
{
	local DRIVER="$1"
	local DRVEXPR='^(qeth|lcs|ctc|ctcm)$'
	[[ "$DRIVER" =~ $DRVEXPR ]]
	case $? in
		0)
			return 0
			;;
		1)
			return 1
			;;
		2)
			print_error "Internal error"
			exit 1
			;;
	esac
}

#==============================================================================

#
# ask_for_remove
#	$1: interaction not allowed (0 == interaction allowed)
# returns
#	if interaction is allowed and the given question was answered with y|Y
#	or no interaction is allowed
# exits
# 	with error code 0 in any other case
#
function ask_for_remove()
{
	local INTERACTION_NOT_ALLOWED=$1

	if [ "$INTERACTION_NOT_ALLOWED" -eq 0 ]
	then
		printf "%s" "Do you want to continue (y/n)?"
		read REMOVE_ANSWER
		if [[ "$REMOVE_ANSWER" != "y" ]] &&
			[[ "$REMOVE_ANSWER" != "Y" ]]
		then
			exit 0
		fi
	fi
	return 0
}

#==============================================================================

#
# extract_interface_name
# 	$1: list_configured line of a network device
# returns
#	0
# environment
#	REPLY: interface name
function extract_interface_name()
{
	local CFGLINE="$1"
	local IF_NAME=$(expr substr "$CFGLINE" 62 16)
	REPLY=${IF_NAME%% *}
	return 0
}

#==============================================================================
# main
#==============================================================================

# check consistency of installtion
if [ ! -e "$LSZNET" ]
then
	print_error "Could not find tool at $LSZNET"
	exit 1
fi

# determine environment
check_vm_env

# parse command line
DO_LIST_CONFIGURED=0
DO_LIST_UNCONFIGURED=0
DO_ADDALL=0
DO_ADD=0
DEVICE_TO_ADD=""
DO_APPEND_OPTIONS=0
DO_LIST=0
DO_REMOVE=0
DEVICE_TO_REMOVE=""
DO_REMOVEALL=0
NONINTERACTIVE=0
EXCEPT=""
if [ $# -gt 0 ]
then
	args=`getopt -l help,version,add-all,add:,configured,driver:,option:,remove:,remove-all,except:,unconfigured,non-interactive -o hvAa:cd:o:r:Re:un -- $@`;
	if [ $? -ne 0 ]; then
		exit $RC_SYNTAX_ERROR
	fi
	eval set -- "$args"
	while [ $# -gt 0 ]
	do
		case $1 in
			-h|--help)
				print_usage
				exit 0;;
			-v|--version)
				print_version
				exit 0;;
			-A|--add-all)
				DO_ADDALL=1;;
			-a|--add)
				shift
				# get parameter expected to be a
				# ccw device bus id
				DEVICE_TO_ADD="`echo $1 | tr '[A-Z]' '[a-z]'`"
				if [ ${#DEVICE_TO_ADD} -lt 4 ]; then
					DEVICE_TO_ADD=`printf "%04x" 0x${DEVICE_TO_ADD}`
				fi

				# check syntax of ccw device bus id
				if is_complete_ccwdevbusid "$DEVICE_TO_ADD" ||
					is_ccwdevbusid_list "$DEVICE_TO_ADD"
				then
					DO_ADD=1
				elif is_partial_ccwdevbusid "$DEVICE_TO_ADD"
				then
					DEVICE_TO_ADD="0.0.$DEVICE_TO_ADD"
					DO_ADD=1
				elif is_shortccwdevbusid_list "$DEVICE_TO_ADD"
				then
					DEVICE_TO_ADD="0.0.$DEVICE_TO_ADD"
					DEVICE_TO_ADD="${DEVICE_TO_ADD//,/,0.0.}"
					DO_ADD=1
				else
					print_error "Invalid device ID format" \
						"$DEVICE_TO_ADD"

					exit $RC_INVALID_DEVICE_ID_FORMAT
				fi;;
			-c|--configured)
				DO_LIST_CONFIGURED=1;;
			-d|--driver)
				shift
				# ensure driver is supported
				DRIVER="$1"
				if [ "$DRIVER" != "" ] && \
					! is_supported_driver "$DRIVER"
				then
					# unknown driver
					print_error "Unknown driver $DRIVER"
					exit $RC_UNKNOWN_DRIVER
				fi;;
			-o|--option)
				DO_APPEND_OPTIONS=1
				shift
				# get option string
				OPTIONSTRING="$1"

				# store option
				if ! store_option $OPTIONSTRING
				then
					exit $RC_INVALID_ATTRIBUTE_VALUE_PAIR
				fi;;
			-r|--remove)
				DO_REMOVE=1
				shift
				# get device to be removed
				DEVICE_TO_REMOVE="`echo $1 | tr '[A-Z]' '[a-z]'`"
				if [ ${#DEVICE_TO_REMOVE} -lt 4 ]; then
					DEVICE_TO_REMOVE=`printf "%04x" 0x${DEVICE_TO_REMOVE}`
				fi

				# validate it is a
				# ccw dev bus id (short or long)
				if is_partial_ccwdevbusid "$DEVICE_TO_REMOVE"
				then
					DEVICE_TO_REMOVE="0.0.$DEVICE_TO_REMOVE"
				elif ! is_complete_ccwdevbusid \
					"$DEVICE_TO_REMOVE"
				then
					print_error "Invalid device ID format" \
						"$DEVICE_TO_REMOVE"
					exit $RC_INVALID_DEVICE_ID_FORMAT
				fi;;
			-R|--remove-all)
				DO_REMOVEALL=1;;
			-e|--except)
				DO_IGNORE_DEVNOS=1
				shift
				# get device to be ignored
				EXCEPT_DEVNO="`echo $1 | tr '[A-Z]' '[a-z]'`"
				if [ ${#EXCEPT_DEVNO} -lt 4 ]; then
					EXCEPT_DEVNO=`printf "%04x" 0x${EXCEPT_DEVNO}`
				fi

				# validate it is a
				# ccw dev bus id (short or long)
				if is_partial_ccwdevbusid "$EXCEPT_DEVNO"
				then
					EXCEPT_DEVNO="0.0.$EXCEPT_DEVNO"
				elif ! is_complete_ccwdevbusid \
					"$EXCEPT_DEVNO"
				then
					print_error "Invalid device ID format" \
						"$EXCEPT_DEVNO"
					exit $RC_INVALID_DEVICE_ID_FORMAT
				fi

				# create a filter statement
				if [ "$EXCEPT" == "" ]
				then
					EXCEPT="$EXCEPT_DEVNO"
				else
					EXCEPT="$EXCEPT|$EXCEPT_DEVNO"
				fi;;
			-u|--unconfigured)
				DO_LIST_UNCONFIGURED=1;;
			-n|--non-interactive)
				NONINTERACTIVE=1;;
			--)	;;
			*)
				echo "$CMD: Invalid option $1"
				echo "Try '$CMD --help' for more information."
				exit $RC_INVALID_ARGUMENT
				;;
		esac
		shift
	done
fi


# check for too much arguments
if [ $(($DO_ADD + $DO_ADDALL + $DO_LIST_UNCONFIGURED + $DO_LIST_CONFIGURED + \
	 $DO_REMOVEALL + $DO_REMOVE)) -gt 1 ]
then
	print_error "Too much arguments"
	echo "Try '$CMD --help' for more information."
	exit $RC_TOO_MUCH_ARGUMENTS
fi

# check existence of ccwgroup devices
if ! [ -d $CCWGROUPBUS_DIR ]; then
	print_error "There are no ccwgroup devices"
	exit $RC_NO_CCWGROUP
fi

# react to parsed options
if [ $DO_ADD -eq 1 ]
then
	print_scanning_4_nwdevices

	# scan for details like all CCW device bus IDs and the driver name
	if ! is_ccwdevbusid_list "$DEVICE_TO_ADD"
	then
		# read the set of device numbers from the LSZNET
		CANDIDATE=$($LSZNET_CALL|awk \
			"\$7 ~ /$DEVICE_TO_ADD/ {print \$5,\$7}")
		read PROPOSED_DRIVER DEVICES <<< "$CANDIDATE"
		if [ "$DEVICES" == "" ]
		then
			print_error "No configuration found for device ID" \
				"$DEVICE_TO_ADD"
			exit $RC_NO_CONFIG_FOUND_FOR_DEV
		fi
	else
		# define set of device numbers to be used
		DEVICES="$DEVICE_TO_ADD"

		# ensure none of the CCW devices is already in use
		CCW_DEVS=${DEVICES//,/ }
		for CCW_DEVNO in $CCW_DEVS
		do
			CCW_DEV_ONLINEFILE="$CCWDEV_DIR/$CCW_DEVNO/online"
			if [ ! -e  $CCW_DEV_ONLINEFILE ]
			then
				print_error "Device $CCW_DEVNO does not exist"
				exit $RC_DEVICE_DOES_NOT_EXIST
			fi

			read CCW_DEV_ONLINE < $CCW_DEV_ONLINEFILE
			if [ "$CCW_DEV_ONLINE" == "1" ]
			then
				print_error "$CCW_DEVNO is already in use"
				exit $RC_DEVICE_ALREADY_IN_USE
			fi
		done

		# try to find a driver for the given set of device numbers
		CANDIDATE=$($LSZNET_CALL|
			awk "\$7~/${DEVICE_TO_ADD//,// && \$7~/}/ {print \$5}")
		read PROPOSED_DRIVER <<< "$CANDIDATE"
	fi

	# compute the expected group device number
	CCWGROUPDEVNO=${DEVICES%%,*}

	# check whether an appropriate driver was determined automatically or
	# not, if not, one has to be given by the user via -d
	if [ "$DRIVER" == "" ]
	then
		DRIVER="$PROPOSED_DRIVER"
	fi
	if [ "$DRIVER" == "" ]
	then
		print_warning "No driver found for $DEVICE_TO_ADD. Using qeth."
		DRIVER="qeth"
	fi
	add_net_device $DEVICES $CCWGROUPDEVNO $DRIVER \
		$ATTRIBUTE_COUNT "${ATTRIBUTE_NAME[@]}" "${ATTRIBUTE_VALUE[@]}"
	exit $?
fi
if [ $DO_ADDALL -eq 1 ]
then
	print_scanning_4_nwdevices
	ADDALL_RC=0
	if [ "$EXCEPT" == "" ]
	then
		PATTERNSWITCH=""
	else
		PATTERNSWITCH="-v"
	fi
	PATTERN='([[:xdigit:]]|\.|,)*'
	PATTERN=" $PATTERN$EXCEPT$PATTERN "
	$LSZNET_CALL|grep $PATTERNSWITCH -E -e "$PATTERN"|awk '{print $5,$7}'| {
		while read PROPOSED_DRIVER CCWDEVNOS
		do
			DRV="$DRIVER"
			if [ "$DRV" == "" ]
			then
				DRV="$PROPOSED_DRIVER"
			fi
			CCWGROUPDEVNO=${CCWDEVNOS/,*/""}
			if ! add_net_device $CCWDEVNOS $CCWGROUPDEVNO "$DRV" \
				$ATTRIBUTE_COUNT "${ATTRIBUTE_NAME[@]}" \
				"${ATTRIBUTE_VALUE[@]}"
			then
				ADDALL_RC=$RC_SOME_DEVICES_FAILED
			fi
		done
		exit "$ADDALL_RC"
	}
	exit $?
fi
if [ $DO_REMOVE -eq 1 ]
then
	# ensure that device exists
	CONFIGURED_DEVICE_STRING=$(list_configured 1|
		grep "$DEVICE_TO_REMOVE")
	CCWDEVBUSIDS_TO_REMOVE=${CONFIGURED_DEVICE_STRING%% *}
	CCWGROUPDEVID_TO_REMOVE=${CCWDEVBUSIDS_TO_REMOVE%%,*}

	if [[ -z "$CCWGROUPDEVID_TO_REMOVE" ]]
	then
		print_error "$DEVICE_TO_REMOVE is not a configured device"
		exit $RC_DEVICE_NOT_CONFIGURED
	fi

	echo "Remove network device $CCWGROUPDEVID_TO_REMOVE" \
	 	"($CCWDEVBUSIDS_TO_REMOVE)?"
	echo "Warning: this may affect network connectivity!"
	NETWORK_DEVICES_NUMBER=$(find $CCWGROUPBUS_DEVICEDIR -type l| wc -l)
	if [ $NETWORK_DEVICES_NUMBER -eq 0 ]; then
		exit 0
	fi
	ask_for_remove "$NONINTERACTIVE" "$REMOVE_QUESTION"

	# remove it
	extract_interface_name "$CONFIGURED_DEVICE_STRING"
	remove_net_device $CCWGROUPDEVID_TO_REMOVE $REPLY
	exit $?
fi
if [ $DO_REMOVEALL -eq 1 ]
then
	if [ "$EXCEPT" == "" ]
	then
		echo "Remove all network devices"
		PATTERNSWITCH=""
	else
		echo "Remove all network devices except ${EXCEPT//|/ and }"
		PATTERNSWITCH="-v"
	fi
	echo "Warning: this will affect network connectivity!"
	ask_for_remove "$NONINTERACTIVE"

	PATTERN='([[:xdigit:]]|\.|,)*'
	PATTERN="^$PATTERN$EXCEPT$PATTERN "
	list_configured 1|grep $PATTERNSWITCH -E -e "$PATTERN"|{
	while read -e d
		do
			CCWGROUPDEVID_TO_REMOVE=${d%%,*}
			extract_interface_name "$d"
			remove_net_device $CCWGROUPDEVID_TO_REMOVE $REPLY
		done
	}
	exit 0
fi
if [ $DO_LIST_UNCONFIGURED -eq 1 ]
then
	list_unconfigured
	exit 0
fi
if [ $DO_LIST_CONFIGURED -eq 1 ]
then
	list_configured
	exit 0
fi

# fallthrough: nothing was done
# print hint to user
print_short_usage
exit 0
