7 =head1 NAME $RCSfile: rexec.pl,v $
9 Run arbitrary command on another machine
17 Andrew DeFaria <Andrew@ClearSCM.com>
25 Tue Jan 8 15:57:27 MST 2008
29 $Date: 2008/02/29 15:09:15 $
35 Usage: rexec.pl [-usa|ge] [-h|elp] [-v|erbose] [-d|ebug]
36 [-use|rname <username>] [-p|assword <password>]
38 [-m|achines <host1>,<host2>,...]
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)
54 <command>: Commands to execute (Enclose multiple commands in quotes)
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).
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:
80 new: Create new Machines object
82 find: Find machines based on a specified condition (e.g. OS = "Ubuntu 18.04")
84 next: Return the next qualifying machine
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
104 use Term::ANSIColor qw(:constants);
105 use POSIX ":sys_wait_h";
107 use lib "$FindBin::Bin/../lib", "$FindBin::Bin/../clearadm/lib";
115 my ($currentHost, $log);
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},
130 my $host = $currentHost->{host} || 'Unknown Host';
132 display BLUE . "\nInterrupted execution on $host" . RESET;
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 . ")?";
141 my $answer = ReadKey (0);
144 if ($answer eq "\n") {
150 $answer = lc $answer;
152 if ($answer eq "s") {
154 display "Skipping $host";
155 } elsif ($answer eq "a") {
156 display RED . "Aborting run". RESET;
159 display "Continuing...";
165 sub connectHost ($) {
169 $currentHost = Rexec->new (
171 username => $opts{username},
172 password => $opts{password},
176 # Problem with creating Rexec object. Log error if logging and return.
177 if ($@ || !$currentHost) {
179 $log->err ("Unable to connect to $host") if $opts{log};
181 display RED . 'ERROR:' . RESET . " Unable to connect";
192 my $logdir = $opts{logdir} ? "$opts{logdir}/$machine" : $machine;
194 mkdir $logdir or error "Unable to make directory $logdir", 1;
204 my ($msg, $nocrlf) = @_;
207 $log->msg($msg, $nocrlf);
209 verbose $msg, $nocrlf;
214 my ($msg, $exit) = @_;
217 $log->err($msg, $exit);
224 my ($host, $cmd, $prompt) = @_;
228 Log "Connecting to machine $host...", 1;
230 display_nolf BOLD . CYAN . "$host:" . RESET if $opts{verbose};
232 connectHost $host unless $currentHost and $currentHost->{host} eq $host;
234 return (1, ()) unless $currentHost;
238 Log "$host:" . UNDERLINE . $cmd . RESET;
240 @lines = $currentHost->execute ($cmd);
242 my $status = $currentHost->status;
244 return ($status, @lines);
247 $SIG{INT} = \&Interrupted;
266 $opts{debug} = get_debug if ref $opts{debug} eq 'CODE';
267 $opts{verbose} = get_verbose if ref $opts{verbose} eq 'CODE';
269 my $cmd = join ' ', @ARGV;
271 $opts{machines} = [$ENV{REXEC_HOST}] if $ENV{REXEC_HOST};
273 unless ($opts{machines}) {
274 # Connect to Machines module
277 unless ($opts{database}) {
278 require Machines; Machines->import;
280 $machines = Machines->new(filename => $opts{filename});
282 require Machines::MySQL; Machines::MySQL->import;
284 $machines = Machines::MySQL->new;
286 my %machines = $machines->select($opts{condition});
288 $opts{machines} = [keys %machines];
292 my ($status, @lines);
294 for my $machine (sort @{$opts{machines}}) {
298 ($status, @lines) = execute $machine, $cmd;
300 display BOLD . CYAN . "$machine:" . UNDERLINE . WHITE . $cmd . RESET;
302 logError "Execution of $cmd on $machine failed", $status if $status;
305 $log->log($_) for @lines;
308 display $_ for @lines;
313 verbose_nolf "Connecting to machine $machine...";
315 connectHost $machine;
318 my $cmdline = CmdLine->new ();
320 $cmdline->set_prompt (BOLD . CYAN . "$machine:" . RESET . WHITE);
325 $cmd = $cmdline->get();
328 $log->msg('') if $log;
333 last if $cmd =~ /^\s*(exit|quit)\s*$/i;
334 next if $cmd =~ /^\s*$/;
340 ($status, @lines) = execute $machine, $cmd;
342 logError "Execution of $cmd on $machine failed", $status if $status;
345 display $_ for @lines;