Skip to main content

mrrogers

(2014-07-17)

MrRogers knows who is in your neighbourhood.

mrrogers is a perl script which uses snmp to query a switch to list its LLDP neighbours. Originally I wanted a perl script that would list the LLDP neighbour on a specific port so that I could run it in a Nagios test and have it bark if an important link was no longer there.

The script requires the CPAN module Net::SNMP

Usage:

[dave@store01 ~]$ mrrogers --HELP
mrrogers.pl [-c STRING ] [-p] TARGET
    TARGET         query the switch at ip, or named, TARGET
    -c STRING      use STRING as the SNMP community
    -l PORT        show neighbour on PORT only
    -p             display remote PortID strings as well as PortDesc strings
    -s             show local PortID
    --HELP         print this screen

The snmp community defaults to public Examples:

[dave@store01 ~]$ mrrogers --HELP
mrrogers.pl [-c STRING ] [-p] TARGET
    TARGET         query the switch at ip, or named, TARGET
    -c STRING      use STRING as the SNMP community
    -l PORT        show neighbour on PORT only
    -p             display remote PortID strings as well as PortDesc strings
    -s             show local PortID
    --HELP         print this screen

This is being run against a Juniper EX2200 connected mostly to other Juniper EX2200 switches, along with a couple of Dell PowerConnects (3548s or 5324s) and a DLink DES-1210-28P.

[dave@store01 ~]$ mrrogers -s sa3-37
(731) ge-1/0/43.0 -> sa4-39 ge-0/0/9.0
[dave@store01 ~]$ mrrogers -s -p sa3-37
(731) ge-1/0/43.0 -> sa4-39 ge-0/0/9.0 (560)
[dave@store01 ~]$ mrrogers -l 587 sa4-39
ge-0/0/32.0 -> sa6-41 ge-0/0/47.0

History

2015-03-03

  • Dell switches store Ethernet Port in their PortDesc table instead of something useful. We happen to know that the PortID value is useful, so if we see the generic string "Ethernet" as the PortDesc, we'll use the value from the PortID instead.
  • I found a DLink that stores nothing in PortDesc, so if we find an empty PortDesc, we'll use PortID instead.

The Script

#!/usr/bin/perl
# vi:syntax=perl
#
# Name:
#   mrrogers
#   Licensed under Don't Worry, Be Happy
#    see: http://wiki.xdroop.com/space/Software+Licensing
#
# Synopsis
#   list LLDP neighbours listed in SNMP
#
# Options
#  see &usage
#
# History:
#   2014-07-16 D.Mackintosh
#   Initial rev
#
# Usefull Values.

use Net::SNMP qw(snmp_dispatcher oid_lex_sort);

$BASENAME = $0;
$BASENAME =~ s|\\|/|g;
if ($BASENAME =~ m|(.*/)(.*)|)
{
    $BASENAME = $2;
}
#
# code here.

