File Coverage

blib/lib/App/clad.pm
Criterion Covered Total %
statement 231 283 81.6
branch 73 102 71.5
condition 13 21 61.9
subroutine 57 64 89.0
pod 0 37 0.0
total 374 507 73.7


line stmt bran cond sub pod time code
1             package App::clad;
2              
3 9     9   2830366 use strict;
  9         25  
  9         291  
4 9     9   74 use warnings;
  9         25  
  9         265  
5 9     9   182 use 5.010;
  9         39  
6 9     9   895 use Getopt::Long 1.24 qw( GetOptionsFromArray :config pass_through);
  9         9068  
  9         255  
7 9     9   2362 use Pod::Usage qw( pod2usage );
  9         44616  
  9         671  
8 9     9   833 use Clustericious::Config 1.03;
  9         165430  
  9         216  
9 9     9   5847 use Term::ANSIColor ();
  9         75980  
  9         370  
10 9     9   105 use Sys::Hostname qw( hostname );
  9         20  
  9         757  
11 9     9   63 use YAML::XS qw( Dump );
  9         51  
  9         464  
12 9     9   79 use File::Basename qw( basename );
  9         18  
  9         498  
13 9     9   56 use File::Glob qw( bsd_glob );
  9         15  
  9         467  
14 9     9   4267 use AE;
  9         50558  
  9         318  
15 9     9   4615 use Clustericious::Admin::RemoteHandler;
  9         40  
  9         391  
16 9     9   4532 use Clustericious::Admin::Dump qw( perl_dump );
  9         28  
  9         636  
17 9     9   4637 use File::chdir;
  9         15550  
  9         1094  
18 9     9   3895 use Path::Class ();
  9         129558  
  9         33473  
