#!/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){
    # every Net::OpenSSH debug flag except 512, its OS tracer
    $Net::OpenSSH::debug = 1|2|4|8|16|32|64|128;
}

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 => "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 *

L<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<https://github.com/netdisco/netdisco/wiki/SSH-Platforms> - All Supported Platforms

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

Each host's SSH key must already be in the netdisco user's
F<~/.ssh/known_hosts> file, otherwise the connection is refused. Add a new
host's key by connecting to it once as the netdisco user, or with
C<ssh-keyscan>. Host key checking can be relaxed for a single host in the
netdisco user's F<~/.ssh/config>.

=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
