1# 2# (c) Jan Gehring <jan.gehring@gmail.com> 3# 4# vim: set ts=2 sw=2 tw=0: 5# vim: set expandtab: 6 7=head1 NAME 8 9Rex::Commands::Rsync - Simple Rsync Frontend 10 11=head1 DESCRIPTION 12 13With this module you can sync 2 directories via the I<rsync> command. 14 15Version <= 1.0: All these functions will not be reported. 16 17All these functions are not idempotent. 18 19=head1 DEPENDENCIES 20 21=over 4 22 23=item Expect 24 25The I<Expect> Perl module is required to be installed on the machine 26executing the rsync task. 27 28=item rsync 29 30The I<rsync> command has to be installed on both machines involved in 31the execution of the rsync task. 32 33=back 34 35=head1 SYNOPSIS 36 37 use Rex::Commands::Rsync; 38 39 sync "dir1", "dir2"; 40 41=head1 EXPORTED FUNCTIONS 42 43=cut 44 45package Rex::Commands::Rsync; 46 47use 5.010001; 48use strict; 49use warnings; 50 51our $VERSION = '1.13.4'; # VERSION 52 53BEGIN { 54 use Rex::Require; 55 Expect->use; 56 $Expect::Log_Stdout = 0; 57} 58 59require Rex::Exporter; 60 61use base qw(Rex::Exporter); 62use vars qw(@EXPORT); 63 64use Net::OpenSSH::ShellQuoter; 65use Rex::Commands qw(FALSE TRUE); 66use Rex::Helper::IP; 67use Rex::Helper::Path; 68use Rex::Helper::Run; 69use Rex::Interface::Shell; 70 71@EXPORT = qw(sync); 72 73=head2 sync($source, $dest, $opts) 74 75This function executes rsync to sync $source and $dest. The C<rsync> command is 76invoked with the C<--recursive --links --verbose --stats> options set. 77 78If you want to use sudo, you need to disable I<requiretty> option for this user. You can do this with the following snippet in your sudoers configuration. 79 80 Defaults:username !requiretty 81 82=over 4 83 84=item UPLOAD - Will upload all from the local directory I<html> to the remote directory I</var/www/html>. 85 86 task "sync", "server01", sub { 87 sync "html/*", "/var/www/html", { 88 exclude => "*.sw*", 89 parameters => '--backup --delete', 90 }; 91 }; 92 93 task "sync", "server01", sub { 94 sync "html/*", "/var/www/html", { 95 exclude => ["*.sw*", "*.tmp"], 96 parameters => '--backup --delete', 97 }; 98 }; 99 100=item DOWNLOAD - Will download all from the remote directory I</var/www/html> to the local directory I<html>. 101 102 task "sync", "server01", sub { 103 sync "/var/www/html/*", "html/", { 104 download => 1, 105 parameters => '--backup', 106 }; 107 }; 108 109=back 110 111=cut 112 113sub sync { 114 my ( $source, $dest, $opt ) = @_; 115 116 my $current_connection = Rex::get_current_connection(); 117 my $server = $current_connection->{server}; 118 my $cmd; 119 120 my ( $port, $servername ); 121 122 if ( defined $server->to_s ) { 123 ( $servername, $port ) = 124 Rex::Helper::IP::get_server_and_port( $server->to_s, 22 ); 125 } 126 127 my $local_connection = TRUE; 128 129 if ( defined $servername && $servername ne '<local>' ) { 130 $local_connection = FALSE; 131 } 132 133 my $auth = $current_connection->{conn}->get_auth; 134 135 if ( !exists $opt->{download} && $source !~ m/^\// ) { 136 137 # relative path, calculate from module root 138 $source = Rex::Helper::Path::get_file_path( $source, caller() ); 139 } 140 141 Rex::Logger::debug("Syncing $source -> $dest with rsync."); 142 if ($Rex::Logger::debug) { 143 $Expect::Log_Stdout = 1; 144 } 145 146 my $params = ""; 147 if ( $opt && exists $opt->{'exclude'} ) { 148 my $excludes = $opt->{'exclude'}; 149 $excludes = [$excludes] unless ref($excludes) eq "ARRAY"; 150 for my $exclude (@$excludes) { 151 $params .= " --exclude=" . $exclude; 152 } 153 } 154 155 if ( $opt && exists $opt->{parameters} ) { 156 $params .= " " . $opt->{parameters}; 157 } 158 159 my @rsync_cmd = (); 160 161 my $exec = Rex::Interface::Exec->create; 162 my $quoter = Net::OpenSSH::ShellQuoter->quoter( $exec->shell->name ); 163 164 if ( $opt && exists $opt->{'download'} && $opt->{'download'} == 1 ) { 165 $dest = resolv_path($dest); 166 Rex::Logger::debug("Downloading $source -> $dest"); 167 push @rsync_cmd, "rsync -rl --verbose --stats $params "; 168 169 if ( !$local_connection ) { 170 push @rsync_cmd, "-e '\%s'"; 171 $source = $auth->{user} . "\@$servername:$source"; 172 } 173 } 174 else { 175 $source = resolv_path($source); 176 Rex::Logger::debug("Uploading $source -> $dest"); 177 178 push @rsync_cmd, "rsync -rl --verbose --stats $params"; 179 180 if ( !$local_connection ) { 181 push @rsync_cmd, "-e '\%s'"; 182 $dest = $auth->{user} . "\@$servername:$dest"; 183 } 184 } 185 186 $source = $quoter->quote_glob($source); 187 $dest = $quoter->quote_glob($dest); 188 189 push @rsync_cmd, $source; 190 push @rsync_cmd, $dest; 191 192 if (Rex::is_sudo) { 193 push @rsync_cmd, "--rsync-path='sudo rsync'"; 194 } 195 196 $cmd = join( " ", @rsync_cmd ); 197 198 if ( !$local_connection ) { 199 my $pass = $auth->{password}; 200 my @expect_options = (); 201 202 my $auth_type = $auth->{auth_type}; 203 if ( $auth_type eq "try" ) { 204 if ( $server->get_private_key && -f $server->get_private_key ) { 205 $auth_type = "key"; 206 } 207 else { 208 $auth_type = "pass"; 209 } 210 } 211 212 if ( $auth_type eq "pass" ) { 213 $cmd = sprintf( $cmd, 214 "ssh -o StrictHostKeyChecking=no -o PubkeyAuthentication=no -p $port", 215 ); 216 push( 217 @expect_options, 218 [ 219 qr{Are you sure you want to continue connecting}, 220 sub { 221 Rex::Logger::debug("Accepting key.."); 222 my $fh = shift; 223 $fh->send("yes\n"); 224 exp_continue; 225 } 226 ], 227 [ 228 qr{password: ?$}i, 229 sub { 230 Rex::Logger::debug("Want Password"); 231 my $fh = shift; 232 $fh->send( $pass . "\n" ); 233 exp_continue; 234 } 235 ], 236 [ 237 qr{password for.*:$}i, 238 sub { 239 Rex::Logger::debug("Want Password"); 240 my $fh = shift; 241 $fh->send( $pass . "\n" ); 242 exp_continue; 243 } 244 ], 245 [ 246 qr{rsync error: error in rsync protocol}, 247 sub { 248 Rex::Logger::debug("Error in rsync"); 249 die; 250 } 251 ], 252 [ 253 qr{rsync error: remote command not found}, 254 sub { 255 Rex::Logger::info("Remote rsync command not found"); 256 Rex::Logger::info( 257 "Please install rsync, or use Rex::Commands::Sync sync_up/sync_down" 258 ); 259 die; 260 } 261 ], 262 263 ); 264 } 265 else { 266 if ( $auth_type eq "key" ) { 267 $cmd = sprintf( $cmd, 268 'ssh -i ' 269 . $server->get_private_key 270 . " -o StrictHostKeyChecking=no -p $port" ); 271 } 272 else { 273 $cmd = sprintf( $cmd, 'ssh -o StrictHostKeyChecking=no -p ' . "$port" ); 274 } 275 push( 276 @expect_options, 277 [ 278 qr{Are you sure you want to continue connecting}, 279 sub { 280 Rex::Logger::debug("Accepting key.."); 281 my $fh = shift; 282 $fh->send("yes\n"); 283 exp_continue; 284 } 285 ], 286 [ 287 qr{password: ?$}i, 288 sub { 289 Rex::Logger::debug("Want Password"); 290 my $fh = shift; 291 $fh->send( $pass . "\n" ); 292 exp_continue; 293 } 294 ], 295 [ 296 qr{Enter passphrase for key.*: $}, 297 sub { 298 Rex::Logger::debug("Want Passphrase"); 299 my $fh = shift; 300 $fh->send( $pass . "\n" ); 301 exp_continue; 302 } 303 ], 304 [ 305 qr{rsync error: error in rsync protocol}, 306 sub { 307 Rex::Logger::debug("Error in rsync"); 308 die; 309 } 310 ], 311 [ 312 qr{rsync error: remote command not found}, 313 sub { 314 Rex::Logger::info("Remote rsync command not found"); 315 Rex::Logger::info( 316 "Please install rsync, or use Rex::Commands::Sync sync_up/sync_down" 317 ); 318 die; 319 } 320 ], 321 322 ); 323 } 324 325 Rex::Logger::debug("cmd: $cmd"); 326 327 eval { 328 my $exp = Expect->spawn($cmd) or die($!); 329 330 eval { 331 $exp->expect( 332 Rex::Config->get_timeout, 333 @expect_options, 334 [ 335 qr{total size is [\d,]+\s+speedup is }, 336 sub { 337 Rex::Logger::debug("Finished transfer very fast"); 338 die; 339 } 340 341 ] 342 ); 343 344 $exp->expect( 345 undef, 346 [ 347 qr{total size is [\d,]+\s+speedup is }, 348 sub { 349 Rex::Logger::debug("Finished transfer"); 350 exp_continue; 351 } 352 ], 353 [ 354 qr{rsync error: error in rsync protocol}, 355 sub { 356 Rex::Logger::debug("Error in rsync"); 357 die; 358 } 359 ], 360 ); 361 362 }; 363 364 $exp->soft_close; 365 $? = $exp->exitstatus; 366 }; 367 } 368 else { 369 Rex::Logger::debug("Executing command: $cmd"); 370 371 i_run $cmd, fail_ok => 1; 372 373 if ( $? != 0 ) { 374 die 'Error during local rsync operation'; 375 } 376 } 377 378 if ($@) { 379 Rex::Logger::info($@); 380 } 381 382} 383 3841; 385