#!/bin/bash
#
# iscsi_offload
#
# Configure iSCSI offload engines for use with open-iscsi
# Usage:
#    iscsi_offload [-d | -f | -i <ipaddr> | -t ] <nic>
#
# Copyright (c) 2011 Hannes Reinecke, SUSE Labs
# This script is licensed under the GPL.
#
# The script creates an open-iscsi interface definition
# in the style <nic>-<module>, where <nic> matches the
# network interface passed on the commandline.
# If '-t' (test mode) is passed as an option, the script
# will not create nor modify any setting but just print
# the currently active ones.
#
# Currently the script works with Broadcom (bnx2i) and
# Chelsio T3 (cxgbi) iSCSI offload engines.
# Should work with Chelsio T4, but has not been tested.
# ServerEngines (be2iscsi) and QLogic (qla4xxx) can only
# be configured via BIOS, open-iscsi support is still in
# development.
#

#
# Return codes:
#    0: Success
#    1: Invalid command line parameter
#    2: iSCSI offloading not supported
#    3: Error during module loading
#    4: Cannot configure interface via iscsiadm, use BIOS setup
#    5: internal error running iscsiadm
#
# Output:
#    <mac> [none|dhcp|ip <ipaddr>|ibft]
# where
#    <mac>: MAC Address of the iSCSI offload engine
#    none:  No IP configuration set for the iSCSI offload engine
#    dhcp:  iSCSI offload engine configured for DHCP
#    ip:    iSCSI offload engine configured with static IP address <ipaddr>
#    ibft:  iSCSI offload engine configured from iBFT values
#

#
# Figure out the MAC address of the iSCSI offload engine
# corresponding to a NIC from a given PCI device.
# bnx2 is using one PCI device per port for both network and iSCSI offloading
# cxgb3 is using one PCI device for everything.
#
iscsi_macaddress_from_pcidevice()
{
    local path=$1
    local if=$2
    local h
    local host

    for h in $path/host* ; do
	if [ -d "$h" ] ; then
	    host=${h##*/}
	    read netdev < /sys/class/iscsi_host/$host/netdev
	    if [ "$netdev" = "$IFNAME" ] ; then
		read mac < /sys/class/iscsi_host/$host/hwaddress
		if [ "$mac" != "00:00:00:00:00:00" ] ; then
		    echo "$mac"
		fi
		break;
	    fi
	fi
    done
}

#
# Figure out the MAC address of the iSCSI offload engine
# corresponding to a NIC from a given PCI function.
# It is assumed that the MAC address of the iSCSI offload
# engine is equal of the MAC address of the NIC plus one.
# Suitable for be2iscsi and qla4xxx
#
iscsi_macaddress_from_pcifn()
{
    local path=$1
    local if=$2
    local h
    local host
    local ifmac
    local olemacoffset=$3

    ifmac=$(ip addr show dev $if | sed -n 's/ *link\/ether \(.*\) brd.*/\1/p')
    m5=$(( 0x${ifmac##*:} ))
    m5=$(( $m5 + $olemacoffset ))
    ifmac=$(printf "%s:%02x" ${ifmac%:*} $m5)
    for host in /sys/class/iscsi_host/host* ; do
	if [ -L "$host" ] ; then
	    read mac < $host/hwaddress
	    if [ "$mac" = "$ifmac" ] ; then
		echo "$mac"
		break;
	    fi
	fi
    done
}

update_iface_setting() {
    local iface="$1"
    local name="$2"
    local value="$3"

    iface_value=$(iscsiadm -m iface -I $iface | sed -n "s/$name = \(.*\)/\1/p")
    if [ "$iface_value" = "<empty>" ] ; then
	iface_value=
    fi
    if [ "$iface_value" != "$value" ] ; then
	if ! iscsiadm -m iface -I $iface -o update -n "$name" -v "$value" ; then
	    return 1
	fi
    fi
    return 0
}

while getopts di:t options ; do
    case $options in
	d ) mode=dhcp;;
	i ) mode=static
	    optaddr=$OPTARG
	    ;;
	f ) mode=firmware;;
	t ) dry_run=1;;
	?)  printf "Usage: %s [-d|-t|-i ipaddr|-f] ifname\n" $0
	    exit 1;;
    esac
done
shift $(($OPTIND - 1))

IFNAME=$1
ibft_mode="none"

if [ -z "$IFNAME" ] ; then
    echo "No interface specified"
    exit 1
