Skip to content

Instantly share code, notes, and snippets.

@jonathanstowe
Created March 4, 2017 11:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jonathanstowe/06f510654a5d0b35962ff5f56a62c75a to your computer and use it in GitHub Desktop.
Save jonathanstowe/06f510654a5d0b35962ff5f56a62c75a to your computer and use it in GitHub Desktop.
Foo
package MAP::Util::RunRemote;
use strict;
use warnings;
our ($VERSION) = q$Revision: 105583 $ =~ /Revision:\s*(\d+)/;
use IPC::Run;
use Storable qw(nfreeze thaw);
use Scalar::Util qw(blessed);
use Params::Util qw(_INVOCANT _INSTANCE _IDENTIFIER);
use MAP::Exception;
use MAP::Util::Executables;
require MAP::Platform;
use base qw(MAP::Logger);
use base qw(MAP::ElevateAttribute);
# handle unexpected objects in serialize structures
require MAP::Util::IncHook;
=head1 NAME
MAP::Util::RunRemote
=head1 DESCRIPTION
This module provides a method for running a method of an object on a specified
remote machine.
This is achieved by serializing the provided object and the arguments
(using L<Storable>) and then using ssh to execute the script 'elevator'
on the remote machine/ which accepts the serialized contents of @_
over STDIN and executing the appropriate method (which was passed as an
argument) and then returning the serialized return values over STDOUT.
Exceptions are also returned over STDOUT and rethrown, the script will have
indicated that it is returning an exception by returning an exit code of 1
(rather than 0).
The wrapper code will refuse to run the script if it is writable by the
user that invoked the method (throw a MAP::Exception::Security ).
=head2 METHODS
=over 4
=item run_remote
This is a class method that allows a named method to be executed on
a remote machine on the supplied object. This is achieved by the
method described above.
Parameters: $host ; name of target host (or MAP::Host)
$method : method name to be executed
$object : invocant of the method.
@args : arguments to the method (not including object)
=cut
sub run_remote
{
my ( $self, $host, $method, $object, @args ) = @_;
my $want = wantarray ? 1 : 0;
my $target_user = MAP::Platform->data_owner();
my $target_uid = getpwnam $target_user;
if ( !defined $host )
{
MAP::Exception::Remote->throw( error => "no host provided" );
}
else
{
if ( _INSTANCE( $host, 'MAP::Host' ) )
{
$host = $host->name();
}
}
if ( !_IDENTIFIER($method) )
{
MAP::Exception::Remote->throw(error => "invalid or missing method name");
}
if ( !_INVOCANT($object) )
{
MAP::Exception::Remote->throw( error => "not a valid method invocant");
}
my $ssh = MAP::Util::Executables->get_executable_path('ssh');
if ( !defined $ssh )
{
MAP::Exception::Remote->throw( error => "Can't resolve 'ssh'" );
}
# of course running remote we'd have to do this remotely which
# is a bootstrap problem :-O
my $script = '/opt/map/scripts/elevator'; # MAP::Util::Executables->get_platform_script('elevator');
my $class = ref($object) ? ref($object) : $object;
my @cmd = ( $ssh, $host );
if ( $> != $target_uid )
{
push @cmd, 'sudo', '-u', $target_user;
}
push @cmd, ( $script, $class, $method, $want );
$self->log_debug( "About to run: '" . join ' ', @cmd );
my ( $ret, @ret );
unshift @args, $object; # put the object onto the args
my $frozen_object = nfreeze( \@args );
my $rr = $self->_run_elevator( \@cmd, $frozen_object );
if ($want)
{
if ( defined $rr )
{
@ret = @{$rr};
}
}
else
{
$ret = $rr->[0];
}
return $want ? @ret : $ret;
}
=item _run_elevator
This runs the command specified by $cmd (an array ref that will be
passed to L<IPC::Run>'s C<run> function.) It is assumed that the
command will result in the running of a script that behaves the same
as C<elevator> which has arguments of methodname and classname and
context and accepts the invocant and arguments on STDIN as a storable
frozen object and returns the metod return on STDOUT using Storable.
This will execute $cmd using run, passing $object as STDIN and will
handle the response, thawing the STDOUT handle or STDERR handle as
appropriate and if there has been an exception throwing an exception
as appopriate or if the command was run successfully it will return an
array ref containing the response from the executed command - it is
the callers responsibility to determine the context of the original
call and to either dereference this return or select the first element.
This is only intended to be used by C<run_remote> above and C<Elevate>
in L<MAP::ElevateAttribute> which basically function in the same way.
Parameters: $cmd : ARRAYREF containing command to be run
$object : $ containing a L<Storable> frozen array of objects.
Returns : ARRAYREF containg the response.
=cut
sub _run_elevator
{
my ( $self, $cmd, $object ) = @_;
my $ret = [];
if ( defined $cmd && defined $object )
{
my ( $rethandle, $errhandle );
$ENV{PATH} = '/usr/bin:/bin';
IPC::Run::run $cmd, \$object, \$rethandle, \$errhandle;
$self->log_debug("run_remote $errhandle") if $errhandle;
if ($?)
{
$self->log_debug("run_remote $errhandle");
if ( $? == -1 )
{
MAP::Exception::Remote->throw(error => "elevator failed to execute - $!" );
}
elsif ( $? & 127 )
{
MAP::Exception::Remote->throw(error => "elevator was killed with signal @{[$? & 127]}" );
}
else
{
my $rr;
if ($rethandle)
{
eval
{
MAP::Util::IncHook->install();
$rr = thaw($rethandle);
MAP::Util::IncHook->remove();
};
if ($@)
{
MAP::Exception::Remote->throw( error => "Unexpected error while deserializing expected exception - $@"
);
}
}
if ( blessed($rr) )
{
if ( $rr->can('rethrow') )
{
$self->log_debug( "looks like we got a " . ref($rr) );
$rr->rethrow();
}
else
{
die $rr;
}
}
else
{
$rr = "Unexpected fault in called method" unless $rr;
MAP::Exception::Elevator->throw( error => $rr );
}
}
}
elsif ( defined $rethandle && length($rethandle) )
{
eval
{
MAP::Util::IncHook->install();
$ret = thaw($rethandle);
MAP::Util::IncHook->remove();
};
if ($@)
{
MAP::Exception::Remote->throw(
error => "unexpected error while deserializing response - $@" );
}
}
}
return $ret;
}
=back
=head2 METHODS FROM TRAITS
=over 4
=back
=head1 INHERITS FROM
=over 4
=back
=cut
1;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment