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   362225 use strict;
  5         33  
  5         139  
10 5     5   26 use warnings;
  5         10  
  5         127  
11 5     5   25 use feature qw/:5.10/;
  5         8  
  5         706  
12 5     5   33 use Carp;
  5         9  
  5         279  
13 5     5   2692 use POSIX qw/ceil/;
  5         31715  
  5         24  
14 5     5   10687 use Data::Dumper qw/Dumper/;
  5         27476  
  5         284  
15 5     5   2627 use English qw/ -no_match_vars /;
  5         17630  
  5         28  
16 5     5   1768 use base qw/Exporter/;
  5         10  
  5         727  
17 5     5   4236 use Path::Tiny;
  5         66631  
  5         237  
18 5     5   2929 use IO::Handle;
  5         30779  
  5         215  
19 5     5   33 use POSIX qw/:errno_h/;
  5         10  
  5         26  
20              
21             our $VERSION = '0.24';
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 133 my ($map) = @_;
27 12         18 my @hosts;
28              
29 12         43 my $int_re = qr/ [0-9a-zA-Z] /xms;
30 12         102 my $range_re = qr/ ($int_re) (?:[.][.]|-) ($int_re)/xms;
31 12         84 my $group_re = qr/ (?: $int_re | $range_re ) /xms;
32 12         105 my $seperated_re = qr/ $group_re (?: , $group_re ) * /xms;
33 12         101 my $num_range_re = qr/ [[{] ( $seperated_re ) [\]}] /xms;
34              
35 12         31 while ( my $host_range = shift @{$map} ) {
  26         69  
36 14         112 my ($num_range) = $host_range =~ /$num_range_re/;
37              
38 14 100       45 if (!$num_range) {
39 1         3 push @hosts, $host_range;
40 1         4 next;
41             #if ( is_host($host_range) ) {
42             # push @hosts, $host_range;
43             # next;
44             #}
45             #else {
46             # unshift @{$hosts}, $host_range;
47             # last;
48             #}
49             }
50              
51 13 100       38 my @numbs = map { /$range_re/ ? ($1 .. $2) : ($_) } split /,/, $num_range;
  19         130  
52 13         30 my @hostmaps = map { $a=$host_range; $a =~ s/$num_range_re/$_/e; $a } @numbs;
  46         68  
  46         212  
  46         108  
  46         99  
53              
54 13 100       45 if ( $hostmaps[0] =~ /$num_range_re/ ) {
55 1         3 push @{$map}, @hostmaps;
  1         3  
56             }
57             else {
58 12         48 push @hosts, @hostmaps;
59             }
60             }
61              
62 12         90 return @hosts;
63             }
64              
65             sub is_host {
66 0     0 1 0 my $full_name = `host $_[0]`;
67 0         0 return $full_name !~ /not found/;
68             }
69              
70             sub shell_quote {
71 38     38 1 4123 my ($text) = @_;
72              
73 38 100       121 if ($text =~ /[\s$|><;&*?#]/xms) {
74 34         112 $text =~ s/'/'\\''/gxms;
75 34         60 $text = "'$text'";
76             }
77              
78 38         107 return $text;
79             }
80              
81             sub multi_run {
82 0     0 1 0 my ($hosts, $remote_cmd, $option) = @_;
83              
84 0 0       0 if ($option->{tmux}) {
85 0         0 my @cmds = map {"ssh $_ " . shell_quote($remote_cmd)} @$hosts;
  0         0  
86 0 0       0 exec tmux($option, @cmds) if !$option->{test};
87 0         0 print tmux($option, @cmds) . "\n";
88 0         0 return;
89             }
90              
91             # store child processes if forking
92 0         0 my @children;
93              
94             # loop over each host and run the remote command
95 0         0 for my $host (@$hosts) {
96 0         0 my $cmd = "ssh $host " . shell_quote($remote_cmd);
97 0 0 0     0 print "$cmd\n" if $option->{verbose} > 1 || $option->{test};
98 0 0       0 next if $option->{test};
99              
100 0 0       0 if ( $option->{parallel} ) {
101 0         0 my $child = fork;
102              
103 0 0       0 if ( $child ) {
    0          
104             # parent stuff
105 0         0 push @children, $child;
106              
107 0 0       0 if ( @children == $option->{parallel} ) {
108 0 0       0 warn "Waiting for children to finish\n" if $option->{verbose} > 1;
109             # reap children if reached max fork count
110 0         0 while ( my $pid = shift @children ) {
111 0         0 waitpid $pid, 0;
112             }
113             }
114             }
115             elsif ( defined $child ) {
116             # child code
117 0 0       0 if ( $option->{interleave} ) {
118 0 0       0 print "$host -\n" if $option->{verbose};
119              
120 0         0 require IPC::Open3::Callback;
121 0         0 my ($pid, $in, $out, $err) = IPC::Open3::Callback::safe_open3($cmd);
122              
123 0         0 close $in;
124 0         0 $out->blocking(0);
125 0         0 $err->blocking(0);
126 0   0     0 while ($out && $err) {
127 0         0 $out = _read_label_line($out, \*STDOUT, $host);
128 0         0 $err = _read_label_line($err, \*STDERR, $host);
129             }
130 0         0 waitpid $pid, 0;
131 0         0 exit 0;
132             }
133             else {
134 0         0 my $out = `$cmd 2>&1`;
135              
136 0 0       0 print "$host -\n" if $option->{verbose};
137 0         0 print $out;
138             }
139 0         0 exit;
140             }
141             else {
142 0         0 die "Error: $!\n";
143             }
144             }
145             else {
146 0 0       0 print "$host -\n" if $option->{verbose};
147 0         0 system $cmd;
148             }
149             }
150              
151             # reap any outstanding children
152 0         0 wait;
153             }
154              
155             sub _read_label_line {
156 0     0   0 my ($in_fh, $out_fh, $host) = @_;
157 0         0 state %hosts;
158 0         0 my @colours = (qw/
159             red on_red bright_red
160             green on_green bright_green
161             blue on_blue bright_blue
162             magenta on_magenta bright_magenta
163             cyan on_cyan
164             yellow on_yellow
165             /);
166 0 0       0 return if !$in_fh;
167              
168 0         0 my $line = <$in_fh>;
169              
170 0 0 0     0 if ( !defined $line && $! != EAGAIN ) {
171 0         0 close $in_fh;
172 0         0 return;
173             }
174              
175 0 0       0 if (defined $line) {
176 0   0     0 $hosts{$host} ||= $colours[rand @colours];
177 0         0 require Term::ANSIColor;
178 0         0 print {$out_fh} '[', Term::ANSIColor::colored($host, $hosts{$host}), '] ', $line;
  0         0  
179             }
180              
181 0         0 return $in_fh;
182             }
183              
184             sub tmux {
185 5     5 1 3881 my ($option, @commands) = @_;
186              
187 5 50       17 confess "No commands for tmux to run!\n" if !@commands;
188              
189 5         11 my $layout = layout(@commands);
190 5         8 my $tmux = '';
191 5         8 my $final = '';
192 5         9 my $pct = int( 100 / scalar @commands );
193              
194 5         10 for my $ssh (@commands) {
195 15 50 66     56 if ( !$tmux && $option->{tmux_nested} ) {
196 0         0 $tmux = ' rename-window mssh';
197 0         0 $final = '; bash -c ' . shell_quote("echo $ssh; echo 'set-window-option synchronize-panes on|off'") . '\\;' . shell_quote($ssh);
198             }
199             else {
200 15 100       31 my $cmd = !$tmux ? 'new-session' : '\\; split-window -d -p ' . $pct;
201              
202 15         34 $tmux .= " $cmd " . shell_quote("echo $ssh") . '\\;' . shell_quote($ssh);
203             }
204             }
205              
206 5 50       12 my $sync = $option->{tmux_sync} ? 'on' : 'off';
207 5 50       19 $tmux .= " \\; set-window-option synchronize-panes $sync" if $commands[0] !~ /\s$/xms;
208              
209 5         26 return "tmux$tmux \\; select-layout tiled \\; setw synchronize-panes $sync$final";
210             }
211              
212             sub layout {
213 16     16 1 9211 my (@commands) = @_;
214 16         48 my $rows = int sqrt @commands + 1;
215 16         74 my $cols = ceil @commands / $rows;
216 16         31 my $out = [];
217 16 100       43 if ( $cols > $rows + 1 ) {
218 1         2 my $tmp = $rows;
219 1         2 $rows++;
220 1         2 $cols--;
221             }
222             ROW:
223 16         38 for my $row ( 0 .. $rows - 1 ) {
224 33         56 for my $col ( 0 .. $cols - 1 ) {
225 89 100       568 last ROW if !@commands;
226 81         156 $out->[$row][$col] = shift @commands;
227             }
228             }
229              
230 16         34 return $out;
231             }
232              
233             sub config {
234 0     0 1   state $config;
235 0 0         return $config if $config;
236              
237 0           my $config_file = path($ENV{HOME}, '.mssh');
238 0 0         if (!-f $config_file) {
239 0           $config = {};
240              
241             # create a default config file
242 0           $config_file->spew("---\ngroups:\n");
243              
244 0           return $config;
245             }
246              
247 0           require YAML;
248 0           $config = YAML::LoadFile($config_file);
249              
250 0           return $config;
251             }
252              
253             sub get_groups {
254 0     0 1   my ($groups) = @_;
255 0           my $config = config();
256 0           my @hosts;
257              
258 0           for my $group (@$groups) {
259 0 0 0       if ($config->{groups} && $config->{groups}{$group}) {
260             push @hosts,
261             ref $config->{groups}{$group}
262 0           ? @{ $config->{groups}{$group} }
263 0 0         : $config->{groups}{$group};
264             }
265             else {
266 0           warn "No host group '$group' defined in the config!\n";
267             }
268             }
269              
270 0           return @hosts;
271             }
272              
273             1;
274              
275             __END__