diff options
author | root <root@DEBIAN.TEMPLATE> | 2016-06-02 19:07:59 +0200 |
---|---|---|
committer | root <root@DEBIAN.TEMPLATE> | 2016-06-02 19:07:59 +0200 |
commit | 8b580cc21e08b187451cc36a379974ca898e5105 (patch) | |
tree | 54545f735e4d626d128bd61effa4360be164756b | |
parent | 5222b78af21f90e230d50c075174fc84f45d4172 (diff) |
lldpdiscover imported
-rwxr-xr-x | extras/tools/lldp/dotnet.sh | 9 | ||||
-rwxr-xr-x | extras/tools/lldp/draw-neighbors.pl | 35 | ||||
-rwxr-xr-x | extras/tools/lldp/lldpdiscover.pl | 302 | ||||
-rw-r--r-- | include/FixedSNMP.pm | 125 | ||||
-rw-r--r-- | include/nms/snmp.pm | 92 |
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; |