#! /bin/bash
#
# Copyright(c) 2009 Intel Corporation. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms and conditions of the GNU General Public License,
# version 2, as published by the Free Software Foundation.
#
# This program is distributed in the hope it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
#
# Maintained at www.Open-FCoE.org

cmdname=`basename $0`

usage()
{
	echo usage: $cmdname \
		'[--fcoeif <ethX> --reset | --create | --destroy]' \
		'[--debug] [--syslog]' \
		'[--netif <ethX> --qos-disable |' \
		'--qos-enable  <pri>[,<pri>]...]' >&2
	exit 1
}

#
# Tunable parameters
#
QOS_DEF=3			# default user priority
FCOE_ETHERTYPE=35078		# Ethertype (0x8906): tc filter show is base 10
FIP_ETHERTYPE=35092             # Ethertype (0x8914): tc filter show is base 10
FCOE_FILTER=0xfc0e		# filter handle (must be lower-case hex)

FCOE_FILTER_KEY=12345
FIP_FILTER_KEY=67890

qdisc_id=1:
qos_list=
cmd=
# automake paths
prefix=/usr
exec_prefix=${prefix}
bindir=${exec_prefix}/bin
sbindir=${exec_prefix}/sbin
sysconfdir=/etc
# make sure there's a sane path to find basic commands and system tools (tc)
PATH=$PATH:/sbin:/usr/sbin:/bin:/usr/bin
FCOEADM=${sbindir}/fcoeadm	# command to create/destroy FCoE instances
CONFIG_DIR=${sysconfdir}/fcoe
LOGGER=
DEBUG_LOGGING=

find_multiq_qdisc()
{
	ifname=$1

	found=0
	type=unknown
	if set -- `tc qdisc show dev $ifname` none none none
	then
		type=$2
		qdisc_id=$3
	fi
	[ "$type" == "multiq" ] && found=1

	return $found
}

add_multiq_qdisc()
{
	ifname=$1
	qdisc_id=$2

	[ ${DEBUG_LOGGING} ] && $LOGGER \
		"tc qdisc add dev $ifname root handle $qdisc_id multiq"
	tc qdisc add dev $ifname root handle $qdisc_id multiq
}

get_filter_id()
{
	ifname=$1
	filter_key=$2

	retry_count=0
	while true
	do
		[ $retry_count -eq 0 ] && break
		[ -f /var/run/fcoemon.pid ] && break
		sleep 1
		retry_count=$(($retry_count-1))
	done

	echo "`echo "$ifname $filter_key" | \
	    awk '{ printf("0x%x%06x", substr($1,4), $2) }'`"
}

find_skbedit_filter()
{
	ifname=$1
	ethertype=$2
	filter_id=$3

	found=`tc filter show dev $ifname | awk '
	BEGIN {
		x1 = 0
		x2 = 0
		x3 = 0
		queue = 8
	}
	/^filter.*parent.*protocol \['$ethertype'\].* handle '$filter_id'/ {
		if (x1 == 0 && x2 == 0 && x3 == 0)
			x1 = 1
	}
	/cmp.*u16 at 12 layer 1 mask 0xffff eq '$ethertype'.*\)/ {
		if (x1 == 1 && x2 == 0 && x3 == 0)
			x2 = 1
	}
	/action order [0-9][0-9]*:  skbedit queue_mapping/ {
		if (x1 == 1 && x2 == 1 && x3 == 0) {
			x3 = 1
			queue = $6
		}
	}
	END {
		print queue
	}'`

	return $found
}

delete_skbedit_filter()
{
	ifname=$1
	queue=$2
	ethertype=$3
	filter_id=$4

	[ ${DEBUG_LOGGING} ] && $LOGGER \
		"tc filter delete dev $ifname skbedit queue_mapping $queue"
	PARENT=`tc filter show dev $ifname | awk \
		'/^filter.*parent.*protocol \['$ethertype'\].* handle '$filter_id'/ \
		{ print $3 }'`
	PRIO=`tc filter show dev $ifname | awk \
		'/^filter.*parent.*protocol \['$ethertype'\].* handle '$filter_id'/ \
		{ print $7 }'`
	tc filter delete dev $ifname parent $PARENT \
		protocol $ethertype pref $PRIO handle $filter_id basic match \
		'cmp(u16' at 12 layer 1 mask 0xffff eq $ethertype')' \
		action skbedit queue_mapping $queue
	tc filter delete dev $ifname parent $PARENT \
		protocol $ethertype pref $PRIO basic
}

