1 package OpenILS::Application::Trigger::Reactor::SendFile;
2 use OpenILS::Application::Trigger::Reactor;
3 use base 'OpenILS::Application::Trigger::Reactor';
5 # use OpenSRF::Utils::SettingsClient;
6 use OpenSRF::Utils::Logger qw/:logger/;
10 use Net::SSH2; # because uFTP doesn't handle SSH keys (yet?)
13 $Data::Dumper::Indent = 0;
23 The SendFile Reactor Module attempts to transfer a file to a remote server.
24 Net::uFTP is used, encapsulating the available options of SCP, FTP and SFTP.
26 No default template is assumed, and all information is expected to be gathered
27 by the Event Definition through event parameters:
28 ~ remote_host (required)
34 ~ type (FTP, SFTP or SCP -- default FTP)
38 The latter three are optionally passed to the Net::uFTP constructor.
40 Note: none of the parameters are actually required, except remote_host.
41 That is because remote_user, remote_password and remote_account can all be
42 extrapolated from other sources, as the Net::FTP docs describe:
44 If no arguments are given then Net::FTP uses the Net::Netrc package
45 to lookup the login information for the connected host.
47 If no information is found then a login of anonymous is used.
49 If no password is given and the login is anonymous then anonymous@
50 will be used for password.
52 Note that specifying a password will require you to specify a user.
53 Similarly, specifying an account requires both user and password.
54 That is, there are no assumed defaults when the latter arguments are used.
58 The use of ssh keys is preferred.
60 The reactor attempts to use SSH keys where they are specified or otherwise found
61 in the runtime environment. If only one key is specified, we attempt to derive
62 the corresponding filename based on the ssh-keygen defaults. If either key is
63 specified, but both are not found (and readable) then the result is failure. If
64 no key is specified, but keys are found, the key-based connections will be attempted,
65 but failure will be non-fatal.
71 # returns plausible locations of a .ssh subdir where SSH keys might be stashed
72 # NOTE: these would need to be properly genericized w/ Makefule vars
73 # in order to support Debian packaging and multiple EG's on one box.
74 # Until that happens, we just rely on $HOME
77 # '/openils/conf', # __EG_CONFIG_DIR__
79 ($ENV{HOME}) and unshift @bases, $ENV{HOME};
81 return grep {-d $_} map {"$_/.ssh"} @bases;
85 # populates %keyfiles hash
86 # %keyfiles maps SSH_PRIVATEKEY => SSH_PUBLICKEY
87 my $force = (@_ ? shift : 0);
88 return %keyfiles if (%keyfiles and not $force); # caching
89 $logger->info("Checking for SSH keyfiles" . ($force ? ' (ignoring cache)' : ''));
90 %keyfiles = (); # reset to empty
91 my @dirs = plausible_dirs();
92 $logger->debug(scalar(@dirs) . " plausible dirs: " . join(', ', @dirs));
93 foreach my $dir (@dirs) {
94 foreach my $key (qw/rsa dsa/) {
95 my $private = "$dir/id_$key";
96 my $public = "$dir/id_$key.pub";
97 unless (-r $private) {
98 $logger->debug("Key '$private' cannot be read: $!");
101 unless (-r $public) {
102 $logger->debug("Key '$public' cannot be read: $!");
105 $keyfiles{$private} = $public;
114 if ($params->{ssh_publickey } and not $params->{ssh_privatekey}) {
115 $params->{ssh_privatekey} = $params->{ssh_publickey}; # try to guess missing private key name
116 unless ($params->{ssh_privatekey} =~ s/\.pub$// and -r $params->{ssh_privatekey}) {
117 $logger->error("No ssh_privatekey specified or found to pair with " . $params->{ssh_publickey});
121 if ($params->{ssh_privatekey} and not $params->{ssh_publickey }) {
122 $params->{ssh_publickey} = $params->{ssh_privatekey} . '.pub'; # guess missing public key name
123 unless (-r $params->{ssh_publickey}) {
124 $logger->error("No ssh_publickey specified or found to pair with " . $params->{ssh_privatekey});
129 # so now, we have either both ssh_p*key params or neither
130 foreach (qw/ssh_publickey ssh_privatekey/) {
131 unless (-r $params->{$_}) {
132 $logger->error("$_ '" . $params->{$_} . "' cannot be read: $!");
133 return; # quit w/ error if we fail on any user-specified key
135 $keys{$params->{ssh_privatekey}} = $params->{ssh_publickey};
143 my $params = $env->{params};
145 my $host = $params->{remote_host};
147 $logger->error("No remote_host specified in env");
151 my $text = $self->run_TT($env) or return;
152 my $tmp = File::Temp->new(); # magical self-destructing tempfile
154 $logger->info("SendFile Reactor: using tempfile $tmp");
158 my @put_args = ($tmp->filename); # same for scp_put and uFTP put
159 push @put_args, $params->{remote_file} if $params->{remote_file}; # user can specify remote_file name, optionally
161 unless ($params->{type} and $params->{type} eq 'FTP') {
162 if ($params->{ssh_publickey} || $params->{ssh_privatekey}) {
164 %keys = param_keys($params) or return; # we got one or both params, but they didn't pan out
166 %keys = get_keyfiles(); # optional "force" arg could be used here to empty cache
171 my $ssh2 = Net::SSH2->new();
172 unless($ssh2->connect($host)) {
173 $logger->warn("SSH2 connect FAILED: $!" . join(" ", $ssh2->error));
174 $specific and return;
175 %keys = (); # forget the keys, we cannot connect
177 foreach (keys %keys) {
180 publickey => $keys{$_},
181 rank => [qw/ publickey hostbased password /],
183 $params->{remote_user } and $auth_args{username} = $params->{remote_user };
184 $params->{remote_password} and $auth_args{password} = $params->{remote_password};
185 $params->{remote_host } and $auth_args{hostname} = $params->{remote_host };
187 if ($ssh2->auth(%auth_args)) {
188 if ($ssh2->scp_put(@put_args)) {
189 $logger->info("SendFile Reactor: successfully sent ${host} " . join(' --> ', @put_args));
192 $logger->error("SendFile Reactor: put to $host failed with error: $!");
195 } elsif ($specific) {
196 $logger->error("Abort reactor: ssh2->auth FAILED for $host using $_: $!");
199 $logger->notice("Unsuccessful keypair: ssh2->auth FAILED for $host using $_: $!");
203 # my $conf = OpenSRF::Utils::SettingsClient->new;
204 # $$env{something_hardcoded} = $conf->config_value('category', 'whatever');
206 # Try w/ non-key uFTP methods
208 foreach (qw/debug type port/) {
209 $options{$_} = $params->{$_} if $params->{$_};
211 my $ftp = Net::uFTP->new($host, %options);
214 foreach (qw/remote_user remote_password remote_account/) {
215 push @login_args, $params->{$_} if $params->{$_};
217 unless ($ftp->login(@login_args)) {
218 $logger->error("SendFile Reactor: failed login to $host w/ args(" . join(',', @login_args) . ")");
222 my $filename = $ftp->put(@put_args);
224 $logger->info("SendFile Reactor: successfully sent ${host} $tmp --> $filename");
227 $logger->error("SendFile Reactor: put to $host failed with error: $!");