File Coverage

blib/lib/App/MultiSsh.pm
Criterion Covered Total %
statement 89 166 53.6
branch 18 62 29.0
condition 2 18 11.1
subroutine 15 20 75.0
pod 8 8 100.0
total 132 274 48.1


line stmt bran cond sub pod time code
1              
2             # Created on: 2014-09-04 17:12:36
3             # Create by: Ivan Wills
4             # $Id$
5             # $Revision$, $HeadURL$, $Date$
6             # $Revision$, $Source$, $Date$
7              
8             use strict;
9 5     5   475233 use warnings;
  5         28  
  5         121  
10 5     5   24 use feature qw/:5.10/;
  5         9  
  5         116  
11 5     5   29 use Carp;
  5         7  
  5         539  
12 5     5   29 use POSIX qw/ceil/;
  5         7  
  5         249  
13 5     5   26 use Data::Dumper qw/Dumper/;
  5         9  
  5         46  
14 5     5   9827 use English qw/ -no_match_vars /;
  5         23016  
  5         236  
15 5     5   2048 use base qw/Exporter/;
  5         14533  
  5         24  
16 5     5   1457 use Path::Tiny;
  5         10  
  5         470  
17 5     5   3627 use IO::Handle;
  5         59162  
  5         203  
18 5     5   2456 use POSIX qw/:errno_h/;
  5         26077  
  5         183  
19 5     5   28  
  5         9  
  5         23  
20             our $VERSION = '0.25';
21             our @EXPORT_OK = qw/hosts_from_map is_host multi_run shell_quote tmux/;
22             our %EXPORT_TAGS = ();
23              
24             my ($map) = @_;
25             my @hosts;
26 12     12 1 132  
27 12         16 my $int_re = qr/ [0-9a-zA-Z] /xms;
28             my $range_re = qr/ ($int_re) (?:[.][.]|-) ($int_re)/xms;
29 12         32 my $group_re = qr/ (?: $int_re | $range_re ) /xms;
30 12         86 my $seperated_re = qr/ $group_re (?: , $group_re ) * /xms;
31 12         76 my $num_range_re = qr/ [[{] ( $seperated_re ) [\]}] /xms;
32 12         79  
33 12         93 while ( my $host_range = shift @{$map} ) {
34             my ($num_range) = $host_range =~ /$num_range_re/;
35 12         21  
  26         59  
36 14         81 if (!$num_range) {
37             push @hosts, $host_range;
38 14 100       30 next;
39 1         2 #if ( is_host($host_range) ) {
40 1         4 # push @hosts, $host_range;
41             # next;
42             #}
43             #else {
44             # unshift @{$hosts}, $host_range;
45             # last;
46             #}
47             }
48              
49             my @numbs = map { /$range_re/ ? ($1 .. $2) : ($_) } split /,/, $num_range;
50             my @hostmaps = map { $a=$host_range; $a =~ s/$num_range_re/$_/e; $a } @numbs;
51 13 100       30  
  19         110  
52 13         22 if ( $hostmaps[0] =~ /$num_range_re/ ) {
  46         55  
  46         171  
  46         89  
  46         77  
53             push @{$map}, @hostmaps;
54 13 100       42 }
55 1         1 else {
  1         4  
56             push @hosts, @hostmaps;
57             }
58 12         39 }
59              
60             return @hosts;
61             }
62 12         72  
63             my $full_name = `host $_[0]`;
64             return $full_name !~ /not found/;
65             }
66 0     0 1 0  
67 0         0 my ($text) = @_;
68              
69             if ($text =~ /[\s$|><;&*?#]/xms) {
70             $text =~ s/'/'\\''/gxms;
71 38     38 1 3994 $text = "'$text'";
72             }
73 38 100       96  
74 34         47 return $text;
75 34         49 }
76              
77             my ($hosts, $remote_cmd, $option) = @_;
78 38         93  
79             if ($option->{tmux}) {
80             my @cmds = map {"ssh $_ " . shell_quote($remote_cmd)} @$hosts;
81             exec tmux($option, @cmds) if !$option->{test};
82 0     0 1 0 print tmux($option, @cmds) . "\n";
83             return;
84 0 0       0 }
85 0         0  
  0         0  