add_skbedit_filter()
{
	ifname=$1
	qdisc_id=$2
	queue=$3
	ethertype=$4
	filter_id=$5

	[ ${DEBUG_LOGGING} ] && $LOGGER \
		"tc filter add dev $ifname skbedit queue_mapping $queue"
	tc filter add dev $ifname parent $qdisc_id protocol $ethertype \
		handle $filter_id basic match 'cmp(u16' at 12 \
		layer 1 mask 0xffff eq $ethertype')' \
		action skbedit queue_mapping $queue
}

replace_skbedit_filter()
{
	ifname=$1
	queue=$2
	ethertype=$3
	filter_id=$4

	[ ${DEBUG_LOGGING} ] && $LOGGER \
		"tc filter replace dev $ifname skbedit queue_mapping $queue"
	PARENT=`tc filter show dev $ifname | awk \
		'/^filter.*parent.*protocol \['$ethertype'\].* handle '$filter_id'/ \
		{ print $3 }'`
	PRIO=`tc filter show dev $ifname | \
		awk '/^filter.*parent.*protocol \['$ethertype'\].* handle '$filter_id'/ \
		{ print $7 }'`
	tc filter replace dev $ifname parent $PARENT protocol \
		$ethertype pref $PRIO handle $filter_id basic match \
		'cmp(u16' at 12 layer 1 mask 0xffff eq $ethertype')' \
		action skbedit queue_mapping $queue
}

remove_fcoe_interface()
{
	ifname=$1

	STATUS=`$FCOEADM -i $ifname 2>&1 | \
		awk '/Symbolic Name:/ && /'$ifname'$/{print $6}'`
	if [ "$STATUS" == "$ifname" ]; then
		[ ${DEBUG_LOGGING} ] && $LOGGER "$FCOEADM -d $ifname"
		$FCOEADM -d $ifname
	else
		[ ${DEBUG_LOGGING} ] && $LOGGER \
			"FCoE interface $ifname doesn't exist"
	fi
}

create_fcoe_interface()
{
	ifname=$1

	STATUS=`$FCOEADM -i $ifname 2>&1 | \
		awk '/Symbolic Name:/ && /'$ifname'$/{print $6}'`
	if [ -z "$STATUS" ]; then
		[ ${DEBUG_LOGGING} ] && $LOGGER "$FCOEADM -c $ifname"
		$FCOEADM -c $ifname
	else
		[ ${DEBUG_LOGGING} ] && $LOGGER \
			"FCoE interface $ifname already created"
	fi
}

config_logging()
{
    if [ $DEBUG_LOGGING ] ; then
	if [ $USE_SYSLOG ] ; then
	    LOGGER="logger -t fcoeplumb"
	else
	    LOGGER="echo"
	fi
    else
	LOGGER=
    fi
}

[ "$#" -lt 2 ] && usage

[ ${DEBUG_LOGGING} ] && $LOGGER "fcoeplumb arguments: ($*)"

while [ "$#" -ge 1 ]
do
	case "$1" in
	    --fcoeif)
		shift
		fcoe_ifname=$1
		shift
		case "$1" in
		    --reset | -r)
			cmd=reset
			;;
		    --create | -c)
			cmd=create
			;;
		    --destroy | -d)
			cmd=destroy
			;;
		    *)
			echo "$cmdname: unknown fcoe parameter '$1'" >&2
			usage
			;;
		esac
		;;
	    --netif)
		shift
		net_ifname=$1
		shift
		case "$1" in
		    --qos-enable)
			[ "$#" -lt 2 ] && usage
			qos_list=$2
			shift
			;;
		    --qos-disable)
			[[ "$2" =~ ^[0-9]+$ ]] && shift
			qos_list="disable"
			;;
		    *)
			echo "$cmdname: unknown qos parameter '$1'" >&2
			usage
			;;
		esac
		;;
	    --debug)
		DEBUG_LOGGING=1
		config_logging
		;;
            --syslog)
		USE_SYSLOG=1
		config_logging
		;;
	    *)
		echo "$cmdname: unknown parameter '$1'" >&2
		usage
		;;
	esac
	shift
done

# This must be the first to do after parsing the command arguments!
# Notice that the filter ID is used in find_skbedit_filter(),
# add_skbedit_filter(), replace_skbedit_filter().
fcoe_filter_id=`get_filter_id $net_ifname $FCOE_FILTER_KEY`
fip_filter_id=`get_filter_id $net_ifname $FIP_FILTER_KEY`

