New bice using IPsets
[clearscm.git] / bin / rexec
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 (@machines) {
202     my %system = %$_;
203      
204     if ($thread_count <= $opts{parallel}) {
205       debug "Processing $system{name} ($thread_count)";
206       $thread_count++;
207
208       if (my $pid = fork) {
209         # In parent process - record this host and its status
210         display "Putting $system{name} into workerName{$pid}";
211         $workerNames{$pid} = $system{host};
212       } else {
213         # In spawned child...
214         $pid = $$;
215
216         debug "Starting process for $system{name} [$pid]";
217
218         $workerNames{$pid} = $system{name};
219         
220         # Mark currentHost for interrupt (How does this work in the presence
221         # of parallelization?).
222         $currentHost = $system{name};
223        
224         my ($status, @lines) = execute $cmd, $system{name};
225
226         $log = Logger->new (name => $system{name});
227
228         $log->log ($_) foreach (@lines);
229
230         exit $status;
231       } # if
232     } else {
233       # Wait for somebody to finish;
234       debug "Waiting for somebody to exit...";
235       
236       display "Current status of workerNames:";
237       display $_ foreach (keys %workerNames);
238       my $reaped = wait;
239
240       if ($workerNames{$reaped}) {
241         display "workerNames for pid $reaped exists!";
242       } else {
243         display "workerNames for pid $reaped does not exist!";
244       }
245       debug "Reaped $workerNames{$reaped} [$reaped] (Status: $?)";
246       $workerStatuses{$reaped} = $? >> 8 if $reaped != -1;
247
248       $thread_count--;
249     } # if
250   } # foreach
251
252   # Wait for all kids
253   my %threads = %workerNames;
254
255   foreach (keys %threads) {
256     if (waitpid ($_, 0) == -1) {
257       delete $threads{$_};
258     } else {
259       $workerStatuses{$_} = $? >> 8;
260       debug "$threads{$_} [$_] exited with a status of $workerStatuses{$_}";
261     } # if
262   } # foreach
263
264   debug "All processed completed - Status:";
265
266   if (get_debug) {
267     foreach (sort keys %workerStatuses) {
268       debug "$workerNames{$_}\t[$_]:\tStatus: $workerStatuses{$_}";
269     } # foreach
270   } # if
271
272   # Gather output...
273   display "Output of all executions";
274   foreach (@machines) {
275     my %system = %$_;
276     
277     if (-f "$system{name}.log") {
278       display "$system{name}:$_" foreach (ReadFile ("$system{name}.log"));
279
280       unlink "$system{name}.log";
281     } else {
282       warning "Unable to find output for $system{name} ($system{name}.log missing)";
283     } # if
284   } # foreach
285   
286   return;
287 } # parallelize
288
289 # Print the totals if interrupted
290 $SIG{INT} = \&Interrupted;
291
292 # Get our options
293 GetOptions (
294   \%opts,
295   'usage',
296   'help',
297   'verbose',
298   'debug',
299   'log',
300   'quiet',
301   'type=s',
302   'parallel=i',
303 );
304
305 my $cmd = join ' ', @ARGV;
306
307 pod2usage ('No command specified') unless $cmd;
308
309 my $machines = Clearadm->new;
310
311 if ($opts{parallel} > 0) {
312   my @machines = $machines->SearchSystem ("type='$opts{type}'");
313   
314   parallelize ($cmd, @machines);
315   Stats \%total, $log;
316   exit;
317 } # if
318
319 display "NOTE: Logging outputs to <host>.log" if $opts{log};
320
321 foreach ($machines->SearchSystem ("type='$opts{type}'")) {
322   my %system = %$_;
323   $total{Machines}++;
324
325   my ($status, @lines) = execute $cmd, $system{name};
326
327   if ($skip) {
328     $skip = 0;
329     next;
330   } # if
331
332   if (defined $status) {
333     if ($status == 0) {
334       $total{Executions}++;
335     } else {
336       $total{Failures}++;
337
338       next;
339     } # if
340   } # if
341
342   if ($opts{log}) {
343     $log->log ($_) foreach (@lines);
344   } else {
345     display $_ foreach (@lines);
346   } # if
347 } # foreach
348
349 Stats \%total, $log;