While setting up an sshcollector, I found that the script doesn't handle an
arpless device very gracefully. The log looks like:
[46567] 2018-04-10 16:59:08 warn WARNING: no entries received from <IP>
Can't use string ("1") as an ARRAY ref while "strict refs" in use at perl5/bin/netdisco-sshcollector line 114, <__ANONIO__> line 3.
This is fixed by making the process function always return the expected
structure, even if it's empty. After this error, the following errors showed
up:
[53232] 2018-04-10 17:06:15 warn WARNING: no entries received from <IP>
[53210] 2018-04-10 17:06:15 info [134.71.0.126] arpnip - retrieved 0 entries
Use of uninitialized value in sprintf at perl5/bin/netdisco-sshcollector line 119, <__ANONIO__> line 3.
[53210] 2018-04-10 17:06:15 info arpnip - processed ARP Cache entries from 1 devices
This is because $stats{entry} only becomes an integer by being incremented, so
if there aren't any entries, it never exists. This is fixed by starting it at 0.
295 lines
7.4 KiB
Perl
Executable File
295 lines
7.4 KiB
Perl
Executable File
#!/usr/bin/env perl
|
|
|
|
use warnings;
|
|
use strict;
|
|
|
|
our $home;
|
|
|
|
BEGIN {
|
|
use FindBin;
|
|
FindBin::again();
|
|
|
|
$home = ($ENV{NETDISCO_HOME} || $ENV{HOME});
|
|
|
|
# try to find a localenv if one isn't already in place.
|
|
if (!exists $ENV{PERL_LOCAL_LIB_ROOT}) {
|
|
use File::Spec;
|
|
my $localenv = File::Spec->catfile($FindBin::RealBin, 'localenv');
|
|
exec($localenv, $0, @ARGV) if -f $localenv;
|
|
$localenv = File::Spec->catfile($home, 'perl5', 'bin', 'localenv');
|
|
exec($localenv, $0, @ARGV) if -f $localenv;
|
|
|
|
die "Sorry, can't find libs required for App::Netdisco.\n"
|
|
if !exists $ENV{PERLBREW_PERL};
|
|
}
|
|
}
|
|
|
|
BEGIN {
|
|
use Path::Class;
|
|
|
|
# stuff useful locations into @INC and $PATH
|
|
unshift @INC,
|
|
dir($FindBin::RealBin)->parent->subdir('lib')->stringify,
|
|
dir($FindBin::RealBin, 'lib')->stringify;
|
|
|
|
unshift @INC,
|
|
split m/:/, ($ENV{NETDISCO_INC} || '');
|
|
|
|
use Config;
|
|
$ENV{PATH} = $FindBin::RealBin . $Config{path_sep} . $ENV{PATH};
|
|
}
|
|
|
|
use App::Netdisco;
|
|
use App::Netdisco::Util::Node qw/check_mac store_arp/;
|
|
use App::Netdisco::Util::FastResolver 'hostnames_resolve_async';
|
|
use Dancer ':script';
|
|
|
|
use Data::Printer;
|
|
use Module::Load ();
|
|
use Net::OpenSSH;
|
|
use MCE::Loop Sereal => 1;
|
|
use Pod::Usage 'pod2usage';
|
|
|
|
use Getopt::Long;
|
|
Getopt::Long::Configure ("bundling");
|
|
|
|
my ($debug, $sqltrace) = (undef, 0);
|
|
my $result = GetOptions(
|
|
'debug|D' => \$debug,
|
|
'sqltrace|Q+' => \$sqltrace,
|
|
) or pod2usage(
|
|
-msg => 'error: bad options',
|
|
-verbose => 0,
|
|
-exitval => 1,
|
|
);
|
|
|
|
my $CONFIG = config();
|
|
$CONFIG->{logger} = 'console';
|
|
$CONFIG->{log} = ($debug ? 'debug' : 'info');
|
|
$ENV{DBIC_TRACE} ||= $sqltrace;
|
|
|
|
# reconfigure logging to force console output
|
|
Dancer::Logger->init('console', $CONFIG);
|
|
|
|
#this may be helpful with SSH issues:
|
|
#$Net::OpenSSH::debug = ~0;
|
|
|
|
MCE::Loop::init { chunk_size => 1 };
|
|
my %stats;
|
|
$stats{entry} = 0;
|
|
|
|
exit main();
|
|
|
|
sub main {
|
|
my @input = @{ setting('sshcollector') };
|
|
|
|
my @mce_result = mce_loop {
|
|
my ($mce, $chunk_ref, $chunk_id) = @_;
|
|
my $host = $chunk_ref->[0];
|
|
|
|
my $hostlabel = (!defined $host->{hostname} or $host->{hostname} eq "-")
|
|
? $host->{ip} : $host->{hostname};
|
|
|
|
if ($hostlabel) {
|
|
my $ssh = Net::OpenSSH->new(
|
|
$hostlabel,
|
|
user => $host->{user},
|
|
password => $host->{password},
|
|
timeout => 30,
|
|
async => 0,
|
|
master_opts => [
|
|
-o => "StrictHostKeyChecking=no",
|
|
-o => "BatchMode=no"
|
|
],
|
|
);
|
|
|
|
MCE->gather( process($hostlabel, $ssh, $host) );
|
|
}
|
|
} \@input;
|
|
|
|
return 0 unless scalar @mce_result;
|
|
|
|
foreach my $host (@mce_result) {
|
|
$stats{host}++;
|
|
info sprintf ' [%s] arpnip - retrieved %s entries',
|
|
$host->[0], scalar @{$host->[1]};
|
|
store_arpentries($host->[1]);
|
|
}
|
|
|
|
info sprintf 'arpnip - processed %s ARP Cache entries from %s devices',
|
|
$stats{entry}, $stats{host};
|
|
return 0;
|
|
}
|
|
|
|
sub process {
|
|
my ($hostlabel, $ssh, $args) = @_;
|
|
|
|
my $class = "App::Netdisco::SSHCollector::Platform::".$args->{platform};
|
|
Module::Load::load $class;
|
|
|
|
my $device = $class->new();
|
|
my $arpentries = [ $device->arpnip($hostlabel, $ssh, $args) ];
|
|
|
|
# debug p $arpentries;
|
|
if (not scalar @$arpentries) {
|
|
warning "WARNING: no entries received from <$hostlabel>";
|
|
}
|
|
hostnames_resolve_async($arpentries);
|
|
return [$hostlabel, $arpentries];
|
|
}
|
|
|
|
sub store_arpentries {
|
|
my ($arpentries) = @_;
|
|
|
|
foreach my $arpentry ( @$arpentries ) {
|
|
# skip broadcast/vrrp/hsrp and other wierdos
|
|
next unless check_mac( undef, $arpentry->{mac} );
|
|
|
|
debug sprintf ' arpnip - stored entry: %s / %s',
|
|
$arpentry->{mac}, $arpentry->{ip};
|
|
store_arp({
|
|
node => $arpentry->{mac},
|
|
ip => $arpentry->{ip},
|
|
dns => $arpentry->{dns},
|
|
});
|
|
|
|
$stats{entry}++;
|
|
}
|
|
}
|
|
|
|
=head1 NAME
|
|
|
|
netdisco-sshcollector - Collect ARP data for Netdisco from devices without
|
|
full SNMP support
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
# install dependencies:
|
|
~netdisco/bin/localenv cpanm --notest Net::OpenSSH Expect
|
|
|
|
# run manually, or add to cron:
|
|
~/bin/netdisco-sshcollector [-DQ]
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
Collects ARP data for Netdisco from devices without full SNMP support.
|
|
Currently, ARP tables can be retrieved from the following device classes:
|
|
|
|
=over 4
|
|
|
|
=item * L<App::Netdisco::SSHCollector::Platform::GAIAEmbedded> - Check Point GAIA Embedded
|
|
|
|
=item * L<App::Netdisco::SSHCollector::Platform::CPVSX> - Check Point VSX
|
|
|
|
=item * L<App::Netdisco::SSHCollector::Platform::ACE> - Cisco ACE
|
|
|
|
=item * L<App::Netdisco::SSHCollector::Platform::ASA> - Cisco ASA
|
|
|
|
=item * L<App::Netdisco::SSHCollector::Platform::IOS> - Cisco IOS
|
|
|
|
=item * L<App::Netdisco::SSHCollector::Platform::NXOS> - Cisco NXOS
|
|
|
|
=item * L<App::Netdisco::SSHCollector::Platform::IOSXR> - Cisco IOS XR
|
|
|
|
=item * L<App::Netdisco::SSHCollector::Platform::BigIP> - F5 Networks BigIP
|
|
|
|
=item * L<App::Netdisco::SSHCollector::Platform::FreeBSD> - FreeBSD
|
|
|
|
=item * L<App::Netdisco::SSHCollector::Platform::Linux> - Linux
|
|
|
|
=item * L<App::Netdisco::SSHCollector::Platform::PaloAlto> - Palo Alto
|
|
|
|
=back
|
|
|
|
The collected arp entries are then directly stored in the netdisco database.
|
|
|
|
=head1 CONFIGURATION
|
|
|
|
The following should go into your Netdisco 2 configuration file, "C<<
|
|
~/environments/deployment.yml >>"
|
|
|
|
=over 4
|
|
|
|
=item C<sshcollector>
|
|
|
|
Data is collected from the machines specified in this setting. The format is a
|
|
list of dictionaries. The keys C<ip>, C<user>, C<password>, and C<platform>
|
|
are required. Optionally the C<hostname> key can be used instead of the
|
|
C<ip>. For example:
|
|
|
|
sshcollector:
|
|
- ip: '192.0.2.1'
|
|
user: oliver
|
|
password: letmein
|
|
platform: IOS
|
|
- hostname: 'core-router.example.com'
|
|
user: oliver
|
|
password: letmein
|
|
platform: IOS
|
|
|
|
Platform is the final part of the classname to be instantiated to query the
|
|
host, e.g. platform B<ACE> will be queried using
|
|
C<App::Netdisco::SSHCollector::Platform::ACE>.
|
|
|
|
If the password is "-", public key authentication will be attempted.
|
|
|
|
=back
|
|
|
|
=head1 ADDING DEVICES
|
|
|
|
Additional device classes can be easily integrated just by adding and
|
|
additonal class to the C<App::Netdisco::SSHCollector::Platform> namespace.
|
|
This class must implement an C<arpnip($hostname, $ssh)> method which returns
|
|
an array of hashrefs in the format
|
|
|
|
@result = ({ ip => IPADDR, mac => MACADDR }, ...)
|
|
|
|
The parameter C<$ssh> is an active C<Net::OpenSSH> connection to the host.
|
|
Depending on the target system, it can be queried using simple methods like
|
|
|
|
my @data = $ssh->capture("show whatever")
|
|
|
|
or automated via Expect - this is mostly useful for non-Linux appliances which
|
|
don't support command execution via ssh:
|
|
|
|
my ($pty, $pid) = $ssh->open2pty or die "unable to run remote command";
|
|
my $expect = Expect->init($pty);
|
|
my $prompt = qr/#/;
|
|
my ($pos, $error, $match, $before, $after) = $expect->expect(10, -re, $prompt);
|
|
$expect->send("terminal length 0\n");
|
|
# etc...
|
|
|
|
The returned IP and MAC addresses should be in a format that the respective
|
|
B<inetaddr> and B<macaddr> datatypes in PostgreSQL can handle.
|
|
|
|
=head1 DEBUG LEVELS
|
|
|
|
The flags "C<-DQ>" can be specified, multiple times, and enable the following
|
|
items in order:
|
|
|
|
=over 4
|
|
|
|
=item C<-D>
|
|
|
|
Netdisco debug log level
|
|
|
|
=item C<-Q>
|
|
|
|
L<DBIx::Class> trace enabled
|
|
|
|
=back
|
|
|
|
=head1 DEPENDENCIES
|
|
|
|
=over 4
|
|
|
|
=item L<App::Netdisco>
|
|
|
|
=item L<Net::OpenSSH>
|
|
|
|
=item L<Expect>
|
|
|
|
=back
|
|
|
|
=cut
|