#!/bin/bash
# Check if this host's MAC address is correct in /etc/ethers.
# On each subnet, the Linux server with the lexically lowest IP address
# will also ping every other host on the subnet and check if its MAC
# address is correct.  Command line args:
#	-s	If given, hosts in /etc/hosts but not /etc/ethers are
#		complained about; if omitted, they are ignored silently.
#	-d	Print irrelevant stuff useful for debugging.
#	-a	Ping all hosts even though you aren't the leader, for debugging.

# Timing (on Intel Core Duo E8400 at 3.0 GHz), load average was zilch.
# Task was to check its own MAC address, plus ping all on a subnet with
# 222 assigned IP addresses (most but not all were up).
# User 1.008 sec, system 1.540 sec, elapsed 175 sec, load average 0.014
# Not counting load on YP server.

# $Header: /src/math/lib/daily/RCS/D60ethers,v 1.1 2008/11/06 21:01:20 jimc Exp $

# $Log: D60ethers,v $
# Revision 1.1  2008/11/06 21:01:20  jimc
# Initial revision
#

echo "== Check Ethernet addresses"
opt_s=0
opt_d=''
while [ $# -gt 0 ] ; do
    case X$1 in
	X-s )	opt_s=1 ; ;;
	X-d )	opt_d=1 ; ;;
	X-a )	opt_a=1 ; ;;
	* )	echo "Unrecognized command line argument '$1' ignored" ; ;;
    esac
    shift
done

if [ -z "$scrfil" ] ; then scrfil=/tmp/system/D60ethers.tmp ; fi

# Prints an error message (all command line args) preceeded by the global
# variables host, ifc, mac, macsun, inet.
function barf() {
    echo -e "'$host' $ifc '$mac'/'$macsun'\n    '$inet': $*"
}

# Converts an IP address as a dotted quad to a 32-bit integer, possibly signed.
function ip2int () {
    local ip=$1
    local IFSold="$IFS"
    IFS=' .'
    local int=0
    local octet
    for octet in $ip ; do
	int=$(((int<<8)+octet))
    done
    IFS="$IFSold"
    echo $int
}

# $1 = an IP address, $2 = CIDR number of bits, $3 = filename, $4 = nonnull or
# null string.  The file's column 1 = IP addresses, 2 = IP as integer.  
# This subroutine emits on stdout the IP addresses that are on the same
# subnet as $1.  If $4 is nonnull, only the first is emitted (else all of
# them).  
function insubnet () {
    local mask=$((~((1<<(32-$2))-1)))
    local subnet=`ip2int $1`
    subnet=$((subnet&mask))
    local fname=$3
    local once=''
    if [ -n "$4" ] ; then once=";exit" ; fi
    awk -v subnet=$subnet -v mask=$mask '$1 ~ /[0-9][0-9]*\./ {if (and($2, mask) == subnet) {print $1'"$once"'}}' $fname
}

# Canonicalizes a MAC address to Sun format: lower case, no leading 0's.
# Input on stdin, output on stdout.
function canosun () {
    sed -e 'y/ABCDEF/abcdef/' -e 's/0\([0-9a-f]\)/\1/g'
}

# Does the research to check a MAC address, and prints error messages on stdout.
# Arguments: $1 = MAC address, $2 = IP address, $3 = 1 to complain if a host
# is not in /etc/ethers, 0 to skip it.  
# Returns 0 if OK, 1 if error.
function checkmac () {
    mac=$1
    inet=$2
    strict=$3
    sleep 0.1
		# Canonicalize MAC address to Sun format.
    macsun=`echo $mac | canosun`
		# Reverse map to the hostname
    host=`dig +short -x $inet | sed -e 's/\.$//'`
    if [ -z "$host" ] ; then
	barf "IP has no hostname"
	return 1
    fi
		# What NIS thinks the MAC address is.  For Sun machines this
		# will be lower case without leading 0's, but this is not
		# necessarily true for Linux, so normalize it.
    local ethersrev=`ypmatch $host ethers.byname 2> /dev/null | awk '{print $1}' | canosun`
    if [ -n "$ethersrev" ] ; then
	:
    elif [ $strict -eq 0 ] ; then
	return 0
    else
	barf "host is not in /etc/ethers"
	return 1
    fi
    if [ "$macsun" != "$ethersrev" ] ; then
	barf "discrepancy: /etc/ethers says $ethersrev"
	return 1
    fi
    ethersfwd=`ypmatch $macsun ethers.byaddr 2> /dev/null`
    if [ -z "$ethersfwd" ] ; then
	barf "actual MAC is not in /etc/ethers"
	return 1
    fi
    return 0
}


