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>,...
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)
52 <command>: Commands to execute (Enclose multiple commands in quotes)
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).
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:
78 new: Create new Machines object
80 find: Find machines based on a specified condition (e.g. OS = "Ubuntu 18.04")
82 next: Return the next qualifying machine
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
102 use Term::ANSIColor qw(:constants);
103 use POSIX ":sys_wait_h";
105 use lib "$FindBin::Bin/../lib", "$FindBin::Bin/../clearadm/lib";
113 my ($currentHost, $log);
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',
128 display BLUE . "\nInterrupted execution on $currentHost->{host}" . RESET;
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 . ")?";
137 my $answer = ReadKey (0);
140 if ($answer eq "\n") {
146 $answer = lc $answer;
148 if ($answer eq "s") {
150 display "Skipping $currentHost->{host}";
151 } elsif ($answer eq "a") {
152 display RED . "Aborting run". RESET;
155 display "Continuing...";
161 sub connectHost ($) {
165 $log = Logger->new (name => $host) if $opts{log};
168 $currentHost = Rexec->new (
170 username => $opts{username},
171 password => $opts{password},
175 # Problem with creating Rexec object. Log error if logging and return.
176 if ($@ || !$currentHost) {
178 $log->err ("Unable to connect to $host") if $opts{log};
180 display RED . 'ERROR:' . RESET . " Unable to connect";
188 my ($host, $cmd, $prompt) = @_;
192 verbose_nolf "Connecting to machine $host...";
194 display_nolf BOLD . YELLOW . "$host:" . RESET if $opts{verbose};
196 connectHost $host unless $currentHost and $currentHost->{host} eq $host;
198 return (1, ()) unless $currentHost;
200 verbose " connected";
202 display WHITE . UNDERLINE . "$cmd" . RESET if $opts{verbose};
204 @lines = $currentHost->execute ($cmd);
206 verbose "Disconnected from $host";
208 my $status = $currentHost->status;
210 return ($status, @lines);
213 $SIG{INT} = \&Interrupted;
231 $opts{debug} = get_debug if ref $opts{debug} eq 'CODE';
232 $opts{verbose} = get_verbose if ref $opts{verbose} eq 'CODE';
234 my $cmd = join ' ', @ARGV;
236 unless ($opts{machines}) {
237 $opts{machines} = [$ENV{REXEC_HOST}] if $ENV{REXEC_HOST};
240 # Connect to Machines module
243 unless ($opts{database}) {
244 require Machines; Machines->import;
246 $machines = Machines->new(filename => $opts{filename});
248 require Machines::MySQL; Machines::MySQL->import;
250 $machines = Machines::MySQL->new;
253 my %machines = $machines->select;
255 my ($status, @lines);
257 for my $machine (sort keys %machines) {
259 ($status, @lines) = execute $machine, $cmd;
261 display BOLD . YELLOW . "$machine:" . RESET . WHITE . $cmd;
263 error "Execution of $cmd on $machine yielded error $status" if $status;
265 display $_ for @lines;
269 verbose_nolf "Connecting to machine $machine...";
271 connectHost $machine;
274 my $cmdline = CmdLine->new ();
276 $cmdline->set_prompt (BOLD . YELLOW . "$machine:" . RESET . WHITE);
280 $cmd = $cmdline->get();
287 last if $cmd =~ /^\s*(exit|quit)\s*$/i;
288 next if $cmd =~ /^\s*$/;
292 ($status, @lines) = execute $machine, $cmd;
294 error "Execution of $cmd on $machine yielded error $status" if $status;
296 display $_ for @lines;