1#!/usr/bin/env perl 2 3use warnings; 4use strict; 5 6our $home; 7 8BEGIN { 9 use FindBin; 10 FindBin::again(); 11 12 $home = ($ENV{NETDISCO_HOME} || $ENV{HOME}); 13 14 # try to find a localenv if one isn't already in place. 15 if (!exists $ENV{PERL_LOCAL_LIB_ROOT}) { 16 use File::Spec; 17 my $localenv = File::Spec->catfile($FindBin::RealBin, 'localenv'); 18 exec($localenv, $0, @ARGV) if -f $localenv; 19 $localenv = File::Spec->catfile($home, 'perl5', 'bin', 'localenv'); 20 exec($localenv, $0, @ARGV) if -f $localenv; 21 22 die "Sorry, can't find libs required for App::Netdisco.\n" 23 if !exists $ENV{PERLBREW_PERL}; 24 } 25} 26 27BEGIN { 28 use Path::Class; 29 30 # stuff useful locations into @INC and $PATH 31 unshift @INC, 32 dir($FindBin::RealBin)->parent->subdir('lib')->stringify, 33 dir($FindBin::RealBin, 'lib')->stringify; 34 35 unshift @INC, 36 split m/:/, ($ENV{NETDISCO_INC} || ''); 37 38 use Config; 39 $ENV{PATH} = $FindBin::RealBin . $Config{path_sep} . $ENV{PATH}; 40} 41 42use App::Netdisco; 43use App::Netdisco::Util::Node qw/check_mac store_arp/; 44use App::Netdisco::Util::FastResolver 'hostnames_resolve_async'; 45use Dancer ':script'; 46 47use Data::Printer; 48use Module::Load (); 49use Net::OpenSSH; 50use MCE::Loop Sereal => 1; 51use Pod::Usage 'pod2usage'; 52 53use Getopt::Long; 54Getopt::Long::Configure ("bundling"); 55 56my ($debug, $sqltrace, $device, $opensshdebug, $workers) = (undef, 0, undef, undef, "auto"); 57my $result = GetOptions( 58 'debug|D' => \$debug, 59 'sqltrace|Q' => \$sqltrace, 60 'device|d=s' => \$device, 61 'opensshdebug|O' => \$opensshdebug, 62 'workers|w=i' => \$workers, 63) or pod2usage( 64 -msg => 'error: bad options', 65 -verbose => 0, 66 -exitval => 1, 67); 68 69my $CONFIG = config(); 70$CONFIG->{logger} = 'console'; 71$CONFIG->{log} = ($debug ? 'debug' : 'info'); 72$ENV{DBIC_TRACE} ||= $sqltrace; 73 74# reconfigure logging to force console output 75Dancer::Logger->init('console', $CONFIG); 76 77# silent exit unless explicitly requested 78exit(0) unless setting('use_legacy_sshcollector'); 79 80if ($opensshdebug){ 81 $Net::OpenSSH::debug = ~0; 82} 83 84MCE::Loop::init { chunk_size => 1, max_workers => $workers }; 85my %stats; 86$stats{entry} = 0; 87 88exit main(); 89 90sub main { 91 my @input = @{ setting('sshcollector') }; 92 93 if ($device){ 94 @input = grep{ ($_->{hostname} && $_->{hostname} eq $device) 95 || ($_->{ip} && $_->{ip} eq $device) } @input; 96 } 97 98 #one-line Fisher-Yates from https://www.perlmonks.org/index.pl?node=Array%20One-Liners 99 my ($i,$j) = (0); 100 @input[-$i,$j] = @input[$j,-$i] while $j = rand(@input - $i), ++$i < @input; 101 102 my @mce_result = mce_loop { 103 my ($mce, $chunk_ref, $chunk_id) = @_; 104 my $host = $chunk_ref->[0]; 105 106 my $hostlabel = (!defined $host->{hostname} or $host->{hostname} eq "-") 107 ? $host->{ip} : $host->{hostname}; 108 109 if ($hostlabel) { 110 my $ssh = Net::OpenSSH->new( 111 $hostlabel, 112 user => $host->{user}, 113 password => $host->{password}, 114 timeout => 30, 115 async => 0, 116 default_stderr_file => '/dev/null', 117 master_opts => [ 118 -o => "StrictHostKeyChecking=no", 119 -o => "BatchMode=no" 120 ], 121 ); 122 123 124 if ($ssh->error){ 125 warning "WARNING: Couldn't connect to <$hostlabel> - " . $ssh->error; 126 }else{ 127 MCE->gather( process($hostlabel, $ssh, $host) ); 128 } 129 } 130 } \@input; 131 132 return 0 unless scalar @mce_result; 133 134 foreach my $host (@mce_result) { 135 $stats{host}++; 136 info sprintf ' [%s] arpnip - retrieved %s entries', 137 $host->[0], scalar @{$host->[1]}; 138 store_arpentries($host->[1]); 139 } 140 141 info sprintf 'arpnip - processed %s ARP Cache entries from %s devices', 142 $stats{entry}, $stats{host}; 143 return 0; 144} 145 146sub process { 147 my ($hostlabel, $ssh, $args) = @_; 148 149 my $class = "App::Netdisco::SSHCollector::Platform::".$args->{platform}; 150 Module::Load::load $class; 151 152 my $device = $class->new(); 153 my $arpentries = [ $device->arpnip($hostlabel, $ssh, $args) ]; 154 155 # debug p $arpentries; 156 if (not scalar @$arpentries) { 157 warning "WARNING: no entries received from <$hostlabel>"; 158 } 159 hostnames_resolve_async($arpentries); 160 return [$hostlabel, $arpentries]; 161} 162 163sub store_arpentries { 164 my ($arpentries) = @_; 165 166 foreach my $arpentry ( @$arpentries ) { 167 # skip broadcast/vrrp/hsrp and other wierdos 168 next unless check_mac( $arpentry->{mac} ); 169 170 debug sprintf ' arpnip - stored entry: %s / %s', 171 $arpentry->{mac}, $arpentry->{ip}; 172 store_arp({ 173 node => $arpentry->{mac}, 174 ip => $arpentry->{ip}, 175 dns => $arpentry->{dns}, 176 }); 177 178 $stats{entry}++; 179 } 180} 181 182=head1 NAME 183 184netdisco-sshcollector - DEPRECATED! 185 186=head1 DEPRECATION NOTICE 187 188The functionality of this standalone script has been incorporated into Netdisco core. 189 190Please read the deprecation notice if you are using C<netdisco-sshcollector>: 191 192=over 4 193 194=item * 195 196L<https://github.com/netdisco/netdisco/wiki/sshcollector-Deprecation> 197 198=back 199 200=head1 SYNOPSIS 201 202 # install dependencies: 203 ~/bin/localenv cpanm --notest Net::OpenSSH Expect 204 205 # run manually, or add to cron: 206 ~/bin/netdisco-sshcollector [-DQO] [-w <max_workers>] 207 208 # limit run to a single device defined in the config 209 ~/bin/netdisco-sshcollector [-DQO] [-w <max_workers>] -d <device> 210 211=head1 DESCRIPTION 212 213Collects ARP data for Netdisco from devices without full SNMP support. 214Currently, ARP tables can be retrieved from the following device classes: 215 216=over 4 217 218=item * L<App::Netdisco::SSHCollector::Platform::GAIAEmbedded> - Check Point GAIA Embedded 219 220=item * L<App::Netdisco::SSHCollector::Platform::CPVSX> - Check Point VSX 221 222=item * L<App::Netdisco::SSHCollector::Platform::ACE> - Cisco ACE 223 224=item * L<App::Netdisco::SSHCollector::Platform::ASA> - Cisco ASA 225 226=item * L<App::Netdisco::SSHCollector::Platform::IOS> - Cisco IOS 227 228=item * L<App::Netdisco::SSHCollector::Platform::IOSXR> - Cisco IOS XR 229 230=item * L<App::Netdisco::SSHCollector::Platform::NXOS> - Cisco NXOS 231 232=item * L<App::Netdisco::SSHCollector::Platform::BigIP> - F5 Networks BigIP 233 234=item * L<App::Netdisco::SSHCollector::Platform::FreeBSD> - FreeBSD 235 236=item * L<App::Netdisco::SSHCollector::Platform::Linux> - Linux 237 238=item * L<App::Netdisco::SSHCollector::Platform::PaloAlto> - Palo Alto 239 240=back 241 242The collected arp entries are then directly stored in the netdisco database. 243 244=head1 CONFIGURATION 245 246The following should go into your Netdisco configuration file, 247F<~/environments/deployment.yml>. 248 249=over 4 250 251=item C<sshcollector> 252 253Data is collected from the machines specified in this setting. The format is a 254list of dictionaries. The keys C<ip>, C<user>, C<password>, and C<platform> 255are required. Optionally the C<hostname> key can be used instead of the 256C<ip>. For example: 257 258 sshcollector: 259 - ip: '192.0.2.1' 260 user: oliver 261 password: letmein 262 platform: IOS 263 - hostname: 'core-router.example.com' 264 user: oliver 265 password: 266 platform: IOS 267 268Platform is the final part of the classname to be instantiated to query the 269host, e.g. platform B<ACE> will be queried using 270C<App::Netdisco::SSHCollector::Platform::ACE>. 271 272If the password is blank, public key authentication will be attempted with the 273default key for the netdisco user. Password protected keys are currently not 274supported. 275 276=back 277 278=head1 ADDING DEVICES 279 280Additional device classes can be easily integrated just by adding and 281additonal class to the C<App::Netdisco::SSHCollector::Platform> namespace. 282This class must implement an C<arpnip($hostname, $ssh)> method which returns 283an array of hashrefs in the format 284 285 @result = ({ ip => IPADDR, mac => MACADDR }, ...) 286 287The parameter C<$ssh> is an active C<Net::OpenSSH> connection to the host. 288Depending on the target system, it can be queried using simple methods like 289 290 my @data = $ssh->capture("show whatever") 291 292or automated via Expect - this is mostly useful for non-Linux appliances which 293don't support command execution via ssh: 294 295 my ($pty, $pid) = $ssh->open2pty; 296 unless ($pty) { 297 debug "unable to run remote command [$hostlabel] " . $ssh->error; 298 return (); 299 } 300 my $expect = Expect->init($pty); 301 my $prompt = qr/#/; 302 my ($pos, $error, $match, $before, $after) = $expect->expect(10, -re, $prompt); 303 $expect->send("terminal length 0\n"); 304 # etc... 305 306The returned IP and MAC addresses should be in a format that the respective 307B<inetaddr> and B<macaddr> datatypes in PostgreSQL can handle. 308 309=head1 COMMAND LINE OPTIONS 310 311=over 4 312 313=item C<-D> 314 315Netdisco debug log level. 316 317=item C<-Q> 318 319L<DBIx::Class> trace enabled. 320 321=item C<-O> 322 323L<Net::OpenSSH> trace enabled. 324 325=item C<-w> 326 327Set maximum parallel workers for L<MCE::Loop>. The default is B<auto>. 328 329=item C<-d device> 330 331Only run for a single device. Takes an IP or hostname, must exactly match the 332value in the config file. 333 334=back 335 336=head1 DEPENDENCIES 337 338=over 4 339 340=item L<App::Netdisco> 341 342=item L<Net::OpenSSH> 343 344=item L<Expect> 345 346=item L<http://www.openssh.com/> 347 348=back 349 350=cut 351