1package App::Netdisco::Transport::SSH;
2
3use Dancer qw/:syntax :script/;
4
5use App::Netdisco::Util::Device 'get_device';
6use Module::Load ();
7use Net::OpenSSH;
8use Try::Tiny;
9
10use base 'Dancer::Object::Singleton';
11
12=head1 NAME
13
14App::Netdisco::Transport::SSH
15
16=head1 DESCRIPTION
17
18Returns an object which has an active SSH connection which can be used
19for some actions such as arpnip.
20
21 my $cli = App::Netdisco::Transport::SSH->session_for( ... );
22
23=cut
24
25__PACKAGE__->attributes(qw/ sessions /);
26
27sub init {
28  my ( $class, $self ) = @_;
29  $self->sessions( {} );
30  return $self;
31}
32
33=head1 session_for( $ip )
34
35Given an IP address, returns an object instance configured for and connected
36to that device.
37
38Returns C<undef> if the connection fails.
39
40=cut
41
42{
43  package MySession;
44  use Moo;
45
46  has 'ssh'  => ( is => 'rw' );
47  has 'auth' => ( is => 'rw' );
48  has 'host' => ( is => 'rw' );
49  has 'platform' => ( is => 'rw' );
50
51  sub arpnip {
52    my $self = shift;
53    $self->platform->arpnip(@_, $self->host, $self->ssh, $self->auth);
54  }
55}
56
57sub session_for {
58  my ($class, $ip) = @_;
59
60  my $device = get_device($ip) or return undef;
61  my $sessions = $class->instance->sessions or return undef;
62
63  return $sessions->{$device->ip} if exists $sessions->{$device->ip};
64  debug sprintf 'cli session cache warm: [%s]', $device->ip;
65
66  my $auth = (setting('device_auth') || []);
67  if (1 != scalar @$auth) {
68    error sprintf " [%s] require only one matching auth stanza", $device->ip;
69    return undef;
70  }
71  $auth = $auth->[0];
72
73  my @master_opts = qw(-o BatchMode=no);
74  push(@master_opts, @{$auth->{ssh_master_opts}})
75    if $auth->{ssh_master_opts};
76
77  $Net::OpenSSH::debug = $ENV{SSH_TRACE};
78  my $ssh = Net::OpenSSH->new(
79    $device->ip,
80    user => $auth->{username},
81    password => $auth->{password},
82    timeout => 30,
83    async => 0,
84    default_stderr_file => '/dev/null',
85    master_opts => \@master_opts
86  );
87
88  if ($ssh->error) {
89    error sprintf " [%s] ssh connection error [%s]", $device->ip, $ssh->error;
90    return undef;
91  }
92  elsif (! $ssh) {
93    error sprintf " [%s] Net::OpenSSH instantiation error", $device->ip;
94    return undef;
95  }
96
97  my $platform = "App::Netdisco::SSHCollector::Platform::" . $auth->{platform};
98  my $happy = false;
99  try {
100    Module::Load::load $platform;
101    $happy = true;
102  } catch { error $_ };
103  return unless $happy;
104
105  my $sess = MySession->new(
106    ssh  => $ssh,
107    auth => $auth,
108    host => $device->ip,
109    platform => $platform->new(),
110  );
111
112  return ($sessions->{$device->ip} = $sess);
113}
114
115true;
116