For fun, to help improve my Perl skills, and to help understand the Rex module, I decided to clone Rex with Moose using test-driven development and MooseX::App::Cmd::Command.
Rex is a module for running remote commands on a server. It loads a simple DSL from a config file. The config file is simple Perl code. I was able to get it implemented and it works, but I'm sure there is probably a better approach than what I came up with for loading the config file. I'd be interested in hearing some better approaches to help advance my skills. Here is an SSCCE. To get it to work, just put in your user and host on lines 228 and 232. Name the file spin.pl and run it with ./spin.pl uname.
#!/usr/bin/env perl
package Spin::Executor ;
use Moose::Role;
use B::Deparse;
has 'server' => ( is => 'ro', isa => 'ArrayRef', required => 0);
has 'pass' => ( is => 'ro', isa => 'Str', required => 0);
has 'user' => ( is => 'ro', isa => 'Str', required => 1);
has '_ssh' => (is => 'ro', isa => 'Net::SSH::Expect', required => 0, w
+riter => '_set_ssh' );
sub _connect {
my $s = shift;
my $server = shift;
my $ssh;
if ($s->pass) {
$ssh = Net::SSH::Expect->new(
host => $s->server,
user => $s->user,
password => $s->pass,
);
$ssh->login();
} else {
$ssh = Net::SSH::Expect->new(
host => $server,
user => $s->user
);
$ssh->run_ssh();
}
$s->_set_ssh($ssh);
}
sub run_server_tasks {
my $s = shift;
for my $server (@{$s->server}) {
$s->_connect($server);
$s->_ssh->exec("/bin/bash --noprofile --norc");
### SMELLY
### The subroutines imported from our config file are associated w
+ith the
### Spin::Command::spin package but we want them to run in the Spi
+n::Executor
### package to try to make the code associated with executing a ta
+sk in
### a properly named object (Executor). I mostly just did this to
+see if
### it could be done. Credit to LanX on PM for help with this.
my $deparse = B::Deparse->new;
my $newtext = $deparse->coderef2text($s->func);
$newtext =~ s/Spin::Command::spin/Spin::Executor/;
my $code = eval qq( sub {$newtext} );
$deparse = B::Deparse->new;
### SMELLY
### To get the run() command below to work with the Spin::Task obj
+ect, I
### did the same trick used with Spin::Command::spin module (see t
+hat package
### for more explanation). I'm not even exactly sure how this work
+s. Credit
### to "mst" on #moose irc for helping me come up with this soluti
+on.
local our $Object;
$Object = $s;
&$code;
$s->_ssh->exec("exit");
$s->_ssh->close();
}
}
# here is where the run commands get executed
sub run {
our $Object;
my $cmd = shift;
my @ret = ();
if ($Object->_ssh) {
$Object->_ssh->send($cmd);
while(defined (my $line = $Object->_ssh->read_line()) ) {
$line =~ s/[\r\n]//gms;
next if($line =~ m/^$/);
push @ret, $line;
}
shift @ret;
} else {
push @ret, `$cmd`;
chomp @ret;
}
if (scalar(@ret) >= 1) {
print join("\n", @ret);
print "\n";
}
return join("\n", @ret);
}
1; # Magic true value
package Spin::Group ;
use Moose;
has 'name' => (is => 'ro', isa => 'Str', required => 1 );
has 'servers' => (is => 'ro', isa => 'ArrayRef', required => 1 );
package Spin::Task ;
use Moose;
use Net::SSH::Expect;
with 'Spin::Executor';
has 'name' => (is => 'ro', isa => 'Str', required => 1 );
has 'descript' => (is => 'ro', isa => 'Str', required => 0 );
has 'func' => ( is => 'ro', isa => 'CodeRef', required => 1);
sub execute_task {
my $s = shift;
$s->run_server_tasks;
}
package Spin::Command::spin ;
use Moose;
use Spin::Task;
use Spin::Group;
extends qw(Spin::Command MooseX::App::Cmd::Command);
has '_curr_desc' => ( is => 'rw', isa => 'Str', default => '');
has '_tasks' => (
traits => [ 'Hash' ], is => 'ro', isa => 'HashRef[Spin::Task]', defa
+ult => sub { {} },
handles => { set_task => 'set', get_task => 'get', task_exists => 'e
+xists',
task_keys => 'keys', no_tasks => 'is_empty'},
);
has '_user' => (
is => 'ro', isa => 'Str', required => 0, default => '', writer => '_
+set_user'
);
has '_password' => (
is => 'ro', isa => 'Str', required => 0, default => '', writer => '_
+set_password'
);
sub execute {
my $s = shift;
### Initialize object with a config file
$s->_init_command();
### Set Spin::Task
$s->get_task($ARGV[1])->execute_task;
}
### SMELLY
### The following functions in this package work together to load a co
+nfig file
### which consists of perl code (see below). The perl code in the conf
+ig file
### calls the following functions. We load in the $Object variable so
+that
### these functions can modify the Spin::Command::spin object in the $
+Object var.
sub run { }
sub user {
our $Object;
$Object->_set_user(shift);
}
sub password {
our $Object;
$Object->_set_password(shift);
}
sub task {
our $Object;
my $name = shift;
my $desc = $Object->_curr_desc;
$Object->_curr_desc('');
my $next_arg = pop;
my $func;
if (ref($next_arg) eq 'CODE') {
$func = $next_arg;
} else {
$func = pop;
}
my $group = 'ALL';
my @server = ();
if (scalar(@_) >= 1) {
if($_[0] eq 'group') {
$group = $_[1];
if ($Object->group_exists($group)) {
@server = @{$Object->get_group($group)->servers};
} else {
}
} else {
@server = @_;
}
}
my $task = Spin::Task->new(
name => $name, func => $func, server => [ @server ], descript => $
+desc,
user => $Object->_user, pass => $Object->_password);
$Object->set_task($name => $task);
}
sub desc {
our $Object;
my $desc = shift;
$Object->_curr_desc($desc);
}
{
my $done = 0;
sub _init_command {
return if $done;
### Set up $Object so the function calls can modify our
### Spin::Command::spin object
local our $Object;
$Object = shift;
### this block is normally loaded from a file
### change user and host as appropriate to get it to work
### no password required for host if you have ssh keys
eval {
user "your_name";
#password "your_password";
desc "Show Unix version";
task "uname", "your_host", sub {
run "export MYVAR='blah'";
run "uname -a";
run "echo 'Running: ' \$MYVAR";
run "id";
};
};
$done++;
}
}
package Spin::Command ;
use Moose;
use Cwd;
use File::Spec;
use File::UserConfig;
extends qw(MooseX::App::Cmd::Command);
package Spin ;
use Moose;
extends qw(MooseX::App::Cmd);
# set the default command if none supplied
unshift @ARGV, 'spin';
Spin->run;