From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

#
# (c) Jan Gehring <jan.gehring@gmail.com>
#
=head1 NAME
Rex::Task - The Task Object
=head1 DESCRIPTION
The Task Object. Typically you only need this class if you want to manipulate tasks after their initial creation.
=head1 SYNOPSIS
use Rex::Task;
# create a new task
my $task = Rex::Task->new( name => 'testtask' );
$task->set_server('remoteserver');
$task->set_code( sub { say 'Hello'; } );
$task->modify( 'no_ssh', 1 );
# retrieve an existing task
use Rex::TaskList;
my $existing_task = Rex::TaskList->create->get_task('my_task');
=head1 METHODS
=cut
package Rex::Task;
use v5.12.5;
use Time::HiRes qw(time);
our $VERSION = '1.16.0'; # VERSION
use Carp;
require Rex::Commands;
require Rex::Args;
=head2 new
This is the constructor.
$task = Rex::Task->new(
func => sub { some_code_here },
server => [ @server ],
desc => $description,
no_ssh => $no_ssh,
hidden => $hidden,
auth => {
user => $user,
password => $password,
private_key => $private_key,
public_key => $public_key,
},
before => [sub {}, sub {}, ...],
after => [sub {}, sub {}, ...],
around => [sub {}, sub {}, ...],
before_task_start => [sub {}, sub {}, ...],
after_task_finished => [sub {}, sub {}, ...],
name => $task_name,
executor => Rex::Interface::Executor->create,
opts => {key1 => val1, key2 => val2, ...},
args => [arg1, arg2, ...],
);
=cut
sub new {
my $that = shift;
my $proto = ref($that) || $that;
my $self = {@_};
bless( $self, $proto );
if ( !exists $self->{name} ) {
die("You have to define a task name.");
}
$self->{no_ssh} ||= 0;
$self->{func} ||= sub { };
$self->{executor} ||= Rex::Interface::Executor->create;
$self->{opts} ||= {};
$self->{args} ||= [];
$self->{connection} = undef;
# set to true as default
if ( !exists $self->{exit_on_connect_fail} ) {
$self->{exit_on_connect_fail} = 1;
}
return $self;
}
=head2 connection
Returns the current connection object.
=cut
sub connection {
my ($self) = @_;
if ( !exists $self->{connection} || !$self->{connection} ) {
$self->{connection} =
Rex::Interface::Connection->create( $self->get_connection_type );
}
$self->{connection};
}
sub set_connection {
my ( $self, $conn ) = @_;
$self->{connection} = $conn;
}
=head2 executor
Returns the current executor object.
=cut
sub executor {
my ($self) = @_;
$self->{executor}->set_task($self);
return $self->{executor};
}
=head2 hidden
Returns true if the task is hidden. (Should not be displayed on ,,rex -T''.)
=cut
sub hidden {
my ($self) = @_;
return $self->{hidden};
}
=head2 server
Returns the servers on which the task should be executed as an ArrayRef.
=cut
sub server {
my ($self) = @_;
my @server = @{ $self->{server} };
my @ret = ();
if ( ref( $server[-1] ) eq "HASH" ) {
Rex::deprecated(
undef, "0.40",
"Defining extra credentials within the task creation is deprecated.",
"Please use set auth => task => 'taskname' instead."
);
# use extra defined credentials
my $data = pop(@server);
$self->set_auth( "user", $data->{'user'} );
$self->set_auth( "password", $data->{'password'} );
if ( exists $data->{"private_key"} ) {
$self->set_auth( "private_key", $data->{"private_key"} );
$self->set_auth( "public_key", $data->{"public_key"} );
}
}
if ( ref( $self->{server} ) eq "ARRAY"
&& scalar( @{ $self->{server} } ) > 0 )
{
for my $srv ( @{ $self->{server} } ) {
if ( ref($srv) eq "CODE" ) {
push( @ret, &$srv() );
}
else {
if ( ref $srv && $srv->isa("Rex::Group::Entry::Server") ) {
push( @ret, $srv->get_servers );
}
else {
push( @ret, $srv );
}
}
}
}
elsif ( ref( $self->{server} ) eq "CODE" ) {
push( @ret, &{ $self->{server} }() );
}
else {
push( @ret, Rex::Group::Entry::Server->new( name => "<local>" ) );
}
return [@ret];
}
=head2 set_server(@server)
With this method you can set new servers on which the task should be executed on.
=cut
sub set_server {
my ( $self, @server ) = @_;
$self->{server} = \@server;
}
=head2 delete_server
Delete every server registered to the task.
=cut
sub delete_server {
my ($self) = @_;
delete $self->{current_server};
delete $self->{server};
$self->rethink_connection;
}
=head2 current_server
Returns the current server on which the tasks gets executed right now.
=cut
sub current_server {
my ($self) = @_;
return $self->{current_server}
|| Rex::Group::Entry::Server->new( name => "<local>" );
}
=head2 desc
Returns the description of a task.
=cut
sub desc {
my ($self) = @_;
return $self->{desc};
}
=head2 set_desc($description)
Set the description of a task.
=cut
sub set_desc {
my ( $self, $desc ) = @_;
$self->{desc} = $desc;
}
=head2 is_remote
Returns true (1) if the task will be executed remotely.
=cut
sub is_remote {
my ($self) = @_;
if ( exists $self->{current_server} ) {
if ( $self->{current_server} ne '<local>' ) {
return 1;
}
}
else {
if ( exists $self->{server} && scalar( @{ $self->{server} } ) > 0 ) {
return 1;
}
}
return 0;
}
=head2 is_local
Returns true (1) if the task gets executed on the local host.
=cut
sub is_local {
my ($self) = @_;
return $self->is_remote() == 0 ? 1 : 0;
}
=head2 is_http
Returns true (1) if the task gets executed over http protocol.
=cut
sub is_http {
my ($self) = @_;
return ( $self->{"connection_type"}
&& lc( $self->{"connection_type"} ) eq "http" );
}
=head2 is_https
Returns true (1) if the task gets executed over https protocol.
=cut
sub is_https {
my ($self) = @_;
return ( $self->{"connection_type"}
&& lc( $self->{"connection_type"} ) eq "https" );
}
=head2 is_openssh
Returns true (1) if the task gets executed with openssh.
=cut
sub is_openssh {
my ($self) = @_;
return ( $self->{"connection_type"}
&& lc( $self->{"connection_type"} ) eq "openssh" );
}
=head2 want_connect
Returns true (1) if the task will establish a connection to a remote system.
=cut
sub want_connect {
my ($self) = @_;
return $self->{no_ssh} == 0 ? 1 : 0;
}
=head2 get_connection_type
This method tries to guess the right connection type for the task and returns it.
Current return values are below:
=over 4
=item * SSH: connect to the remote server using Net::SSH2
=item * OpenSSH: connect to the remote server using Net::OpenSSH
=item * Local: runs locally (without any connections)
=item * HTTP: uses experimental HTTP connection
=item * HTTPS: uses experimental HTTPS connection
=item * Fake: populate the connection properties, but do not connect
So you can use this type to iterate over a list of remote hosts, but don't let rex build a connection. For example if you want to use Sys::Virt or other modules.
=back
=cut
sub get_connection_type {
my ($self) = @_;
if ( $self->is_http ) {
return "HTTP";
}
elsif ( $self->is_https ) {
return "HTTPS";
}
elsif ( $self->is_remote && $self->is_openssh && $self->want_connect ) {
return "OpenSSH";
}
elsif ( $self->is_remote && $self->want_connect ) {
return Rex::Config->get_connection_type();
}
elsif ( $self->is_remote ) {
return "Fake";
}
else {
return "Local";
}
}
=head2 modify($key, $value)
With this method you can modify values of the task.
=cut
sub modify {
my ( $self, $key, $value ) = @_;
if ( ref( $self->{$key} ) eq "ARRAY" ) {
push( @{ $self->{$key} }, $value );
}
else {
$self->{$key} = $value;
}
$self->rethink_connection;
}
=head2 rethink_connection
Deletes current connection object.
=cut
sub rethink_connection {
my ($self) = @_;
delete $self->{connection};
}
=head2 user
Returns the username the task will use.
=cut
sub user {
my ($self) = @_;
if ( exists $self->{auth} && $self->{auth}->{user} ) {
return $self->{auth}->{user};
}
}
=head2 set_user($user)
Set the username of a task.
=cut
sub set_user {
my ( $self, $user ) = @_;
$self->{auth}->{user} = $user;
}
=head2 password
Returns the password that will be used.
=cut
sub password {
my ($self) = @_;
if ( exists $self->{auth} && $self->{auth}->{password} ) {
return $self->{auth}->{password};
}
}
=head2 set_password($password)
Set the password of the task.
=cut
sub set_password {
my ( $self, $password ) = @_;
$self->{auth}->{password} = $password;
}
=head2 name
Returns the name of the task.
=cut
sub name {
my ($self) = @_;
return $self->{name};
}
=head2 code
Returns the code of the task.
=cut
sub code {
my ($self) = @_;
return $self->{func};
}
=head2 set_code(\&code_ref)
Set the code of the task.
=cut
sub set_code {
my ( $self, $code ) = @_;
$self->{func} = $code;
}
=head2 run_hook($server, $hook)
This method is used internally to execute the specified hooks.
=cut
sub run_hook {
my ( $self, $server, $hook, @more_args ) = @_;
my $old_server;
for my $code ( @{ $self->{$hook} } ) {
if ( $hook eq "after" ) { # special case for after hooks
$code->(
$$server,
( $self->{"__was_authenticated"} || 0 ),
{ $self->get_opts }, @more_args
);
}
else {
$old_server = $$server if $server;
$code->( $$server, $server, { $self->get_opts }, @more_args );
if ( $old_server && $old_server ne $$server ) {
$self->{current_server} = $$server;
}
}
}
}
=head2 set_auth($key, $value)
Set the authentication of the task.
$task->set_auth("user", "foo");
$task->set_auth("password", "bar");
=cut
sub set_auth {
my ( $self, $key, $value ) = @_;
if ( scalar(@_) > 3 ) {
my $_d = shift;
$self->{auth} = {@_};
}
else {
$self->{auth}->{$key} = $value;
}
}
=head2 merge_auth($server)
Merges the authentication information from $server into the task.
Tasks authentication information have precedence.
=cut
sub merge_auth {
my ( $self, $server ) = @_;
# merge auth hashs
# task auth as precedence
my %auth = $server->merge_auth( $self->{auth} );
return \%auth;
}
=head2 get_sudo_password
Returns the sudo password.
=cut
sub get_sudo_password {
my ($self) = @_;
my $server = $self->connection->server;
my %auth = $server->merge_auth( $self->{auth} );
return $auth{sudo_password};
}
=head2 parallelism
Get the parallelism count of a task.
=cut
sub parallelism {
my ($self) = @_;
return $self->{parallelism};
}
=head2 set_parallelism($count)
Set the parallelism of the task.
=cut
sub set_parallelism {
my ( $self, $para ) = @_;
$self->{parallelism} = $para;
}
=head2 connect($server)
Initiate the connection to $server.
=cut
sub connect {
my ( $self, $server, %override ) = @_;
if ( !ref $server ) {
$server = Rex::Group::Entry::Server->new( name => $server );
}
$self->{current_server} = $server;
$self->run_hook( \$server, "before" );
# need to be called, in case of a run_task task call.
# see #788
$self->rethink_connection;
my $user = $self->user;
#print Dumper($self);
my $auth = $self->merge_auth($server);
if ( exists $override{auth} ) {
$auth = $override{auth};
$user = $auth->{user};
}
my $rex_int_conf = Rex::Commands::get("rex_internals");
Rex::Logger::debug( Dumper($rex_int_conf) );
Rex::Logger::debug("Auth-Information inside Task:");
for my $key ( keys %{$auth} ) {
my $data = $auth->{$key};
$data = Rex::Logger::masq( "%s", $data ) if $key eq 'password';
$data = Rex::Logger::masq( "%s", $data ) if $key eq 'sudo_password';
$data ||= "";
Rex::Logger::debug("$key => [[$data]]");
}
$auth->{public_key} = resolv_path( $auth->{public_key}, 1 )
if ( $auth->{public_key} );
$auth->{private_key} = resolv_path( $auth->{private_key}, 1 )
if ( $auth->{private_key} );
my $profiler = Rex::Profiler->new;
# task specific auth rules over all
my %connect_hash = %{$auth};
$connect_hash{server} = $server;
# need to get rid of this
Rex::push_connection(
{
conn => $self->connection,
ssh => $self->connection->get_connection_object,
server => $server,
cache => Rex::Interface::Cache->create(),
task => [],
profiler => $profiler,
reporter => Rex::Report->create( Rex::Config->get_report_type ),
notify => Rex::Notify->new(),
}
);
push @{ Rex::get_current_connection()->{task} }, $self;
$profiler->start("connect");
eval {
$self->connection->connect(%connect_hash);
1;
} or do {
if ( !defined Rex::Config->get_fallback_auth ) {
croak $@;
}
};
$profiler->end("connect");
if ( !$self->connection->is_connected ) {
Rex::pop_connection();
croak("Couldn't connect to $server.");
}
elsif ( !$self->connection->is_authenticated ) {
Rex::pop_connection();
my $message =
"Couldn't authenticate against $server. It may be caused by one or more of:\n";
$message .= " - wrong username, password, key or passphrase\n";
$message .= " - changed remote host key\n";
$message .= " - root is not permitted to login over SSH\n"
if ( $connect_hash{user} eq 'root' );
if ( !exists $override{auth} ) {
my $fallback_auth = Rex::Config->get_fallback_auth;
if ( ref $fallback_auth eq "ARRAY" ) {
my $ret_eval;
for my $fallback_a ( @{$fallback_auth} ) {
$ret_eval = eval { $self->connect( $server, auth => $fallback_a ); };
}
return $ret_eval if $ret_eval;
}
}
croak($message);
}
else {
Rex::Logger::debug("Successfully authenticated on $server.")
if ( $self->connection->get_connection_type ne "Local" );
$self->{"__was_authenticated"} = 1;
}
$self->run_hook( \$server, "around" );
return 1;
}
=head2 disconnect
Disconnect from the current connection.
=cut
sub disconnect {
my ( $self, $server ) = @_;
$self->run_hook( \$server, "around", 1 );
$self->connection->disconnect;
my %args = Rex::Args->getopts;
if ( defined $args{'d'} && $args{'d'} > 2 ) {
Rex::Commands::profiler()->report;
}
delete $self->{connection};
pop @{ Rex::get_current_connection()->{task} };
# need to get rid of this
Rex::pop_connection();
$self->run_hook( \$server, "after" );
}
=head2 get_data
Dump task data.
=cut
sub get_data {
my ($self) = @_;
return {
func => $self->{func},
server => $self->{server},
desc => $self->{desc},
no_ssh => $self->{no_ssh},
hidden => $self->{hidden},
auth => $self->{auth},
before => $self->{before},
after => $self->{after},
around => $self->{around},
name => $self->{name},
executor => $self->{executor},
connection_type => $self->{connection_type},
opts => $self->{opts},
args => $self->{args},
};
}
=head2 run($server, %options)
Run the task on C<$server>, with C<%options>.
=cut
sub run {
return pre_40_run(@_) unless ref $_[0];
my ( $self, $server, %options ) = @_;
$options{opts} ||= { $self->get_opts };
$options{args} ||= [ $self->get_args ];
$options{params} ||= $options{opts};
if ( !ref $server ) {
$server = Rex::Group::Entry::Server->new( name => $server );
}
if ( !$_[1] ) {
# run is called without any server.
# so just connect to any servers.
return Rex::TaskList->create()->run( $self, %options );
}
# this is a method call
# so run the task
# TODO: refactor complete task calling
# direct call with function and normal task call
my ( $in_transaction, $start_time );
$start_time = time;
if ( $server ne "<func>" ) {
# this is _not_ a task call via function syntax.
$in_transaction = $options{in_transaction};
eval { $self->connect($server) };
if ($@) {
my $error = $@;
$self->{"__was_authenticated"} = 0;
$self->run_hook( \$server, "after" );
die $error;
}
if ( Rex::Args->is_opt("c") ) {
# get and cache all os info
if ( !Rex::get_cache()->load() ) {
Rex::Logger::debug("No cache found, need to collect new data.");
$server->gather_information;
}
}
if ( !$server->test_perl ) {
Rex::Logger::info(
"There is no perl interpreter found on this system. "
. "Some commands may not work. Sudo won't work.",
"warn"
);
sleep 3;
}
}
else {
# we need to push the connection information of the last task onto this task object
# if we don't do this, the task doesn't have any information of the current connection when called like a function.
# See: #1091
$self->set_connection(
Rex::get_current_connection()->{task}->[-1]->connection )
if Rex::get_current_connection()->{task}->[-1];
push @{ Rex::get_current_connection()->{task} }, $self;
}
# execute code
my @ret;
my $wantarray = wantarray;
eval {
$self->set_opts( %{ $options{params} } )
if ref $options{params} eq "HASH";
if ($wantarray) {
@ret = $self->executor->exec( $options{params}, $options{args} );
}
else {
$ret[0] = $self->executor->exec( $options{params}, $options{args} );
}
my $notify = Rex::get_current_connection()->{notify};
$notify->run_postponed();
} or do {
if ($@) {
my $error = $@;
Rex::get_current_connection()->{reporter}
->report_resource_failed( message => $error );
Rex::get_current_connection()->{reporter}->report_task_execution(
failed => 1,
start_time => $start_time,
end_time => time,
message => $error,
);
Rex::get_current_connection()->{reporter}->write_report();
pop @{ Rex::get_current_connection()->{task} };
die($error);
}
};
if ( $server ne "<func>" ) {
if ( Rex::Args->is_opt("c") ) {
# get and cache all os info
Rex::get_cache()->save();
}
Rex::get_current_connection()->{reporter}->report_task_execution(
failed => 0,
start_time => $start_time,
end_time => time,
);
Rex::get_current_connection()->{reporter}->write_report();
if ($in_transaction) {
$self->run_hook( \$server, "around", 1 );
$self->run_hook( \$server, "after" );
}
else {
$self->disconnect($server);
}
}
else {
pop @{ Rex::get_current_connection()->{task} };
}
if ($wantarray) {
return @ret;
}
else {
return $ret[0];
}
}
sub pre_40_run {
my ( $class, $task_name, $server_overwrite, $params ) = @_;
# static calls to this method are deprecated
Rex::deprecated( "Rex::Task->run()", "0.40" );
my $tasklist = Rex::TaskList->create;
my $task = $tasklist->get_task($task_name);
$task->set_server($server_overwrite) if $server_overwrite;
$tasklist->run( $task, params => $params );
}
=head2 modify_task($task, $key => $value)
Modify C<$task>, by setting C<$key> to C<$value>.
=cut
sub modify_task {
my $class = shift;
my $task = shift;
my $key = shift;
my $value = shift;
Rex::TaskList->create()->get_task($task)->modify( $key => $value );
}
=head2 is_task
Returns true(1) if the passed object is a task.
=cut
sub is_task {
my ( $class, $task ) = @_;
return Rex::TaskList->create()->is_task($task);
}
=head2 get_tasks
Returns list of tasks.
=cut
sub get_tasks {
my ( $class, @tmp ) = @_;
return Rex::TaskList->create()->get_tasks(@tmp);
}
=head2 get_desc
Returns description of task.
=cut
sub get_desc {
my ( $class, @tmp ) = @_;
return Rex::TaskList->create()->get_desc(@tmp);
}
=head2 exit_on_connect_fail
Returns true if rex should exit on connect failure.
=cut
sub exit_on_connect_fail {
my ($self) = @_;
return $self->{exit_on_connect_fail};
}
=head2 set_exit_on_connect_fail
Sets if rex should exit on connect failure.
=cut
sub set_exit_on_connect_fail {
my ( $self, $exit ) = @_;
$self->{exit_on_connect_fail} = $exit;
}
=head2 get_args
Returns arguments of task.
=cut
sub get_args {
my ($self) = @_;
@{ $self->{args} || [] };
}
=head2 get_opts
Returns options of task.
=cut
sub get_opts {
my ($self) = @_;
%{ $self->{opts} || {} };
}
=head2 set_args
Sets arguments for task.
=cut
sub set_args {
my ( $self, @args ) = @_;
$self->{args} = \@args;
}
=head2 set_opt
Sets an option for task.
=cut
sub set_opt {
my ( $self, $key, $value ) = @_;
$self->{opts}->{$key} = $value;
}
=head2 set_opts
Sets options for task.
=cut
sub set_opts {
my ( $self, %opts ) = @_;
$self->{opts} = \%opts;
}
=head2 clone
Clones a task.
=cut
sub clone {
my $self = shift;
return Rex::Task->new( %{ $self->get_data } );
}
1;