aboutsummaryrefslogtreecommitdiffstats
path: root/extras/tools
diff options
context:
space:
mode:
Diffstat (limited to 'extras/tools')
-rwxr-xr-xextras/tools/lldp/dotnet.sh11
-rwxr-xr-xextras/tools/lldp/draw-neighbors.pl79
-rwxr-xr-xextras/tools/lldp/lldpdiscover.pl389
-rwxr-xr-xextras/tools/lldp/lolwhat.pl666
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;
+ }
+ }
+}