File Coverage

blib/lib/App/clad.pm
Criterion Covered Total %
statement 225 277 81.2
branch 72 100 72.0
condition 13 21 61.9
subroutine 55 62 88.7
pod 0 37 0.0
total 365 497 73.4


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