diff --git a/Build.PL b/Build.PL index fe0e4bba..9f20ce47 100644 --- a/Build.PL +++ b/Build.PL @@ -37,6 +37,7 @@ Module::Build->new( 'Dancer::Plugin::Auth::Extensible' => '0.30', 'Dancer::Plugin::Passphrase' => '2.0.1', 'Dancer::Session::Cookie' => '0.27', + 'Expect' => '0', 'File::ShareDir' => '1.03', 'File::Slurper' => '0.009', 'Guard' => '1.022', @@ -54,6 +55,7 @@ Module::Build->new( 'Net::Domain' => '1.23', 'Net::DNS' => '0.72', 'Net::LDAP' => '0', + 'Net::OpenSSH' => '0', 'NetAddr::MAC' => '0.93', 'NetAddr::IP' => '4.068', 'Opcode' => '1.07', @@ -90,8 +92,6 @@ Module::Build->new( recommends => { 'Graph' => '0', 'GraphViz' => '0', - 'Net::OpenSSH' => '0', - 'Expect' => '0', }, test_requires => { 'Test::More' => '1.302083', diff --git a/bin/netdisco-sshcollector b/bin/netdisco-sshcollector index 29c0e154..0f42f7c0 100755 --- a/bin/netdisco-sshcollector +++ b/bin/netdisco-sshcollector @@ -74,6 +74,9 @@ $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; } diff --git a/lib/App/Netdisco/Configuration.pm b/lib/App/Netdisco/Configuration.pm index 9ddd1d0f..cf076069 100644 --- a/lib/App/Netdisco/Configuration.pm +++ b/lib/App/Netdisco/Configuration.pm @@ -84,6 +84,7 @@ if ((setting('snmp_auth') and 0 == scalar @{ setting('snmp_auth') }) config->{'community_rw'} = [ @{setting('community_rw')}, 'private' ]; } # fix up device_auth (or create it from old snmp_auth and community settings) +# also imports legacy sshcollcetor config config->{'device_auth'} = [ App::Netdisco::Util::DeviceAuth::fixup_device_auth() ]; diff --git a/lib/App/Netdisco/Transport/CLI.pm b/lib/App/Netdisco/Transport/CLI.pm new file mode 100644 index 00000000..6f865c7b --- /dev/null +++ b/lib/App/Netdisco/Transport/CLI.pm @@ -0,0 +1,115 @@ +package App::Netdisco::Transport::CLI; + +use Dancer qw/:syntax :script/; + +use App::Netdisco::Util::Device 'get_device'; +use Module::Load (); +use Net::OpenSSH; +use Try::Tiny; + +use base 'Dancer::Object::Singleton'; + +=head1 NAME + +App::Netdisco::Transport::CLI + +=head1 DESCRIPTION + +Returns an object which has an active SSH connection which can be used +for some actions such as arpnip. + + my $cli = App::Netdisco::Transport::CLI->session_for( ... ); + +=cut + +__PACKAGE__->attributes(qw/ sessions /); + +sub init { + my ( $class, $self ) = @_; + $self->sessions( {} ); + return $self; +} + +=head1 session_for( $ip ) + +Given an IP address, returns an object instance configured for and connected +to that device. + +Returns C if the connection fails. + +=cut + +{ + package MySession; + use Moo; + + has 'ssh' => ( is => 'rw' ); + has 'auth' => ( is => 'rw' ); + has 'host' => ( is => 'rw' ); + has 'platform' => ( is => 'rw' ); + + sub arpnip { + my $self = shift; + $self->platform->arpnip(@_, $self->host, $self->ssh, $self->auth); + } +} + +sub session_for { + my ($class, $ip) = @_; + + my $device = get_device($ip) or return undef; + my $sessions = $class->instance->sessions or return undef; + + return $sessions->{$device->ip} if exists $sessions->{$device->ip}; + debug sprintf 'cli session cache warm: [%s]', $device->ip; + + my $auth = (setting('device_auth') || []); + if (1 != scalar @$auth) { + error sprintf " [%s] require only one matching auth stanza", $device->ip; + return undef; + } + $auth = $auth->[0]; + + my @master_opts = qw(-o BatchMode=no); + push(@master_opts, @{$auth->{ssh_master_opts}}) + if $auth->{ssh_master_opts}; + + $Net::OpenSSH::debug = $ENV{SSH_TRACE}; + my $ssh = Net::OpenSSH->new( + $device->ip, + user => $auth->{username}, + password => $auth->{password}, + timeout => 30, + async => 0, + default_stderr_file => '/dev/null', + master_opts => \@master_opts + ); + + if ($ssh->error) { + error sprintf " [%s] ssh connection error [%s]", $device->ip, $ssh->error; + return undef; + } + elsif (! $ssh) { + error sprintf " [%s] Net::OpenSSH instantiation error", $device->ip; + return undef; + } + + my $platform = "App::Netdisco::SSHCollector::Platform::" . $auth->{platform}; + my $happy = false; + try { + Module::Load::load $platform; + $happy = true; + } catch { error $_ }; + return unless $happy; + + my $sess = MySession->new( + ssh => $ssh, + auth => $auth, + host => $device->ip, + platform => $platform->new(), + ); + + return ($sessions->{$device->ip} = $sess); +} + +true; diff --git a/lib/App/Netdisco/Transport/SNMP.pm b/lib/App/Netdisco/Transport/SNMP.pm index 36cf57b3..f342155b 100644 --- a/lib/App/Netdisco/Transport/SNMP.pm +++ b/lib/App/Netdisco/Transport/SNMP.pm @@ -25,7 +25,7 @@ App::Netdisco::Transport::SNMP Singleton for SNMP connections. Returns cached L instance for a given device IP, or else undef. All methods are class methods, for example: - App::Netdisco::Transport::SNMP->reader_for( ... ); + my $snmp = App::Netdisco::Transport::SNMP->reader_for( ... ); =cut diff --git a/lib/App/Netdisco/Util/DeviceAuth.pm b/lib/App/Netdisco/Util/DeviceAuth.pm index bb7f79f5..31f68472 100644 --- a/lib/App/Netdisco/Util/DeviceAuth.pm +++ b/lib/App/Netdisco/Util/DeviceAuth.pm @@ -63,7 +63,23 @@ sub fixup_device_auth { die "error: config: stanza in device_auth must have a tag\n" if not $stanza->{tag} and exists $stanza->{user}; - push @new_stanzas, $stanza + push @new_stanzas, $stanza; + } + + # import legacy sshcollector configuration + my $sshcollector = (setting('sshcollector') || []); + foreach my $stanza (@$sshcollector) { + # defaults + $stanza->{driver} = 'cli'; + $stanza->{read} = 1; + $stanza->{no} ||= []; + + # fixups + $stanza->{only} ||= [ scalar delete $stanza->{ip} || + scalar delete $stanza->{hostname} ]; + $stanza->{username} = scalar delete $stanza->{user}; + + push @new_stanzas, $stanza; } # legacy config diff --git a/lib/App/Netdisco/Worker/Plugin/Arpnip/Nodes.pm b/lib/App/Netdisco/Worker/Plugin/Arpnip/Nodes.pm index 6805737a..3ac3c307 100644 --- a/lib/App/Netdisco/Worker/Plugin/Arpnip/Nodes.pm +++ b/lib/App/Netdisco/Worker/Plugin/Arpnip/Nodes.pm @@ -3,7 +3,7 @@ package App::Netdisco::Worker::Plugin::Arpnip::Nodes; use Dancer ':syntax'; use App::Netdisco::Worker::Plugin; use aliased 'App::Netdisco::Worker::Status'; - +use App::Netdisco::Transport::CLI (); use App::Netdisco::Transport::SNMP (); use App::Netdisco::Util::Node qw/check_mac store_arp/; use App::Netdisco::Util::FastResolver 'hostnames_resolve_async'; @@ -18,9 +18,9 @@ register_worker({ phase => 'main', driver => 'snmp' }, sub { or return Status->defer("arpnip failed: could not SNMP connect to $device"); # get v4 arp table - my $v4 = get_arps($device, $snmp->at_paddr, $snmp->at_netaddr); + my $v4 = get_arps_snmp($device, $snmp->at_paddr, $snmp->at_netaddr); # get v6 neighbor cache - my $v6 = get_arps($device, $snmp->ipv6_n2p_mac, $snmp->ipv6_n2p_addr); + my $v6 = get_arps_snmp($device, $snmp->ipv6_n2p_mac, $snmp->ipv6_n2p_addr); # would be possible just to use now() on updated records, but by using this # same value for them all, we _can_ if we want add a job at the end to @@ -41,7 +41,7 @@ register_worker({ phase => 'main', driver => 'snmp' }, sub { }); # get an arp table (v4 or v6) -sub get_arps { +sub get_arps_snmp { my ($device, $paddr, $netaddr) = @_; my @arps = (); @@ -63,4 +63,45 @@ sub get_arps { return $resolved_ips; } +register_worker({ phase => 'main', driver => 'cli' }, sub { + my ($job, $workerconf) = @_; + + my $device = $job->device; + my $cli = App::Netdisco::Transport::CLI->session_for($device) + or return Status->defer("arpnip failed: could not SSH connect to $device"); + + # should be both v4 and v6 + my $arps = get_arps_cli($device, [$cli->arpnip]); + + # update node_ip with ARP and Neighbor Cache entries + my $now = 'to_timestamp('. (join '.', gettimeofday) .')'; + store_arp(\%$_, $now) for @$arps; + debug sprintf ' [%s] arpnip - processed %s ARP / IPv6 Neighbor Cache entries', + $device->ip, scalar @$arps; + + $device->update({last_arpnip => \$now}); + return Status->done("Ended arpnip for $device"); +}); + +sub get_arps_cli { + my ($device, $entries) = @_; + my @arps = (); + $entries ||= []; + + foreach my $entry (@$entries) { + next unless check_mac($entry->{mac}, $device); + push @arps, { + node => $entry->{mac}, + ip => $entry->{ip}, + dns => $entry->{dns}, + }; + } + + debug sprintf ' resolving %d ARP entries with max %d outstanding requests', + scalar @arps, $ENV{'PERL_ANYEVENT_MAX_OUTSTANDING_DNS'}; + my $resolved_ips = hostnames_resolve_async(\@arps); + + return $resolved_ips; +} + true; diff --git a/share/config.yml b/share/config.yml index 9767019a..5422e4ea 100644 --- a/share/config.yml +++ b/share/config.yml @@ -216,6 +216,7 @@ device_identity: [] community: [] community_rw: [] device_auth: [] +use_legacy_sshcollector: false get_credentials: "" bulkwalk_off: false bulkwalk_no: [] diff --git a/xt/00-compile.t b/xt/00-compile.t index 307058af..f9e3b606 100644 --- a/xt/00-compile.t +++ b/xt/00-compile.t @@ -21,8 +21,8 @@ use Test::Compile; my $test = Test::Compile->new(); -my @plfiles = grep {$_ !~ m/(?:sshcollector|graph)/i} $test->all_pl_files(); -my @pmfiles = grep {$_ !~ m/(?:sshcollector|graph)/i} $test->all_pm_files(); +my @plfiles = grep {$_ !~ m/(?:graph)/i} $test->all_pl_files(); +my @pmfiles = grep {$_ !~ m/(?:graph)/i} $test->all_pm_files(); $test->ok($test->pl_file_compiles($_), "$_ compiles") for @plfiles; $test->ok($test->pm_file_compiles($_), "$_ compiles") for @pmfiles;