From 6a5af958363271a9311e86421ffcd1c7c35a0fbc Mon Sep 17 00:00:00 2001 From: Oliver Gorwits Date: Sun, 19 May 2013 22:06:27 +0100 Subject: [PATCH] arpnip implementation --- Netdisco/bin/netdisco-do | 1 + .../lib/App/Netdisco/DB/ExplicitLocking.pm | 2 +- .../App/Netdisco/DB/ResultSet/NodeWireless.pm | 2 +- .../DB/ResultSet/{Subnets.pm => Subnet.pm} | 2 +- Netdisco/lib/App/Netdisco/Daemon/Queue.pm | 2 +- .../lib/App/Netdisco/Daemon/Worker/Manager.pm | 2 +- .../lib/App/Netdisco/Daemon/Worker/Poller.pm | 1 + .../Netdisco/Daemon/Worker/Poller/Arpnip.pm | 41 ++++ Netdisco/lib/App/Netdisco/Util/Arpnip.pm | 216 ++++++++++++++++++ .../lib/App/Netdisco/Util/DiscoverAndStore.pm | 3 +- TODO | 2 +- 11 files changed, 267 insertions(+), 7 deletions(-) rename Netdisco/lib/App/Netdisco/DB/ResultSet/{Subnets.pm => Subnet.pm} (76%) create mode 100644 Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Arpnip.pm create mode 100644 Netdisco/lib/App/Netdisco/Util/Arpnip.pm diff --git a/Netdisco/bin/netdisco-do b/Netdisco/bin/netdisco-do index aade7701..da996bda 100755 --- a/Netdisco/bin/netdisco-do +++ b/Netdisco/bin/netdisco-do @@ -50,6 +50,7 @@ if (!length $action) { package MyWorker; use Moo; with 'App::Netdisco::Daemon::Worker::Poller::Device'; + with 'App::Netdisco::Daemon::Worker::Poller::Arpnip'; } my $worker = MyWorker->new(); diff --git a/Netdisco/lib/App/Netdisco/DB/ExplicitLocking.pm b/Netdisco/lib/App/Netdisco/DB/ExplicitLocking.pm index 8f691b32..25fe31aa 100644 --- a/Netdisco/lib/App/Netdisco/DB/ExplicitLocking.pm +++ b/Netdisco/lib/App/Netdisco/DB/ExplicitLocking.pm @@ -45,7 +45,7 @@ sub txn_do_locked { my $table_fmt = join ', ', ('%s' x scalar @$table); my $sql = sprintf $sql_fmt, $table_fmt; - if (length $mode) { + if (ref '' eq ref $mode and length $mode) { scalar grep {$_ eq $mode} values %lock_modes or $schema->throw_exception('bad LOCK_MODE to txn_do_locked()'); } diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeWireless.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeWireless.pm index c41b74f2..a86cf712 100644 --- a/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeWireless.pm +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeWireless.pm @@ -1,4 +1,4 @@ -package App::Netdisco::DB::ResultSet::DeviceModule; +package App::Netdisco::DB::ResultSet::NodeWireless; use base 'DBIx::Class::ResultSet'; use strict; diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet/Subnets.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet/Subnet.pm similarity index 76% rename from Netdisco/lib/App/Netdisco/DB/ResultSet/Subnets.pm rename to Netdisco/lib/App/Netdisco/DB/ResultSet/Subnet.pm index 954c81d3..75e85a2e 100644 --- a/Netdisco/lib/App/Netdisco/DB/ResultSet/Subnets.pm +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet/Subnet.pm @@ -1,4 +1,4 @@ -package App::Netdisco::DB::ResultSet::Subnets; +package App::Netdisco::DB::ResultSet::Subnet; use base 'DBIx::Class::ResultSet'; use strict; diff --git a/Netdisco/lib/App/Netdisco/Daemon/Queue.pm b/Netdisco/lib/App/Netdisco/Daemon/Queue.pm index 7dae68f3..d5649dea 100644 --- a/Netdisco/lib/App/Netdisco/Daemon/Queue.pm +++ b/Netdisco/lib/App/Netdisco/Daemon/Queue.pm @@ -33,7 +33,7 @@ sub capacity_for { debug "checking local capacity for action $action"; my $action_map = { - Poller => [qw/refresh discover discovernew discover_neighbors/], + Poller => [qw/refresh discover discovernew discover_neighbors arpnip/], Interactive => [qw/location contact portcontrol portname vlan power/], }; diff --git a/Netdisco/lib/App/Netdisco/Daemon/Worker/Manager.pm b/Netdisco/lib/App/Netdisco/Daemon/Worker/Manager.pm index 3a6871d3..f7ad7673 100644 --- a/Netdisco/lib/App/Netdisco/Daemon/Worker/Manager.pm +++ b/Netdisco/lib/App/Netdisco/Daemon/Worker/Manager.pm @@ -14,7 +14,7 @@ my $fqdn = hostfqdn || 'localhost'; my $role_map = { (map {$_ => 'Poller'} - qw/refresh discover discovernew discover_neighbors/), + qw/refresh discover discovernew discover_neighbors arpnip/), (map {$_ => 'Interactive'} qw/location contact portcontrol portname vlan power/) }; diff --git a/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller.pm b/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller.pm index 5b6ca4a9..1b5a4539 100644 --- a/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller.pm +++ b/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller.pm @@ -10,6 +10,7 @@ use namespace::clean; # add dispatch methods for poller tasks with 'App::Netdisco::Daemon::Worker::Poller::Device'; +with 'App::Netdisco::Daemon::Worker::Poller::Arpnip'; sub worker_body { my $self = shift; diff --git a/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Arpnip.pm b/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Arpnip.pm new file mode 100644 index 00000000..2d4d00ec --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Arpnip.pm @@ -0,0 +1,41 @@ +package App::Netdisco::Daemon::Worker::Poller::Arpnip; + +use Dancer qw/:moose :syntax :script/; +use Dancer::Plugin::DBIC 'schema'; + +use App::Netdisco::Util::SNMP 'snmp_connect'; +use App::Netdisco::Util::Device 'get_device'; +use App::Netdisco::Util::Arpnip ':all'; +use App::Netdisco::Daemon::Util ':all'; + +use NetAddr::IP::Lite ':lower'; + +use Role::Tiny; +use namespace::clean; + +sub arpnip { + my ($self, $job) = @_; + + my $host = NetAddr::IP::Lite->new($job->device); + my $device = get_device($host->addr); + + if ($device->in_storage + and $device->vendor and $device->vendor eq 'netdisco') { + return job_done("Skipped arpnip for pseudo-device $host"); + } + + my $snmp = snmp_connect($device); + if (!defined $snmp) { + return job_error("arpnip failed: could not SNMP connect to $host"); + } + + unless ($snmp->has_layer(3)) { + return job_done("Skipped arpnip for device $host without OSI layer 3 capability"); + } + + do_arpnip($device, $snmp); + + return job_done("Ended arpnip for $host"); +} + +1; diff --git a/Netdisco/lib/App/Netdisco/Util/Arpnip.pm b/Netdisco/lib/App/Netdisco/Util/Arpnip.pm new file mode 100644 index 00000000..ce5f5318 --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Util/Arpnip.pm @@ -0,0 +1,216 @@ +package App::Netdisco::Util::Arpnip; + +use Dancer qw/:syntax :script/; +use Dancer::Plugin::DBIC 'schema'; + +use App::Netdisco::DB::ExplicitLocking ':modes'; +use App::Netdisco::Util::DNS ':all'; +use NetAddr::IP::Lite ':lower'; +use Net::MAC; + +use base 'Exporter'; +our @EXPORT = (); +our @EXPORT_OK = qw/ do_arpnip /; +our %EXPORT_TAGS = (all => \@EXPORT_OK); + +=head1 NAME + +App::Netdisco::Util::Arpnip + +=head1 DESCRIPTION + +Helper subroutine to support parts of the Netdisco application. + +There are no default exports, however the C<:all> tag will export all +subroutines. + +=head1 EXPORT_OK + +=head2 arpnip( $device, $snmp ) + +Given a Device database object, and a working SNMP connection, connect to a +device and discover its ARP cache for IPv4 and Neighbor cache for IPv6. + +=cut + +sub do_arpnip { + my ($device, $snmp) = @_; + + unless ($device->in_storage) { + debug sprintf ' [%s] arpnip - skipping device not yet discovered', $device->ip; + return; + } + + my $port_macs = _get_port_macs($device, $snmp); + + schema('netdisco')->resultset('NodeIp')->txn_do_locked( + EXCLUSIVE, sub { + my $arp_count = _add_arps($device, $snmp, $port_macs); + debug sprintf ' [%s] arpnip - processed %s ARP Cache entries', + $device->ip, $arp_count; + + my $neigh_count = _add_neighbors($device, $snmp, $port_macs); + debug sprintf ' [%s] arpnip - processed %s IPv6 Neighbor Cache entries', + $device->ip, $neigh_count; + + $device->update({last_arpnip => \'now()'}); + }); + + schema('netdisco')->resultset('Subnet') + ->txn_do_locked(EXCLUSIVE, sub { _add_subnets($device, $snmp) }); + # TODO: IPv6 subnets +} + +# add arp table to DB +sub _add_arps { + my ($device, $snmp, $port_macs) = @_; + my $count = 0; + + # Fetch ARP Cache + my $at_paddr = $snmp->at_paddr; + my $at_netaddr = $snmp->at_netaddr; + + while (my ($arp, $node) = each %$at_paddr) { + my $ip = $at_netaddr->{$arp}; + next unless defined $ip; + $count += _check_and_store($device, $port_macs, $node, $ip); + } + + return $count; +} + +# add v6 neighbor cache to db +sub _add_neighbors { + my ($device, $snmp, $port_macs) = @_; + my $count = 0; + + # Fetch v6 Neighbor Cache + my $phys_addr = $snmp->ipv6_n2p_mac; + my $net_addr = $snmp->ipv6_n2p_addr; + + while (my ($arp, $node) = each %$phys_addr) { + my $ip = $net_addr->{$arp}; + next unless defined $ip; + $count += _check_and_store($device, $port_macs, $node, $ip); + } + + return $count; +} + +# checks any arpnip entry for sanity and adds to DB +sub _check_and_store { + my ($device, $port_macs, $node, $ip) = @_; + my $mac = Net::MAC->new(mac => $node, 'die' => 0, verbose => 0); + + # incomplete MAC addresses (BayRS frame relay DLCI, etc) + if ($mac->get_error) { + debug sprintf ' [%s] arpnip - mac [%s] malformed - skipping', + $device->ip, $node; + return 0; + } + else { + # lower case, hex, colon delimited, 8-bit groups + $node = lc $mac->as_IEEE; + } + + # broadcast MAC addresses + return 0 if $node eq 'ff:ff:ff:ff:ff:ff'; + + # CLIP + return 0 if $node eq '00:00:00:00:00:01'; + + # VRRP + if (index($node, '00:00:5e:00:01:') == 0) { + debug sprintf ' [%s] arpnip - VRRP mac [%s] - skipping', + $device->ip, $node; + return 0; + } + + # HSRP + if (index($node, '00:00:0c:07:ac:') == 0) { + debug sprintf ' [%s] arpnip - HSRP mac [%s] - skipping', + $device->ip, $node; + return 0; + } + + # device's own MACs + if (exists $port_macs->{$node}) { + debug sprintf ' [%s] arpnip - mac [%s] is device port - skipping', + $device->ip, $node; + return 0; + } + + debug sprintf ' [%s] arpnip - IP [%s] : mac [%s]', + $device->ip, $ip, $node; + _add_arp($node, $ip); + + return 1; +} + +# add arp cache entry to the node_ip table +sub _add_arp { + my ($mac, $ip) = @_; + + schema('netdisco')->resultset('NodeIp') + ->search({ip => $ip, -bool => 'active'}) + ->update({active => \'false'}); + + schema('netdisco')->resultset('NodeIp') + ->search({mac => $mac, ip => $ip}) + ->update_or_create({ + mac => $mac, + ip => $ip, + dns => hostname_from_ip($ip), + active => \'true', + time_last => \'now()', + }); +} + +# gathers and stores device subnets +sub _add_subnets { + my ($device, $snmp) = @_; + + my $ip_netmask = $snmp->ip_netmask; + my $localnet = NetAddr::IP::Lite->new('127.0.0.0/8'); + + foreach my $entry (keys %$ip_netmask) { + my $ip = NetAddr::IP::Lite->new($entry); + my $addr = $ip->addr; + + next if $addr eq '0.0.0.0'; + next if $ip->within($localnet); + next if setting('ignore_private_nets') and $ip->is_rfc1918; + + my $netmask = $ip_netmask->{$addr}; + next if $netmask eq '255.255.255.255' or $netmask eq '0.0.0.0'; + + my $cidr = NetAddr::IP::Lite->new($addr, $netmask)->network->cidr; + schema('netdisco')->resultset('Subnet') + ->update_or_create({net => $cidr, last_discover => \'now()'}); + + debug sprintf ' [%s] arpnip - found subnet %s', $device->ip, $cidr; + } +} + +# returns table of MACs used by device's interfaces so that we don't bother to +# macsuck them. +sub _get_port_macs { + my ($device, $snmp) = @_; + my $port_macs; + + my $dp_macs = schema('netdisco')->resultset('DevicePort') + ->search({ mac => { '!=' => undef} }); + while (my $r = $dp_macs->next) { + $port_macs->{ $r->mac } = $r->ip; + } + + my $d_macs = schema('netdisco')->resultset('Device') + ->search({ mac => { '!=' => undef} }); + while (my $r = $d_macs->next) { + $port_macs->{ $r->mac } = $r->ip; + } + + return $port_macs; +} + +1; diff --git a/Netdisco/lib/App/Netdisco/Util/DiscoverAndStore.pm b/Netdisco/lib/App/Netdisco/Util/DiscoverAndStore.pm index d855b94e..3b613c4a 100644 --- a/Netdisco/lib/App/Netdisco/Util/DiscoverAndStore.pm +++ b/Netdisco/lib/App/Netdisco/Util/DiscoverAndStore.pm @@ -52,6 +52,7 @@ sub store_device { my $hostname = hostname_from_ip($device->ip); $device->dns($hostname) if length $hostname; + my $localnet = NetAddr::IP::Lite->new('127.0.0.0/8'); # build device aliases suitable for DBIC my @aliases; @@ -60,7 +61,7 @@ sub store_device { my $addr = $ip->addr; next if $addr eq '0.0.0.0'; - next if $ip->within(NetAddr::IP::Lite->new('127.0.0.0/8')); + next if $ip->within($localnet); next if setting('ignore_private_nets') and $ip->is_rfc1918; my $iid = $ip_index->{$addr}; diff --git a/TODO b/TODO index ed4515a9..02082943 100644 --- a/TODO +++ b/TODO @@ -11,7 +11,7 @@ BACKEND DAEMON ====== -* macsuck/arpnip +* macsuck CORE ====