if [ "$cmd" == "destroy" ]; then
	remove_fcoe_interface $fcoe_ifname

elif [ "$cmd" == "create" ]; then
	create_fcoe_interface $fcoe_ifname
fi

if [ "$qos_list" == "disable" ]; then
	# Remove the FCoE filters
	find_skbedit_filter $net_ifname $FCOE_ETHERTYPE $fcoe_filter_id
	found_filter=$?
	[ $found_filter -le 7 ] && delete_skbedit_filter $net_ifname \
		$found_filter $FCOE_ETHERTYPE $fcoe_filter_id

	# Remove the FIP filters
	find_skbedit_filter $net_ifname $FIP_ETHERTYPE $fip_filter_id
	found_filter=$?
	[ $found_filter -le 7 ] && delete_skbedit_filter $net_ifname \
		$found_filter $FIP_ETHERTYPE $fip_filter_id

elif [ -n $qos_list ]; then
	#
	# Choose the best QOS to use for FCoE out of the listed choices.
	#

	# Parse QOS List
	QOS_BEST=
	if [ -n "$qos_list" ]; then
		OLD_IFS="$IFS"
		IFS=,"$IFS"
		set -- $qos_list
		IFS="$OLD_IFS"

		while [ "$#" -ge 1 ]
		do
			case "$1" in
			[0-7])
				;;
			*)
				echo "$cmdname: bad QOS value '$1'" >&2
				usage
				;;
			esac
			if [ -z "$QOS_BEST" ]; then
				QOS_BEST=$1
			elif [ "$1" -eq "$QOS_DEF" ]; then
				QOS_BEST=$1
			fi
			shift
		done
	fi

	# If the best QOS is not found, do nothing.
	[ -z "$QOS_BEST" ] && exit 0

	[ ${DEBUG_LOGGING} ] && $LOGGER "$net_ifname - Choosing QOS '$QOS_BEST'"

	#
	# Setup the traffic classifier for FCoE
	# First see if it is already set up.
	#
	qos_queue=`expr $QOS_BEST`

	find_multiq_qdisc $net_ifname
	found_qdisc=$?

	if [ $found_qdisc -eq 1 ]; then
	        # Adjust the FCoE filter
		[ ${DEBUG_LOGGING} ] && $LOGGER "$net_ifname: Qdisc is found"
		find_skbedit_filter $net_ifname $FCOE_ETHERTYPE $fcoe_filter_id
		found_filter=$?
		if [ $found_filter -gt 7 ]; then
			add_skbedit_filter $net_ifname $qdisc_id $qos_queue \
			    $FCOE_ETHERTYPE $fcoe_filter_id
		elif [ $found_filter -ne $qos_queue ]; then
			[ ${DEBUG_LOGGING} ] && $LOGGER \
			    "$net_ifname: Filter is found and QOS is different"
			replace_skbedit_filter $net_ifname $qos_queue \
				$FCOE_ETHERTYPE $fcoe_filter_id
		else
			[ ${DEBUG_LOGGING} ] && $LOGGER \
				"$net_ifname: Filter is found and is identical"
		fi

		# Adjust the FIP filter
		[ ${DEBUG_LOGGING} ] && $LOGGER "$net_ifname: Qdisc is found"
		find_skbedit_filter $net_ifname $FIP_ETHERTYPE $fip_filter_id
		found_filter=$?
		if [ $found_filter -gt 7 ]; then
			add_skbedit_filter $net_ifname $qdisc_id $qos_queue \
			    $FIP_ETHERTYPE $fip_filter_id
		elif [ $found_filter -ne $qos_queue ]; then
			[ ${DEBUG_LOGGING} ] && $LOGGER \
			    "$net_ifname: Filter is found and QOS is different"
			replace_skbedit_filter $net_ifname $qos_queue \
				$FIP_ETHERTYPE $fip_filter_id
		else
			[ ${DEBUG_LOGGING} ] && $LOGGER \
				"$net_ifname: Filter is found and is identical"
		fi
	else
		add_multiq_qdisc $net_ifname $qdisc_id
		add_skbedit_filter $net_ifname $qdisc_id $qos_queue \
			$FCOE_ETHERTYPE $fcoe_filter_id
		add_skbedit_filter $net_ifname $qdisc_id $qos_queue \
			$FIP_ETHERTYPE $fip_filter_id
	fi
fi

exit 0

