aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSteinar H. Gunderson <sgunderson@bigfoot.com>2014-04-06 19:14:30 +0200
committerSteinar H. Gunderson <sgunderson@bigfoot.com>2014-04-06 19:14:30 +0200
commit6e89dd3ce870714bd2a22615587181d25186fe7a (patch)
tree40c7fc4bfd6d792c0e748b466c03be988367a04b
parent0804c84cbb43a56a021c4963e06c88b302a831a4 (diff)
Add a script to discover new devices based on LLDP information.
-rwxr-xr-xclients/lldpdiscover.pl136
-rw-r--r--include/nms.pm46
-rw-r--r--sql/nms.sql3
3 files changed, 183 insertions, 2 deletions
diff --git a/clients/lldpdiscover.pl b/clients/lldpdiscover.pl
new file mode 100755
index 0000000..f90c5c3
--- /dev/null
+++ b/clients/lldpdiscover.pl
@@ -0,0 +1,136 @@
+#! /usr/bin/perl
+use DBI;
+use POSIX;
+use Time::HiRes;
+use strict;
+use warnings;
+
+use lib '../include';
+use nms;
+
+my $dbh = nms::db_connect();
+$dbh->{AutoCommit} = 0;
+
+# If we are given one switch on the command line, poll that and then exit.
+my ($cmdline_ip, $cmdline_community) = @ARGV;
+if (defined($cmdline_ip) && defined($cmdline_community)) {
+ eval {
+ discover_lldp_neighbors($dbh, $cmdline_ip, $cmdline_community);
+ };
+ if ($@) {
+ mylog("ERROR: $@ (during poll of $cmdline_ip)");
+ $dbh->rollback;
+ }
+ $dbh->disconnect;
+ exit;
+}
+
+# First, find all machines that lack an LLDP chassis ID.
+my $q = $dbh->prepare("SELECT switch, ip, community FROM switches WHERE lldp_chassis_id IS NULL AND ip <> '127.0.0.1'");
+$q->execute;
+
+while (my $ref = $q->fetchrow_hashref) {
+ my ($switch, $ip, $community) = ($ref->{'switch'}, $ref->{'ip'}, $ref->{'community'});
+ eval {
+ my $session = nms::snmp_open_session($ip, $community);
+
+ # Cisco returns completely bogus values if we use get()
+ # on lldpLocChassisId.0, it seems. Work around it by using getnext().
+ my $response = $session->getnext('lldpLocChassisId');
+ my $chassis_id = nms::convert_mac($response);
+ die "SNMP error: " . $session->error() if (!defined($chassis_id));
+ $dbh->do('UPDATE switches SET lldp_chassis_id=? WHERE switch=?', undef,
+ $chassis_id, $switch);
+ mylog("Set chassis ID for $ip to $chassis_id.");
+ };
+ if ($@) {
+ mylog("ERROR: $@ (during poll of $ip)");
+ $dbh->rollback;
+ }
+}
+
+# Now ask all switches for their LLDP neighbor table.
+$q = $dbh->prepare("SELECT ip, community FROM switches WHERE lldp_chassis_id IS NOT NULL AND ip <> '127.0.0.1'");
+$q->execute;
+
+while (my $ref = $q->fetchrow_hashref) {
+ my ($ip, $community) = ($ref->{'ip'}, $ref->{'community'});
+ eval {
+ discover_lldp_neighbors($dbh, $ip, $community);
+ };
+ if ($@) {
+ mylog("ERROR: $@ (during poll of $ip)");
+ $dbh->rollback;
+ }
+}
+
+$dbh->disconnect;
+
+sub discover_lldp_neighbors {
+ my ($dbh, $ip, $community) = @_;
+ my $qexist = $dbh->prepare('SELECT COUNT(*) AS cnt FROM switches WHERE lldp_chassis_id=?');
+
+ my $session = nms::snmp_open_session($ip, $community);
+ my $remtable = $session->gettable('lldpRemTable');
+ my $addrtable;
+ while (my ($key, $value) = each %$remtable) {
+ my $chassis_id = nms::convert_mac($value->{'lldpRemChassisId'});
+ my $sysname = $value->{'lldpRemSysName'};
+
+ # Do not try to poll servers.
+ my %caps = ();
+ nms::convert_lldp_caps($value->{'lldpRemSysCapEnabled'}, \%caps);
+ next if (!$caps{'cap_enabled_bridge'} && !$caps{'cap_enabled_ap'} && !$caps{'cap_enabled_router'});
+
+ my $exists = $dbh->selectrow_hashref($qexist, undef, $chassis_id)->{'cnt'};
+ next if ($exists);
+
+ # Pull in the management address table lazily.
+ $addrtable = $session->gettable("lldpRemManAddrTable") if (!defined($addrtable));
+
+ # Search for this key in the address table.
+ my @v4addrs = ();
+ my @v6addrs = ();
+ while (my ($addrkey, $addrvalue) = each %$addrtable) {
+ #next unless $addrkey =~ /^\Q$key\E\.1\.4\.(.*)$/; # 1.4 = ipv4, 2.16 = ipv6
+ next unless $addrkey =~ /^\Q$key\E\./; # 1.4 = ipv4, 2.16 = ipv6
+ my $addr = $addrvalue->{'lldpRemManAddr'};
+ my $addrtype = $addrvalue->{'lldpRemManAddrSubtype'};
+ if ($addrtype == 1) {
+ push @v4addrs, nms::convert_ipv4($addr);
+ } elsif ($addrtype == 2) {
+ push @v6addrs, nms::convert_ipv6($addr);
+ } else {
+ die "Unknown address type $addr";
+ }
+ }
+ my $addr;
+ if (scalar @v6addrs > 0) {
+ $addr = $v6addrs[0];
+ } elsif (scalar @v4addrs > 0) {
+ $addr = $v4addrs[0];
+ } else {
+ warn "Could not find a management address for chassis ID $chassis_id (sysname=$sysname, lldpRemIndex=$key)";
+ next;
+ }
+
+ # Yay, a new switch! Make a new type for it.
+ # We simply guess that the community is the same as ours.
+ # TODO(sesse): Autopopulate ports from type.
+ # TODO(sesse): Autopopulate port names.
+ mylog("Inserting new switch $sysname ($addr).");
+ my $switchtype = "auto-$sysname-$chassis_id";
+ $dbh->do('INSERT INTO switchtypes (switchtype, ports) VALUES (?, ?)', undef,
+ $switchtype, '');
+ $dbh->do('INSERT INTO switches (ip, sysname, switchtype, community, lldp_chassis_id) VALUES (?, ?, ?, ?, ?)', undef,
+ $addr, $sysname, $switchtype, $community, $chassis_id);
+ $dbh->commit;
+ }
+}
+
+sub mylog {
+ my $msg = shift;
+ my $time = POSIX::ctime(time);
+ $time =~ s/\n.*$//;
+ printf STDERR "[%s] %s\n", $time, $msg;
+}
diff --git a/include/nms.pm b/include/nms.pm
index 0bf557e..e858c8b 100644
--- a/include/nms.pm
+++ b/include/nms.pm
@@ -18,11 +18,15 @@ 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::loadModules('SNMPv2-MIB');
SNMP::loadModules('ENTITY-MIB');
SNMP::loadModules('IF-MIB');
- #SNMP::loadModules('LLDP-MIB');
+ SNMP::loadModules('LLDP-MIB');
}
sub db_connect {
@@ -169,4 +173,44 @@ sub fetch_multi_snmp {
return \%results;
}
+# A few utilities to convert from SNMP binary address format to human-readable.
+
+sub convert_mac {
+ return join(':', map { sprintf "%02x", $_ } unpack('C*', shift));
+}
+
+sub convert_ipv4 {
+ return join('.', map { sprintf "%d", $_ } unpack('C*', shift));
+}
+
+sub convert_ipv6 {
+ return join(':', map { sprintf "%x", $_ } unpack('n*', shift));
+}
+
+sub convert_addr {
+ my ($data, $type) = @_;
+ if ($type == 1) {
+ return convert_ipv4($data);
+ } elsif ($type == 2) {
+ return convert_ipv6($data);
+ } else {
+ die "Unknown address type $type";
+ }
+}
+
+# Convert raw binary SNMP data to list of bits.
+sub convert_bytelist {
+ return split //, unpack("B*", shift);
+}
+
+sub convert_lldp_caps {
+ my ($caps_data, $data) = @_;
+
+ my @caps = convert_bytelist($caps_data);
+ my @caps_names = qw(other repeater bridge ap router telephone docsis stationonly);
+ for (my $i = 0; $i < scalar @caps && $i < scalar @caps_names; ++$i) {
+ $data->{'cap_enabled_' . $caps_names[$i]} = $caps[$i];
+ }
+}
+
1;
diff --git a/sql/nms.sql b/sql/nms.sql
index d102798..acadcc5 100644
--- a/sql/nms.sql
+++ b/sql/nms.sql
@@ -545,7 +545,8 @@ CREATE TABLE switches (
locked boolean DEFAULT false NOT NULL,
priority integer DEFAULT 0 NOT NULL,
poll_frequency interval DEFAULT '00:01:00'::interval NOT NULL,
- community character varying DEFAULT 'public'::character varying NOT NULL
+ community character varying DEFAULT 'public'::character varying NOT NULL,
+ lldp_chassis_id character varying
);