# Everyone checks his own MAC address(es).
# It's in /sys/class/net/*/address (lower case with leading 0's)
# NOTE, restricted to eth* ; if we ever need wlan* or the like, this must 
# be changed.
nifcs=0
for idir in /sys/class/net/eth* ; do
    ifc=${idir##*/}
    if [ ! -r $idir/address ] ; then continue ; fi
		# Capture normal and Sun-style MAC addresses
    mac=`cat $idir/address`
		# Bypass loopback address
    if [ "$mac" = "00:00:00:00:00:00" ] ; then continue ; fi
		# Get the interface's IP address and corresponding hostname.
    inetm=`ip addr show dev $ifc | sed -e '/inet /!d' -e 's/^.*inet //' -e 's/ .*$//'`
    inet=${inetm%/*}
    if [ -z "$inet" -o "$inet" = "0.0.0.0" ] ; then continue ; fi
    inets[$nifcs]=$inet;
    masks[$nifcs]=${inetm#*/}
    ifcs[$nifcs]=$ifc;
    checkmac $mac $inet 1
    nifcs=$((nifcs+1))
done

# For each of my interfaces, am I the (lexically) lowest numbered server on
# that subnet? 
# $scrfil = (IP, IP as int, 1-component hostname) sorted by hostname, excluding 
# zero and 255 broadcast addresses.  Erratic whitespace messes up the sort,
# and "awk" normalizes it.
IFSold="$IFS"
IFS=".$IFS"
ypcat hosts | while read ip1 ip2 ip3 ip4 host1 junk ; do
    case "$ip1" in
        [0-9]* )	: ; ;;		#has numeric IP address
        * )		continue ; ;;	#lose comments
    esac
    case "$ip4" in
	0 | 255 | '' )	continue ; ;;	#lose broadcast addresses
    esac
    echo "$ip1.$ip2.$ip3.$ip4" $(($ip4+256*($ip3+256*($ip2+256*$ip1)))) $host1
done | sort -k 3,3 -u -o "$scrfil"
IFS="$IFSold"

# $scrfil.boxes = IP addresses of all Linux servers, lexically sorted
hostgroup linux@server+amanda-rogue | \
    join -2 3 -o 2.1 2.2 - $scrfil | \
    sort -o $scrfil.boxes
# leaders = list of subnets for which this host is the lowest numbered 
# Linux server.  
if [ -n "$opt_d" ] ; then printf "%-16s %-16s %s\n" Subnet Leader "" ; fi
k=0
while [ $k -lt $nifcs ] ; do
    ifc=${ifcs[$k]}
    inet=${inets[$k]}
    leader=`insubnet $inet ${masks[$k]} $scrfil.boxes first`
    isleader=''
    kldr=0
    if [ "$leader" = "$inet" -o -n "$opt_a" ] ; then 
	leaders="$leaders $inet"
	isleader=' (leader)'
	if [ "$leader" != "$inet" ] ; then isleader=' (debug)' ; fi
	kldr=1
    fi
    isldrs[$k]=$kldr
    if [ -n "$opt_d" ] ; then printf "%-16s %-16s %s\n" $inet $leader $isleader ; fi
    k=$((k+1))
done

# For all subnets (if any) for which I am the lowest numbered host, 
# ping all hosts on that subnet
# and verify the MAC address.  "Down" hosts, and the local host, are not
# detected and are skipped silently.  (Hostgroup server includes no hosts
# on the backdoor net, so someone has to do the check.)
k=0
while [ $k -lt $nifcs ] ; do
    if [ ${isldrs[$k]} -eq 0 ] ; then k=$((k+1)) ; continue ; fi
    ifc=${ifcs[$k]}
    insubnet ${inets[$k]} ${masks[$k]} $scrfil > $scrfil.subnet
    cat $scrfil.subnet | while read inet junk ; do
	ping -q -c 1 -w 2 $inet > /dev/null 2>&1
	mac=`arp -n -a $inet 2> /dev/null | awk '$4 ~ /^[0-9a-fA-F:]*$/ && $4 !~ /incomplete/ {print $4}'`
	if [ -z "$mac" ] ; then continue ; fi
	checkmac $mac $inet $opt_s
    done
    k=$((k+1))
done
if [ -z "$opt_d" ] ; then rm -f $scrfil.boxes $scrfil.subnet ; fi
