304 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			304 lines
		
	
	
		
			7.7 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) = (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') };
 | |
| 
 | |
|     #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"
 | |
|                 ],
 | |
|             );
 | |
| 
 | |
|             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<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::NXOS> - Cisco NXOS
 | |
| 
 | |
| =item * L<App::Netdisco::SSHCollector::Platform::IOSXR> - Cisco IOS XR
 | |
| 
 | |
| =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 2 configuration file, "C<<
 | |
| ~/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: letmein
 | |
|      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 "-", 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<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 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<DBIx::Class> trace enabled
 | |
| 
 | |
| =back
 | |
| 
 | |
| =head1 DEPENDENCIES
 | |
| 
 | |
| =over 4
 | |
| 
 | |
| =item L<App::Netdisco>
 | |
| 
 | |
| =item L<Net::OpenSSH>
 | |
| 
 | |
| =item L<Expect>
 | |
| 
 | |
| =back
 | |
|  
 | |
| =cut
 |