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