aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorroot <root@DEBIAN.TEMPLATE>2016-06-02 19:07:59 +0200
committerroot <root@DEBIAN.TEMPLATE>2016-06-02 19:07:59 +0200
commit8b580cc21e08b187451cc36a379974ca898e5105 (patch)
tree54545f735e4d626d128bd61effa4360be164756b
parent5222b78af21f90e230d50c075174fc84f45d4172 (diff)
lldpdiscover imported
-rwxr-xr-xextras/tools/lldp/dotnet.sh9
-rwxr-xr-xextras/tools/lldp/draw-neighbors.pl35
-rwxr-xr-xextras/tools/lldp/lldpdiscover.pl302
-rw-r--r--include/FixedSNMP.pm125
-rw-r--r--include/nms/snmp.pm92
5 files changed, 563 insertions, 0 deletions
diff --git a/extras/tools/lldp/dotnet.sh b/extras/tools/lldp/dotnet.sh
new file mode 100755
index 0000000..5c1b369
--- /dev/null
+++ b/extras/tools/lldp/dotnet.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+DATE="$(date +%s)"
+if [ -z "$1" ] || [ -z "$2" ]; then
+ echo "Usage: $0 <ip> <community>"
+ exit 1;
+fi
+./lldpdiscover.pl $1 $2 | ./draw-neighbors.pl | dot -Tpng > dotnet-${DATE}.png
+echo File name: dotnet-${DATE}.png
diff --git a/extras/tools/lldp/draw-neighbors.pl b/extras/tools/lldp/draw-neighbors.pl
new file mode 100755
index 0000000..323e676
--- /dev/null
+++ b/extras/tools/lldp/draw-neighbors.pl
@@ -0,0 +1,35 @@
+#!/usr/bin/perl
+
+use strict;
+use JSON;
+
+my $in;
+while (<STDIN>) {
+ $in .= $_;
+}
+
+my %assets = %{JSON::XS::decode_json($in)};
+
+print "strict graph network {\n";
+while (my ($key, $value) = each %assets) {
+ print_tree ($key,0,undef);
+}
+print "}\n";
+
+sub print_tree
+{
+ my ($chassis_id,$indent,$parent,$max) = @_;
+ if (!defined($parent)) {
+ $parent = "";
+ }
+ if ($indent > 50) {
+ die "Possible loop detected.";
+ }
+ print " \"$assets{$chassis_id}{sysName}\" -- {";
+ my @n;
+ while (my ($key, $value) = each %{$assets{$chassis_id}{neighbors}}) {
+ push @n, "\"$assets{$key}{sysName}\"";
+ }
+ print join(",",@n) . "};\n";
+}
+
diff --git a/extras/tools/lldp/lldpdiscover.pl b/extras/tools/lldp/lldpdiscover.pl
new file mode 100755
index 0000000..32840ad
--- /dev/null
+++ b/extras/tools/lldp/lldpdiscover.pl
@@ -0,0 +1,302 @@
+#! /usr/bin/perl
+#
+# Basic tool to discover your neighbourhood systems, using LLDP, as seen
+# through SNMP.
+#
+# Usage: ./lldpdiscover.pl <ip> <community>
+#
+# This will connect to <ip> and poll it for SNMP-data, then add that to an
+# asset database. After that's done, we parse the LLDP neighbor table
+# provided over SNMP and add those systems to assets, then try to probe
+# THEM with SNMP, using the same community, and so on.
+#
+# If the entire internet exposed LLDP and SNMP in a public domain, we could
+# theoretically map the whole shebang.
+#
+# Note that leaf nodes do NOT need to reply to SNMP to be added, but
+# without SNMP, there'll obviously be some missing data.
+#
+# The output is a JSON blob of all assets, indexed by chassis id. It also
+# includes a neighbor table for each asset which can be used to generate a
+# map (See dotnet.sh or draw-neighbors.pl for examples). It can also be
+# used to add the assets to NMS.
+#
+# A sensible approach might be to run this periodically, store the results
+# to disk, then have multiple tools parse the results.
+use POSIX;
+use Time::HiRes;
+use strict;
+use warnings;
+use Data::Dumper;
+
+use lib '/opt/gondul/include';
+use FixedSNMP;
+use nms;
+use nms::snmp;
+
+# Actual assets detected, indexed by chassis ID
+my %assets;
+
+# Tracking arrays. Continue scanning until they are of the same length.
+my @chassis_ids_checked;
+my @chassis_ids_to_check;
+
+# If we are given one switch on the command line, add that and then exit.
+my ($cmdline_ip, $cmdline_community) = @ARGV;
+if (defined($cmdline_ip) && defined($cmdline_community)) {
+ my $chassis_id;
+ eval {
+ # Special-case for the first switch is to fetch chassis id
+ # directly. Everything else is fetched from a neighbour
+ # table.
+ my $session = nms::snmp::snmp_open_session($cmdline_ip, $cmdline_community);
+ $chassis_id = get_lldp_chassis_id($session);
+ $assets{$chassis_id}{'community'} = $cmdline_community;
+ $assets{$chassis_id}{'ip'} = $cmdline_ip;
+ push @chassis_ids_to_check, $chassis_id;
+ };
+ if ($@) {
+ mylog("Error during SNMP : $@");
+ exit 1;
+ }
+
+ # Welcome to the main loop!
+ while (scalar @chassis_ids_to_check > scalar @chassis_ids_checked) {
+ # As long as you call it something else, it's not really a
+ # goto-statement, right!?
+ OUTER: for my $id (@chassis_ids_to_check) {
+ for my $id2 (@chassis_ids_checked) {
+ if ($id2 eq $id) {
+ next OUTER;
+ }
+ }
+ mylog("Adding $id");
+ add_switch($id);
+ mylog("Discovering neighbors for $id");
+ discover_lldp_neighbors($id);
+ push @chassis_ids_checked,$id;
+ }
+ }
+ print JSON::XS::encode_json(\%assets);
+# Creates corrupt output, hooray.
+# print JSON::XS->new->pretty(1)->encode(\%assets);
+ exit;
+} else {
+ print "RTFSC\n";
+}
+# Filter out stuff we don't scan. Return true if we care about it.
+# XXX: Several of these things are temporary to test (e.g.: AP).
+sub filter {
+ my %sys = %{$_[0]};
+ if (!defined($sys{'lldpRemSysCapEnabled'})) {
+ return 0;
+ }
+ my %caps = %{$sys{'lldpRemSysCapEnabled'}};
+ my $sysdesc = $sys{'lldpRemSysDesc'};
+ my $sysname = $sys{'lldpRemSysName'};
+
+ if ($caps{'cap_enabled_ap'}) {
+ return 1;
+ }
+ if ($caps{'cap_enabled_telephone'}) {
+ return 0;
+ }
+ if (!defined($sysdesc)) {
+ return 1;
+ }
+ if ($sysdesc =~ /\b(C1530|C3600|C3700)\b/) {
+ return 0;
+ }
+ if (!$caps{'cap_enabled_bridge'} && !$caps{'cap_enabled_router'}) {
+ return 1;
+ }
+ if ($sysname =~ /BCS-OSL/) {
+ return 1;
+ }
+ return 1;
+}
+
+# Discover neighbours of a switch. The data needed is already present int
+# %assets , so this shouldn't cause any extra SNMP requests. It will add
+# new devices as it finds them.
+sub discover_lldp_neighbors {
+ my $local_id = $_[0];
+ #print "local id: $local_id\n";
+ my $ip = $assets{$local_id}{mgmt};
+ my $local_sysname = $assets{$local_id}{snmp}{sysName};
+ my $community = $assets{$local_id}{community};
+ my $addrtable;
+ while (my ($key, $value) = each %{$assets{$local_id}{snmp_parsed}}) {
+ my $chassis_id = $value->{'lldpRemChassisId'};
+
+ my $sysname = $value->{'lldpRemSysName'};
+ if (!defined($sysname)) {
+ $sysname = $chassis_id;
+ }
+
+ # Do not try to poll servers.
+ if (!filter(\%{$value})) {
+ mylog("Filtered out $sysname ($local_sysname -> $sysname)");
+ next;
+ }
+ $sysname =~ s/\..*$//;
+ if (defined($value->{lldpRemManAddr})) {
+ mylog("Found $sysname ($local_sysname -> $sysname )");
+ mylog("$chassis_id");
+ } else {
+ next;
+ }
+ if (defined($assets{$chassis_id}{'sysName'})) {
+ mylog("Duplicate $sysname: \"$sysname\" vs \"$assets{$chassis_id}{'sysName'}\"");
+ if ($assets{$chassis_id}{'sysName'} eq "") {
+ $assets{$chassis_id}{'sysName'} = $sysname;
+ }
+ } else {
+ $assets{$chassis_id}{'sysName'} = $sysname;
+ }
+
+ # FIXME: We should handle duplicates better and for more
+ # than just sysname. These happen every time we are at
+ # least one tier down (given A->{B,C,D,E}, switch B, C, D
+ # and E will all know about A, thus trigger this). We also
+ # want to _add_ information only, since two nodes might
+ # know about the same switch, but one might have incomplete
+ # information (as is the case when things start up).
+
+ # We simply guess that the community is the same as ours.
+ $assets{$chassis_id}{'community'} = $community;
+ $assets{$chassis_id}{'ip'} = $value->{lldpRemManAddr};
+
+ $assets{$chassis_id}{'neighbors'}{$local_id} = 1;
+ $assets{$local_id}{'neighbors'}{$chassis_id} = 1;
+ check_neigh($chassis_id);
+ #print "checking $chassis_id\n";
+ }
+}
+
+sub mylog {
+ my $msg = shift;
+ my $time = POSIX::ctime(time);
+ $time =~ s/\n.*$//;
+ printf STDERR "[%s] %s\n", $time, $msg;
+}
+
+# Get raw SNMP data for an ip/community.
+# FIXME: This should be seriously improved. Three get()'s and four
+# gettables could definitely be streamlined, but then again, I doubt it
+# matters much unless we start running this tool constantly.
+sub get_snmp_data {
+ my ($ip, $community) = @_;
+ my %ret = ();
+ eval {
+ my $session = nms::snmp::snmp_open_session($ip, $community);
+ $ret{'sysName'} = $session->get('sysName.0');
+ $ret{'sysDescr'} = $session->get('sysDescr.0');
+ $ret{'lldpRemManAddrTable'} = $session->gettable("lldpRemManAddrTable");
+ $ret{'lldpRemTable'} = $session->gettable("lldpRemTable");
+ $ret{'lldpLocChassisIdParsed'} = nms::convert_mac($session->getnext('lldpLocChassisId'));
+ $ret{'lldpLocChassisId'} = $session->get('lldpLocChassisId.0');
+ #print Dumper(\%ret);
+ };
+ if ($@) {
+ mylog("Error during SNMP to $ip : $@");
+ return undef;
+ }
+ return \%ret;
+}
+
+# Filter raw SNMP data over to something more legible.
+# This is the place to add all post-processed results so all parts of the
+# tool can use them.
+sub parse_snmp
+{
+ my $snmp = $_[0];
+ my %result = ();
+ my %lol = ();
+ while (my ($key, $value) = each %{$snmp}) {
+ $result{$key} = $value;
+ }
+ while (my ($key, $value) = each %{$snmp->{lldpRemTable}}) {
+ my $chassis_id = nms::convert_mac($value->{'lldpRemChassisId'});
+ foreach my $key2 (keys %$value) {
+ $lol{$value->{lldpRemIndex}}{$key2} = $value->{$key2};
+ }
+ $lol{$value->{lldpRemIndex}}{'lldpRemChassisId'} = $chassis_id;
+ my %caps = ();
+ nms::convert_lldp_caps($value->{'lldpRemSysCapEnabled'}, \%caps);
+ $lol{$value->{lldpRemIndex}}{'lldpRemSysCapEnabled'} = \%caps;
+ }
+ while (my ($key, $value) = each %{$snmp->{lldpRemManAddrTable}}) {
+ foreach my $key2 (keys %$value) {
+ $lol{$value->{lldpRemIndex}}{$key2} = $value->{$key2};
+ }
+ my $addr = $value->{'lldpRemManAddr'};
+ my $addrtype = $value->{'lldpRemManAddrSubtype'};
+ if ($addrtype == 1) {
+ $lol{$value->{lldpRemIndex}}{lldpRemManAddr} = nms::convert_ipv4($addr);
+ } elsif ($addrtype == 2) {
+ $lol{$value->{lldpRemIndex}}{lldpRemManAddr} = nms::convert_ipv6($addr);
+ }
+ }
+ return \%lol;
+ print Dumper (\%lol);
+}
+
+# Add a chassis_id to the list to be checked, but only if it isn't there.
+# I'm sure there's some better way to do this, but meh, perl. Doesn't even
+# have half-decent prototypes.
+sub check_neigh {
+ my $n = $_[0];
+ for my $v (@chassis_ids_to_check) {
+ if ($v eq $n) {
+ return 0;
+ }
+ }
+ push @chassis_ids_to_check,$n;
+ return 1;
+}
+
+# We've got a switch. Populate it with SNMP data (if we can).
+sub add_switch {
+ my $chassis_id = shift;
+ my $addr;
+ my $snmp = undef;
+ $addr = $assets{$chassis_id}{'ip'};
+ mylog("Probing $addr");
+ $snmp = get_snmp_data($addr, $assets{$chassis_id}{'community'});
+
+ return if (!defined($snmp));
+ my $sysname = $snmp->{sysName};
+ $sysname =~ s/\..*$//;
+ $assets{$chassis_id}{'sysName'} = $sysname;
+ $assets{$chassis_id}{'ip'} = $addr;
+ $assets{$chassis_id}{'snmp'} = $snmp;
+ $assets{$chassis_id}{'snmp_parsed'} = parse_snmp($snmp);
+ return;
+}
+
+sub get_lldp_chassis_id {
+ my ($session) = @_;
+ my $response;
+ #printf "get lldpLocChassisId.0\n";
+ $response = $session->get('lldpLocChassisId.0');
+ #print "\tconverted: " . nms::convert_mac($response) . "\n";
+ #print "\tstring: " . $response . "\n";
+ my $real = nms::convert_mac($response);
+ #printf "getnext lldpLocChassisId.0\n";
+ $response = $session->getnext('lldpLocChassisId.0');
+ #print "\tconverted: " . nms::convert_mac($response) . "\n";
+ #print "\tstring: " . $response . "\n";
+
+ #printf "get lldpLocChassisId\n";
+ $response = $session->get('lldpLocChassisId');
+ #print "\tconverted: " . nms::convert_mac($response) . "\n";
+ #print "\tstring: " . $response . "\n";
+
+ #printf "getnext lldpLocChassisId\n";
+ $response = $session->getnext('lldpLocChassisId');
+ #print "\tconverted: " . nms::convert_mac($response) . "\n";
+ #print "\tstring: " . $response . "\n";
+
+ return $real;
+}
diff --git a/include/FixedSNMP.pm b/include/FixedSNMP.pm
new file mode 100644
index 0000000..1ea3089
--- /dev/null
+++ b/include/FixedSNMP.pm
@@ -0,0 +1,125 @@
+# A bugfix to the gettable functions of SNMP.pm, that deals properly
+# with bulk responses being overridden. Original copyright:
+#
+# Copyright (c) 1995-2006 G. S. Marzot. All rights reserved.
+# This program is free software; you can redistribute it and/or
+# modify it under the same terms as Perl itself.
+#
+# To use, just "use FixedSNMP;" and then use SNMP::Session as usual.
+
+use strict;
+use warnings;
+use SNMP;
+
+package FixedSNMP::Session;
+
+sub _gettable_do_it() {
+ my ($this, $vbl, $parse_indexes, $textnode, $state) = @_;
+
+ my ($res);
+
+ $vbl = $_[$#_] if ($state->{'options'}{'callback'});
+
+ my $num_vbls = scalar @$vbl;
+ my $num_stopconds = scalar @{$state->{'stopconds'}};
+
+ while ($num_vbls > 0 && !$this->{ErrorNum}) {
+ my @found_eof = (0) x $num_stopconds;
+
+ for (my $i = 0; $i <= $#$vbl; $i++) {
+ my $row_oid = SNMP::translateObj($vbl->[$i][0]);
+ my $row_text = $vbl->[$i][0];
+ my $row_index = $vbl->[$i][1];
+ my $row_value = $vbl->[$i][2];
+ my $row_type = $vbl->[$i][3];
+
+ my $stopcond_num = $i % $num_stopconds;
+ my $stopcond = $state->{'stopconds'}[$stopcond_num];
+ if ($row_oid !~ /^\Q$stopcond\E/ || $row_value eq 'ENDOFMIBVIEW') {
+ $found_eof[$stopcond_num] = 1;
+ } else {
+
+ if ($row_type eq "OBJECTID") {
+
+ # If the value returned is an OID, translate this
+ # back in to a textual OID
+
+ $row_value = SNMP::translateObj($row_value);
+
+ }
+
+ # continue past this next time
+
+ $state->{'varbinds'}[$stopcond_num] = [ $row_text, $row_index ];
+
+ # Place the results in a hash
+
+ $state->{'result_hash'}{$row_index}{$row_text} = $row_value;
+ }
+ }
+
+ my @newstopconds = ();
+ my @newvarbinds = ();
+ for (my $i = 0; $i < $num_stopconds; ++$i) {
+ unless ($found_eof[$i]) {
+ push @newstopconds, $state->{'stopconds'}[$i];
+ push @newvarbinds, $state->{'varbinds'}[$i];
+ }
+ }
+ if ($#newstopconds == -1) {
+ last;
+ }
+ $state->{'varbinds'} = \@newvarbinds;
+ $state->{'stopconds'} = \@newstopconds;
+ $vbl = $state->{'varbinds'};
+ $num_vbls = scalar @newvarbinds;
+ $num_stopconds = scalar @newstopconds;
+
+ #
+ # if we've been configured with a callback, then call the
+ # sub-functions with a callback to our own "next" processing
+ # function (_gettable_do_it). or else call the blocking method and
+ # call the next processing function ourself.
+ #
+ if ($state->{'options'}{'callback'}) {
+ if ($this->{Version} ne '1' && !$state->{'options'}{'nogetbulk'}) {
+ $res = $this->getbulk(0, $state->{'repeatcount'}, $vbl,
+ [\&_gettable_do_it, $this, $vbl,
+ $parse_indexes, $textnode, $state]);
+ } else {
+ $res = $this->getnext($vbl,
+ [\&_gettable_do_it, $this, $vbl,
+ $parse_indexes, $textnode, $state]);
+ }
+ return;
+ } else {
+ if ($this->{Version} ne '1' && !$state->{'options'}{'nogetbulk'}) {
+ $res = $this->getbulk(0, $state->{'repeatcount'}, $vbl);
+ } else {
+ $res = $this->getnext($vbl);
+ }
+ }
+ }
+
+ # finish up
+ _gettable_end_routine($state, $parse_indexes, $textnode);
+
+ # return the hash if no callback was specified
+ if (!$state->{'options'}{'callback'}) {
+ return($state->{'result_hash'});
+ }
+
+ #
+ # if they provided a callback, call it
+ # (if an array pass the args as well)
+ #
+ if (ref($state->{'options'}{'callback'}) eq 'ARRAY') {
+ my $code = shift @{$state->{'options'}{'callback'}};
+ $code->(@{$state->{'options'}{'callback'}}, $state->{'result_hash'});
+ } else {
+ $state->{'options'}{'callback'}->($state->{'result_hash'});
+ }
+}
+
+*FixedSNMP::Session::_gettable_end_routine = *SNMP::Session::_gettable_end_routine;
+*SNMP::Session::_gettable_do_it = *FixedSNMP::Session::_gettable_do_it;
diff --git a/include/nms/snmp.pm b/include/nms/snmp.pm
new file mode 100644
index 0000000..b0fe725
--- /dev/null
+++ b/include/nms/snmp.pm
@@ -0,0 +1,92 @@
+#! /usr/bin/perl
+use strict;
+use warnings;
+use SNMP;
+use nms;
+package nms::snmp;
+
+use base 'Exporter';
+our @EXPORT = qw();
+
+BEGIN {
+ # $SNMP::debugging = 1;
+
+ # sudo mkdir /usr/share/mibs/site
+ # cd /usr/share/mibs/site
+ # wget -O- ftp://ftp.cisco.com/pub/mibs/v2/v2.tar.gz | sudo tar --strip-components=3 -zxvvf -
+ SNMP::initMib();
+ SNMP::addMibDirs("/opt/gondul/mibs/StandardMibs");
+ SNMP::addMibDirs("/opt/gondul/mibs/JuniperMibs");
+ SNMP::addMibDirs("/opt/gondul/mibs/CiscoMibs");
+
+ SNMP::loadModules('SNMPv2-MIB');
+ SNMP::loadModules('ENTITY-MIB');
+ SNMP::loadModules('IF-MIB');
+ SNMP::loadModules('LLDP-MIB');
+ SNMP::loadModules('IP-MIB');
+ SNMP::loadModules('IP-FORWARD-MIB');
+}
+
+sub snmp_open_session {
+ my ($ip, $community, $async) = @_;
+
+ $async //= 0;
+
+ my %options = (UseEnums => 1);
+ if ($ip =~ /:/) {
+ $options{'DestHost'} = "udp6:$ip";
+ } else {
+ $options{'DestHost'} = "udp:$ip";
+ }
+
+ if ($community =~ /^snmpv3:(.*)$/) {
+ my ($username, $authprotocol, $authpassword, $privprotocol, $privpassword) = split /\//, $1;
+
+ $options{'SecName'} = $username;
+ $options{'SecLevel'} = 'authNoPriv';
+ $options{'AuthProto'} = $authprotocol;
+ $options{'AuthPass'} = $authpassword;
+
+ if (defined($privprotocol) && defined($privpassword)) {
+ $options{'SecLevel'} = 'authPriv';
+ $options{'PrivProto'} = $privprotocol;
+ $options{'PrivPass'} = $privpassword;
+ }
+
+ $options{'Version'} = 3;
+ } else {
+ $options{'Community'} = $community;
+ $options{'Version'} = 2;
+ }
+
+ my $session = SNMP::Session->new(%options);
+ if (defined($session) && ($async || defined($session->getnext('sysDescr')))) {
+ return $session;
+ } else {
+ die 'Could not open SNMP session to ' . $ip;
+ }
+}
+
+# Not currently in use; kept around for reference.
+sub fetch_multi_snmp {
+ my ($session, @oids) = @_;
+
+ my %results = ();
+
+ # Do bulk reads of 40 and 40; seems to be about the right size for 1500-byte packets.
+ for (my $i = 0; $i < scalar @oids; $i += 40) {
+ my $end = $i + 39;
+ $end = $#oids if ($end > $#oids);
+ my @oid_slice = @oids[$i..$end];
+
+ my $localresults = $session->get_request(-varbindlist => \@oid_slice);
+ return undef if (!defined($localresults));
+
+ while (my ($key, $value) = each %$localresults) {
+ $results{$key} = $value;
+ }
+ }
+
+ return \%results;
+}
+1;