diff options
author | Steinar H. Gunderson <sgunderson@bigfoot.com> | 2014-04-06 19:14:30 +0200 |
---|---|---|
committer | Steinar H. Gunderson <sgunderson@bigfoot.com> | 2014-04-06 19:14:30 +0200 |
commit | 6e89dd3ce870714bd2a22615587181d25186fe7a (patch) | |
tree | 40c7fc4bfd6d792c0e748b466c03be988367a04b | |
parent | 0804c84cbb43a56a021c4963e06c88b302a831a4 (diff) |
Add a script to discover new devices based on LLDP information.
-rwxr-xr-x | clients/lldpdiscover.pl | 136 | ||||
-rw-r--r-- | include/nms.pm | 46 | ||||
-rw-r--r-- | sql/nms.sql | 3 |
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 ); |