19              
20             # ABSTRACT: Parallel SSH client
21             our $VERSION = '1.10'; # VERSION
22              
23              
24             sub _local_default ($$)
25             {
26 79 100   79   188 eval { require Clustericious::Admin::ConfigData }
  79         3662  
27             ? Clustericious::Admin::ConfigData->config($_[0])
28             : $_[1];
29             }
30              
31             sub main
32             {
33 0     0 0 0 my $clad = shift->new(@_);
34 0         0 $clad->run;
35             }
36              
37             # this hook is used for testing
38             # see t/args.t subtest 'color'
39             our $_stdout_is_terminal = sub { -t STDOUT };
40              
41             sub _rc
42             {
43 1     1   70 my $dir = bsd_glob('~/.clad');
44 1 50       9 mkdir $dir unless $dir;
45 1         35 $dir;
46             }
47              
48             sub new
49             {
50 74     74 0 920933 my $class = shift;
51              
52 74         431 my $self = bless {
53             dry_run => 0,
54             color => $_stdout_is_terminal->(),
55             server => 0,
56             verbose => 0,
57             serial => 0,
58             next_color => -1,
59             ret => 0,
60             fat => 0,
61             max => 0,
62             count => 0,
63             summary => 0,
64             files => [],
65             purge => 0,
66             list => 0,
67             }, $class;
68            
69 74         436 my @argv = @_;
70            
71 74         265 my $config_name = 'Clad';
72            
73             GetOptionsFromArray(
74             \@argv,
75             'n' => \$self->{dry_run},
76 1     1   1215 'a' => sub { $self->{color} = 0 },
77             'l=s' => \$self->{user},
78             'server' => \$self->{server},
79             'verbose' => \$self->{verbose},
80             'serial' => \$self->{serial},
81             'config=s' => \$config_name,
82             'fat' => \$self->{fat},
83             'max=s' => \$self->{max},
84             'file=s' => $self->{files},
85             'dir=s' => \$self->{dir},
86             'summary' => \$self->{summary},
87             'purge' => \$self->{purge},
88             'list' => \$self->{list},
89              
90             'log' => sub {
91 1     1   2149 $self->{log_dir} = Path::Class::Dir->new(
92             _rc(),
93             'log',
94             sprintf("%08x.%s", time, $$)
95             );
96             },
97              
98 2     2   4870 'log-dir=s' => sub { $self->{log_dir} = Path::Class::Dir->new($_[1]) },
99              
100 2     2   2577 'help|h' => sub { pod2usage({ -verbose => 2}) },
101             'version' => sub {
102 1   50 1   2012 say STDERR 'App::clad version ', ($App::clad::VERSION // 'dev');
103 1         15 exit 1;
104             },
105 74 50       2648 ) || pod2usage(1);
106            
107 71 100       115631 $self->log_dir->mkpath(0,0700) if $self->log_dir;
108              
109 71         1996 $self->{config} = Clustericious::Config->new($config_name);
110              
111 71 100       18865 return $self if $self->server;
112 51 100       241 return $self if $self->purge;
113 50 100       212 return $self if $self->list;
114            
115             # make sure there is at least one cluster specified
116             # and that it doesn't look like a command line option
117 49 100       227 unless(@argv)
118             {
119 1         20 pod2usage({
120             -exitval => 'NOEXIT',
121             -message => "No clusters specified",
122             -sections => [ qw( SYNOPSIS) ],
123             -verbose => 99,
124             });
125 1         14415 $self->{list} = 1;
126 1         10 $self->ret(1);
127 1         9 return $self;
128             }
129 48 100       289 pod2usage({ -exitvalue => 1, -message => "Unknown option: $1" })
130             if $argv[0] =~ /^--?(.*)$/;
131              
132 47         327 $self->{clusters} = [ split ',', shift @argv ];
133              
134             # make sure there is at least one command argument is specified
135             # and that it doesn't look like a command line option
136 47 100       240 pod2usage({ -exitval => 1, -message => "No commands specified" })
137             unless @argv;
138 46 100       236 pod2usage({ -exitvalue => 1, -message => "Unknown option: $1" })
139             if $argv[0] =~ /^--?(.*)$/;
140            
141 45         211 $self->{command} = [ @argv ];
142              
143 45 100       244 if(my $expanded = $self->alias->{$self->command->[0]})
144             {
145 2 100       24 if(ref $expanded)
146             {
147 1         3 splice @{ $self->command }, 0, 1, @$expanded;
  1         2  
148             }
149             else
150             {
151 1         3 $self->command->[0] = $expanded;
152             }
153             }
154            
155 45 100       318 if($self->config->script(default => {})->{$self->command->[0]})
156             {
157 1         6 my $name = shift @{ $self->command };
  1         5  
158 1         5 unshift @{ $self->command }, '$SCRIPT1';
  1         6  
159 1         7 my $content = $self->config->script(default => {})->{$name};
160 1         25 $self->{script} = [ $name => $content ];
161             }
162            
163 45         168 my $ok = 1;
164            
165 45         217 foreach my $cluster (map { "$_" } $self->clusters)
  46         247  
166             {
167 46         207 $cluster =~ s/^.*@//;
168 46 100       184 unless($self->cluster_list->{$cluster})
169             {
170 2         61 $self->cluster_list->{$cluster} = [$cluster];
171             }
172             }
173            
174 45         4599 foreach my $file ($self->files)
175             {
176 6 100       299 next if -r $file;
177 1         48 say STDERR "unable to find $file";
178 1         68 $ok = 0;
179             }
180            
181 45 100 100     292 if(defined $self->dir && ! -d $self->dir)
182             {
183 1         92 say STDERR "unable to find @{[ $self->dir ]}";
  1         7  
184 1         95 $ok = 0;
185             }
186              
187 45 50       384 unless(-t STDIN)
188             {
189 45         107 $self->{stdin} = do { local $/; <STDIN> };
  45         250  
  45         684  
190             delete $self->{stdin}
191             unless defined $self->{stdin}
192 45 50 66     305 && length $self->{stdin};
193             }
194            
195 45 100       167 exit 2 unless $ok;
196            
197 43         318 $self;
198             }
199              
200 375     375 0 6483 sub config { shift->{config} }
201 32     32 0 173 sub dry_run { shift->{dry_run} }
202 30     30 0 292 sub color { shift->{color} }
203 65     65 0 166 sub clusters { @{ shift->{clusters} } }
  65         345  
204 109     109 0 22081 sub command { shift->{command} }
205 20     20 0 97 sub user { shift->{user} }
206 101     101 0 738 sub server { shift->{server} }
207 44     44 0 650 sub verbose { shift->{verbose} }
208 32     32 0 420 sub serial { shift->{serial} }
209 13     13 0 54 sub max { shift->{max} }
210 59     59 0 1561 sub files { @{ shift->{files} } }
  59         267  
211 63     63 0 3041 sub dir { shift->{dir} }
212 13   100 13 0 33 sub script { @{ shift->{script} // [] } }
  13         98  
213 12     12 0 136 sub stdin { defined shift->{stdin} }
214 32     32 0 9590 sub summary { shift->{summary} }
215 122     122 0 5439 sub log_dir { shift->{log_dir} }
216 64     64 0 4462 sub purge { shift->{purge} }
217 63     63 0 5217 sub list { shift->{list} }
218 0     0 0 0 sub fail_color { shift->config->fail_color ( default => 'bold red' ) }
219 0     0 0 0 sub err_color { shift->config->err_color ( default => 'bold yellow' ) }
220 32     32 0 1795 sub ssh_command { shift->config->ssh_command( default => 'ssh' ) }
221 32     32 0 2496 sub ssh_options { shift->config->ssh_options( default => [ -o => 'StrictHostKeyChecking=no',
222             -o => 'BatchMode=yes',
223             -o => 'PasswordAuthentication=no',
224             '-T', ] ) }
225 32     32 0 2700 sub ssh_extra { shift->config->ssh_extra( default => [] ) }
226 44 50   44 0 123 sub fat { my $self = shift; $self->{fat} || $self->config->fat( default => _local_default 'clad_fat', 0 ) }
  44         311  
227              
228             sub server_command
229             {
230 32     32 0 1290 my($self) = @_;
231            
232 32 50       140 $self->fat
233             ? $self->config->fat_server_command( default => _local_default 'clad_fat_server_command', 'perl' )
234             : $self->config->server_command( default => _local_default 'clad_server_command', 'clad --server' );
235             }
236              
237             sub alias
238             {
239 48     48 0 170 my($self) = @_;
240             $self->config->alias( default => sub {
241 16     16   762 my %deprecated = $self->config->aliases( default => {} );
242 16 50       3256 say STDERR "use of aliases key in configuration is deprecated, use alias instead"
243             if %deprecated;
244 16         78 \%deprecated;
245 48         212 });
246             }
247              
248             sub cluster_list
249             {
250 71     71 0 350 my($self) = @_;
251             $self->config->cluster( default => sub {
252 0     0   0 my %deprecated = $self->config->clusters( default => {} );
253 0 0       0 say STDERR "use of clusters key in configuration is deprecated, use cluster instead"
254             if %deprecated;
255 0         0 \%deprecated;
256 71         278 });
257             }
258              
259             sub ret
260             {
261 17     17 0 76 my($self, $new) = @_;
262 17 100       83 $self->{ret} = $new if defined $new;
263 17         332 $self->{ret};
264             }
265              
266             sub host_length
267             {
268 41     41 0 155 my($self) = @_;
269              
270 41 100       183 unless($self->{host_length})
271             {
272 9         29 my $length = 0;
273            
274 9         194 foreach my $cluster (map { "$_" } $self->clusters)
  10         87  
275             {
276 10 100       128 my $user = $cluster =~ s/^(.*)@// ? $1 : $self->user;
277 10         34 foreach my $host (@{ $self->cluster_list->{$cluster} })
  10         126  
278             {
279 28 100       569 my $prefix = ($user ? "$user\@" : '') . $host;
280 28 100       121 $length = length $prefix if length $prefix > $length;
281             }
282             }
283            
284 9         45 $self->{host_length} = $length;
285             }
286            
287 41         466 $self->{host_length};
288             }
289              
290             sub next_color
291             {
292 8     8 0 39 my($self) = @_;
293 8         33 my @colors = $self->config->colors( default => ['green','cyan'] );
294 8   66     598 $colors[ ++$self->{next_color} ] // $colors[ $self->{next_color} = 0 ];
295             }
296              
297             sub payload
298             {
299 12     12 0 55 my($self, $clustername) = @_;
300            
301 12         48 my %env = $self->config->env( default => {} );
302 12   33     694 $env{CLUSTER} //= $clustername; # deprecate
303 12   33     102 $env{CLAD_CLUSTER} //= $clustername;
304              
305 12   50     56 my $payload = {
306             env => \%env,
307             command => $self->command,
308             verbose => $self->verbose,
309             version => $App::clad::VERSION // 'dev',
310             };
311            
312 12 100       47 if($self->files)
313             {
314 1         7 $payload->{require} = '1.01';
315            
316 1         6 foreach my $filename ($self->files)
317             {
318 2         7 my %h;
319 2         26 open my $fh, '<', $filename;
320 2         211 binmode $fh;
321 2         6 $h{content} = do { local $/; <$fh> };
  2         11  
  2         36  
322 2         16 close $fh;
323 2         36 $h{name} = basename $filename;
324 2         284 $h{mode} = sprintf "%o", (stat $filename)[2] & 0777;
325 2         148 push @{ $payload->{files} }, \%h;
  2         18  
326             }
327             }
328            
329 12 100       51 if($self->script)
330             {
331 1         7 my($name, $content) = $self->script;
332 1         4 $payload->{require} = '1.01';
333            
334 1         4 push @{ $payload->{files} }, {
  1         12  
335             name => $name,
336             content => $content,
337             mode => '0700',
338             env => 'SCRIPT1',
339             };
340             }
341            
342 12 100       47 if($self->dir)
343             {
344 1         7 $payload->{require} = '1.02';
345            
346 1         7 $CWD = $self->dir;
347            
348 1         95 my $recurse;
349             $recurse = sub {
350 4     4   184 my($dir) = @_;
351 4         23 foreach my $child ($dir->children(no_hidden => 1))
352             {
353 7         3233 my $key = $child->relative->stringify;
354 7 100       1846 if($child->is_dir)
355             {
356 3         32 $payload->{dir}->{$key} = {
357             is_dir => 1,
358             };
359 3         28 $recurse->($child);
360             }
361             else
362             {
363 4         33 $payload->{dir}->{$key} = {
364             content => scalar $child->slurp(iomode => '<:bytes'),
365             };
366             }
367 7         1976 $payload->{dir}->{$key}->{mode} = sprintf '%o', $child->stat->mode & 0777;
368             }
369 1         13 };
370            
371 1         19 $recurse->(Path::Class::Dir->new);
372             }
373              
374 12 50       309 if($self->stdin)
375             {
376 0         0 $payload->{require} = '1.04';
377            
378             # TODO:
379             # In Perl 5.22 we could refalias this
380             # and save some memory copies.
381 0         0 $payload->{stdin} = $self->{stdin};
382             }
383            
384 12 50       59 if($self->fat)
385             {
386             # Perl on the remote end may not have YAML
387             # so we dump as Perl data structure
388             # instead.
389 0         0 $payload = perl_dump($payload);
390 0         0 require Clustericious::Admin::Server;
391 0         0 open my $fh, '<', $INC{'Clustericious/Admin/Server.pm'};
392 0         0 my $code = do { local $/; <$fh> };
  0         0  
  0         0  
393 0         0 close $fh;
394 0         0 $code =~ s{\s*$}{"\n"}e;
  0         0  
395 0         0 $payload = $code . $payload;
396             }
397             else
398             {
399 12         1811 $payload = Dump($payload);
400             }
401            
402 12         128 $payload;
403             }
404              
405             sub run
406             {
407 28     28 0 108 my($self) = @_;
408            
409 28 100       94 return $self->run_server if $self->server;
410 11 50       42 return $self->run_purge if $self->purge;
411 11 50       43 return $self->run_list if $self->list;
412            
413 11         29 my @done;
414 11         64 my $max = $self->max;
415              
416            
417 11         43 foreach my $cluster (map { "$_" } $self->clusters)
  12         61  
418             {
419 12 100       104 my $user = $cluster =~ s/^(.*)@// ? $1 : $self->user;
420              
421 12         60 my $payload = $self->payload($cluster);
422              
423 12         37 foreach my $host (@{ $self->cluster_list->{$cluster} })
  12         44  
424             {
425 30 100       459 my $prefix = ($user ? "$user\@" : '') . $host;
426 30 50       415 if($self->dry_run)
427             {
428 0         0 say "$prefix % @{ $self->command }";
  0         0  
429             }
430             else
431             {
432 30         601 my $remote = Clustericious::Admin::RemoteHandler->new(
433             prefix => $prefix,
434             clad => $self,
435             user => $user,
436             host => $host,
437             payload => $payload,
438             );
439              
440 30         305 my $done = $remote->cv;
441            
442             $done->cb(sub {
443 0     0   0 my $count = --$self->{count};
444 0 0       0 $self->{cv}->send if $self->{cv};
445 30 50       261 }) if $max;
446            
447 30 50       131 if($max)
448             {
449 0         0 my $count = ++$self->{count};
450 0 0       0 if($count >= $max)
451             {
452 0         0 $self->{cv} = AE::cv;
453 0         0 $self->{cv}->recv;
454 0         0 delete $self->{cv};
455             }
456             }
457            
458 30 50       174 $self->serial ? $done->recv : push @done, $done;
459             }
460             }
461             }
462            
463 11         171 $_->recv for @done;
464              
465 11 100       1820 say "See @{[ $self->log_dir ]} for all logs" if $self->log_dir;
  1         16  
466            
467 11         156 $self->ret;
468             }
469              
470             sub run_server
471             {
472 17     17 0 526 require Clustericious::Admin::Server;
473 17         223 Clustericious::Admin::Server->_server(*STDIN);
474             }
475              
476             sub run_purge
477             {
478 0     0 0   my $log_dir = Path::Class::Dir->new(
479             _rc(),
480             'log',
481             );
482            
483 0 0         return unless -d $log_dir;
484            
485 0           foreach my $path ($log_dir->children)
486             {
487 0 0         if(-d $path)
488             {
489 0           say "PURGE DIR $path";
490 0           $path->rmtree(1, 1);
491             }
492             else
493             {
494 0           say "PURGE FILE $path";
495 0           $path->remove;
496             }
497             }
498             }
499              
500             sub run_list
501             {
502 0     0 0   my($self) = @_;
503            
504 0           my @clusters = sort keys %{ $self->cluster_list };
  0            
505            
506 0           my $cluster = shift @clusters;
507 0 0         if($cluster)
508             {
509 0           say "Clusters: $cluster";
510 0           say " $_" for @clusters;
511             }
512             else
513             {
514 0           say "Clusters: [none]";
515             }
516              
517 0           my @alias = sort keys %{ $self->alias };
  0            
518 0           my $alias = shift @alias;
519 0 0         if($alias)
520             {
521 0           say "Aliases: $alias";
522 0           say " $_" for @alias;
523             }
524             else
525             {
526 0           say "Aliases: [none]";
527             }
528              
529 0           $self->ret;
530             }
531              
532             1;
533              
534             __END__
535              
536             =pod
537              
538             =encoding UTF-8
539              
540             =head1 NAME
541              
542             App::clad - Parallel SSH client
543              
544             =head1 VERSION
545              
546             version 1.10
547              
548             =head1 SYNOPSIS
549              
550             % perldoc clad
551              
552             =head1 DESCRIPTION
553              
554             This module provides the implementation for the L<clad> command. See
555             the L<clad> command for the public interface.
556              
557             =head1 SEE ALSO
558              
559             =over 4
560              
561             =item L<clad>
562              
563             =back
564              
565             =head1 AUTHOR
566              
567             Graham Ollis <plicease@cpan.org>
568              
569             =head1 COPYRIGHT AND LICENSE
570              
571             This software is copyright (c) 2015 by Graham Ollis.
572              
573             This is free software; you can redistribute it and/or modify it under
574             the same terms as the Perl 5 programming language system itself.
575              
576             =cut