diff --git a/.travis.yml b/.travis.yml index 12f3795e1..7943fd427 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ jobs: - dzil listdeps --author --missing | cpanm - cpanm DBI IPC::Shareable Parallel::ForkManager Digest::HMAC_SHA1 - cpanm String::Escape XML::LibXML Net::SFTP::Foreign IO::Pty - - cpanm File::LibMagic Test::mysqld + - cpanm File::LibMagic Test::mysqld Expect install: - dzil build --in build --notgz script: @@ -85,7 +85,7 @@ jobs: - dzil authordeps --missing | cpanm - dzil listdeps --author --missing | cpanm - cpanm DBI IPC::Shareable Parallel::ForkManager Digest::HMAC_SHA1 - - cpanm String::Escape XML::LibXML Net::SFTP::Foreign IO::Pty + - cpanm String::Escape XML::LibXML Net::SFTP::Foreign IO::Pty Expect install: - dzil build --in build --notgz script: diff --git a/ChangeLog b/ChangeLog index e092a7fa7..d97bd7cbe 100644 --- a/ChangeLog +++ b/ChangeLog @@ -19,10 +19,12 @@ Revision history for Rex [NEW FEATURES] - Enable Bash completion of available CLI options - Add tab completion for Zsh + - Enable local rsync operations [REVISION] - Use author tests to check tidiness of bin files - Use Symbol to manipulate Perl symbols + - Add initial rsync tests 1.11.0 2020-06-05 Ferenc Erki [BUG FIXES] diff --git a/lib/Rex/Commands/Rsync.pm b/lib/Rex/Commands/Rsync.pm index 507548a59..ee130985c 100644 --- a/lib/Rex/Commands/Rsync.pm +++ b/lib/Rex/Commands/Rsync.pm @@ -60,8 +60,10 @@ require Rex::Exporter; use base qw(Rex::Exporter); use vars qw(@EXPORT); +use Rex::Commands qw(FALSE TRUE); use Rex::Helper::IP; use Rex::Helper::Path; +use Rex::Helper::Run; @EXPORT = qw(sync); @@ -111,11 +113,14 @@ sub sync { my $server = $current_connection->{server}; my $cmd; - my $port; - my $servername = $server->to_s; + my ( $port, $servername ); - ( $servername, $port ) = - Rex::Helper::IP::get_server_and_port( $servername, 22 ); + if ( defined $server->to_s ) { + ( $servername, $port ) = + Rex::Helper::IP::get_server_and_port( $servername, 22 ); + } + + my $local_connection = defined $servername ? FALSE : TRUE; my $auth = $current_connection->{conn}->get_auth; @@ -148,16 +153,33 @@ sub sync { if ( $opt && exists $opt->{'download'} && $opt->{'download'} == 1 ) { $dest = resolv_path($dest); Rex::Logger::debug("Downloading $source -> $dest"); - push @rsync_cmd, "rsync -rl -e '\%s' --verbose --stats $params "; - push @rsync_cmd, - "'" . $auth->{user} . "\@" . $servername . ":" . $source . "'"; + push @rsync_cmd, "rsync -rl --verbose --stats $params "; + + if ( !$local_connection ) { + push @rsync_cmd, "-e '\%s'"; + push @rsync_cmd, + "'" . $auth->{user} . "\@" . $servername . ":" . $source . "'"; + } + else { + push @rsync_cmd, "'$source'"; + } + push @rsync_cmd, "'$dest'"; } else { $source = resolv_path($source); Rex::Logger::debug("Uploading $source -> $dest"); - push @rsync_cmd, "rsync -rl -e '\%s' --verbose --stats $params '$source' "; - push @rsync_cmd, "'" . $auth->{user} . "\@$servername:$dest" . "'"; + push @rsync_cmd, "rsync -rl --verbose --stats $params"; + + if ( !$local_connection ) { + push @rsync_cmd, "-e '\%s'"; + push @rsync_cmd, "'$source'"; + push @rsync_cmd, "'" . $auth->{user} . "\@$servername:$dest" . "'"; + } + else { + push @rsync_cmd, "'$source'"; + push @rsync_cmd, "'$dest'"; + } } if (Rex::is_sudo) { @@ -166,157 +188,109 @@ sub sync { $cmd = join( " ", @rsync_cmd ); - my $pass = $auth->{password}; - my @expect_options = (); - - my $auth_type = $auth->{auth_type}; - if ( $auth_type eq "try" ) { - if ( $server->get_private_key && -f $server->get_private_key ) { - $auth_type = "key"; + if ( !$local_connection ) { + my $pass = $auth->{password}; + my @expect_options = (); + + my $auth_type = $auth->{auth_type}; + if ( $auth_type eq "try" ) { + if ( $server->get_private_key && -f $server->get_private_key ) { + $auth_type = "key"; + } + else { + $auth_type = "pass"; + } } - else { - $auth_type = "pass"; - } - } - if ( $auth_type eq "pass" ) { - $cmd = sprintf( $cmd, - "ssh -o StrictHostKeyChecking=no -o PubkeyAuthentication=no -p $port", - ); - push( - @expect_options, - [ - qr{Are you sure you want to continue connecting}, - sub { - Rex::Logger::debug("Accepting key.."); - my $fh = shift; - $fh->send("yes\n"); - exp_continue; - } - ], - [ - qr{password: ?$}i, - sub { - Rex::Logger::debug("Want Password"); - my $fh = shift; - $fh->send( $pass . "\n" ); - exp_continue; - } - ], - [ - qr{password for.*:$}i, - sub { - Rex::Logger::debug("Want Password"); - my $fh = shift; - $fh->send( $pass . "\n" ); - exp_continue; - } - ], - [ - qr{rsync error: error in rsync protocol}, - sub { - Rex::Logger::debug("Error in rsync"); - die; - } - ], - [ - qr{rsync error: remote command not found}, - sub { - Rex::Logger::info("Remote rsync command not found"); - Rex::Logger::info( - "Please install rsync, or use Rex::Commands::Sync sync_up/sync_down" - ); - die; - } - ], - - ); - } - else { - if ( $auth_type eq "key" ) { + if ( $auth_type eq "pass" ) { $cmd = sprintf( $cmd, - 'ssh -i ' - . $server->get_private_key - . " -o StrictHostKeyChecking=no -p $port" ); - } - else { - $cmd = sprintf( $cmd, 'ssh -o StrictHostKeyChecking=no -p ' . "$port" ); - } - push( - @expect_options, - [ - qr{Are you sure you want to continue connecting}, - sub { - Rex::Logger::debug("Accepting key.."); - my $fh = shift; - $fh->send("yes\n"); - exp_continue; - } - ], - [ - qr{password: ?$}i, - sub { - Rex::Logger::debug("Want Password"); - my $fh = shift; - $fh->send( $pass . "\n" ); - exp_continue; - } - ], - [ - qr{Enter passphrase for key.*: $}, - sub { - Rex::Logger::debug("Want Passphrase"); - my $fh = shift; - $fh->send( $pass . "\n" ); - exp_continue; - } - ], - [ - qr{rsync error: error in rsync protocol}, - sub { - Rex::Logger::debug("Error in rsync"); - die; - } - ], - [ - qr{rsync error: remote command not found}, - sub { - Rex::Logger::info("Remote rsync command not found"); - Rex::Logger::info( - "Please install rsync, or use Rex::Commands::Sync sync_up/sync_down" - ); - die; - } - ], - - ); - } - - Rex::Logger::debug("cmd: $cmd"); - - eval { - my $exp = Expect->spawn($cmd) or die($!); - - eval { - $exp->expect( - Rex::Config->get_timeout, + "ssh -o StrictHostKeyChecking=no -o PubkeyAuthentication=no -p $port", + ); + push( @expect_options, [ - qr{total size is [\d,]+\s+speedup is }, + qr{Are you sure you want to continue connecting}, sub { - Rex::Logger::debug("Finished transfer very fast"); + Rex::Logger::debug("Accepting key.."); + my $fh = shift; + $fh->send("yes\n"); + exp_continue; + } + ], + [ + qr{password: ?$}i, + sub { + Rex::Logger::debug("Want Password"); + my $fh = shift; + $fh->send( $pass . "\n" ); + exp_continue; + } + ], + [ + qr{password for.*:$}i, + sub { + Rex::Logger::debug("Want Password"); + my $fh = shift; + $fh->send( $pass . "\n" ); + exp_continue; + } + ], + [ + qr{rsync error: error in rsync protocol}, + sub { + Rex::Logger::debug("Error in rsync"); + die; + } + ], + [ + qr{rsync error: remote command not found}, + sub { + Rex::Logger::info("Remote rsync command not found"); + Rex::Logger::info( + "Please install rsync, or use Rex::Commands::Sync sync_up/sync_down" + ); die; } + ], - ] ); - - $exp->expect( - undef, + } + else { + if ( $auth_type eq "key" ) { + $cmd = sprintf( $cmd, + 'ssh -i ' + . $server->get_private_key + . " -o StrictHostKeyChecking=no -p $port" ); + } + else { + $cmd = sprintf( $cmd, 'ssh -o StrictHostKeyChecking=no -p ' . "$port" ); + } + push( + @expect_options, + [ + qr{Are you sure you want to continue connecting}, + sub { + Rex::Logger::debug("Accepting key.."); + my $fh = shift; + $fh->send("yes\n"); + exp_continue; + } + ], [ - qr{total size is [\d,]+\s+speedup is }, + qr{password: ?$}i, sub { - Rex::Logger::debug("Finished transfer"); + Rex::Logger::debug("Want Password"); + my $fh = shift; + $fh->send( $pass . "\n" ); + exp_continue; + } + ], + [ + qr{Enter passphrase for key.*: $}, + sub { + Rex::Logger::debug("Want Passphrase"); + my $fh = shift; + $fh->send( $pass . "\n" ); exp_continue; } ], @@ -327,13 +301,72 @@ sub sync { die; } ], + [ + qr{rsync error: remote command not found}, + sub { + Rex::Logger::info("Remote rsync command not found"); + Rex::Logger::info( + "Please install rsync, or use Rex::Commands::Sync sync_up/sync_down" + ); + die; + } + ], + ); + } + + Rex::Logger::debug("cmd: $cmd"); + eval { + my $exp = Expect->spawn($cmd) or die($!); + + eval { + $exp->expect( + Rex::Config->get_timeout, + @expect_options, + [ + qr{total size is [\d,]+\s+speedup is }, + sub { + Rex::Logger::debug("Finished transfer very fast"); + die; + } + + ] + ); + + $exp->expect( + undef, + [ + qr{total size is [\d,]+\s+speedup is }, + sub { + Rex::Logger::debug("Finished transfer"); + exp_continue; + } + ], + [ + qr{rsync error: error in rsync protocol}, + sub { + Rex::Logger::debug("Error in rsync"); + die; + } + ], + ); + + }; + + $exp->soft_close; + $? = $exp->exitstatus; }; + } + else { + Rex::Logger::debug("Executing command: $cmd"); + + i_run $cmd, fail_ok => 1; - $exp->soft_close; - $? = $exp->exitstatus; - }; + if ( $? != 0 ) { + die 'Error during local rsync operation'; + } + } if ($@) { Rex::Logger::info($@); diff --git a/t/rsync.t b/t/rsync.t new file mode 100644 index 000000000..17b532df8 --- /dev/null +++ b/t/rsync.t @@ -0,0 +1,96 @@ +use strict; +use warnings; +use autodie; + +BEGIN { + use Test::More; + use Rex::Commands::Run; + + can_run('rsync') or plan skip_all => 'Could not find rsync command'; + + eval 'use Rex::Commands::Rsync; 1' + or plan skip_all => 'Could not load Rex::Commands::Rsync module'; +} + +use Cwd qw(getcwd); +use File::Find; +use File::Spec::Functions qw(catfile rel2abs); +use File::Temp qw(tempdir); +use Rex::Task; + +plan tests => 2; + +sub setup { + my $target_dir = tempdir( CLEANUP => 1 ); + + ok( -d $target_dir, "$target_dir is a directory" ); + + opendir( my $DIR, $target_dir ); + my @contents = readdir $DIR; + closedir $DIR; + + my @empty = qw(. ..); + + is_deeply( \@contents, \@empty, "$target_dir is empty" ); + + return $target_dir; +} + +sub test_results { + my ( $source, $target ) = @_; + my ( @expected, @result ); + + # expected results + find( + { + wanted => sub { + s:^(t|.*/t)(?=/)::; + push @expected, $_; + }, + no_chdir => 1 + }, + $source + ); + + # actual results + find( + { + wanted => sub { + s/$target//; + push @result, $_ if length($_); + }, + no_chdir => 1 + }, + $target + ); + + is_deeply( \@result, \@expected, 'synced dir matches' ); +} + +subtest 'local rsync with absolute path' => sub { + my $cwd = getcwd(); + + my $source = catfile( $cwd, 't/sync' ); + my $target = setup(); + + my $task = Rex::Task->new( name => 'local_rsync' ); + isa_ok( $task, 'Rex::Task', 'task exists' ); + + sync $source, $target; + + test_results( $source, $target ); +}; + +subtest 'local rsync with relative path' => sub { + my $cwd = getcwd(); + + my $source = 't/sync'; + my $target = setup(); + + my $task = Rex::Task->new( name => 'local_rsync' ); + isa_ok( $task, 'Rex::Task', 'task exists' ); + + sync $source, $target; + + test_results( $source, $target ); +}; diff --git a/t/sync/dir/file3 b/t/sync/dir/file3 new file mode 100644 index 000000000..e69de29bb diff --git a/t/sync/dir/file4 b/t/sync/dir/file4 new file mode 100644 index 000000000..e69de29bb diff --git a/t/sync/file1 b/t/sync/file1 new file mode 100644 index 000000000..e69de29bb diff --git a/t/sync/file2 b/t/sync/file2 new file mode 100644 index 000000000..e69de29bb