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