-#!/usr/bin/perl
+#!/usr/bin/env perl
use strict;
use warnings;
=item Revision
-$Revision: 1.0 $
+$Revision: 2.0 $
=item Created:
Usage: rexec.pl [-usa|ge] [-h|elp] [-v|erbose] [-d|ebug]
[-use|rname <username>] [-p|assword <password>]
[-log]
- -m|achines <host1>,<host2>,...
+ [-m|achines <host1>,<host2>,...]
+ [-f|ile <machines>]
<command>
-use|rname: User name to login as (Default: $USER - Env: REXEC_USER)
-p|assword: Password to use (Default: None - Env: REXEC_PASSWD)
-m|achines: Machine(s) to run the command on
+ -f|ile: File containing machine info
-l|og: Log output (<machine>.log)
<command>: Commands to execute (Enclose multiple commands in quotes)
This script will perform and arbitrary command on a set of machines. It uses the
Rexec module which utilizes Perl::Expect to attempt a connection using ssh, rsh
-and finally telnet. Username and password can be suppliec (or set up ssh
+and finally telnet. Username and password can be supplied (or set up ssh
pre-shared key) to log in. This is especially important when ssh'ing into
Windows machines using Cygwin and wanting to use network resources. If you ssh
into a Windows box using pre-shared key then Windows will not have your
wish to access remotely mounted file systems. Instead supply the username and
password (hopefully in a secure manner).
+Machines:
+
+The list of machines that will be operated on can be specified in the machines
+option but is more often obtained using a Machines module. The default Machines
+module will parse a flat file that lists machine names and the characteristics
+of those machines (OS version, CPU count, owner - See Machines.pm). If a
+different mechanism is used to store and retrieve machine information then the
+use can write a replacement for the Machines module. This replacement must
+present an object oriented approach at supplying the qualifying machines by
+supporting the following methods:
+
+new: Create new Machines object
+
+find: Find machines based on a specified condition (e.g. OS = "Ubuntu 18.04")
+
+next: Return the next qualifying machine
+
+Logging and reruning:
+
+If -log is specified then a directory will be created based on the machine's
+name in -logdir (default current directory) where all output will be written to
+a log file named $machine/$machine.log. The command attempted will be written to
+$machine/command and the status will be written to $machine/status. If instead
+the we were not able to connect to the remote machine, often because the machine
+was down, then the $machine directory will only have the command file indicating
+that the command was not run on the remote machine. This allows the -restart
+parameter to work. When run with -restart, rexec will exam all log directories
+to see which ones only contain a command file and attempt to execute them on
+$machine again.
+
=cut
use FindBin;
use Display;
use Logger;
use Rexec;
+use Machines;
-my ($currentHost, $skip, $log);
+my ($currentHost, $log);
my %opts = (
usage => sub { pod2usage },
help => sub { pod2usage (-verbose => 2)},
verbose => sub { set_verbose },
debug => sub { set_debug },
- username => $ENV{REXEC_USER} ? $ENV{REXEC_USER} : $ENV{USER},
+ username => $ENV{REXEC_USER} || $ENV{USER},
password => $ENV{REXEC_PASSWD},
+ database => 1,
);
sub Interrupted {
use Term::ReadKey;
- display BLUE . "\nInterrupted execution on $currentHost->{host}" . RESET;
+ my $host = $currentHost->{host} || 'Unknown Host';
+
+ display BLUE . "\nInterrupted execution on $host" . RESET;
- display_nolf "Executing on " . YELLOW . $currentHost->{host} . RESET . " - "
+ display_nolf "Executing on " . CYAN . $host . RESET . " - "
. CYAN . BOLD . "C" . RESET . CYAN . "ontinue" . RESET . " or "
. MAGENTA . BOLD . "A" . RESET . MAGENTA . "bort run" . RESET . " ("
. CYAN . BOLD . "C" . RESET . "/"
if ($answer eq "s") {
*STDOUT->flush;
- display "Skipping $currentHost->{host}";
+ display "Skipping $host";
} elsif ($answer eq "a") {
display RED . "Aborting run". RESET;
exit;
sub connectHost ($) {
my ($host) = @_;
- # Start a log...
- $log = Logger->new (name => $host) if $opts{log};
-
eval {
$currentHost = Rexec->new (
host => $host,
return;
} # connectHost
+sub initLog($) {
+ my ($machine) = @_;
+
+ if ($opts{log}) {
+ my $logdir = $opts{logdir} ? "$opts{logdir}/$machine" : $machine;
+
+ mkdir $logdir or error "Unable to make directory $logdir", 1;
+
+ $log = Logger->new(
+ name => 'output',
+ path => $logdir,
+ );
+ } # if
+} # initLog
+
+sub Log($;$) {
+ my ($msg, $nocrlf) = @_;
+
+ if ($log) {
+ $log->msg($msg, $nocrlf);
+ } else {
+ verbose $msg, $nocrlf;
+ } #
+} # Log
+
+sub logError ($;$) {
+ my ($msg, $exit) = @_;
+
+ if ($log) {
+ $log->err($msg, $exit);
+ } else {
+ error $msg, $exit;
+ } # if
+} # logError
+
sub execute ($$;$) {
my ($host, $cmd, $prompt) = @_;
my @lines;
- verbose_nolf "Connecting to machine $host...";
+ Log "Connecting to machine $host...", 1;
- display_nolf BOLD . YELLOW . "$host:" . RESET if $opts{verbose};
+ display_nolf BOLD . CYAN . "$host:" . RESET if $opts{verbose};
connectHost $host unless $currentHost and $currentHost->{host} eq $host;
return (1, ()) unless $currentHost;
- verbose " connected";
+ Log ' connected';
- display WHITE . UNDERLINE . "$cmd" . RESET if $opts{verbose};
+ Log "$host:" . UNDERLINE . $cmd . RESET;
@lines = $currentHost->execute ($cmd);
- verbose "Disconnected from $host";
-
my $status = $currentHost->status;
return ($status, @lines);
'username=s',
'password=s',
'log',
+ 'logdir',
+ 'filename=s',
+ 'database!',
'machines=s@',
+ 'condition=s',
) or pod2usage;
$opts{debug} = get_debug if ref $opts{debug} eq 'CODE';
my $cmd = join ' ', @ARGV;
+$opts{machines} = [$ENV{REXEC_HOST}] if $ENV{REXEC_HOST};
+
unless ($opts{machines}) {
- $opts{machines} = [$ENV{REXEC_HOST}] if $ENV{REXEC_HOST};
-} # unless
+ # Connect to Machines module
+ my $machines;
+
+ unless ($opts{database}) {
+ require Machines; Machines->import;
-pod2usage 'Must specify -machines to run on' unless $opts{machines};
+ $machines = Machines->new(filename => $opts{filename});
+ } else {
+ require Machines::MySQL; Machines::MySQL->import;
-my @machines;
+ $machines = Machines::MySQL->new;
-push @machines, (split /,/, join (',', $_)) for (@{$opts{machines}});
+ my %machines = $machines->select($opts{condition});
-$opts{machines} = [@machines];
+ $opts{machines} = [keys %machines];
+ } # if
+} # if
my ($status, @lines);
-for my $machine (@{$opts{machines}}) {
+for my $machine (sort @{$opts{machines}}) {
+ initLog $machine;
+
if ($cmd) {
($status, @lines) = execute $machine, $cmd;
- display BOLD . YELLOW . "$machine:" . RESET . WHITE . $cmd;
+ display BOLD . CYAN . "$machine:" . UNDERLINE . WHITE . $cmd . RESET;
+
+ logError "Execution of $cmd on $machine failed", $status if $status;
- error "Execution of $cmd on $machine yielded error $status" if $status;
+ if ($log) {
+ $log->log($_) for @lines;
+ } # if
display $_ for @lines;
undef $currentHost;
+ undef $log;
} else {
verbose_nolf "Connecting to machine $machine...";
if ($currentHost) {
my $cmdline = CmdLine->new ();
- $cmdline->set_prompt (BOLD . YELLOW . "$machine:" . RESET . WHITE);
+ $cmdline->set_prompt (BOLD . CYAN . "$machine:" . RESET . WHITE);
while () {
- #$cmd = <STDIN>;
+ Log "$machine:";
+
$cmd = $cmdline->get();
unless ($cmd) {
+ $log->msg('') if $log;
display '';
last;
} # unless
chomp $cmd;
+ Log $cmd;
+
($status, @lines) = execute $machine, $cmd;
- error "Execution of $cmd on $machine yielded error $status" if $status;
+ logError "Execution of $cmd on $machine failed", $status if $status;
+ Log $_ for @lines;
display $_ for @lines;
} # while
} # if
+
+ undef $log;
} # if
} # for