File Coverage

blib/lib/Sys/Run.pm
Criterion Covered Total %
statement 11 13 84.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 16 18 88.8


line stmt bran cond sub pod time code
1             package Sys::Run;
2             {
3             $Sys::Run::VERSION = '0.16';
4             }
5             BEGIN {
6 1     1   23347 $Sys::Run::AUTHORITY = 'cpan:TEX';
7             }
8             # ABSTRACT: Run commands and handle their output.
9              
10 1     1   36 use 5.010_000;
  1         4  
  1         47  
11 1     1   1282 use mro 'c3';
  1         962  
  1         7  
12 1     1   45 use feature ':5.10';
  1         2  
  1         117  
13              
14 1     1   539 use Moose;
  0            
  0            
15             use namespace::autoclean;
16              
17             # use IO::Handle;
18             # use autodie;
19             # use MooseX::Params::Validate;
20              
21             use Carp;
22             use File::Temp qw();
23             use File::Blarf;
24             use Net::Domain qw();
25             use Time::HiRes qw(gettimeofday tv_interval);
26              
27             has 'ssh_agent' => (
28             'is' => 'rw',
29             'isa' => 'Bool',
30             'default' => 0,
31             );
32              
33             has 'ssh_hostkey_check' => (
34             'is' => 'rw',
35             'isa' => 'Bool',
36             'default' => 1,
37             );
38              
39             has 'log_times' => (
40             'is' => 'rw',
41             'isa' => 'Bool',
42             'default' => 0,
43             );
44              
45             with qw(Log::Tree::RequiredLogger);
46              
47             sub check_ssh_login {
48             my $self = shift;
49             my $target = shift;
50             my $opts = shift || {};
51              
52             # check if pw-less ssh access works
53             if ( $self->run_remote_cmd( $target, '/bin/true', $opts ) ) {
54             $self->logger()->log( message => 'Password-less SSH access to '.$target.' is OK', level => 'debug', );
55             return 1;
56             }
57             else {
58             $self->logger()->log( message => 'Password-less SSH access to '.$target.' does not work. Aborting!', level => 'error', );
59             return;
60             }
61             }
62              
63             sub clear_caches {
64             my $self = shift;
65             my $opts = shift || {};
66              
67             if(
68             $self->run_cmd( 'echo 3 > /proc/sys/vm/drop_caches', $opts )
69             &&
70             $self->run_cmd( 'sync', $opts )
71             ) { return 1; }
72              
73             return;
74             }
75              
76             sub run_cmd {
77             my $self = shift;
78             my $cmd = shift;
79             my $opts = shift || {};
80              
81             my $outfile;
82             my $tempdir;
83             if ( $opts->{Logfile} ) {
84             $cmd .= ' >>' . $opts->{Logfile} . ' 2>&1';
85             }
86             elsif ( $opts->{CaptureOutput} ) {
87             if ( $opts->{Outfile} ) {
88             if ( $opts->{Append} ) {
89             $cmd .= ' >>'.$opts->{Outfile};
90             } else {
91             $cmd .= ' >' .$opts->{Outfile};
92             }
93             } else {
94             # mktemp, redirect to tempfile
95             $tempdir = File::Temp::->newdir( CLEANUP => 1, );
96             $outfile = $tempdir . '/cmd.out';
97             $cmd .= ' >'.$outfile;
98             }
99             # only redirect STDERR if not already redirected
100             if($cmd !~ m/\s2>/) {
101             $cmd .= ' 2>&1';
102             }
103             }
104             else {
105             if ( !$opts->{Verbose} && $cmd !~ m/>/ ) {
106             $cmd .= ' >/dev/null 2>&1';
107             }
108             }
109              
110             my $msg = 'CMD: '.$cmd;
111             $self->logger()->log( message => $msg, level => 'debug', );
112              
113             if ( $opts->{Logfile} ) {
114             local $opts->{Append} = 1;
115             File::Blarf::blarf( $opts->{Logfile}, time().' - '.$msg . "\n", $opts );
116             }
117              
118             my $rv = undef;
119             my $timeout = $opts->{Timeout} // 0;
120             my $prev_timeout = 0;
121             my $t0 = [gettimeofday];
122             eval {
123             local $SIG{ALRM} = sub { die "alarm-sys-run-cmd\n"; };
124             $prev_timeout = alarm $timeout if $timeout > 0;
125             if( $opts->{DryRun} ) {
126             $rv = 0;
127             } else {
128             $rv = system($cmd) >> 8;
129             }
130             };
131             alarm $prev_timeout if $timeout > 0;
132             if ( $self->log_times() ) {
133             my $d0 = tv_interval( $t0 );
134             $self->logger()->log( message => 'CMD ran for '.$d0.'s', level => 'debug', );
135             }
136             if ( $@ && $@ eq "alarm-sys-run-cmd\n" ) {
137             $rv = 1;
138             $self->logger()->log( message => 'CMD timed out after '.$timeout, level => 'warning', );
139             }
140             if ( $opts->{Logfile} ) {
141             local $opts->{Append} = 1;
142             my $output = time().' - CMD finished. Exit Code: '.$rv."\n";
143             if( $opts->{DryRun} ) {
144             $output = 'CMD finished in DryRun mode. Faking exit code: 0.'."\n";
145             }
146             File::Blarf::blarf( $opts->{Logfile}, $output, $opts );
147             }
148             if ( defined($rv) && $rv == 0 ) {
149             $self->logger()->log( message => 'Command completed successfully', level => 'debug', );
150             if ( $opts->{CaptureOutput} && !$opts->{Outfile} ) {
151             return File::Blarf::slurp( $outfile, $opts );
152             }
153             else {
154             if ( $opts->{ReturnRV} ) {
155             return $rv;
156             }
157             else {
158             return 1;
159             }
160             }
161             }
162             else {
163             $rv ||= '';
164             $self->logger()->log( message => 'Could not execute '.$cmd.' without error. Exit Code: '.$rv.', Error: ' . $!, level => 'warning', );
165             if ( $opts->{ReturnRV} ) {
166             return $rv;
167             }
168             else {
169             return;
170             }
171             }
172             }
173              
174             sub run {
175             my $self = shift;
176             my $host = shift;
177             my $cmd = shift;
178             my $opts = shift || {};
179              
180             if ( $host eq 'localhost' || $host eq Net::Domain::hostname() || $host eq Net::Domain::hostfqdn() ) {
181             return $self->run_cmd( $cmd, $opts );
182             }
183             else {
184             return $self->run_remote_cmd( $host, $cmd, $opts );
185             }
186             }
187              
188             sub _ssh_opts {
189             my $self = shift;
190             my $opts = shift || {};
191              
192             my $ssh_opts = '-oBatchMode=yes ';
193             if ( $opts->{NoSSHStrictHostKeyChecking} || !$self->ssh_hostkey_check() ) {
194             $ssh_opts .= '-oStrictHostKeyChecking=no ';
195             $ssh_opts .= '-oUserKnownHostsFile=/dev/null ';
196             }
197             if ( $opts->{SSHVerbose} ) {
198             $ssh_opts .= q{-v };
199             } else {
200             # if we're not supposed to be verbose, we're quiet
201             $ssh_opts .= q{-q };
202             }
203             # add any extra ssh options, like ports et.al.
204             if ( $opts->{SSHOpts} ) {
205             $ssh_opts .= $opts->{SSHOpts}.q{ };
206             }
207             return $ssh_opts;
208             }
209              
210             sub run_remote_cmd {
211             my $self = shift;
212             my $host = shift;
213             my $cmd = shift;
214             my $opts = shift || {};
215              
216             if ( $opts->{NoHup} ) {
217              
218             # run remote cmds in background, this requires nohup
219             $cmd = 'nohup ' . $cmd;
220             if ( $cmd !~ m/>/ ) {
221              
222             # redirect output if not already done
223             $cmd .= ' >/dev/null 2>/dev/null';
224             }
225             if ( $cmd !~ m/</ ) {
226              
227             # redirect input if not already done
228             $cmd .= ' </dev/null';
229             }
230             $cmd .= ' &';
231             }
232              
233             my $rcmd = 'ssh '.$self->_ssh_opts( $opts ).q{ }.$host.q{ '}.$cmd.q{'};
234              
235             # Do not use a forwarded SSH agent unless
236             # explicitly asked for. Otherwise a long running operation, e.g. a sync,
237             # may be started in a screen w/ the ssh auth of the user. When this users
238             # logs off and a new ssh connection is opened it will fail if there
239             # is no host key.
240             local $ENV{SSH_AGENT_PID} = $ENV{SSH_AGENT_PID};
241             local $ENV{SSH_AUTH_SOCK} = $ENV{SSH_AUTH_SOCK};
242             if ( !$opts->{UseSSHAgent} || !$self->ssh_agent() ) {
243              
244             # DGR: already properly localized above
245             ## no critic (RequireLocalizedPunctuationVars)
246             $ENV{SSH_AGENT_PID} = q{};
247             $ENV{SSH_AUTH_SOCK} = q{};
248             ## use critic
249             }
250             $self->logger()->log( message => 'CMD: '.$rcmd, level => 'debug', );
251             my $rv = $self->run_cmd( $rcmd, $opts );
252              
253             # WARNING: $rv IS NOT the OS return code! run_cmd has already
254             # interpreted it and changed a OS-return-code of 0 to a true value (1)
255             # UNLESS ReturnRV was set!
256             #
257             # unfortunately ReturnRV changes the semantics of $rv here
258             # if ReturnRV is NOT set $rv must have a (perl) true value to indicate
259             # success
260             # if ReturnRV is set $rv must be exactly zer0 (i.e. a perl false) to indicate
261             # sucess, any other value (usually) indicates an error
262             if ( ( $opts->{ReturnRV} && defined($rv) && $rv == 0 ) || $rv ) {
263             $self->logger()->log( message => 'Command successful', level => 'debug', );
264             return $rv;
265             }
266             elsif ( $opts->{Retry} ) {
267             $self->logger()->log( message => 'Command failed. Retrying.', level => 'notice', );
268             my $i = 0;
269             my $sleep = $opts->{Sleep} || 10;
270             while ( $i++ < $opts->{Retry} ) {
271             sleep $sleep;
272             if ( my $rv_rtr = $self->run_cmd( $rcmd, $opts ) ) {
273             $self->logger()->log( message => 'Command successful', level => 'debug', );
274             return $rv_rtr;
275             }
276             }
277             $self->logger()->log( message => 'Command failed. After ' . $opts->{Retry} . ' retries.', level => 'notice', );
278             if ( $opts->{ReturnRV} ) {
279             return $rv;
280             }
281             else {
282             return;
283             }
284             }
285             else {
286             $self->logger()->log( message => 'Command failed. Without retry.', level => 'notice', );
287             if ( $opts->{ReturnRV} ) {
288             return $rv;
289             }
290             else {
291             return;
292             }
293             }
294             }
295              
296             sub check_binary {
297             my $self = shift;
298             my $binary = shift;
299             my $opts = shift || {};
300              
301             my @path = split /:/, $ENV{PATH};
302              
303             # add common locations to search path, in case they are missing in PATH
304             push( @path, qw(/sbin /bin /usr/sbin /usr/bin /usr/local/sbin /usr/local/bin) );
305             foreach my $dir (@path) {
306             my $loc = "$dir/$binary";
307             if ( -x $loc ) {
308             $self->logger()->log( message => 'Found binary '.$binary.' at '.$loc, level => 'debug', );
309             return $loc;
310             }
311             }
312             $self->logger()->log( message => 'Binary '.$binary.' not found in path ' . join( ':', @path ), level => 'notice', );
313             return;
314             }
315              
316             sub check_remote_binary {
317             my $self = shift;
318             my $host = shift;
319             my $binary = shift;
320             my $opts = shift || {};
321              
322             local $opts->{CaptureOutput} = 1;
323             local $opts->{Retry} = 2;
324             local $opts->{Chomp} = 1;
325              
326             if ( $binary !~ m#^/# ) {
327             $binary = $self->run_remote_cmd( $host, 'which ' . $binary, $opts );
328             }
329             if ( $binary !~ m#^/# ) {
330             my $msg = 'Command '.$binary.' not found on host '.$host."!\n";
331             $self->logger()->log( message => $msg, level => 'warning', );
332             return;
333             }
334             local $opts->{CaptureOutput} = 0;
335              
336             return $self->run_remote_cmd( $host, 'test -x ' . $binary, $opts );
337             }
338              
339             no Moose;
340             __PACKAGE__->meta->make_immutable;
341              
342             1;
343              
344             __END__
345              
346             =pod
347              
348             =encoding UTF-8
349              
350             =head1 NAME
351              
352             Sys::Run - Run commands and handle their output.
353              
354             =head1 SYNOPSIS
355              
356             use Sys::Run;
357             my $Sys = Sys::Run::->new({
358             'logger' => Log::Tree::->new(),
359             });
360             my $ok = $Sys->run('sleep 60');
361              
362             =head1 METHODS
363              
364             =head2 check_ssh_login
365              
366             Make sure an password-less SSH access to the target is working.
367              
368             =head2 clear_caches
369              
370             Clear all OS-level (linux) caches.
371              
372             =head2 run_cmd
373              
374             Run the given command.
375              
376             Available options:
377             - Logfile
378             - CaptureOutput
379             -- Outfile
380             --- Append
381             - Verbose
382             - Timeout
383             - ReturnRV
384              
385             =head2 run
386              
387             Run the given command on the given hostname (maybe localhost).
388              
389             =head2 run_remote_cmd
390              
391             Run the given command on the remote host.
392              
393             Available Options:
394             - NoHup
395             - UseSSHAgent
396             - NoSSHStrictHostKeyChecking
397             - SSHOpts
398             - ReturnRV
399             - Retry
400              
401             =head2 check_binary
402              
403             Make sure the given (unqalified) binary exists somewhere in the search path.
404              
405             =head2 check_remote_binary
406              
407             Make sure the given command is an executeable binary on the remote host.
408              
409             =head1 NAME
410              
411             Sys::Run - Run commands and handle their output.
412              
413             =head1 DESCIRPTION
414              
415             Run commands and handle output.
416              
417             =head1 AUTHOR
418              
419             Dominik Schulz <tex@cpan.org>
420              
421             =head1 COPYRIGHT AND LICENSE
422              
423             This software is copyright (c) 2012 by Dominik Schulz.
424              
425             This is free software; you can redistribute it and/or modify it under
426             the same terms as the Perl 5 programming language system itself.
427              
428             =cut