39359d4e41b0ebebf74872155d0893f4f0ed8bff
[clearscm.git] / bin / rexec
1 #!/usr/local/bin/perl
2 ################################################################################
3 #
4 # File:         $RCSfile: rexec,v $
5 # Revision:     $Revision: 1.1 $
6 # Description:  Remotely run processes on other machines
7 # Author:       Andrew@ClearSCM.com
8 # Created:      Tue Jan  8 15:57:27 MST 2008
9 # Modified:     $Date: 2008/02/29 15:09:15 $
10 # Language:     perl
11 #
12 # (c) Copyright 2008, ClearSCM, Inc., all rights reserved
13 #
14 ################################################################################
15 use strict;
16 use warnings;
17
18 use FindBin;
19 use Getopt::Long;
20 use Term::ANSIColor qw(:constants);
21 use POSIX ":sys_wait_h";
22
23 my $libs;
24
25 BEGIN {
26   $libs = $ENV{SITE_PERLLIB} ? $ENV{SITE_PERLLIB} : "$FindBin::Bin/lib";
27
28   die "Unable to find libraries\n" if !$libs and !-d $libs;
29 }
30
31 use lib $libs;
32
33 use Display;
34 use Logger;
35 use Machines;
36 use Rexec;
37 use Utils;
38
39 our $_host;
40 our $_skip                      = 0;
41 our $_currentHost;
42
43 my $_log                        = 0;
44 my $_quiet                      = 0;
45 my $_alternateFile;
46 my $_parallel                   = 0;
47
48 my $_totalMachines              = 0;
49 my $_totalExecutions            = 0;
50 my $_totalFailures              = 0;
51 my $_totalConnectFailures       = 0;
52 my $_totalSkips                 = 0;
53
54 my (%_workerStatuses, %_workerNames);
55
56 sub Usage {
57   my $msg = shift;
58
59   display "ERROR: $msg\n" if defined $msg;
60
61   display "rexec\t[-v] [-d] [-u] <cmd>";
62   display "\t-v\tTurn on verbose mode";
63   display "\t-d\tTurn on debug mode";
64   display "\t-u\tThis usage message";
65   display "<cmd>\tCommand to execute remotely";
66
67   exit 1;
68 } # Usage
69
70 sub printStats {
71   display YELLOW  . "Machines: "                . RESET . "$_totalMachines " .
72           MAGENTA . "Executions/Failures: "     . RESET . "($_totalExecutions/$_totalFailures) " .
73           BLUE    . "Connect Failures/Skips: "  . RESET . "($_totalConnectFailures/$_totalSkips)";
74 } # printStats
75
76 sub Interrupted {
77   use Term::ReadKey;
78
79   display BLUE . "\nInterrupted execution on $_host" . RESET;
80
81   printStats;
82
83   display_nolf "Executing on " . YELLOW . $_host . RESET . " - "
84     . GREEN     . BOLD . "S" . RESET . GREEN    . "kip"         . RESET . ", "
85     . CYAN      . BOLD . "C" . RESET . CYAN     . "ontinue"     . RESET . " or "
86     . MAGENTA   . BOLD . "A" . RESET . MAGENTA  . "bort run"    . RESET . " ("
87     . GREEN     . BOLD . "s" . RESET . "/"
88     . CYAN      . BOLD . "C" . RESET . "/"
89     . MAGENTA   . BOLD . "a" . RESET . ")?";
90
91   ReadMode ("cbreak");
92   my $answer = ReadKey (0);
93   ReadMode ("normal");
94
95   if ($answer eq "\n") {
96     display "c";
97   } else {
98     display $answer;
99   } # if
100
101   $answer = lc $answer;
102
103   if ($answer eq "s") {
104     *STDOUT->flush;
105     display "Skipping $_host";
106     $_skip = 1;
107     $_totalSkips++;
108   } elsif ($answer eq "a") {
109     display RED . "Aborting run". RESET;
110     printStats;
111     exit;
112   } else {
113     display "Continuing...";
114     $_skip = 0;
115   } # if
116 } # Interrupted
117
118 sub workerDeath {
119   while ((my $worker = waitpid (-1, WNOHANG)) > 0) {
120     my $status  = $?;
121
122     # Ignore all child deaths except for processes we started
123     next if !exists $_workerStatuses{$worker};
124
125     $_workerStatuses{$worker} = $status;
126   } # while
127
128   $SIG{CHLD} = \&workerDeath;
129 } # workerDeath
130
131 sub execute ($$$) {
132   my ($cmd, $host, $prompt) = @_;
133
134   my @lines;
135
136   verbose_nolf "Connecting to machine $host...";
137
138   eval {
139     $_currentHost = new Rexec (
140       host      => $host,
141       prompt    => $prompt,
142     );
143   };
144
145   # Problem with creating Rexec object. Log error if logging and return.
146   if ($@ or !$_currentHost) {
147     if ($_log) {
148       my $log = new Logger (name => $_host);
149
150       $log->err ("Unable to connect to $host to execute command\n$cmd");
151     } # if
152
153     $_totalConnectFailures++;
154
155     return (1, ());
156   } # if
157
158   verbose " connected";
159
160   display YELLOW . "$host:" . RESET . UNDERLINE . "$cmd" . RESET unless $_quiet;
161
162   @lines = $_currentHost->exec ($cmd);
163
164   if ($_skip) {
165     # Kick current connection
166     kill INT => $_currentHost->{handle}->pid;
167   } # if
168
169   if ($_parallel != 0) {
170     if ($_log) {
171       my $log = new Logger (name => $_host);
172
173       $log->err ("Unable to connect to $host to execute command\n$cmd");
174     } # if
175
176     $_totalConnectFailures++;
177   } # if
178
179   verbose "Disconnected from $host";
180
181   my $status = $_currentHost->status;
182
183   undef $_currentHost;
184
185   return ($status, @lines);
186 } # execute
187
188 sub parallelize ($%) {
189   my ($cmd, %machines) = @_;
190
191   my $thread_count = 1;
192
193   foreach $_host (sort keys %machines) {
194     if ($thread_count <= $_parallel) {
195       debug "Processing $_host ($thread_count)";
196       $thread_count++;
197
198       if (my $pid = fork) {
199         # In parent process - record this host and its status
200         $_workerNames{$pid} = $_host;
201       } else {
202         # In spawned child...
203         $pid = $$;
204
205         debug "Starting process for $_host [$pid]";
206
207         $_workerNames{$pid} = $_host;
208        
209         my ($status, @lines) = execute $cmd, $_host, $machines{$_host};
210
211         my $log = new Logger (name => $_host);
212
213         $log->log ($_) foreach (@lines);
214
215         exit $status;
216       } # if
217     } else {
218       # Wait for somebody to finish;
219       debug "Waiting for somebody to exit...";
220       my $reaped = wait;
221
222       debug "Reaped $_workerNames{$reaped} [$reaped] (Status: $?)";
223       $_workerStatuses{$reaped} = $? >> 8 if $reaped != -1;
224
225       $thread_count--;
226     } # if
227   } # foreach
228
229   # Wait for all kids
230   my %threads = %_workerNames;
231
232   foreach (keys %threads) {
233     if (waitpid ($_, 0) == -1) {
234       delete $threads{$_};
235     } else {
236       $_workerStatuses{$_} = $? >> 8;
237       debug "$threads{$_} [$_] exited with a status of $_workerStatuses{$_}";
238     } # if
239   } # foreach
240
241   debug "All processed completed - Status:";
242
243   if (get_debug) {
244     foreach (sort keys %_workerStatuses) {
245       debug "$_workerNames{$_}\t[$_]:\tStatus: $_workerStatuses{$_}";
246     } # foreach
247   } # if
248
249   # Gather output...
250   display "Output of all executions";
251   foreach $_host (sort keys %machines) {
252     if (-f "$_host.log") {
253       display "$_host:$_" foreach (ReadFile ("$_host.log"));
254
255       #unlink "$_host.log";
256     } else {
257       warning "Unable to find output for $_host ($_host.log missing)";
258     } # if
259   } # foreach
260 } # parallelize
261
262 # Print the totals if interrupted
263 $SIG{INT} = \&Interrupted;
264
265 # Get our options
266 GetOptions (
267   "usage"       => sub { Usage "" },
268   "verbose"     => sub { set_verbose },
269   "debug"       => sub { set_debug },
270   "log"         => \$_log,
271   "quiet"       => \$_quiet,
272   "file=s"      => \$_alternateFile,
273   "parallel:i"  => \$_parallel,
274 ) || Usage "Unknown parameter";
275
276 my $cmd = join " ", @ARGV;
277
278 error "No command specified", 1 if !$cmd;
279
280 my $machines = Machines->new (file => $_alternateFile);
281 my %machines = $machines->all ();
282
283 if ($_parallel > 0) {
284   parallelize ($cmd, %machines);
285   printStats;
286   exit;
287 } # if
288
289 display "NOTE: Logging output to <host>.log" if $_log;
290
291 foreach $_host (sort keys (%machines)) {
292   $_totalMachines++;
293
294   my ($status, @lines) = execute $cmd, $_host, $machines{$_host};
295
296   if ($_skip) {
297     $_skip = 0;
298     next;
299   } # if
300
301   if (defined $status) {
302     if ($status == 0) {
303       $_totalExecutions++;
304     } else {
305       if ($_log) {
306         my $log = new Logger (name => $_host);
307
308         $log->err ("Unable to execute command on $_host\n$cmd");
309       } # if
310        
311       $_totalFailures++;
312
313       next;
314     } # if
315   } # if
316
317   if ($_log) {
318     my $log = new Logger (name => $_host);
319
320     $log->log ($_) foreach (@lines);
321   } else {
322     display $_ foreach (@lines);
323   } # if
324 } # foreach
325
326 printStats;