86 0 0       0 # store child processes if forking
87 0         0 my @children;
88 0         0  
89             # loop over each host and run the remote command
90             for my $host (@$hosts) {
91             my $cmd = "ssh $host " . shell_quote($remote_cmd);
92 0         0 print "$cmd\n" if $option->{verbose} > 1 || $option->{test};
93             next if $option->{test};
94              
95 0         0 if ( $option->{parallel} ) {
96 0         0 my $child = fork;
97 0 0 0     0  
98 0 0       0 if ( $child ) {
99             # parent stuff
100 0 0       0 push @children, $child;
101 0         0  
102             if ( @children == $option->{parallel} ) {
103 0 0       0 warn "Waiting for children to finish\n" if $option->{verbose} > 1;
    0          
104             # reap children if reached max fork count
105 0         0 while ( my $pid = shift @children ) {
106             waitpid $pid, 0;
107 0 0       0 }
108 0 0       0 }
109             }
110 0         0 elsif ( defined $child ) {
111 0         0 # child code
112             if ( $option->{interleave} ) {
113             print "$host -\n" if $option->{verbose};
114              
115             require IPC::Open3::Callback;
116             my ($pid, $in, $out, $err) = IPC::Open3::Callback::safe_open3($cmd);
117 0 0       0  
118 0 0       0 close $in;
119             $out->blocking(0);
120 0         0 $err->blocking(0);
121 0         0 while ($out && $err) {
122             $out = _read_label_line($out, \*STDOUT, $host);
123 0         0 $err = _read_label_line($err, \*STDERR, $host);
124 0         0 }
125 0         0 waitpid $pid, 0;
126 0   0     0 exit 0;
127 0         0 }
128 0         0 else {
129             my $out = `$cmd 2>&1`;
130 0         0  
131 0         0 print "$host -\n" if $option->{verbose};
132             print $out;
133             }
134 0         0 exit;
135             }
136 0 0       0 else {
137 0         0 die "Error: $!\n";
138             }
139 0         0 }
140             else {
141             print "$host -\n" if $option->{verbose};
142 0         0 system $cmd;
143             }
144             }
145              
146 0 0       0 # reap any outstanding children
147 0         0 wait;
148             }
149              
150             my ($in_fh, $out_fh, $host) = @_;
151             state %hosts;
152 0         0 my @colours = (qw/
153             red on_red bright_red
154             green on_green bright_green
155             blue on_blue bright_blue
156 0     0   0 magenta on_magenta bright_magenta
157 0         0 cyan on_cyan
158 0         0 yellow on_yellow
159             /);
160             return if !$in_fh;
161              
162             my $line = <$in_fh>;
163              
164             if ( !defined $line && $! != EAGAIN ) {
165             close $in_fh;
166 0 0       0 return;
167             }
168 0         0  
169             if (defined $line) {
170 0 0 0     0 $hosts{$host} ||= $colours[rand @colours];
171 0         0 require Term::ANSIColor;
172 0         0 print {$out_fh} '[', Term::ANSIColor::colored($host, $hosts{$host}), '] ', $line;
173             }
174              
175 0 0       0 return $in_fh;
176 0   0     0 }
177 0         0  
178 0         0 my ($option, @commands) = @_;
  0         0  
