351 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			351 lines
		
	
	
		
			8.9 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, $device, $opensshdebug, $workers) = (undef, 0, undef, undef, "auto");
 | 
						|
my $result = GetOptions(
 | 
						|
  'debug|D' => \$debug,
 | 
						|
  'sqltrace|Q' => \$sqltrace,
 | 
						|
  'device|d=s' => \$device,
 | 
						|
  'opensshdebug|O' => \$opensshdebug,
 | 
						|
  'workers|w=i' => \$workers,
 | 
						|
) 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);
 | 
						|
 | 
						|
# silent exit unless explicitly requested
 | 
						|
exit(0) unless setting('use_legacy_sshcollector');
 | 
						|
 | 
						|
if ($opensshdebug){
 | 
						|
    $Net::OpenSSH::debug = ~0;
 | 
						|
}
 | 
						|
 | 
						|
MCE::Loop::init { chunk_size => 1, max_workers => $workers };
 | 
						|
my %stats;
 | 
						|
$stats{entry} = 0;
 | 
						|
 | 
						|
exit main();
 | 
						|
 | 
						|
sub main {
 | 
						|
    my @input = @{ setting('sshcollector') };
 | 
						|
 | 
						|
    if ($device){
 | 
						|
        @input = grep{ ($_->{hostname} && $_->{hostname} eq $device) 
 | 
						|
            || ($_->{ip} && $_->{ip} eq $device) } @input; 
 | 
						|
    }
 | 
						|
 | 
						|
    #one-line Fisher-Yates from https://www.perlmonks.org/index.pl?node=Array%20One-Liners
 | 
						|
    my ($i,$j) = (0);
 | 
						|
    @input[-$i,$j] = @input[$j,-$i] while $j = rand(@input - $i), ++$i < @input;
 | 
						|
 | 
						|
    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,
 | 
						|
                default_stderr_file => '/dev/null',
 | 
						|
                master_opts => [
 | 
						|
                    -o => "StrictHostKeyChecking=no",
 | 
						|
                    -o => "BatchMode=no"
 | 
						|
                ],
 | 
						|
            );
 | 
						|
 | 
						|
           
 | 
						|
            if ($ssh->error){ 
 | 
						|
                warning "WARNING: Couldn't connect to <$hostlabel> - " . $ssh->error;
 | 
						|
            }else{
 | 
						|
                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( $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 - DEPRECATED!
 | 
						|
 | 
						|
=head1 DEPRECATION NOTICE
 | 
						|
 | 
						|
The functionality of this standalone script has been incorporated into Netdisco core.
 | 
						|
 | 
						|
Please read the deprecation notice if you are using C<netdisco-sshcollector>:
 | 
						|
 | 
						|
=over 4
 | 
						|
 | 
						|
=item *
 | 
						|
 | 
						|
https://github.com/netdisco/netdisco/wiki/sshcollector-Deprecation
 | 
						|
 | 
						|
=back
 | 
						|
 | 
						|
=head1 SYNOPSIS
 | 
						|
 | 
						|
 # install dependencies:
 | 
						|
 ~/bin/localenv cpanm --notest Net::OpenSSH Expect
 | 
						|
 | 
						|
 # run manually, or add to cron:
 | 
						|
 ~/bin/netdisco-sshcollector [-DQO] [-w <max_workers>] 
 | 
						|
 | 
						|
 # limit run to a single device defined in the config
 | 
						|
 ~/bin/netdisco-sshcollector [-DQO] [-w <max_workers>] -d <device> 
 | 
						|
 | 
						|
=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::IOSXR> - Cisco IOS XR
 | 
						|
 | 
						|
=item * L<App::Netdisco::SSHCollector::Platform::NXOS> - Cisco NXOS
 | 
						|
 | 
						|
=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 configuration file,
 | 
						|
F<~/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:
 | 
						|
     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 blank, public key authentication will be attempted with the
 | 
						|
default key for the netdisco user. Password protected keys are currently not
 | 
						|
supported.
 | 
						|
 | 
						|
=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;
 | 
						|
 unless ($pty) {
 | 
						|
   debug "unable to run remote command [$hostlabel] " . $ssh->error;
 | 
						|
   return ();
 | 
						|
 }
 | 
						|
 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 COMMAND LINE OPTIONS
 | 
						|
 | 
						|
=over 4
 | 
						|
 | 
						|
=item C<-D>
 | 
						|
 | 
						|
Netdisco debug log level.
 | 
						|
 | 
						|
=item C<-Q>
 | 
						|
 | 
						|
L<DBIx::Class> trace enabled.
 | 
						|
 | 
						|
=item C<-O>
 | 
						|
 | 
						|
L<Net::OpenSSH> trace enabled.
 | 
						|
 | 
						|
=item C<-w>
 | 
						|
 | 
						|
Set maximum parallel workers for L<MCE::Loop>. The default is B<auto>. 
 | 
						|
 | 
						|
=item C<-d device>
 | 
						|
 | 
						|
Only run for a single device. Takes an IP or hostname, must exactly match the
 | 
						|
value in the config file.
 | 
						|
 | 
						|
=back
 | 
						|
 | 
						|
=head1 DEPENDENCIES
 | 
						|
 | 
						|
=over 4
 | 
						|
 | 
						|
=item L<App::Netdisco>
 | 
						|
 | 
						|
=item L<Net::OpenSSH>
 | 
						|
 | 
						|
=item L<Expect>
 | 
						|
 | 
						|
=item L<http://www.openssh.com/>
 | 
						|
 | 
						|
=back
 | 
						|
 | 
						|
=cut
 |