fi

if [ "$dry_run" ] ; then
    if [ "$mode" = "dhcp" ] ; then
	echo "'-t' specified, ignoring '-d'"
	mode=
    elif [ "$mode" = "static" ] ; then
	echo "'-t' specified, ignoring '-s'"
	mode=
    fi
fi

if [ ! -L /sys/class/net/$IFNAME ] ; then
    echo "Interface $IFNAME not found"
    exit 1
fi

if [ "$optaddr" ] && ! ip route get $optaddr ; then
    echo "Invalid IP address $optaddr"
    exit 1
fi
if [ "$dry_run" ] ; then
    mode=
fi


ifpath=$(cd -P /sys/class/net/$IFNAME; echo $PWD)
pcipath=$(cd -P $ifpath/device; echo $PWD)

if [ -d $pcipath ] ; then
    drvlink=$(readlink $pcipath/driver)
    driver=${drvlink##*/}
fi

if [ -z "$driver" ] ; then
    echo "No driver found for interface $IFNAME"
    exit 1
fi

case "$driver" in
    bnx2*)
	mod=bnx2i
	;;
    cxgb*)
	mod=cxgb3i
	;;
    be2*)
	mod=be2iscsi
	;;
    qla*)
	mod=qla4xxx
	;;
    qed*)
	mod=qedi
	;;
esac

if [ -z "$mod" ] ; then
    echo "iSCSI offloading not supported on interface $IFNAME"
    exit 2
fi

# Check if the required modules are already loaded
loaded=$(sed -n "/^$mod/p" /proc/modules)
if [ -z "$loaded" ] ; then
    modprobe $mod
fi

loaded=$(sed -n "/^$mod/p" /proc/modules)
if [ -z "$loaded" ] ; then
    echo "Loading of $mod.ko failed, please check dmesg"
    exit 3
fi

# Get the correct MAC address for the various devices
if [ "$mod" = "bnx2i" ] ; then
    mac=$(iscsi_macaddress_from_pcidevice $pcipath $IFNAME)
elif [ "$mod" = "cxgb3i" ] ; then
    mac=$(iscsi_macaddress_from_pcidevice $pcipath $IFNAME)
elif [ "$mod" = "be2iscsi" ] ; then
    mac=$(iscsi_macaddress_from_pcifn $pcipath $IFNAME 1)
elif [ "$mod" = "qla4xxx" ] ; then
    mac=$(iscsi_macaddress_from_pcifn $pcipath $IFNAME 1)
elif [ "$mod" = "qede" -o "$mod" = "qedi" ] ; then
    mac=$(iscsi_macaddress_from_pcifn $pcipath $IFNAME 4)
fi

if [ -z "$mac" ] ; then
    echo "iSCSI offloading not supported on interface $IFNAME"
    exit 2
fi

gen_iface="$mod.$mac"
ioe_iface="${IFNAME}-${mod}"

# Get existing settings
if iscsiadm -m iface -I $ioe_iface > /dev/null 2>&1 ; then
    ioe_mac=$(iscsiadm -m iface -I $ioe_iface 2> /dev/null| sed -n "s/iface\.hwaddress = \(.*\)/\1/p")
    ioe_mod=$(iscsiadm -m iface -I $ioe_iface 2> /dev/null| sed -n "s/iface\.transport_name = \(.*\)/\1/p")
    ipaddr=$(iscsiadm -m iface -I $ioe_iface 2> /dev/null| sed -n "s/iface\.ipaddress = \(.*\)/\1/p")
    if [ "$ipaddr" == "<empty>" ] ; then
	ipaddr=
    fi
elif [ "$mod" = "be2iscsi" ] ; then
    ioe_mac=$mac
    ioe_mod=$mod
else
    # Create new interface
    iscsiadm -m iface -I $ioe_iface --op=new 2> /dev/null
    ioe_mac=
    ioe_mod=
    ipaddr=
fi

if [ -z "$dry_run" ] ; then
    if [ "$ioe_mac" != "$mac" ] ; then
	if [ -n "$ioe_mac" ] ; then
	    echo "Warning: Updating MAC address on iface $ioe_iface"
	fi
	update_iface_setting $ioe_iface iface.hwaddress "$mac"
    fi

    if [ "$ioe_mod" != "$mod" ] ; then
	if [ -n "$ioe_mod" ] ; then
	    echo "Warning: Update transport on iface $ioe_iface"
	fi
	update_iface_setting $ioe_iface iface.transport_name "$mod"
    fi