179              
180             confess "No commands for tmux to run!\n" if !@commands;
181 0         0  
182             my $layout = layout(@commands);
183             my $tmux = '';
184             my $final = '';
185 5     5 1 3132 my $pct = int( 100 / scalar @commands );
186              
187 5 50       13 for my $ssh (@commands) {
188             if ( !$tmux && $option->{tmux_nested} ) {
189 5         10 $tmux = ' rename-window mssh';
190 5         7 $final = '; bash -c ' . shell_quote("echo $ssh; echo 'set-window-option synchronize-panes on|off'") . '\\;' . shell_quote($ssh);
191 5         5 }
192 5         9 else {
193             my $cmd = !$tmux ? 'new-session' : '\\; split-window -d -p ' . $pct;
194 5         7  
195 15 50 66     36 $tmux .= " $cmd " . shell_quote("echo $ssh") . '\\;' . shell_quote($ssh);
196 0         0 }
197 0         0 }
198              
199             my $sync = $option->{tmux_sync} ? 'on' : 'off';
200 15 100       26 $tmux .= " \\; set-window-option synchronize-panes $sync" if $commands[0] !~ /\s$/xms;
201              
202 15         27 return "tmux$tmux \\; select-layout tiled \\; setw synchronize-panes $sync$final";
203             }
204              
205             my (@commands) = @_;
206 5 50       10 my $rows = int sqrt @commands + 1;
207 5 50       13 my $cols = ceil @commands / $rows;
208             my $out = [];
209 5         22 if ( $cols > $rows + 1 ) {
210             my $tmp = $rows;
211             $rows++;
212             $cols--;
213 16     16 1 7769 }
214 16         35 ROW:
215 16         61 for my $row ( 0 .. $rows - 1 ) {
216 16         29 for my $col ( 0 .. $cols - 1 ) {
217 16 100       36 last ROW if !@commands;
218 1         2 $out->[$row][$col] = shift @commands;
219 1         1 }
220 1         2 }
221              
222             return $out;
223 16         29 }
224 33         38  
225 89 100       123 state $config;
226 81         120 return $config if $config;
227              
228             my $config_file = path($ENV{HOME}, '.mssh');
229             if (!-f $config_file) {
230 16         31 $config = {};
231              
232             # create a default config file
233             $config_file->spew("---\ngroups:\n");
234 0     0 1    
235 0 0         return $config;
236             }
237 0            
238 0 0         require YAML;
239 0           $config = YAML::LoadFile($config_file);
240              
241             return $config;
242 0           }
243              
244 0           my ($groups) = @_;
245             my $config = config();
246             my @hosts;
247 0            
248 0           for my $group (@$groups) {
249             if ($config->{groups} && $config->{groups}{$group}) {
250 0           push @hosts,
251             ref $config->{groups}{$group}
252             ? @{ $config->{groups}{$group} }
253             : $config->{groups}{$group};
254 0     0 1   }
255 0           else {
256 0           warn "No host group '$group' defined in the config!\n";
257             }
258 0           }
259 0 0 0        
260             return @hosts;
261             }
262 0            
263 0 0         1;
264              
265              
266 0           =head1 NAME
267              
268             App::MultiSsh - Multi host ssh executer
269              
270 0           =head1 VERSION
271              
272             This documentation refers to App::MultiSsh version 0.25
273              
274             =head1 SYNOPSIS
275              
276             use App::MultiSsh;
277              
278             # Brief but working code example(s) here showing the most common usage(s)
279             # This section will be as far as many users bother reading, so make it as
280             # educational and exemplary as possible.
281              
282              
283             =head1 DESCRIPTION
284              
285             =head1 SUBROUTINES/METHODS
286              
287             =over 4
288              
289             =item C<hosts_from_map ($host)>
290              
291             Splits C<$host> into all hosts that it represents.
292              
293             e.g.
294              
295             host0[012] -> host00, host01, host02
296             host0[0-2] -> host00, host01, host02
297              
298             =item C<is_host ($host)>
299              
300             Gets the full name of C<$host>
301              
302             =item C<shell_quote ($text)>
303              
304             Quotes C<$text> for putting into a shell command
305              
306             =item C<multi_run ($hosts, $remote_cmd, $option)>
307              
308             Run the command on all hosts
309              
310             =item C<tmux (@commands)>
311              
312             Generate a tmux session with all commands run in separate windows
313              
314             =item C<layout (@commands)>
315              
316             Generate a desired tmux layout
317              
318             =item C<config ()>
319              
320             Read the ~/.mssh config file and return it's data
321              
322             =item C<get_groups (@groups)>
323              
324             Return all hosts represented in C<@groups>
325              
326             =back
327              
328             =head1 DIAGNOSTICS
329              
330             =head1 CONFIGURATION AND ENVIRONMENT
331              
332             =head1 DEPENDENCIES
333              
334             =head1 INCOMPATIBILITIES
335              
336             =head1 BUGS AND LIMITATIONS
337              
338             There are no known bugs in this module.
339              
340             Please report problems to Ivan Wills (ivan.wills@gmail.com).
341              
342             Patches are welcome.
343              
344             =head1 AUTHOR
345              
346             Ivan Wills - (ivan.wills@gmail.com)
347              
348             =head1 LICENSE AND COPYRIGHT
349              
350             Copyright (c) 2014 Ivan Wills (14 Mullion Close, Hornsby Heights, NSW Australia 2077).
351             All rights reserved.
352              
353             This module is free software; you can redistribute it and/or modify it under
354             the same terms as Perl itself. See L<perlartistic>. This program is
355             distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
356             without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
357             PARTICULAR PURPOSE.
358              
359             =cut