#!/bin/bash
# vim: cindent:shiftwidth=4:tabstop=4:smarttab:textwidth=100

#$title$ PHYsical layer network interface configuration
#$check$ auto-negotiation, duplex, speed settings
#$ref$ KB:InterfaceBonding
#$author$ Rafal Rzeczkowski
#$version$ 0.9.1

set -o posix
set -o errexit
set -o pipefail
set -o nounset
#set -o xtrace

level_check short

#CHANGELOG
#0.50	initial based on nic-errors
#0.51	skip virtual interfaces in an HVM
#0.52	remove TMP file for skipped interfaces
#0.53	override using hard-coded ethtool data from /etc/network/interfaces
#0.54	skip virtio_net under Debian 8 (now supports driver info query)
#0.55	display driver name in good status summary line
#0.56	accommodate systems which report "Link detected: yes" for inactive interfaces
#0.57	Microsoft Hyper-V uses "tulip" driver - no status monitoring (https://wiki.debian.org/WindowsServerHyperV)
#0.58	redesigned ethtool data collection for direct ingestion via shell eval
#0.59	accept disabled auto-negotiation on PHYs that do not support it
#0.60	check SFP transceiver status
#0.61	restyle according to https://kb.clearcable.ca/KB/ProgrammingStyleStandards
#0.62	drop Sun_Fire_V240 workaround
#0.63	ignore status analysis on SFP+ ports with Direct Attach Copper connected
#0.64	check large_receive_offload status on active interfaces (packet loss on 10GE+LACP)
#0.65	ignore features which are not available, e.g. udp-fragmentation-offload on Linux 4.14
#0.66	ignore all feature metadata (not only "[fixed]")
#0.67	enumerate physical network interfaces based on sysfs symlink name components
#0.68	skip Cisco VIC interfaces
#0.69	skip module-info check (only) on Cisco VIC interfaces
#0.70	exclude non-Ethernet interfaces from enumeration (e.g. Controller Area Network)
#0.8.0	analyze both alarm and warning exceptions for SFP+ FIBRE modules
#0.8.1	ignore SFP+ module serial number if not available
#0.8.2	report link down status on interfaces configured for auto startup
#0.8.3	ignore ports without link that are part of a bridge
#0.8.4	ignore Wireless LAN interfaces
#0.9.0	enumerate configured interfaces via systemd 252 networkctl JSON API on SOE5
#0.9.1	assign half duplex by default to IEEE 802.11 interfaces

declare -r SYSFS='/sys/class/net'

declare -r EXCEPTION_STATE_OK='Off'
declare -A EXCEPTION_TYPE_MAP
EXCEPTION_TYPE_MAP[alarm]='critical'
EXCEPTION_TYPE_MAP[warning]='warning'
readonly EXCEPTION_TYPE_MAP

NIC_PHY=$(find $SYSFS -type l \
	-not -lname '*/devices/virtual/*' \
	-not -lname '*/devices/vif*' \
	-not -name 'wlan*' \
	-printf '%f\n' |
	sort --numeric-sort)