elif [ -z "$ipaddr" ] ; then
    ipaddr=$(iscsiadm -m iface -I $gen_iface 2> /dev/null| sed -n "s/iface\.ipaddress = \(.*\)/\1/p")
    if [ "$ipaddr" = "<empty>" ] ; then
	ipaddr=
    fi
elif [ "$ioe_mod" != "$mod" ] ; then
    echo "Warning: Transport mismatch on iface $ioe_iface: $ioe_mod should be $mod"
fi

# Check iBFT setting
for d in /sys/firmware/* ; do
    [ -d $d ] || continue
    [ -d $d/ethernet0 ] || continue
    iboot_dir=$d
done
if [ -n "$iboot_dir" ] && [ -d "$iboot_dir" ] ; then
    for if in ${iboot_dir}/ethernet* ; do
	read ibft_mac < $if/mac
	[ "$ibft_mac" = "$mac" ] || continue
	ibft_origin=0
	[ -f ${if}/origin ] && read ibft_origin < $if/origin
	if [ "$ibft_origin" -eq 1 ] ; then
	    ibft_mode="static"
	elif [ "$ibft_origin" -eq 3 ] ; then
	    ibft_mode="dhcp"
	fi
	[ -f $if/dhcp ] && read ibft_dhcp < $if/dhcp
	if [ -n "$ibft_dhcp" -a "$ibft_mode" != "dhcp" ] ; then
	    ibft_mode=dhcp
	fi
	if [ "$ibft_mode" = "dhcp" ] ; then
	    ibft_ipaddr="0.0.0.0"
	    ibft_gateway=
	    ibft_mask=
	    break
	fi
	[ -f $if/ip-addr ] && read ibft_ipaddr < $if/ip-addr
	[ -f $if/gateway ] && read ibft_gateway < $if/gateway
	[ -f $if/subnet-mask ] && read ibft_mask < $if/subnet-mask
	break
    done
fi

if [ -z "$optaddr" ] && [ "$ibft_ipaddr" ] ; then
    optaddr=$ibft_ipaddr
fi

# Check if the interface needs to be configured
if [ -z "$mode" ] ; then
    if [ "$ibft_mode" != "none" ] ; then
	echo "$mac ibft"
	mode="ibft"
    elif [ -z "$ipaddr" ] ; then
	echo "$mac none"
	mode="none"
    elif [ "$ipaddr" = "0.0.0.0" ] ; then
	echo "$mac dhcp"
	ipaddr=
	mode="dhcp"
    else
	echo "$mac ip $ipaddr"
	mode="static"
    fi
    [ "$dry_run" ] && exit 0
elif [ "$mode" = "dhcp" ] ; then
    if [ "$ipaddr" = "0.0.0.0" ] ; then
	echo "$mac dhcp"
	exit 0
    fi
    optaddr="0.0.0.0"
elif [ "$mode" = "static" ] && [ "$ipaddr" = "$optaddr" ] ; then
    echo "$mac ip $ipaddr"
    exit 0
fi

if [ "$mod" = "be2iscsi" ] ; then
    exit 4
fi

if ! update_iface_setting $ioe_iface iface.ipaddress "$optaddr" ; then
    echo "Failed to set IP address: $?"
    exit 1
fi
if ! update_iface_setting $gen_iface iface.ipaddress "$optaddr" ; then
    echo "Failed to set IP address for generic interface: $?"
    exit 1
fi

if ! update_iface_setting $ioe_iface iface.gateway "$ibft_gateway" ; then
    echo "Failed to set gateway address: $?"
    exit 1
fi

if ! update_iface_setting $gen_iface iface.gateway "$ibft_gateway" ; then
    echo "Failed to set gateway address for generic interface: $?"
    exit 1
fi

if ! update_iface_setting $ioe_iface iface.subnet_mask "$ibft_mask" ; then
    echo "Failed to set subnet mask: $?"
    exit 1
fi

if ! update_iface_setting $gen_iface iface.subnet_mask "$ibft_mask" ; then
    echo "Failed to set subnet mask for generic interface: $?"
    exit 1
fi

if [ "$mod" = "qla4xxx" ] ; then
    iscsiadm -m iface -H $mac -o applyall
fi
ip link set dev $IFNAME up

exit 0

