1c61a3faecdad967d8bbf22ea963c5e33de9727a
[clearscm.git] / bin / rexec.pl
1 #!/usr/bin/perl
2 use strict;
3 use warnings;
4
5 =pod
6
7 =head1 NAME $RCSfile: rexec.pl,v $
8
9 Run arbitrary command on another machine
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.pl [-usa|ge] [-h|elp] [-v|erbose] [-d|ebug]
36                  [-use|rname <username>] [-p|assword <password>]
37                  [-log]
38                  -m|achines <host1>,<host2>,...
39
40               <command>
41
42  Where:
43    -usa|ge:    Print this usage
44    -h|elp:     Print detailed usage
45    -v|erbose:  Verbose mode
46    -d|ebug:    Print debug messages
47    -use|rname: User name to login as (Default: $USER - Env: REXEC_USER)
48    -p|assword: Password to use (Default: None - Env: REXEC_PASSWD)
49    -m|achines: Machine(s) to run the command on
50    -l|og:      Log output (<machine>.log)
51
52    <command>:  Commands to execute (Enclose multiple commands in quotes)
53
54 =head1 DESCRIPTION
55
56 This script will perform and arbitrary command on a set of machines. It uses the
57 Rexec module which utilizes Perl::Expect to attempt a connection using ssh, rsh
58 and finally telnet. Username and password can be supplied (or set up ssh 
59 pre-shared key) to log in. This is especially important when ssh'ing into
60 Windows machines using Cygwin and wanting to use network resources. If you ssh
61 into a Windows box using pre-shared key then Windows will not have your 
62 password and it needs it to authenticate your user to determine access to remote
63 file systems. Therefore on Windows machines, do not set up preshared key if you
64 wish to access remotely mounted file systems. Instead supply the username and 
65 password (hopefully in a secure manner).
66
67 Machines:
68
69 The list of machines that will be operated on can be specified in the machines
70 option but is more often obtained using a Machines module. The default Machines
71 module will parse a flat file that lists machine names and the characteristics
72 of those machines (OS version, CPU count, owner - See Machines.pm). If a
73 different mechanism is used to store and retrieve machine information then the
74 use can write a replacement for the Machines module. This replacement must
75 present an object oriented approach at supplying the qualifying machines by
76 supporting the following methods:
77
78 new: Create new Machines object
79
80 find: Find machines based on a specified condition (e.g. OS = "Ubuntu 18.04")
81
82 next: Return the next qualifying machine
83
84 Logging and reruning:
85
86 If -log is specified then a directory will be created based on the machine's
87 name in -logdir (default current directory) where all output will be written to 
88 a log file named $machine/$machine.log. The command attempted will be written to
89 $machine/command and the status will be written to $machine/status. If instead
90 the we were not able to connect to the remote machine, often because the machine
91 was down, then the $machine directory will only have the command file indicating
92 that the command was not run on the remote machine. This allows the -restart
93 parameter to work. When run with -restart, rexec will exam all log directories
94 to see which ones only contain a command file and attempt to execute them on
95 $machine again.
96
97 =cut
98
99 use FindBin;
100 use Getopt::Long;
101 use Pod::Usage;
102 use Term::ANSIColor qw(:constants);
103 use POSIX ":sys_wait_h";
104
105 use lib "$FindBin::Bin/../lib", "$FindBin::Bin/../clearadm/lib";
106
107 use CmdLine;
108 use Display;
109 use Logger;
110 use Rexec;
111 use Machines;
112
113 my ($currentHost, $log);
114
115 my %opts = (
116   usage    => sub { pod2usage },
117   help     => sub { pod2usage (-verbose => 2)},
118   verbose  => sub { set_verbose },
119   debug    => sub { set_debug },
120   username => $ENV{REXEC_USER} || $ENV{USER},
121   password => $ENV{REXEC_PASSWD},
122   filename => $ENV{REXEC_MACHINES_FILE} || '/opt/clearscm/data/machines',
123 );
124
125 sub Interrupted {
126   use Term::ReadKey;
127
128   display BLUE . "\nInterrupted execution on $currentHost->{host}" . RESET;
129
130   display_nolf "Executing on " . YELLOW . $currentHost->{host}  . RESET . " - "
131     . CYAN      . BOLD . "C" . RESET . CYAN     . "ontinue"     . RESET . " or "
132     . MAGENTA   . BOLD . "A" . RESET . MAGENTA  . "bort run"    . RESET . " ("
133     . CYAN      . BOLD . "C" . RESET . "/"
134     . MAGENTA   . BOLD . "a" . RESET . ")?";
135
136   ReadMode ("cbreak");
137   my $answer = ReadKey (0);
138   ReadMode ("normal");
139
140   if ($answer eq "\n") {
141     display "c";
142   } else {
143     display $answer;
144   } # if
145
146   $answer = lc $answer;
147
148   if ($answer eq "s") {
149     *STDOUT->flush;
150     display "Skipping $currentHost->{host}";
151   } elsif ($answer eq "a") {
152     display RED . "Aborting run". RESET;
153     exit;
154   } else {
155     display "Continuing...";
156   } # if
157
158   return;
159 } # Interrupted
160
161 sub connectHost ($) {
162   my ($host) = @_;
163
164   # Start a log...
165   $log = Logger->new (name => $host) if $opts{log};
166
167   eval {
168     $currentHost = Rexec->new (
169       host     => $host,
170       username => $opts{username},
171       password => $opts{password},
172     );
173   };
174
175   # Problem with creating Rexec object. Log error if logging and return.
176   if ($@ || !$currentHost) {
177     if ($opts{log}) {
178       $log->err ("Unable to connect to $host") if $opts{log};
179     } else {
180       display RED . 'ERROR:' . RESET . " Unable to connect";
181     } # if
182   } # if
183
184   return;
185 } # connectHost
186
187 sub execute ($$;$) {
188   my ($host, $cmd, $prompt) = @_;
189
190   my @lines;
191
192   verbose_nolf "Connecting to machine $host...";
193
194   display_nolf BOLD . YELLOW . "$host:" . RESET if $opts{verbose};
195
196   connectHost $host unless $currentHost and $currentHost->{host} eq $host;
197
198   return (1, ()) unless $currentHost;
199
200   verbose " connected";
201
202   display WHITE . UNDERLINE . "$cmd" . RESET if $opts{verbose};
203
204   @lines = $currentHost->execute ($cmd);
205
206   verbose "Disconnected from $host";
207
208   my $status = $currentHost->status;
209
210   return ($status, @lines);
211 } # execute
212
213 $SIG{INT} = \&Interrupted;
214
215 # Get our options
216 GetOptions (
217   \%opts,
218   'usage',
219   'help',
220   'verbose',
221   'debug',
222   'username=s',
223   'password=s',
224   'log',
225   'logdir',
226   'filename=s',
227   'database',
228   'machines=s@',
229 ) or pod2usage;
230
231 $opts{debug}   = get_debug   if ref $opts{debug}   eq 'CODE';
232 $opts{verbose} = get_verbose if ref $opts{verbose} eq 'CODE';
233
234 my $cmd = join ' ', @ARGV;
235
236 unless ($opts{machines}) {
237   $opts{machines} = [$ENV{REXEC_HOST}] if $ENV{REXEC_HOST};
238 } # unless
239
240 # Connect to Machines module
241 my $machines;
242
243 unless ($opts{database}) {
244   require Machines; Machines->import;
245
246   $machines = Machines->new(filename => $opts{filename});
247 } else {
248   require Machines::MySQL; Machines::MySQL->import;
249
250   $machines = Machines::MySQL->new;
251 } # if
252
253 my %machines = $machines->select;
254
255 my ($status, @lines);
256
257 for my $machine (sort keys %machines) {
258   if ($cmd) {
259     ($status, @lines) = execute $machine, $cmd;
260
261     display BOLD . YELLOW . "$machine:" . RESET . WHITE . $cmd;
262
263     error "Execution of $cmd on $machine yielded error $status" if $status;
264
265     display $_ for @lines;
266
267     undef $currentHost;
268   } else {
269     verbose_nolf "Connecting to machine $machine...";
270
271     connectHost $machine;
272
273     if ($currentHost) {
274       my $cmdline = CmdLine->new ();
275
276       $cmdline->set_prompt (BOLD . YELLOW . "$machine:" . RESET . WHITE);
277
278       while () {
279         #$cmd = <STDIN>;
280         $cmd = $cmdline->get(); 
281
282         unless ($cmd) {
283           display '';
284           last;
285         } # unless
286
287         last if $cmd =~ /^\s*(exit|quit)\s*$/i;
288         next if $cmd =~ /^\s*$/;
289
290         chomp $cmd;
291
292         ($status, @lines) = execute $machine, $cmd;
293
294         error "Execution of $cmd on $machine yielded error $status" if $status;
295
296         display $_ for @lines;
297       } # while
298     } # if
299   } # if
300 } # for