# # (c) Jan Gehring # =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 warnings; use Data::Dumper; use Time::HiRes qw(time); our $VERSION = '1.16.0'; # VERSION use Rex::Logger; use Rex::TaskList; use Rex::Interface::Connection; use Rex::Interface::Executor; use Rex::Group::Entry::Server; use Rex::Profiler; use Rex::Hardware; use Rex::Interface::Cache; use Rex::Report; use Rex::Helper::Run; use Rex::Helper::Path; use Rex::Notify; 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 => "" ) ); } 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 => "" ); } =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 '' ) { 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 "" ) { # 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 "" ) { 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;