while ($#ARGV > -1)
{
    $_ = shift @ARGV;
    if (/^-c$/) { $COMMUNITY      = shift @ARGV; next; }
    if (/^-s$/) { $SHOWLOCALPORTS = 1;           next; }
    if (/^-p$/) { $PORTID         = 1;           next; }
    if (/^-l$/) { $SpecificPort   = shift @ARGV; next; }
    if (/^--DEBUG$/) { $DEBUG = 1; &debug("Debugging on"); next; }
    if (/^--HELP$/) { &usage("__SHOW_HELP"); }
    if (!$switch) { $switch = $_; next; }
    &usage("$_");
}

#
# display the help message.
#  I find it usefull to leave this subroutine here, next to the parameter
#  parsing, as it makes it much easer to keep the help and the
#  actual parsing in sync.
sub usage
{
    $gripe = shift @_;
    print <<"EO_USAGE";
$BASENAME [-c STRING ] [-p] TARGET
    TARGET         query the switch at ip, or named, TARGET
    -c STRING      use STRING as the SNMP community
    -l PORT        show neighbour on PORT only
    -p             display remote PortID strings as well as PortDesc strings
    -s             show local PortID
    --HELP         print this screen
EO_USAGE
    exit 0 if ($gripe eq "__SHOW_HELP");
    &die("Don't understand:$gripe");
}

$switch    = $switch    || '192.168.1.1';
$COMMUNITY = $COMMUNITY || 'public';

$sysname = &getSnmpName();
if (!$sysname)
{
    print "No sysname, snmp error?\n";
    exit 1;
}

if ($PORTID)
{
    &loadLLDPPortID();
}

&loadLLDPPortDesc();
&loadLLDPSysName();
&loadLLDPPortID();

if ($SpecificPort)
{
    print &showNeighbour($SpecificPort) . "\n";
    exit;
}

foreach $localPort (sort(keys(%RemotePortDesc)))
{
    print &showNeighbour($localPort) . "\n";
}

#
# Return the ifName for a given ifIndex.

sub ifName
{
    my $ifIndex = shift(@_);
    my $ifName;
    my $ifNameOID     = '1.3.6.1.2.1.31.1.1.1.1';
    my $ifNameOIDPort = "$ifNameOID.$ifIndex";
    &debug("ifName: ifNameOIDPort -> $ifNameOIDPort");
    my ($nameSession, $nameError) = Net::SNMP->session(
        -hostname  => $switch,
        -community => $COMMUNITY,
        -port      => 161,
    );
    if (!defined($nameSession))
    {
        &die("ifName: no session");
    }
    $ifName = $nameSession->get_request(-varbindlist => [$ifNameOIDPort],);
    $ifName = $ifName->{$ifNameOIDPort};
    $nameSession->close;
    return ($ifName);
}

#
# load the LLDPPortDesc column into %RemotePortDesc

sub getSnmpName
{
    my $SnmpNameOID = '.1.3.6.1.2.1.1.5';
    my ($session, $error) = Net::SNMP->session(
        -hostname  => $switch,
        -community => $COMMUNITY,
        -port      => 161,
        -translate => [-octetstring => 0x0]    # http://rt.cpan.org/Public/Bug/Display.html?id=1946
    );
    if (!defined($session))
    {
        printf("ERROR: %s\n", $error);
        exit 1;
    }
    if (defined($result = $session->get_table(-baseoid => $SnmpNameOID)))
    {
        foreach (oid_lex_sort(keys(%{$result})))
        {
            $raw        = $_;
            $value      = $result->{$_};
            $SystemName = $value;
            &debug("SystemName: [$raw] -> [$value]");
        }
    }
    else
    {
        printf("ERROR: %s\n(getSnmpName)\n", $session->error());
    }
    $session->close;
    return $SystemName;
}

#
# load the LLDPPortDesc column into %RemotePortDesc

sub loadLLDPPortDesc
{
    my $LLDPPortDesc = '1.0.8802.1.1.2.1.4.1.1.8';
    my ($session, $error) = Net::SNMP->session(
        -hostname  => $switch,
        -community => $COMMUNITY,
        -port      => 161,
        -translate => [-octetstring => 0x0]    # http://rt.cpan.org/Public/Bug/Display.html?id=1946
    );
    if (!defined($session))
    {
        printf("ERROR: %s\n", $error);
        exit 1;
    }
    if (defined($result = $session->get_table(-baseoid => $LLDPPortDesc)))
    {
        foreach (oid_lex_sort(keys(%{$result})))
        {
            $raw   = $_;
            $value = $result->{$_};
            if ($raw =~ /\.(\d+)\.\d+$/)
            {
                $raw = $1;
            }
            $RemotePortDesc{$raw} = $value;
            &debug("LLDPPortDesc: [$raw] -> [$value]");
        }
    }
    else
    {
        printf("ERROR: %s\n(load-LLDPPortDesc)\n", $session->error());
    }
    $session->close;
}

#
# Load the LLDPPortID column into %RemotePortID

sub loadLLDPPortID
{
    my $LLDPPortID = '1.0.8802.1.1.2.1.4.1.1.7';
    my ($session, $error) = Net::SNMP->session(
        -hostname  => $switch,
        -community => $COMMUNITY,
        -port      => 161,
        -translate => [-octetstring => 0x0]    # http://rt.cpan.org/Public/Bug/Display.html?id=1946
    );
    if (!defined($session))
    {
        printf("ERROR: %s\n", $error);
        exit 1;
    }
    if (defined($result = $session->get_table(-baseoid => $LLDPPortID)))
    {
        foreach (oid_lex_sort(keys(%{$result})))
        {
            $raw   = $_;
            $value = $result->{$_};
            if ($raw =~ /\.(\d+)\.\d+$/)
            {
                $raw = $1;
            }
            $RemotePortID{$raw} = $value;
            &debug("LLDPortID: [$raw] -> [$value]");
        }
    }
    else
    {
        printf("ERROR: %s\n(loadLLDPPortID)\n", $session->error());
    }
    $session->close;
}

#
# Load the LLDPSysName column into %RemoteSysName

sub loadLLDPSysName
{
    my $LLDPSysName = '1.0.8802.1.1.2.1.4.1.1.9';
    my ($session, $error) = Net::SNMP->session(
        -hostname  => $switch,
        -community => $COMMUNITY,
        -port      => 161,
        -translate => [-octetstring => 0x0]    # http://rt.cpan.org/Public/Bug/Display.html?id=1946
    );
    if (!defined($session))
    {
        printf("ERROR: %s\n", $error);
        exit 1;
    }
    if (defined($result = $session->get_table(-baseoid => $LLDPSysName)))
    {
        foreach (oid_lex_sort(keys(%{$result})))
        {
            $raw   = $_;
            $value = $result->{$_};
            if ($raw =~ /\.(\d+)\.\d+$/)
            {
                $raw = $1;
            }
            $RemoteSysName{$raw} = $value;
            &debug("LLDPSysName: [$raw] -> [$value]");
        }
    }
    else
    {
        printf("ERROR: %s\n(loadLLDPSysName)\n", $session->error());
    }
    $session->close;
}

#
# Return the output string for the given ifIndex

sub showNeighbour
{
    my $ifIndex = shift(@_);
    my $LocalPortName;
    my $RemoteDescription;
    my $out;
    $LocalPortName = &ifName($ifIndex);
    # Dell switches describe their ports as "Ethernet Port"
    # and keep useful information in the portID table instead
    # so if we hit something generic, use portID value instead.
    # I also found a DLink that stores nothing in PortDesc, so
    # if we find an empty PortDesc, we'll use PortID instead.
    if ($RemotePortDesc{$ifIndex} =~ /^Ethernet/ or !$RemotePortDesc{$ifIndex})
    {
        $RemoteDescription = $RemotePortID{$ifIndex};
    }
    else
    {
        $RemoteDescription = $RemotePortDesc{$ifIndex};
    }
    if ($SHOWLOCALPORTS)
    {
        $out = "($ifIndex) ";
    }
    $out = $out . "$LocalPortName -> $RemoteSysName{$ifIndex} $RemoteDescription";
    if ($PORTID)
    {
        $out = $out . " ($RemotePortID{$ifIndex})";
    }
    return $out;
}

# These subroutines come from my Perl template and are used just for
#  my convenience.
#
# an easy way to change those back-ticks into
#  system() calls when we fuck up.

sub doThis
{
    local ($result);
    local ($thing) = pop @_;
    &debug("doing [$thing]");

    #       $result=system($thing);
    $result = `$thing`;
    &debug("finished shelling; output:\n$result");
    &debug("output ends");
    return $result;
}

#
# Log something to the local syslog

sub logItem
{
    local ($message);

#
# Log something to the local syslog

sub logItem
{
    local ($message);
    $message = pop(@_);
    setlogsock('unix');
    openlog($BASENAME, '', 'user');
    syslog('info', $message);
    closelog;
}

#
# A (braindead) friend for our undertaker.

sub warn
{
    local ($gripe);
    $gripe = pop(@_);
    print STDERR "$BASENAME:$gripe\n";
    &logItem($gripe);
}

#
# A simple debug outputer.

sub debug
{
    local ($gripe);
    $gripe = pop(@_);
    $DEBUG && print STDERR "DEBUG:$gripe\n";
}

#
# A (braindead) undertaker.

sub die
{
    local ($gripe);
    $gripe = pop(@_);
    &warn("fatal:$gripe");
    &logItem("fatal:$gripe");
    exit 1;
}

Notes

This script should really be named after a character from Sesame Street because

these are the people in your neighbourhood
in your neighbourhood
in your neigh-bour-hood yeah
these are the people in your neighbourhood
they're the people that you see
when you do lldp
they're the people that you see
each day

...but I can't remember which character was known for singing it (it might have been a bunch of them, so that would be confusing)… and I may have put too much thought into this. So to help you forget all this confusion, here's Bob singing it.