1 package OpenILS::Utils::RemoteAccount;
3 # use OpenSRF::Utils::SettingsClient;
4 use OpenSRF::Utils::Logger qw/:logger/;
8 use Net::SSH2; # because uFTP doesn't handle SSH keys (yet?)
13 $Data::Dumper::Indent = 0;
25 accound_object => undef,
28 remote_password => undef,
29 remote_account => undef,
31 remote_path => undef, # not really doing anything with this... yet.
32 ssh_privatekey => undef,
33 ssh_publickey => undef,
47 The Remote Account module attempts to transfer a file to/from a remote server.
48 Net::uFTP is used, encapsulating the available options of SCP, FTP and SFTP.
50 All information is expected to be gathered by the Event Definition through event parameters:
51 ~ remote_host (required)
57 ~ type (FTP, SFTP or SCP -- default FTP)
61 The latter three are optionally passed to the Net::uFTP constructor.
63 Note: none of the parameters are actually required, except remote_host.
64 That is because remote_user, remote_password and remote_account can all be
65 extrapolated from other sources, as the Net::FTP docs describe:
67 If no arguments are given then Net::FTP uses the Net::Netrc package
68 to lookup the login information for the connected host.
70 If no information is found then a login of anonymous is used.
72 If no password is given and the login is anonymous then anonymous@
73 will be used for password.
75 Note that specifying a password will require you to specify a user.
76 Similarly, specifying an account requires both user and password.
77 That is, there are no assumed defaults when the latter arguments are used.
81 The use of ssh keys is preferred.
83 We attempt to use SSH keys where they are specified or otherwise found
84 in the runtime environment. If only one key is specified, we attempt to derive
85 the corresponding filename based on the ssh-keygen defaults. If either key is
86 specified, but both are not found (and readable) then the result is failure. If
87 no key is specified, but keys are found, the key-based connections will be attempted,
88 but failure will be non-fatal.
93 # returns plausible locations of a .ssh subdir where SSH keys might be stashed
94 # NOTE: these would need to be properly genericized w/ Makefule vars
95 # in order to support Debian packaging and multiple EG's on one box.
96 # Until that happens, we just rely on $HOME
99 # '/openils/conf', # __EG_CONFIG_DIR__
101 ($ENV{HOME}) and unshift @bases, $ENV{HOME};
103 return grep {-d $_} map {"$_/.ssh"} @bases;
107 # populates %keyfiles hash
108 # %keyfiles maps SSH_PRIVATEKEY => SSH_PUBLICKEY
110 my $force = (@_ ? shift : 0);
111 return %keyfiles if (%keyfiles and not $force); # caching
112 $logger->info("Checking for SSH keyfiles" . ($force ? ' (ignoring cache)' : ''));
113 %keyfiles = (); # reset to empty
114 my @dirs = plausible_dirs();
115 $logger->debug(scalar(@dirs) . " plausible dirs: " . join(', ', @dirs));
116 foreach my $dir (@dirs) {
117 foreach my $key (qw/rsa dsa/) {
118 my $private = "$dir/id_$key";
119 my $public = "$dir/id_$key.pub";
120 unless (-r $private) {
121 $logger->debug("Key '$private' cannot be read: $!");
124 unless (-r $public) {
125 $logger->debug("Key '$public' cannot be read: $!");
128 $keyfiles{$private} = $public;
137 if ($self->ssh_publickey and not $self->ssh_privatekey) {
138 my $private = $self->ssh_publickey;
139 unless ($private and $private =~ s/\.pub$// and -r $self->ssh_privatekey) { # try to guess missing private key name
140 $logger->error("No ssh_privatekey specified or found to pair with " . $self->ssh_publickey);
143 $self->ssh_privatekey($private);
145 if ($self->ssh_privatekey and not $self->ssh_publickey) {
146 my $pub = $self->ssh_privatekey . '.pub'; # guess missing public key name
148 $logger->error("No ssh_publickey specified or found to pair with " . $self->ssh_privatekey);
151 $self->ssh_publickey($pub);
154 # so now, we have either both ssh_p*keys params or neither
155 foreach (qw/ssh_publickey ssh_privatekey/) {
156 unless (-r $self->{$_}) {
157 $logger->error("$_ '" . $self->{$_} . "' cannot be read: $!");
158 return; # quit w/ error if we fail on any user-specified key
161 $keys{$self->ssh_privatekey} = $self->ssh_publickey;
167 my $text = shift || $self->content || '';
168 my $tmp = File::Temp->new(); # magical self-destructing tempfile
169 # print $tmp "THIS IS TEXT\n";
170 print $tmp $text or $logger->error($self->_error("could not write to tempfile '$tmp'"));
172 $self->tempfile($tmp); # save the object
173 $self->local_file($tmp->filename); # save the filename
174 $logger->info(_pkg("using tempfile $tmp"));
175 return $self->local_file; # return the filename
182 unless (defined $self->content or $self->local_file) { # content can be emptystring
183 $logger->error($self->_error("No content or local_file specified -- nothing to send"));
187 # tricky subtlety: we want to use the most recently specified options
188 # with priority order: filename, content, old filename, old content.
190 # The $params->{x} will already match $self->x after the secondary init,
191 # so the checks using $params below are for whether the value was specified NOW (e.g. via put()) or not.
193 # if we got a new local_file value, we use it
194 # else if the content is new to this call, build a new tempfile w/ it,
195 # else use existing local_file,
196 # else build new tempfile w/ content already specified via new()
198 return $params->{local_file} || (
199 (defined $params->{content}) ?
200 $self->new_tempfile($self->content) : # $self->content is same value as $params->{content}
201 ($self->local_file || $self->new_tempfile($self->content))
209 return if ($params->{type} and $params->{type} eq 'FTP'); # Forget it, user specified regular FTP
210 return if ( $self->type and $self->type eq 'FTP'); # Forget it, user specified regular FTP
212 if ($self->ssh_publickey || $self->ssh_privatekey) {
214 return $self->param_keys(); # we got one or both params, but they didn't pan out
216 return local_keyfiles(); # optional "force" arg could be used here to empty cache
221 # TODO: delete for both uFTP and SSH2
222 # TODO: handle IO::Scalar and IO::File for uFTP
228 $params = {remote_file => $params} ;
231 $self->init($params); # secondary init
233 $self->{get_args} = [$self->remote_file]; # same for scp_put and uFTP put
234 push @{$self->{get_args}}, $self->local_file if defined $self->local_file;
236 # $self->content($content);
238 my %keys = $self->key_check($params);
240 my $try = $self->get_ssh2(\%keys, @{$self->{get_args}});
241 return $try if $try; # if we had keys and they worked, we're done
244 # Otherwise, try w/ non-key uFTP methods
245 return $self->get_uftp(@{$self->{get_args}});
252 $params = {local_file => $params} ;
255 $self->init($params); # secondary init
257 my $local_file = $self->outbound_file($params) or return;
259 $self->{put_args} = [$local_file]; # same for scp_put and uFTP put
260 if (defined $self->remote_path and not defined $self->remote_file) {
261 $self->remote_file($self->remote_path . '/' . basename($local_file)); # if we know just the dir
263 if (defined $self->remote_file) {
264 push @{$self->{put_args}}, $self->remote_file; # user can specify remote_file name, optionally
267 my %keys = $self->key_check($params);
269 $self->put_ssh2(\%keys, @{$self->{put_args}}) and return $self->remote_file;
270 # if we had keys and they worked, we're done
273 # Otherwise, try w/ non-key uFTP methods
274 return $self->put_uftp(@{$self->{put_args}});
282 unshift @targets, ($params || '.'); # If it was just a string, it's the first target, else default pwd
283 delete $self->{remote_file}; # overriding any target in the object previously.
284 $params = {}; # make params a normal hashref again
286 if ($params->{remote_file} and @_) {
287 $logger->warn($self->_error("Ignoring ls parameter remote_file for subsequent args"));
288 delete $params->{remote_file};
290 $self->init($params); # secondary init
291 $self->remote_file and (! @targets) and push @targets, $self->remote_file; # if remote_file is there, and there's nothing else, use it
292 delete $self->{remote_file};
295 $self->{ls_args} = \@targets;
297 my %keys = $self->key_check($params);
299 # $logger->info("*** calling ls_ssh2(keys, '" . join("', '", (scalar(@targets) ? map {defined $_ ? $_ : '' } @targets : ())) . "') with ssh keys");
300 my @try = $self->ls_ssh2(\%keys, @targets);
301 return @try if @try; # if we had keys and they worked, we're done
304 # Otherwise, try w/ non-key uFTP methods
305 return $self->ls_uftp(@targets);
312 $self->{ssh2} and return $self->{ssh2}; # caching
315 my $ssh2 = Net::SSH2->new();
316 unless($ssh2->connect($self->remote_host)) {
317 $logger->warn($self->error("SSH2 connect FAILED: $!" . join(" ", $ssh2->error)));
318 return; # we cannot connect
322 my @privates = keys %$keys;
323 my $count = scalar @privates;
324 foreach (@privates) {
325 if ($self->auth_ssh2($ssh2, $self->auth_ssh2_args($_, $keys->{$_}))) {
331 $logger->error($self->error("All ($count) keypair(s) FAILED for " . $self->remote_host));
334 return $self->{ssh2} = $ssh2;
343 my $host = $auth_args{hostname} || 'UNKNOWN';
344 my $key = $auth_args{privatekey} || 'UNKNOWN';
345 my $msg = "ssh2->auth by keypair for $host using $key";
346 if ($ssh2->auth(%auth_args)) {
347 $logger->info("Successful $msg");
351 if ($self->specific) {
352 $logger->error($self->error("Aborting. FAILED $msg: " . ($ssh2->error || '')));
354 $logger->warn($self->error("Unsuccessful keypair: FAILED $msg: " . ($ssh2->error || '')));
364 rank => [qw/ publickey hostbased password /],
366 $self->remote_user and $auth_args{username} = $self->remote_user ;
367 $self->remote_password and $auth_args{password} = $self->remote_password;
368 $self->remote_host and $auth_args{hostname} = $self->remote_host ;
374 my $keys = shift; # could have many keypairs here
376 $logger->error($self->_error("put_ssh2 called without target: nothing to put!"));
380 $logger->info("*** attempting put (" . join(", ", @_) . ") with ssh keys");
381 my $ssh2 = $self->_ssh2($keys) or return;
383 if ($res = $ssh2->scp_put( @_ )) {
384 $logger->info(_pkg("successfully sent", $self->remote_host, join(' --> ', @_ )));
387 $logger->error($self->_error(sprintf "put with keys to %s failed with error: $!", $self->remote_host));
393 my $keys = shift; # could have many keypairs here
395 $logger->error($self->_error("get_ssh2 called without target: nothing to get!"));
399 $logger->info("*** get args: " . Dumper(\@_));
400 $logger->info("*** attempting get (" . join(", ", map {$_ =~ /\S/ ? $_ : '*Object'} map {$_ || '*Object'} @_) . ") with ssh keys");
401 my $ssh2 = $self->_ssh2($keys) or return;
403 if ($res = $ssh2->scp_get( @_ )) {
404 $logger->info(_pkg("successfully got", $self->remote_host, join(' --> ', @_ )));
407 $logger->error($self->_error(sprintf "get with keys from %s failed with error: $!", $self->remote_host));
413 my @list = $self->ls_ssh2_full(@_);
414 @list and return sort map {$_->{slash_path}} @list;
415 # @list and return sort grep {$_->{name} !~ /./ and {$_->{name} !~ /./ } map {$_->{slash_path}} @list;
420 my $keys = shift; # could have many keypairs here
421 my @targets = grep {defined} @_;
423 $logger->info("*** attempting ls ('" . join("', '", @targets) . "') with ssh keys");
424 my $ssh2 = $self->_ssh2($keys) or return;
425 my $sftp = $ssh2->sftp or return;
428 foreach my $target (@targets) {
430 $dir = $sftp->opendir($target);
432 $file = $sftp->stat($target);
434 $file->{slash_path} = $self->_slash_path($target, $file->{name}); # it was a file, not a dir. That's OK.
437 $logger->warn($self->_error("sftp->opendir($target) failed: " . $sftp->error));
441 while ($file = $dir->read()) {
442 $file->{slash_path} = $self->_slash_path($target, $file->{name});
444 # foreach (sort keys %$line) { printf " %20s => %s\n", $_, $line->{$_}; }
451 sub _slash_path { # not OO
453 my $dir = shift || '.';
454 my $file = shift || '';
455 return $dir . ($dir =~ /\/$/ ? '' : '/') . $file;
461 $self->{uftp} and return $self->{uftp}; # caching
462 foreach (qw/debug type port/) {
463 $options{$_} = $self->{$_} if $self->{$_};
466 my $ftp = Net::uFTP->new($self->remote_host, %options);
468 $logger->error($self->_error('Net::uFTP->new("' . $self->remote_host . ", ...) FAILED: $@"));
473 foreach (qw/remote_user remote_password remote_account/) {
475 push @login_args, $self->{$_};
477 eval { $ftp->login(@login_args) };
479 $logger->error($self->_error("failed login to", $self->remote_host, "w/ args(" . join(',', @login_args) . ") : $@"));
482 return $self->{uftp} = $ftp;
487 my $ftp = $self->_uftp or return;
489 eval { $filename = $ftp->put(@{$self->{put_args}}) };
490 if ($@ or ! $filename) {
491 $logger->error($self->_error("put to", $self->remote_host, "failed with error: $@"));
494 $self->remote_file($filename);
495 $logger->info(_pkg("successfully sent", $self->remote_host, $self->local_file, '-->', $filename));
501 my $ftp = $self->_uftp or return;
503 eval { $filename = $ftp->get(@{$self->{get_args}}) };
504 if ($@ or ! $filename) {
505 $logger->error($self->_error("get from", $self->remote_host, "failed with error: $@"));
508 $self->local_file($filename);
509 $logger->info(_pkg("successfully retrieved $filename <--", $self->remote_host . '/' . $self->remote_file));
510 return $self->local_file;
515 my $ftp = $self->_uftp or return;
519 eval { @part = $ftp->ls($_) };
521 $logger->error($self->_error("ls from", $self->remote_host, "failed with error: $@"));
531 my $ftp = $self->_uftp or return;
532 return $ftp->delete(shift);
536 return __PACKAGE__ . ' : ' unless @_;
537 return __PACKAGE__ . ' : ' . join(' ', @_);
542 return _pkg($self->error(join(' ',@_)));
548 my @required = @_; # qw(remote_host) ; # nothing required now
550 if ($params->{account_object}) { # if we got passed an object, we initialize off that first
551 $self->{remote_host } = $params->{account_object}->host;
552 $self->{remote_user } = $params->{account_object}->username;
553 $self->{remote_password} = $params->{account_object}->password;
554 $self->{remote_account } = $params->{account_object}->account;
555 $self->{remote_path } = $params->{account_object}->path; # not really the same as remote_file, maybe expand on this later
558 foreach (keys %{$self->{_permitted}}) {
559 $self->{$_} = $params->{$_} if defined $params->{$_}; # possibly override settings from object
562 foreach (@required) {
563 unless ($self->{$_}) {
564 $logger->error("Required parameter $_ not specified");
572 my ($class, %args) = @_;
573 my $self = { _permitted => \%fields, %fields };
577 $self->init(\%args); # or croak "Initialization error caused by bad args";
582 # in order to create, we must first ...
584 $self->{ssh2} and $self->{ssh2}->disconnect(); # let the other end know we're done.
585 $self->{uftp} and $self->{uftp}->quit(); # let the other end know we're done.
590 my $class = ref($self) or croak "AUTOLOAD error: $self is not an object";
591 my $name = $AUTOLOAD;
593 $name =~ s/.*://; # strip leading package stuff
595 unless (exists $self->{_permitted}->{$name}) {
596 croak "Cannot access '$name' field of class '$class'";
600 return $self->{$name} = shift;
602 return $self->{$name};