declare -a nic_configured
if [[ -d '/etc/systemd/network' ]]; then
	# from nic-config:
	networkctl_list=$(networkctl list --full --json=pretty |
    perl -e'
use JSON;
use feature qw{say};

%export=%{decode_json join q{}, <>};
my @keys = qw{Index Name Type OnlineState AdministrativeState OperationalState CarrierState AddressState};

foreach my $interface_ref ( @{ $export{Interfaces} } ) {
    say join qq{\t}, map { $interface_ref->{$_}//"NULL" } @keys;
}')
	while read -r Index Name Type OnlineState AdministrativeState OperationalState CarrierState AddressState; do
		#echodebug "$(printf '%3d %-6s %-8s %-11s %-10s' $Index $Name $Type $OperationalState $AdministrativeState)"
		if [[ $AdministrativeState = 'configured' ]]; then
			nic_configured+=($Name)
		fi
	done <<< "$networkctl_list"
elif [[ -f '/etc/network/interfaces' ]]; then
	ifquery_list=$(ifquery --all --list)
	mapfile -t nic_configured <<< "$ifquery_list"
else
	echodebug 'unsupported network configuration framework'
	exit 1
fi
echodebug "NIC(s) configured: ${nic_configured[@]}"

cd $SYSFS
VALID_IF_COUNT=0
for ETH in $NIC_PHY; do
test -d $ETH || continue
INDENT=${ETH//[[:alnum:]]/ }	# subsequent output lines

BROADCAST_ADDRESS=$(<$ETH/broadcast)
if [[ "$BROADCAST_ADDRESS" != 'ff:ff:ff:ff:ff:ff' ]]; then
	# not Ethernet
	continue
fi

TMP_DRIVER=$(mktemp)
if ! ethtool --driver $ETH >$TMP_DRIVER; then
	rm $TMP_DRIVER
	echodebug "$ETH driver query failed - possibly a virtual device (skip)"
	continue
fi

DRIVER=$(awk -F': ' '{if ($1=="driver"){print $2}}' $TMP_DRIVER)
rm $TMP_DRIVER

test -n "$DRIVER"
case $DRIVER in
	virtio_net)
		echodebug "$ETH {$DRIVER} is a generic virtual device (skip)"
		continue
		;;
	tulip)
		echodebug "$ETH {$DRIVER} is a Microsoft Hyper-V virtual device (skip)"
		continue
		;;
esac

((VALID_IF_COUNT++))
eval $(
ethtool $ETH |
awk '{if (!sub("^[[:space:]]","",$0)){next};if ($0~":"){print ""};print}' |
awk '
BEGIN {
	RS=""
	FS=":[[:space:]]+"
}
{
	gsub("[^[:alnum:]]","_",$1);

	# value normalization
	if ($1=="Speed")
		sub("Mb/s","",$2)
	if ($1=="Duplex")
		$2=tolower($2)
	if ($1=="Supported_ports")
		gsub("[][]","",$2)

	if ($1~"s$")
		print $1 "=(" $2 ")"
	else
		print $1 "=\"" $2 "\""
}')

if [[ "$Link_detected" != 'yes' ]]; then
	if [[ -d /sys/class/net/$ETH/brport ]]; then
		echodebug "$ETH link status is {$Link_detected} (ignored; part of a bridge)"
		continue
	elif ! is_element_of $ETH ${nic_configured[@]}; then
		echodebug "$ETH link status is {$Link_detected} (ignored; not configured for auto start)"
		continue
	else
		fail warning "$ETH link status is {$Link_detected}"
		helpmsg 'check cable and switch configuration'
		exit
	fi
fi

# hard-coded PHY setting overrides (SOE4 only)
if [[ -f '/etc/network/interfaces' ]]; then
eval $(awk --assign DEV=$ETH '
{
if ($1 == "iface") { iface=$2; }
if ($1 == "") { iface=""; }
if (iface == DEV) {
	if ($1 == "pre-up" && $2 == "ethtool" && $3 == "--change" && $4 == "$IFACE" ) {
		$1="";$2="";$3="";$4="";
		split($0,args," ");
		for (i=1; i<length(args); i+=2) {
			print args[i] "=" args[i+1];
		}
	}
}
}' '/etc/network/interfaces')
fi

if [[ -L /sys/class/net/$ETH/phy80211 ]]; then
	# IEEE 802.11
	duplex='half'
fi

SPEED_FIXED=''
SPEED_TARGET=$Speed
if [[ ${speed+xxx} ]]; then
	if [[ -n ${speed} ]]; then
		SPEED_TARGET=$speed
		SPEED_FIXED='!'
	fi
fi

DUPLEX_FIXED=''
DUPLEX_TARGET='full'
if [[ ${duplex+xxx} ]]; then
	if [[ -n ${duplex} ]]; then
		DUPLEX_TARGET=$duplex
		DUPLEX_FIXED='!'
	fi
fi

AUTONEG_FIXED=''
AUTONEG_TARGET='on'
if [[ ${autoneg+xxx} ]]; then
	if [[ -n ${autoneg} ]]; then
		AUTONEG_TARGET=$autoneg
		AUTONEG_FIXED='!'
	fi
fi
if [[ $Supports_auto_negotiation = 'No' ]]; then
	AUTONEG_TARGET='off'
	AUTONEG_FIXED='*'
fi

if [[ $Auto_negotiation != $AUTONEG_TARGET ]]; then
	fail warning "$ETH auto-negotiation is $Auto_negotiation, expected $AUTONEG_TARGET$AUTONEG_FIXED"
	helpmsg 'auto-negotiation is REQUIRED for GE+ speeds; check switch configuration'
	exit
fi

if [[ $Duplex != $DUPLEX_TARGET ]]; then
	fail warning "$ETH duplex is $Duplex, expected $DUPLEX_TARGET$DUPLEX_FIXED"
	helpmsg 'check switch configuration'
	exit
fi

if [[ $Speed != $SPEED_TARGET ]]; then
	fail warning "$ETH speed is $Speed, expected $SPEED_TARGET$SPEED_FIXED"
	helpmsg 'check switch configuration'
	exit
fi

echodebug "$ETH $DRIVER $Port PHY OK ($Speed$SPEED_FIXED/$Duplex$DUPLEX_FIXED/$Auto_negotiation$AUTONEG_FIXED)"
unset speed duplex autoneg

# features
large_receive_offload=""
eval $(ethtool --show-features $ETH 2>/dev/null |
awk '
BEGIN {
	FS=":[[:space:]]+"
}
{
	if (NR==1)
		next
	sub("^[[:space:]]+","",$1)
	gsub("-","_",$1)
	sub("[[:space:]][[][[:alpha:][:space:]]+[]]$","",$2)
	print $1"="$2
}')
echodebug "$INDENT large_receive_offload=$large_receive_offload"
if [[ $large_receive_offload != 'off' ]]; then
	fail warning "$ETH interface feature {large_receive_offload} is enabled"
	helpmsg "disable with {ethtool --features $ETH lro off}"
	exit
fi

if [[ $Port = 'FIBRE' ]]; then
	unset "${!MI_@}" # cleanup variables from the previous loop (if any)

	eval $(
	ethtool --module-info $ETH |
	awk '
BEGIN {
	FS=":[[:space:]]+"
}
{
	gsub("^[[:space:]]+","",$1)
	gsub("[[:space:]]+$","",$1)
	gsub("[^[:alnum:]]","_",$1)
	gsub("[[:space:]]+$","",$2)

	if (match($2,/^([[:digit:]]+[.][[:digit:]]+) V$/,value))
		$2=value[1]
	else if (match($2,/^([-]?[[:digit:]]+[.][[:digit:]]+) degrees C/,value))
		$2=value[1]
	else if (match($2,/^([[:digit:]]+[.][[:digit:]]+) mW [/] ([-]?[[:digit:]]+[.][[:digit:]]+) dBm/,value))
		$2=value[2]
	else if (match($2,/^([[:digit:]]+[.][[:digit:]]+) mA/,value))
		$2=value[1]

	print "MI_" $1 "=\"" $2 "\""
}')

	if [[ -n "${MI_Vendor_name+xxx}" ]]; then
		echodebug "$INDENT $MI_Vendor_name $MI_Vendor_PN/$MI_Vendor_rev (${MI_Vendor_SN:-SN:N/A}) $MI_Laser_wavelength $MI_BR__Nominal"
		echodebug "$INDENT TX:${MI_Laser_output_power}dBm; RX:${MI_Receiver_signal_average_optical_power}dBm; Vcc:${MI_Module_voltage}V"

		for EXCEPTION_TYPE in 'alarm' 'warning'; do
			declare -i PARAMETER_COUNT=0
		    for KEY in ${!MI_@}; do
				if [[ $KEY =~ _${EXCEPTION_TYPE}$ ]]; then
					PARAMETER_COUNT=$((PARAMETER_COUNT+1))
					VALUE=${!KEY}
					KEY=${KEY##MI_}
					KEY=${KEY%%_${EXCEPTION_TYPE}}
					KEY=${KEY//_/ }
					if [[ $VALUE != $EXCEPTION_STATE_OK ]]; then
						fail ${EXCEPTION_TYPE_MAP[$EXCEPTION_TYPE]} "$ETH FIBRE module ${EXCEPTION_TYPE}: $KEY"
						exit
					fi
				fi
			done
			echodebug "$INDENT all $PARAMETER_COUNT ${EXCEPTION_TYPE}s are $EXCEPTION_STATE_OK"
		done
	fi # module-info available
fi # FIBRE

done # ETH

if [[ $VALID_IF_COUNT -gt 0 ]]; then
	ok
else
	echodebug 'no physical Ethernet interfaces found'
	unknown
fi
