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