#!/usr/bin/env perl

use warnings;
use strict;

our $home;

  use FindBin;

  $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};

  use Path::Class;

  # stuff useful locations into @INC and $PATH
  unshift @INC,
    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(
                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;
                MCE->gather( process($hostlabel, $ssh, $host) );
    } \@input;

    return 0 unless scalar @mce_result;

    foreach my $host (@mce_result) {
        info sprintf ' [%s] arpnip - retrieved %s entries',
            $host->[0], scalar @{$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>";
    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};
            node => $arpentry->{mac},
            ip => $arpentry->{ip},
            dns => $arpentry->{dns},


=head1 NAME

netdisco-sshcollector - DEPRECATED!


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 *




 # 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> 


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


The collected arp entries are then directly stored in the netdisco database.


The following should go into your Netdisco configuration file,

=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:

   - ip: ''
     user: oliver
     password: letmein
     platform: IOS
   - hostname: 'core-router.example.com'
     user: oliver
     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

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



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.   


=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.



=over 4

=item L<App::Netdisco>

=item L<Net::OpenSSH>

=item L<Expect>

=item L<http://www.openssh.com/>