#!/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, default_stderr_file => '/dev/null', 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( $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 - Check Point GAIA Embedded =item * L - Check Point VSX =item * L - Cisco ACE =item * L - Cisco ASA =item * L - Cisco IOS =item * L - Cisco NXOS =item * L - Cisco IOS XR =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 2 configuration file, "C<< ~/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: letmein 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 "-", 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 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 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 and B 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 trace enabled =back =head1 DEPENDENCIES =over 4 =item L =item L =item L =back =cut