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.

The particularly smelly bits are marked with triple hashes (###) and contain explanations for what's going on. Thanks!

#!/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;

$PM = "Perl Monk's";
$MCF = "Most Clueless Friar Abbot Bishop Pontiff Deacon Curate Priest Vicar";
$nysus = $PM . ' ' . $MCF;
Click here if you love Perl Monks


In reply to Destinkifying some weird Moose code by nysus

Title:
Use:  <p> text here (a paragraph) </p>
and:  <code> code here </code>
to format your post, it's "PerlMonks-approved HTML":



  • Posts are HTML formatted. Put <p> </p> tags around your paragraphs. Put <code> </code> tags around your code and data!
  • Titles consisting of a single word are discouraged, and in most cases are disallowed outright.
  • Read Where should I post X? if you're not absolutely sure you're posting in the right place.
  • Please read these before you post! —
  • Posts may use any of the Perl Monks Approved HTML tags:
    a, abbr, b, big, blockquote, br, caption, center, col, colgroup, dd, del, details, div, dl, dt, em, font, h1, h2, h3, h4, h5, h6, hr, i, ins, li, ol, p, pre, readmore, small, span, spoiler, strike, strong, sub, summary, sup, table, tbody, td, tfoot, th, thead, tr, tt, u, ul, wbr
  • You may need to use entities for some characters, as follows. (Exception: Within code tags, you can put the characters literally.)
            For:     Use:
    & &amp;
    < &lt;
    > &gt;
    [ &#91;
    ] &#93;
  • Link using PerlMonks shortcuts! What shortcuts can I use for linking?
  • See Writeup Formatting Tips and other pages linked from there for more info.