#!/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: =over 4 =item * L =back =head1 SYNOPSIS # install dependencies: ~/bin/localenv cpanm --notest Net::OpenSSH Expect # run manually, or add to cron: ~/bin/netdisco-sshcollector [-DQO] [-w ] # limit run to a single device defined in the config ~/bin/netdisco-sshcollector [-DQO] [-w ] -d =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 - Check Point GAIA Embedded =item * L - Check Point VSX =item * L - Cisco ACE =item * L - Cisco ASA =item * L - Cisco IOS =item * L - Cisco IOS XR =item * L - Cisco NXOS =item * L - F5 Networks BigIP =item * L - FreeBSD =item * L - Linux =item * L - 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 Data is collected from the machines specified in this setting. The format is a list of dictionaries. The keys C, C, C, and C are required. Optionally the C key can be used instead of the C. 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 will be queried using C. 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 namespace. This class must implement an C method which returns an array of hashrefs in the format @result = ({ ip => IPADDR, mac => MACADDR }, ...) The parameter C<$ssh> is an active C 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 and B datatypes in PostgreSQL can handle. =head1 COMMAND LINE OPTIONS =over 4 =item C<-D> Netdisco debug log level. =item C<-Q> L trace enabled. =item C<-O> L trace enabled. =item C<-w> Set maximum parallel workers for L. The default is B. =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 =item L =item L =item L =back =cut