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