#!/usr/bin/perl
# Program:	hp-search-mac
# Version:      0.1.3
# Summary:
#	This program takes a mac address (and some switches to query)
#	and output the physical port address that it belongs to in best
#	match order.
#
# Copyright:
#
#   Copyright (C) 2002-2008 Ola Lundqvist <ola@inguza.com>
#
#  This program is free software; you can redistribute it and/or modify it
#  under the terms of the GNU General Public License as published by the
#  Free Software  Foundation; either version 2, or (at your option) any
#  later version.
#
#  This program is distributed in the hope that 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 with
# the source package as the file COPYING.  If not, write to the Free
# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
# Notes:
#    Needs the the perl modules: libnet-snmp-perl and opalmod
#    to work properly.
#

###############################################################################
########################## Uses ###############################################
###############################################################################

use OpaL::action qw(pdebug setDebugLevel);
use strict;
use vars qw(%switchMacAddresses
	    %switchMacCount
	    %switches
	    $snmplookedup);
use Net::SNMP;

###############################################################################
########################## Defaults ###########################################
###############################################################################

my $macToPortOid = "17.4.3.1.2";
my $ifDescr = "2.2.1.2";
my $base = ".1.3.6.1.2.1";
%switchMacAddresses = ();
%switchMacCount = ();
$snmplookedup = 0;
my $etcconfigfile = "/etc/hp-search-mac.conf";
my $userconfigfile = "$ENV{HOME}/.hp-search-mac.conf";
%switches = ();

###############################################################################
######################### Configuration #######################################
###############################################################################

if (-e $etcconfigfile) {
  my $t = do $etcconfigfile;
  unless ($t) {
    pdebug(4, "Loading config file $etcconfigfile:\n\t$!\n\t$@");
  }
}

if (-e $userconfigfile) {
  my $t = do $userconfigfile;
  unless ($t) {
    pdebug(4, "Loading config file $userconfigfile:\n\t$!\n\t$@");
  }
}

###############################################################################
########################### HELP ##############################################
###############################################################################

my $help = "
hp-search-mac [options] mac [ [options] mac ] [...]

options:
  --dl n
		Set debug level to n.
  --help
		Print this help.
  --hostname host[=>community] | -h host[=>community]
		Set a switch name to search. You can also set this in
		$etcconfigfile or $userconfigfile
		The hash to set is \%switches.

Mac:
  You can use some different formats:
	ff:ff:ff:ff:ff:ff (hex)
	ff-ff-ff-ff-ff-ff (hex)

        0.123.0.12.0.12.0 (dec)

