diff options
Diffstat (limited to 'extras/tools')
-rwxr-xr-x | extras/tools/lldp/dotnet.sh | 11 | ||||
-rwxr-xr-x | extras/tools/lldp/draw-neighbors.pl | 79 | ||||
-rwxr-xr-x | extras/tools/lldp/lldpdiscover.pl | 389 | ||||
-rwxr-xr-x | extras/tools/lldp/lolwhat.pl | 666 |
4 files changed, 1145 insertions, 0 deletions
diff --git a/extras/tools/lldp/dotnet.sh b/extras/tools/lldp/dotnet.sh new file mode 100755 index 0000000..a3f5623 --- /dev/null +++ b/extras/tools/lldp/dotnet.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -xe +DATE="$(date --iso-8601=second | sed s/:/./g)" +OUT=out/ +mkdir -p $OUT +JSON=${OUT}lolwhat-${DATE}.json +./lolwhat.pl $* > ${JSON} +./draw-neighbors.pl < ${JSON} | dot -Tpng > ${OUT}lolwhat-${DATE}.png +./draw-neighbors.pl full < ${JSON} | dot -Tpng > ${OUT}lolwhat-${DATE}-full.png +./draw-neighbors.pl lldp < ${JSON} | dot -Tpng > ${OUT}lolwhat-${DATE}-lldp.png +echo File name: ${OUT}lolwhat-${DATE}*png diff --git a/extras/tools/lldp/draw-neighbors.pl b/extras/tools/lldp/draw-neighbors.pl new file mode 100755 index 0000000..6535a3c --- /dev/null +++ b/extras/tools/lldp/draw-neighbors.pl @@ -0,0 +1,79 @@ +#!/usr/bin/perl + +use strict; +use JSON; + +my $in; +my ($full) = @ARGV; +while (<STDIN>) { + $in .= $_; +} + +my %assets = %{JSON::XS::decode_json($in)}; +my %peermap = %{$assets{peermap}}; +my %map2 = %{$assets{lldpmap}}; +my %ipmap = %{$assets{ipmap}}; + +print "strict graph network {\n"; +if ($full ne "lldp") { + while (my ($key, $value) = each %peermap) { + print_tree ($key,0,undef); + } +} else { + while (my ($key, $value) = each %map2) { + print_lldp ($key, 0, undef); + } +} +print "}\n"; + +sub print_lldp +{ + my ($id,$indent,$parent,$max) = @_; + my $name = $id; + if (defined($assets{lldppeers}{$id}{name}) and $assets{lldppeers}{$id}{name} ne "") { + $name = $assets{lldppeers}{$id}{name}; + } + print " \"$name\" -- {"; + my @n; + while (my ($key, $value) = each %{$map2{$id}}) { + my $peer = $key; + if (defined($assets{lldppeers}{$key}{name}) and $assets{lldppeers}{$key}{name} ne "") { + $peer = $assets{lldppeers}{$key}{name}; + } + push @n, "\"$peer\""; + } + print join(",",@n) . "};\n"; +} +sub print_tree +{ + my ($ip) = @_; + my $name = $ip; + if ($assets{snmpresults}{$ip}{sysName}) { + $name = $assets{snmpresults}{$ip}{sysName}; + } + print " \"$name\" -- {"; + my @n; + while(my ($peer, $garbage) = each %{$peermap{$ip}}) { + my $name = get_name($peer); + if ($name ne $peer or $full eq "full") { + push @n, "\"$name\""; + } + } + print join(",",@n) . "};\n"; +} + +sub get_name { + my ($ip) = @_; + if (defined($ipmap{$ip})) { + $ip = $ipmap{$ip}; + } + my $name = $ip; + if (defined($assets{snmpresults}{$ip}{sysName})) { + $name = $assets{snmpresults}{$ip}{sysName}; + if ($name eq "") { + $name = $assets{snmpresults}{$ip}{lldpLocChassisId} || $ip; + } + return $name; + } + return $name; +} diff --git a/extras/tools/lldp/lldpdiscover.pl b/extras/tools/lldp/lldpdiscover.pl new file mode 100755 index 0000000..e5ac46f --- /dev/null +++ b/extras/tools/lldp/lldpdiscover.pl @@ -0,0 +1,389 @@ +#! /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; +my %arp; + +# 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; + } + } + while (my ($mac, $entry) = each %arp) { + if (!defined($entry->{'neighbors'})) { + delete $arp{$mac}; + next; + } + my $sysn = $entry->{sysName}; + for my $aentry (@{$entry->{'neighbors'}}) { + if (defined($entry->{chassis_id}) and $aentry->{origin} eq $entry->{chassis_id}) { + next; + } + if ($aentry->{ipNetToMediaType} eq "static") { + next; + } + $arp{$mac}{ips}{$aentry->{ipNetToMediaNetAddress}} = 1; + if (!defined($arp{$mac}{sysName})) { + $arp{$mac}{sysName}= $aentry->{ipNetToMediaNetAddress}; + $arp{$mac}{namefromip} = 1; + } + $arp{$mac}{nsystems}{$aentry->{origin_sysname}} = 1; + } + } + my %map = (); + while (my ($mac, $entry) = each %arp) { + while (my ($sys, $ientry) = each %{$entry->{nsystems}}) { + $map{$entry->{sysName}}{$sys} = 1; + $map{$sys}{$entry->{sysName}} = 1; + } + } + + my %major = (); + $major{assets} = \%assets; + $major{arp} = \%arp; + $major{map} = \%map; + print JSON::XS::encode_json(\%major); +# 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; + } + + if (defined($value->{lldpRemManAddr})) { + $sysname =~ s/\..*$//; + } else { + next; + } + # Do not try to poll servers. + if (!filter(\%{$value})) { + mylog("\tFiltered out $sysname ($local_sysname -> $sysname)"); + next; + } + mylog("\tFound $sysname ($chassis_id) ($local_sysname -> $sysname )"); + if (defined($assets{$chassis_id}{'sysName'})) { + mylog("\t\tDuplicate $sysname: \"$sysname\" vs \"$assets{$chassis_id}{'sysName'}\" - $value->{'lldpRemManAddr'} vs $assets{$chassis_id}{'ip'}"); + 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{'ipNetToMediaTable'} = $session->gettable('ipNetToMediaTable'); + $ret{'ifTable'} = $session->gettable('ifTable'); + $ret{'ifXTable'} = $session->gettable('ifXTable'); + #print Dumper(\%ret); + }; + if ($@) { + my $tmp = "$@"; + chomp($tmp); + mylog("\t" . $tmp); + 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}}) { + my $old = 0; + if (defined($lol{$value->{lldpRemIndex}}{lldpRemManAddr})) { + mylog("\t\tFound existing address: $lol{$value->{lldpRemIndex}}{lldpRemManAddr}"); + $old = $lol{$value->{lldpRemIndex}}{lldpRemManAddrSubtype}; + } + my $addr = $value->{'lldpRemManAddr'}; + my $addrtype = $value->{'lldpRemManAddrSubtype'}; + foreach my $key2 (keys %$value) { + if ($key2 eq 'lldpRemManAddr') { + next; + } + $lol{$value->{lldpRemIndex}}{$key2} = $value->{$key2}; + } + if ($addrtype == 1) { + $lol{$value->{lldpRemIndex}}{lldpRemManAddr} = nms::convert_ipv4($addr); + } elsif ($addrtype == 2 && $old == 0) { + $lol{$value->{lldpRemIndex}}{lldpRemManAddr} = nms::convert_ipv6($addr); + } + } + while (my ($key, $value) = each %{$snmp->{ipNetToMediaTable}}) { + $value->{ipNetToMediaPhysAddress} = nms::convert_mac($value->{ipNetToMediaPhysAddress}); + push @{$lol{$value->{ipNetToMediaIfIndex}}{ARP}}, $value; + } + while (my ($key, $value) = each %{$snmp->{ifTable}}) { + $value->{ifPhysAddress} = nms::convert_mac($value->{ifPhysAddress}); + foreach my $key2 (keys %$value) { + $lol{$value->{ifIndex}}{$key2} = $value->{$key2}; + } + } + while (my ($key, $value) = each %{$snmp->{ifXTable}}) { + foreach my $key2 (keys %$value) { + $lol{$key}{$key2} = $value->{$key2}; + } + } + return \%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'}; + my $name = $assets{$chassis_id}{'sysName'} || "$addr"; + mylog("\tProbing $addr ($name)"); + $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); + populate_arp($chassis_id); + return; +} + +sub populate_arp { + my ($id) = @_; + while (my ($key, $value) = each %{$assets{$id}{snmp_parsed}}) { + my $mac = $value->{'ifPhysAddress'}; + if (!defined($mac) || $mac eq "") { + next; + } + mylog("mac: $mac - $assets{$id}{sysName}"); + $arp{$mac}{chassis_id} = $id; + $arp{$mac}{port} = $key; + $arp{$mac}{sysName} = $assets{$id}{sysName}; + if (!defined($value->{ARP})) { + next; + } + for my $entry (@{$value->{ARP}}) { + my $nmac = $entry->{ipNetToMediaPhysAddress}; + if (!defined($arp{$nmac}{neighbors})) { + @{$arp{$nmac}{neighbors}} = (); + } + $entry->{'origin'} = $id; + $entry->{'origin_sysname'} = $assets{$id}{sysName}; + $entry->{'origin_mgmt'} = $assets{$id}{ip}; + push @{$arp{$nmac}{neighbors}}, $entry; + } + } +} + +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/extras/tools/lldp/lolwhat.pl b/extras/tools/lldp/lolwhat.pl new file mode 100755 index 0000000..64e0140 --- /dev/null +++ b/extras/tools/lldp/lolwhat.pl @@ -0,0 +1,666 @@ +#! /usr/bin/perl +# +# What? WHAAAAT? What's going on? +# +# Simple to to figure out what those tools in the network department have +# actually done. Uses SNMP to gather information about your network, using +# multiple techniques to father information about "next hop". +# +# Usage: ./lolwhat.pl <ip> <community> +# +# This will connect to <ip> and poll it for SNMP-data, then add that to an +# asset database. +use POSIX; +use Time::HiRes; +use strict; +use warnings; +use Data::Dumper; + +use lib '/opt/gondul/include'; +use FixedSNMP; +use nms; +use nms::snmp; + +# If we are given one switch on the command line, add that and then exit. +my $cmdline_community = shift; +my @ips = @ARGV; + +# Actual SNMP results - keyed by IP +my %snmpresults = (); + +# LLDP peers detected from remotes. +# Keyed by chassis ID - as seen remotely. +my %lldppeers = (); + +my %lldpmap = (); +my %lldpnamemap = (); + +my %lldpresolver = (); +my %ipmap = (); +my %peermap = (); +my %claimedips = (); + +# tracking for log indentation +my $mylogindent = ""; + +mylog("Polling initial systems"); + +logindent(1); +foreach my $target (@ips) { + probe_sys($target, $cmdline_community); +} +logindent(-1); + + +peerdetect(); + +# List of chassis IDs checked. +# Note that we deliberately don't add the chassis id's from systems prboed +# directly from the command line. Experience has shown us that +# lldpLocChassisId can be unreliable, so we don't trust it, even if it does +# mean we might end up polling the same system multiple times. +my @chassis_ids_checked = (); +while (scalar keys %lldppeers > scalar @chassis_ids_checked) { + mylog("Probed auto-detected peers: " . scalar @chassis_ids_checked . " Total detected: " . scalar keys %lldppeers ); + logindent(1); + OUTER: for my $id (keys %lldppeers) { + for my $id2 (@chassis_ids_checked) { + if ($id2 eq $id) { + next OUTER; + } + } + probe_sys($lldppeers{$id}{ip}, $cmdline_community); + push @chassis_ids_checked,$id; + } + peerdetect(); + logindent(-1); +} +mylog("Probing complete. Trying to make road in the velling"); + +pad_snmp_results(); +deduplicate(); + +populate_lldpmap(); +populate_ipmap(); +populate_peermap(); + +my %result = ( snmpresults => \%snmpresults, ipmap => \%ipmap, peermap => \%peermap, lldpmap => \%lldpmap, lldppeers => \%lldppeers); + +mylog("Done. Outputting JSON."); +print JSON::XS::encode_json(\%result); +exit; + +sub compare_targets_depth { + my ($t1, $t2) = @_; + my $res = 0; + my $matches = 0; + while (my ($port, $data) = each %{$snmpresults{$t1}{interfaces}}) { + my $one = $data->{ifPhysAddress}; + my $two = $snmpresults{$t2}{interfaces}{$port}{ifPhysAddress}; + if (!defined($one) and !defined($two)) { + next; + } + if ($one ne $two) { + $res++; + } else { + if ($one ne "" and $two ne "") { + if ($one ne "00:00:00:00:00:00") { + $matches++; + } + } + } + } + if ($matches > 10 and $res == 0) { + mylog("$matches interfaces share MAC address. Calling it OK."); + return $res; + } else { + mylog("$res mismatched interfaces versus $matches matched. Not enough for confidence."); + $res++; + } + return $res; +} +sub compare_targets { + my ($t1, $t2) = @_; + my $res = 0; + my $fuckperl = 0; + my $bad = ""; + $bad = "$t2 not found on $t1"; + foreach my $t1ip (@{$snmpresults{$t1}{ips}}) { + if ($fuckperl == 0) { + if ($t1ip eq $t2) { + $res = 1; + $fuckperl = 1; + } + } + } + $fuckperl = 0; + if ($res == 0) { + $bad = "$t1 not found on $t2"; + } + foreach my $t2ip (@{$snmpresults{$t2}{ips}}) { + if ($fuckperl == 0) { + if ($t2ip eq $t1) { + $res = $res + 1; + $fuckperl = 1; + } + } + } + if ($res == 1) { + mylog("... So there are two systems that look 50% similar. $bad (But not the other way around)"); + } + if ($res != 2) { + mylog("Lacking confidence. Doing in-depth comparison instead"); + logindent(1); + $res = compare_targets_depth($t1, $t2); + if ($res == 0) { + $res = 2; + mylog("Gained confidence. Injecting missing IPs to both canidates."); + logindent(1); + inject_ips($t1, $t2); + logindent(-1); + } + logindent(-1); + } + return $res; +} +# Get raw SNMP data for an ip/community. +sub get_snmp_data { + my ($ip, $community) = @_; + my %ret = (); + mylog("Polling $ip"); + eval { + my $session = nms::snmp::snmp_open_session($ip, $community); + $ret{'sysName'} = $session->get('sysName.0'); + $ret{'sysDescr'} = $session->get('sysDescr.0'); + $ret{'lldpLocChassisId'} = $session->get('lldpLocChassisId.0'); + $ret{'lldpRemManAddrTable'} = $session->gettable("lldpRemManAddrTable"); + $ret{'lldpRemTable'} = $session->gettable("lldpRemTable"); + $ret{'ipNetToMediaTable'} = $session->gettable('ipNetToMediaTable'); + $ret{'ifTable'} = $session->gettable('ifTable'); + $ret{'ifXTable'} = $session->gettable('ifXTable'); + $ret{'ipAddressTable'} = $session->gettable('ipAddressTable'); + $ret{'ipAddrTable'} = $session->gettable('ipAddrTable'); + #print Dumper(\%ret); + }; + if ($@) { + my $tmp = "$@"; + chomp($tmp); + mylog($tmp); + return undef; + } + if (!defined($ret{sysName})) { + return undef; + } + return \%ret; +} + +# Compare a new LLDP with old ones. +sub compare +{ + my %in = %{$_[0]}; + if (!defined($lldppeers{$in{id}})) { + return 1; + } + my %old = %{$lldppeers{$in{id}}}; + if ($old{name} eq $in{name}) { + return 0; + } else { + mylog("\t\tXXX: Fu... chassis ID collision from remote ID $in{id}."); + mylog("\t\t\tXXX: Old sysname/ip: $old{name} / $old{ip}"); + mylog("\t\t\tXXX: New sysname/ip: $in{name} / $in{ip}"); + return -1; + } +} + +sub peerdetect +{ + mylog("Detecting new candidates for probes"); + + logindent(1); + while (my ($target, $value) = each %snmpresults) { + while (my ($port, $data) = each %{$value->{interfaces}}) { + my %d = ( ip => $data->{lldpRemManAddr}, id => $data->{lldpRemChassisId}, name => $data->{lldpRemSysName} ); + if (!defined($d{ip})) { + next; + } + if (!defined($d{id})) { + mylog("wtf is up here? Man addr but no chassis id? I HATE NETWORK EQUIPMENT"); + next; + } + if (!defined($d{name})) { + mylog("I. Hate. Network. Equipment. We got IP, chassis id but no sysname."); + next; + } + if (compare(\%d) > 0) { + %{$lldppeers{$d{id}}} = %d; + mylog("Adding peer $d{name} / $d{ip} / $d{id}"); + } + } + } + logindent(-1); +} + +# Deduplicate entries of SNMP (in case we spotted the same thing twice) and +# detect collisions of sysnames and sort it out. +sub deduplicate +{ + my %remmap = (); + my %syscollisions = (); + my %locmap = (); + my %okips = (); + mylog("Building inventory of decent results/ips"); + logindent(1); + while (my ($target, $value) = each %snmpresults) { + if (defined($value->{fakeSnmp}) and $value->{fakeSnmp} eq "yes") { + next; + } + if (defined($value->{sysName}) and defined($value->{ips})) { + mylog("Ok: $target"); + foreach my $ip (@{$value->{ips}}) { + $okips{$ip} = 1; + } + } + } + logindent(-1); + mylog("Checking for empty SNMP results that are covered by other results"); + logindent(1); + my @removals = (); + while (my ($target, $value) = each %snmpresults) { + if (defined($value->{fakeSnmp}) and $value->{fakeSnmp} eq "yes") { + if (defined($okips{$target})) { + push @removals,$target; + } + } + } + mylog("Removing " . join(", ", @removals)); + foreach my $removal (@removals) { + delete $snmpresults{$removal}; + } + logindent(-1); + + mylog("Checking for duplicated/corrupt chassis ids"); + logindent(1); + mylog("Building sysname -> chassis id table where possible"); + logindent(1); + while (my ($target, $value) = each %snmpresults) { + while (my ($port, $data) = each %{$value->{interfaces}}) { + if (!defined($data->{lldpRemSysName})) { + next; + } + my $name = $data->{lldpRemSysName}; + my $id = $data->{lldpRemChassisId}; + if (defined($remmap{$name}) and $remmap{$name} ne $id) { + mylog("sysName collision. This is only an issue if there is _also_ chassis id collision."); + push @{$syscollisions{$name}}, $id; + } else { + $remmap{$name} = $id; + } + } + } + logindent(-1); + mylog("Building local chassis id map"); + logindent(1); + while (my ($target, $value) = each %snmpresults) { + my $locchassis = $value->{lldpLocChassisId}; + if (!defined($locchassis)) { + next; + } + push @{$locmap{$locchassis}}, $target; + } + logindent(-1); + mylog("Checking for chassis id's with duplicate systems"); + logindent(1); + my %fixlist = (); + my %tested = (); + while (my ($id, $value) = each %locmap) { + if (@$value > 1) { + mylog("Duplicate or collision: chassis id ($id) : " . join(", ", @$value)); + logindent(1); + my @removed = (); + foreach my $test (@$value) { + foreach my $isremoved1 (@removed) { + if ($isremoved1 eq $test) { + next; + } + } + foreach my $test2 (@$value) { + if ($test2 eq $test) { + next; + } + foreach my $isremoved2 (@removed) { + if ($isremoved2 eq $test2) { + next; + } + } + if (defined($tested{$test}{$test2}) or defined($tested{$test2}{$test})) { + next; + } elsif (compare_targets($test, $test2) != 2) { + mylog("Collision between $test and $test2. Adding to fixlist."); + mylog("a: " . $snmpresults{$test}{sysName} . " b: ". $snmpresults{$test2}{sysName}); + $tested{$test}{$test2} = 1; + $tested{$test2}{$test} = 1; + $fixlist{$test} = 1; + $fixlist{$test2} = 1; + } else { + $tested{$test}{$test2} = 1; + $tested{$test2}{$test} = 1; + push @removed, $test2; + } + } + } + foreach my $old (@removed) { + delete $snmpresults{$old}; + } + if (@removed > 0) { + mylog("Removed duplicates: " . join(", ", @removed)); + } + logindent(-1); + } + } + logindent(-1); + mylog("Applying fixes for ". (keys %fixlist) . " collisions"); + logindent(1); + while (my ($ip, $value) = each %fixlist) { + my $sysname = $snmpresults{$ip}; + if (defined($syscollisions{$sysname})) { + mylog("Can't really fix $ip because there's also a sysname collision for that sysname"); + $snmpresults{$ip}{lldpLocChassisId} = undef; + } elsif (defined($remmap{$sysname})) { + if ($remmap{$sysname} eq $snmpresults{$ip}{lldpLocChassisId}) { + mylog("Couldn't fix $ip / $sysname. The extrapolated chassis id seen from remote is the same as the colliding one.... "); + $snmpresults{$ip}{lldpLocChassisId} = undef; + } else { + mylog("Switching chassis ID from colliding $snmpresults{$ip}{lldpLocChassisId} to what remotes think matches: $remmap{$sysname}"); + $snmpresults{$ip}{lldpLocChassisId} = $remmap{$sysname}; + } + } else { + mylog("No alternate chassis IDs seen for $ip. Unsetting broken chassis ID anyway."); + $snmpresults{$ip}{lldpLocChassisId} = undef; + } + } + logindent(-1); + logindent(-1); +} + +# 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 $targetip = $_[1]; + my %result = (); + my %lol = (); + if (!defined($snmp)) { + mylog("XXX: No snmp data. Probably disconnect?"); + return; + } + my $sysname = $snmp->{sysName}; + mylog("Post-processing SNMP. Sysname: $sysname"); + logindent(1); + $result{sysName} = $sysname; + $result{sysDescr} = $snmp->{sysDescr}; + my $chassis_id = nms::convert_mac($snmp->{lldpLocChassisId}); + if (defined($chassis_id)) { + $result{lldpLocChassisId} = $chassis_id; + mylog("Chassis id: $chassis_id"); + } else { + mylog("XXX: No lldpLocChassisId found. Bummer. Enable LLDP?"); + } + @{$result{ips}} = (); + @{$result{peers}} = (); + mylog("Parsing lldp neighbors"); + logindent(1); + while (my ($key, $value) = each %{$snmp->{lldpRemTable}}) { + my $idx = $value->{lldpRemLocalPortNum}; + my $rem_chassis_id = nms::convert_mac($value->{'lldpRemChassisId'}); + my $remname = $value->{lldpRemSysName}; + mylog("Spotted $rem_chassis_id / $remname"); + foreach my $key2 (keys %$value) { + $lol{$idx}{$key2} = $value->{$key2}; + } + $lol{$idx}{key} = $key; + $lol{$idx}{'lldpRemChassisId'} = $rem_chassis_id; + my %caps = (); + nms::convert_lldp_caps($value->{'lldpRemSysCapEnabled'}, \%caps); + $lol{$idx}{'lldpRemSysCapEnabled'} = \%caps; + my %caps2 = (); + nms::convert_lldp_caps($value->{'lldpRemSysCapSupported'}, \%caps2); + $lol{$idx}{'lldpRemSysCapSupported'} = \%caps2; + } + logindent(-1); + mylog("Parsing lldp neighbors management interfaces"); + logindent(1); + while (my ($key, $value) = each %{$snmp->{lldpRemManAddrTable}}) { + my $old = 0; + my $idx = $value->{lldpRemLocalPortNum}; + my $remname = $lol{$idx}{lldpRemSysName}; + if (defined($lol{$idx}{lldpRemManAddr})) { + $old = $lol{$idx}{lldpRemManAddrSubtype}; + } + my $addr = $value->{'lldpRemManAddr'}; + my $addrtype = $value->{'lldpRemManAddrSubtype'}; + foreach my $key2 (keys %$value) { + if ($key2 eq 'lldpRemManAddr') { + next; + } + $lol{$idx}{$key2} = $value->{$key2}; + } + my $remip; + if ($addrtype == 1) { + $remip = nms::convert_ipv4($addr); + } elsif ($addrtype == 2) { + $remip = nms::convert_ipv6($addr); + } else { + next; + } + if ($old != 0) { + if ($old != $addrtype) { + mylog("Two management IPs discovered on port. v4 vs v6."); + mylog("\t$lol{$idx}{lldpRemManAddr} vs $remip"); + } + if ($old > $addrtype) { + $lol{$idx}{lldpRemManAddr} = $remip; + } + } else { + $lol{$idx}{lldpRemManAddr} = $remip; + } + push @{$result{peers}}, $remip; + push @{$lol{$idx}{peers}}, $remip; + } + logindent(-1); + mylog("Parsing local interfaces"); + logindent(1); + while (my ($key, $value) = each %{$snmp->{ifTable}}) { + $value->{ifPhysAddress} = nms::convert_mac($value->{ifPhysAddress}); + foreach my $key2 (keys %$value) { + $lol{$value->{ifIndex}}{$key2} = $value->{$key2}; + } + } + logindent(-1); + mylog("Parsing ARP table"); + logindent(1); + while (my ($key, $value) = each %{$snmp->{ipNetToMediaTable}}) { + my $mac = nms::convert_mac($value->{ipNetToMediaPhysAddress}); + my $idx = $value->{ipNetToMediaIfIndex}; + $value->{ipNetToMediaPhysAddress} = $mac; + push @{$lol{$idx}{ARP}}, $value; + if ($lol{$idx}{ifPhysAddress} eq $mac) { + push @{$lol{$idx}{ips}}, $value->{ipNetToMediaNetAddress}; + push @{$result{ips}}, $value->{ipNetToMediaNetAddress}; + mylog("Found my self: " . $value->{ipNetToMediaNetAddress}); + } else { + push @{$lol{$idx}{peers}}, $value->{ipNetToMediaNetAddress}; + push @{$result{peers}}, $value->{ipNetToMediaNetAddress}; + } + } + while (my ($key, $value) = each %{$snmp->{ifXTable}}) { + foreach my $key2 (keys %$value) { + $lol{$key}{$key2} = $value->{$key2}; + } + } + logindent(-1); + mylog("Parsing ipAddressTable"); + logindent(1); + while (my ($key, $value) = each %{$snmp->{ipAddressTable}}) { + my $addr = $value->{ipAddressAddr}; + my $addrtype = $value->{ipAddressAddrType}; + if ($addrtype == 1) { + $addr = nms::convert_ipv4($addr); + } elsif ($addrtype == 2) { + $addr = nms::convert_ipv6($addr); + } else { + next; + } + mylog("Local IP added: $addr"); + push @{$result{ips}}, $addr; + } + logindent(-1); + mylog("Parsing ipAddrTable"); + logindent(1); + while (my ($key, $value) = each %{$snmp->{ipAddrTable}}) { + push @{$result{ips}}, $value->{ipAdEntAddr}; + mylog("Adding local ipv4 ip $value->{ipAdEntAddr}"); + } + logindent(-1); + @{$result{peers}} = sort @{$result{peers}}; + @{$result{ips}} = sort @{$result{ips}}; + $result{interfaces} = \%lol; + mylog("Ensuring some sanity: Checking if the $targetip is among claimed IPs for the SNMP results"); + logindent(1); + my $sanitytest = 0; + foreach my $ip (@{$result{ips}}) { + if ($ip eq $targetip) { + mylog("Phew, it is..."); + $sanitytest = 1; + } + } + if ($sanitytest == 0) { + mylog("Didn't find myself. Hoping deduplication will take care of it?"); + $result{badSelf} = 1; + } + logindent(-1); + mylog("Registering known ips for " . ($sysname || "?" )); + foreach my $ip (@{$result{ips}}) { + $claimedips{$ip} = $sysname || "?"; + } + logindent(-1); + + return \%result; +} +sub inject_ips { + my ($t1, $t2) = @_; + push @{$snmpresults{$t1}{ips}}, $t1; + push @{$snmpresults{$t1}{ips}}, $t2; + push @{$snmpresults{$t2}{ips}}, $t1; + push @{$snmpresults{$t2}{ips}}, $t2; +} +sub logindent { + my $change = $_[0]; + if ($change > 0) { + $mylogindent = $mylogindent . "\t"; + } else { + chop $mylogindent; + } +} +sub mylog { + my $msg = shift; + my $time = POSIX::ctime(time); + $time =~ s/\n.*$//; + printf STDERR "[%s] %s%s\n", $time, $mylogindent, $msg; +} +# Porbe a single system. +sub probe_sys +{ + my ($target, $community) = @_; + if (defined($snmpresults{$target})) { + mylog("Already probed $target. Skipping."); + return; + } + if (defined($claimedips{$target})) { + mylog("IP claimed by $claimedips{$target}. Skipping."); + return; + } + my $snmp = get_snmp_data($target, $community); + if (!defined($snmp)) { + return; + } + my $parsed = parse_snmp($snmp,$target); + if (!defined($parsed)) { + return; + } + $snmpresults{$target} = $parsed; +} +sub populate_lldpmap +{ + mylog("Populate LLDP map"); + while (my ($ip, $value) = each %snmpresults) { + my $sysname = $value->{sysName}; + my $id = $value->{lldpLocChassisId}; + while (my ($if, $data) = each %{$value->{interfaces}}) { + if (!defined($data->{lldpRemSysName})) { + next; + } else { + if (defined($id)) { + $lldpmap{$id}{$data->{lldpRemChassisId}} = 1; + } + $lldpnamemap{$sysname}{$data->{lldpRemSysName}} = 1; + } + } + } +} + +sub populate_ipmap +{ + mylog("Populating ipmap"); + my @conflicts = (); + logindent(1); + while (my ($ip, $value) = each %snmpresults) { + my $sysname = $value->{sysName}; + if (defined($ipmap{$ip})) { + mylog("Conflict for ip $ip"); + } + $ipmap{$ip} = $ip; + foreach my $localip (@{$value->{ips}}) { + if (!defined($localip)) { + next; + } elsif (defined($ipmap{$localip}) and $ipmap{$localip} ne $ip) { + mylog("IP conflict: $localip found multiple places ($ipmap{$localip} vs $ip)"); + push @conflicts, $localip; + } + $ipmap{$localip} = $ip; + } + } + mylog("Removing conflicting IPs"); + foreach my $contested (@conflicts) { + delete $ipmap{$contested}; + } + logindent(-1); +} + +sub pad_snmp_results +{ + mylog("Checking if there are peers from LLDP with no basic SNMP info"); + logindent(1); + while (my ($id, $value) = each %lldppeers) { + if (!defined($value->{ip}) or defined($snmpresults{$value->{ip}}{sysName})) { + next; + } + mylog("Adding basic info for $value->{ip} / $value->{name} to snmp results"); + $snmpresults{$value->{ip}}{sysName} = $value->{name}; + $snmpresults{$value->{ip}}{lldpLocChassisId} = $value->{id}; + $snmpresults{$value->{ip}}{fakeSnmp} = "yes"; + push @{$snmpresults{$value->{ip}}{ips}}, $value->{ip}; + } + logindent(-1); +} +sub populate_peermap +{ + mylog("Populate layer3 peermap"); + while (my ($ip, $value) = each %snmpresults) { + my $sysname = $value->{sysName}; + foreach my $peer (@{$value->{peers}}) { + $peermap{$ip}{$peer} = 1; + } + } +} |