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