\%switches:
	This is a normal perl hash of the form
	\%switches = (\"host\" => \"community\", ...);

";

###############################################################################
########################## Arguments ##########################################
###############################################################################

while ($_ = shift @ARGV) {
    my $h = "[0-9a-fA-F]";
    my $dh = "$h$h?";
    my $d = "[0-9]";
    my $dd = "$d$d?$d?";
    if (/^--/) {
	if (/^--help$/) {
	    print($help);
	    exit;
	}
	elsif (/^--dl$/) {
	    setDebugLevel(shift @ARGV);
	}
	elsif (/^--hostname$/) {
		my @split = split /=>/, shift @ARGV;
		my $host = $split[0];
		my $port = $split[1];
		$port = "public" if ("" eq $port);
		$switches{$host} = $port;
	}
    }
    elsif (/^-/) {
	s/^\-//;
        my $t = $_;
        foreach (split //, $t) {
	    if (/h/) {
		my @split = split /=>/, shift @ARGV;
		my $host = $split[0];
		my $port = $split[1];
		$port = "public" if ("" eq $port);
		$switches{$host} = $port;
	    }
	    else {
		pdebug(2, "Unknown argument $_.");
	    }
	}
    }
    elsif (/^$dh:$dh:$dh:$dh:$dh:$dh/) {
	print ("MAC (hex): $_");
	$_ = hexToDec($_);
	s/:/./g;
	print(" (dec): $_\n");
	&searchFor($_);
    }
    elsif (/^$dh-$dh-$dh-$dh-$dh-$dh/) {
	print ("MAC (hex): $_");
	$_ = hexToDec($_);
	s/-/./g;
	print(" (dec): $_\n");
	&searchFor($_);
    }
    elsif (/^$dd.$dd.$dd.$dd.$dd.$dd/) {
	print ("MAC (dec): $_\n");
	&searchFor($_);
    }
    else {
	pdebug(2, "Unknown argument '$_'.");
    }
}

###############################################################################
# Name:		searchPrepare
# Uses:		%switchMacAddresses, %switchMacCount, $snmplookedup
# Needs:	%switches
# Description:	Fetch data from the switches using SNMP.
# Changes:
#	2002-02-26	Ola Lundqvist <ola@inguza.com>
#		Wrote it.
###############################################################################
sub searchPrepare() {
    if (%switches == ()) {
	pdebug(2, "No switches to search. Set it using the --hostname option\n".
	       "or the %switches hash in one of the configuration files.");
    }
    if (! $snmplookedup) {
	%switchMacAddresses = lookupMacOnServers(%switches);
	%switchMacCount = lookupCountOfMac(%switchMacAddresses);
	$snmplookedup = 1;
    }
}

###############################################################################
# Name:		searchFor
# Arguments:	A mac address in decimal form.
# Changes:
#	2002-02-26	Ola Lundqvist <ola@inguza.com>
#		Wrote it.
###############################################################################
sub searchFor($) {
    my ($decMac) = @_;
    &searchPrepare();
    my %matchCount = switchCountForMatching($decMac, %switches);
    my @orderedList = keysForHashSortedByValues(%matchCount);
    print("Search in count order (lowest count => best match):\n");
    foreach my $switch (@orderedList) {
	print("Switch: $switch, ");
	my $port = $switchMacAddresses{$switch}{$decMac};
	my $sport = hpPortNameForDecMac($port, $switch, $switches{$switch});
	print("port: $sport ($port), ");
	print("count: ".$switchMacCount{$switch}{$port});
	print("\n");
    }
}

###############################################################################
# Name:		keysForHashSortedByValues
# Arguments:	a hash
# Returns:	A list of the keys sorted by the value.
# Changes:
#	2002-02-26	Ola Lundqvist <ola@inguza.com>
#		Wrote it.
###############################################################################
sub keysForHashSortedByValues(%) {
    my (%hash) = @_;
    return sort { $hash{$a} <=> $hash{$b} } keys(%hash);
}

###############################################################################
# Name:		switchCountForMatching
# Arguments:	mac address in decimal form
#		switch hash (host => community)
# Returns:	hash with counts of the number of macs for each port matching
#		a specific mac address.
#		switch => count (mr of mac:s found)
# Changes:
#	2002-02-26	Ola Lundqvist <ola@inguza.com>
#		Wrote it.
###############################################################################
sub switchCountForMatching($%) {
    my ($decMac, %switches) = @_;
    my %ret = ();
    foreach my $switch (keys %switches) {
	my $port = ${$switchMacAddresses{$switch}}{$decMac};
	$ret{$switch} =
	    $switchMacCount{$switch}{$port} if (defined $port);
    }
    return %ret;
}

###############################################################################
# Name:		lookupMacOnServers
# Arguments:	switch hash (host => community)
# Returns:	hash with mac to port mapping for each switch
#		switch => mac => port
# Changes:
#	2002-02-26	Ola Lundqvist <ola@inguza.com>
#		Wrote it.
###############################################################################
sub lookupMacOnServers(%) {
    my %switches = @_;
    my %ret = ();
    foreach my $switch (keys %switches) {
	my %temp = hpSnmpToDecMac("$base.$macToPortOid",
				  getTable("$base.$macToPortOid",
					   $switch,
					   $switches{$switch}));
	$ret{$switch} = { %temp }; #{ %temp };
    }
    return %ret;
}

###############################################################################
# Name:		lookupCountOfMac
# Arguments:	switch hash (host => community)
# Returns:	hash with port to count mapping for each switch.
#		switch => port => count
# Changes:
#	2002-02-26	Ola Lundqvist <ola@inguza.com>
#		Wrote it.
###############################################################################
sub lookupCountOfMac(%) {
    my (%switchMacAddresses) = @_;
    my %ret = ();
    foreach my $switch (keys %switchMacAddresses) {
	my %temp = countsOfHashValues(%{$switchMacAddresses{$switch}});
	$ret{$switch} = { %temp };
    }
    return %ret;
}

###############################################################################
# Name:		hpSnmpToDecMac
# Arguments:	A base OID to strip.
#		A hash with the OID from the HP SNMP result as keys.
# Returns:	The same hash but with the OIDs converted (tripped the
#		prepended base) to decimal MAC addresses.
# Changes:
#	2002-02-25	Ola Lundqvist <ola@inguza.com>
#		Wrote it.
###############################################################################
sub hpSnmpToDecMac($%) {
    my ($base, %hash) = @_;
    my %ret = ();
    foreach my $key (keys %hash) {
	my $newkey = $key;
	$newkey =~ s/^$base\.//;
	$ret{$newkey} = $hash{$key};
    }
    return %ret;
}

###############################################################################
# Name:		hpPortNameForDecMac
# Arguments:	port (logical switch port)
#		switch to connect to
#		community to use on that switch
# Returns:	The "physical" name of that logical switch port
#		(ie. description).
# Changes:
#	2002-02-26	Ola Lundqvist <ola@inguza.com>
#		Wrote it.
###############################################################################
sub hpPortNameForDecMac($$$) {
    my ($port, $server, $community) = @_;
    my $ret = get("$base.$ifDescr.$port", $server, $community);
    $ret = $port if ("" eq $ret);
    return $ret;
}

###############################################################################
# Name:		countsOfHashValues
# Argument:	A hash.
# Returns:	A hash with the values of the argument hash as keys
#		and for each time that key is used it values is incresed by
#		one.
# Changes:
#	2002-02-25	Ola Lundqvist <ola@inguza.com>
#		Wrote it.
###############################################################################
sub countsOfHashValues(%) {
    my (%hash) = @_;
    my %ret = ();
    foreach my $key (keys %hash) {
	$ret{$hash{$key}}++;
    }
    return %ret;
}

###############################################################################
# Name:		getSession
# Arguments:	server (switch) and optionally (defult public) community.
# Returns:	A session object that can be used to query the SNMP database.
# Changes:
#	2002-02-25	Ola Lundqvist <ola@inguza.com>
#		Wrote it.
#	2002-09-12	Ola Lundqvist <ola@inguza.com>
#		Now is a bit more verbose when the connection fails.
###############################################################################
sub getSession($;$) {
    my ($server, $community) = @_;
    $community = "public" if ("" eq $community);
    return Net::SNMP->session(-hostname => $server,
			      -community => $community);
}

###############################################################################
# Name:		get
# Arguments:	oid to get value of
#		server to query
#		community (optionally) to use.
# Returns:	the value of that oid, undefined if not found.
# Changes:
#	2002-02-25	Ola Lundqvist <ola@inguza.com>
#		Wrote it.
###############################################################################
sub get($$;$) {
    my ($oid, $server, $community) = @_;
    my ($session, $error) = getSession($server, $community);
    if (!defined($session)) {
	pdebug(2, sprintf("Get session %s", $error));
    }
    my $response = $session->get_request($oid);
    $session->close();	
    if (!defined($response)) {
	return undef;
    }
    return $response->{$oid};
}

###############################################################################
# Name:		getTable
# Arguments:	oid to get all values of
#		server to query
#		community (optionally) to use.
# Returns:	the hash table of that oid, undefined if not found.
#		oid => value
# Changes:
#	2002-02-25	Ola Lundqvist <ola@inguza.com>
#		Wrote it.
#	2002-09-12	Ola Lundqvist <ola@inguza.com>
#		Now is a bit more verbose when the connection fails.
###############################################################################
sub getTable($$;$) {
    my ($baseoid, $server, $community) = @_;
    my ($session, $error) = getSession($server, $community);
    if (!defined($session)) {
	pdebug(2, $error);
    }
    my $ref = $session->get_table($baseoid);
    $session->close();
    if (! defined $ref) {
	pdebug(3,
	       "Can not get table from $server, probably authentication ".
	       "or connection failure.");
	return undef;
    }
    return %$ref;
}

###############################################################################
# Name:		hexToDec
# Argument:	A string.
# Returns:	A string with all things that can be treated as numbers
#		converted to decimal form.
# Example:	00 is converted o 0
#		!banjo is converted to !202njo
# Changes:
#	2002-02-25	Ola Lundqvist <ola@inguza.com>
#		Wrote it.
###############################################################################
sub hexToDec($) {
    my ($arg) = @_;
    my $ret = "";
    my $val;
    undef $val;
    foreach my $tmp (split //, $arg) {
	if ($tmp =~ /[0-9a-fA-F]/) {
	    if ($tmp eq "a" || $tmp eq "A") {
		$tmp = 10;
	    }
	    if ($tmp eq "b" || $tmp eq "B") {
		$tmp = 11;
	    }
	    if ($tmp eq "c" || $tmp eq "C") {
		$tmp = 12;
	    }
	    if ($tmp eq "d" || $tmp eq "D") {
		$tmp = 13;
	    }
	    if ($tmp eq "e" || $tmp eq "E") {
		$tmp = 14;
	    }
	    if ($tmp eq "f" || $tmp eq "F") {
		$tmp = 15;
	    }
	    if (defined $val) {
		$val = $val * 16 + $tmp;
	    }
	    else {
		$val = $tmp;
	    }
	}
	else {
	    $ret .= "$val$tmp";
	    undef $val;
	}
    }
    $ret .= $val;
    return $ret;
}

__END__

=head1 NAME

hp-search-mac  -- HP switch search tool.

=head1 DESCRIPTION

 HP-search-MAC is an util that can query HP switches for their
 connection table. It then allow you to search for a MAC address and tell you
 where it is physically connected (best match first).
 .
 The functionality is similar to traceroute but on Ethernet level and only
 for HP switches.

=head1 USAGE

hp-search-mac [options] [MAC] [...]

=head1 OPTIONS

hp-search-mac [options] mac [ [options] mac ] [...]

options:
  --dl n
		Set debug level to n.
  --help
		Print this help.
  --hostname host[=>community] | -h host[=>community]
		Set a switch name to search. You can also set this in
                /etc/hp-search-mac.conf or ~/.hp-search-mac.conf.
		The hash to set is \%switches.

Mac:
  You can use some different formats:
	ff:ff:ff:ff:ff:ff (hex)
	ff-ff-ff-ff-ff-ff (hex)

        0.123.0.12.0.12.0 (dec)

CONFIGURATION:

\%switches:
	This is a normal perl hash of the form
	\%switches = (\"host\" => \"community\", ...);

=head1 AUTHOR

Ola Lundqvist <ola@inguza.com>

=head1 SEE ALSO

snmp-walk(1)
snmp-get(1)

=cut
