File Coverage

blib/lib/Sys/RevoBackup/Worker.pm
Criterion Covered Total %
statement 11 13 84.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 16 18 88.8


line stmt bran cond sub pod time code
1             package Sys::RevoBackup::Worker;
2             {
3             $Sys::RevoBackup::Worker::VERSION = '0.27';
4             }
5             BEGIN {
6 1     1   1816 $Sys::RevoBackup::Worker::AUTHORITY = 'cpan:TEX';
7             }
8             # ABSTRACT: a Revobackup Worker, does all the work
9              
10 1     1   26 use 5.010_000;
  1         4  
  1         40  
11 1     1   7 use mro 'c3';
  1         2  
  1         8  
12 1     1   27 use feature ':5.10';
  1         2  
  1         103  
13              
14 1     1   580 use Moose;
  0            
  0            
15             use namespace::autoclean;
16              
17             # use IO::Handle;
18             # use autodie;
19             # use MooseX::Params::Validate;
20             use English qw( -no_match_vars );
21              
22             use File::Blarf;
23             use Sys::FS;
24              
25             use Sys::RotateBackup;
26             use Sys::RevoBackup::Utils;
27              
28             extends 'Sys::Bprsync::Worker';
29              
30             sub _check_timeframe {
31             return 1;
32             }
33              
34             foreach my $key (qw(bank vault)) {
35             has $key => (
36             'is' => 'ro',
37             'isa' => 'Str',
38             'required' => 1,
39             );
40             }
41              
42             has 'rotation' => (
43             'is' => 'ro',
44             'isa' => 'Str',
45             'lazy' => 1,
46             'builder' => '_init_rotation',
47             );
48              
49             has 'dir_daily' => (
50             'is' => 'rw',
51             'isa' => 'Str',
52             'required' => 0,
53             );
54              
55             has 'dir_last_tree' => (
56             'is' => 'rw',
57             'isa' => 'Str',
58             'required' => 0,
59             );
60              
61             has 'linkdir' => (
62             'is' => 'rw',
63             'isa' => 'ArrayRef[Str]',
64             'default' => sub { [] },
65             );
66              
67             # loosen the inherited requirement
68             # the base class (bprsync) requires a destination
69             # but revobackup generates it itself
70             # based on the bank, vault and rotation
71             has '+destination' => ( 'required' => 0, );
72              
73             has 'fs' => (
74             'is' => 'rw',
75             'isa' => 'Sys::FS',
76             'lazy' => 1,
77             'builder' => '_init_fs',
78             );
79              
80             sub _init_fs {
81             my $self = shift;
82              
83             my $FS = Sys::FS::->new(
84             {
85             'logger' => $self->logger(),
86             'sys' => $self->sys(),
87             }
88             );
89              
90             return $FS;
91             }
92              
93             sub _init_job_prefix {
94             return 'Vaults';
95             }
96              
97             sub _init {
98             my $self = shift;
99              
100             return 1 if $self->_init_done();
101              
102             $self->{'hardlink'} = 1;
103             $self->{'delete'} = 1;
104             $self->{'numericids'} = 1;
105             $self->{'verbose'} = 1;
106             $self->{'description'} = $self->{'name'} unless $self->{'description'};
107              
108             # ok, now we have a config and a job name, we should be able to
109             # get everything else from the config ...
110             # scalars ...
111             my $common_config_prefix = $self->parent()->config_prefix() . q{::} . $self->_job_prefix() . q{::} . $self->name() . q{::};
112             foreach my $key (qw(description timeframe excludefrom rsh rshopts compression options bwlimit source nocrossfs sudo)) {
113             my $predicate = 'has_'.$key;
114             if ( !$self->$predicate() ) {
115             my $config_key = $common_config_prefix . $key;
116             my $val = $self->parent()->config()->get($config_key);
117             if ( defined($val) ) {
118             $self->parent()->logger()->log( message => 'Set '.$key.' ('.$config_key.') for job ' . $self->name() . ' to '.$val, level => 'debug', );
119             $self->{$key} = $val;
120             }
121             else {
122             my $msg = 'Recommended configuration key '.$key.' ('.$config_key.') not found!';
123             $self->parent()->logger()->log( message => $msg, level => 'debug', );
124             }
125             } else {
126             $self->parent()->logger()->log( message => 'Key '.$key.' ('.$common_config_prefix.$key.') was already set to '.$self->$key(), level => 'debug', );
127             }
128             }
129              
130             # arrays ...
131             foreach my $key (qw(execpre execpost exclude linkdir)) {
132             if ( !defined( $self->{$key} ) || ref( $self->{$key} ) ne 'ARRAY' || scalar( @{ $self->{$key} } ) < 1 ) {
133             my $config_key = $common_config_prefix . $key;
134             my @vals = $self->parent()->config()->get_array($config_key);
135             if (@vals) {
136             $self->parent()->logger()->log( message => 'Set '.$key.' ('.$config_key.') for job ' . $self->name() . ' to ' . join( q{:}, @vals ), level => 'debug', );
137             $self->{$key} = [@vals] if @vals;
138             }
139             }
140             }
141              
142             if ( !defined( $self->{'nocrossfs'} ) ) {
143             $self->logger()->log( message => 'Setting default value of nocrossfs to 1 because it was not previously defined.', level => 'debug', );
144             $self->{'nocrossfs'} = 1;
145             }
146              
147             $self->_init_done(1);
148              
149             return 1;
150             }
151              
152             sub _init_rotation {
153             my $self = shift;
154              
155             my $logfile = $self->fs()->filename( ( $self->bank(), $self->vault(), 'daily', '0', 'log' ) );
156              
157             # if less
158             if ( -e $logfile ) {
159             my @log = File::Blarf::slurp($logfile);
160             if ( $log[0] =~ m/^BACKUP-STARTING:\s+(\d+)$/ ) {
161             my $ts = $1;
162             my $d = time() - $ts;
163             if ( $d < ( 23 * 60 * 60 ) ) {
164             $self->logger()->log( message => 'Found timestamp ('.$ts.'), it is younger than one day ('.$d.' s old). Using 0 as rotation.', level => 'debug', );
165             return '0';
166             }
167             else {
168             $self->logger()->log( message => 'Found timestamp ('.$ts.'), but it is older than one day ('.$d.' s old). Creating new rotation.', level => 'debug', );
169             }
170             }
171             else {
172             $self->logger()->log( message => 'No timestamp found in logfile at '.$logfile.'. Creating new rotation.', level => 'debug', );
173             }
174             }
175             else {
176             $self->logger()->log( message => 'No logfile found at '.$logfile.'. Creating new rotation.', level => 'debug', );
177             }
178              
179             return 'inprogress';
180             }
181              
182             sub _prepare {
183             my $self = shift;
184              
185             # Write timestamp to logfile
186             my $logfile = $self->fs()->filename( ( $self->bank(), $self->vault(), 'daily', $self->rotation(), 'log' ) );
187             File::Blarf::blarf( $logfile, 'BACKUP-STARTING: ' . time(), { Append => 1, Flock => 1, Newline => 1, } );
188             File::Blarf::blarf( $logfile, '# Localtime: ' . localtime(), { Append => 1, Flock => 1, Newline => 1, } );
189              
190             return 1;
191             }
192              
193             sub BUILD {
194             my $self = shift;
195              
196             $self->dir_daily( $self->fs()->filename( ( $self->bank(), $self->vault(), 'daily' ) ) );
197              
198             $self->{'destination'} = $self->fs()->filename( ( $self->dir_daily(), $self->rotation(), 'tree' ) ) . q{/};
199             my $last_rotation = '0';
200             if ( $self->rotation() eq 'inprogress' ) {
201             $last_rotation = '0';
202              
203             # remove old inprogress-dir, if any
204             my $progressdir = $self->fs()->filename( ( $self->dir_daily(), $self->rotation() ) );
205             if ( -d $progressdir ) {
206             my $cmd = 'rm -rf "' . $progressdir . q{"};
207             $self->sys()->run_cmd($cmd);
208             }
209             }
210             elsif ( $self->rotation() =~ m/^\d+$/ ) {
211             $last_rotation = $self->rotation() - 1;
212             }
213             my $last_tree = $self->fs()->filename( ( $self->dir_daily(), $last_rotation, 'tree' ) );
214              
215             if ( !-d $self->destination() ) {
216             my $cmd = 'mkdir -p ' . $self->destination();
217             if ( $self->fs()->makedir( $self->destination() ) ) {
218             $self->logger()->log( message => 'Created destination ' . $self->destination(), level => 'debug', );
219             }
220             else {
221             $self->logger()->log( message => 'Could not create destination at ' . $self->destination() . ' - '.$OS_ERROR, level => 'error', );
222             }
223             }
224              
225             # we'll hardlink against last_tree if it exists
226             if ( -d $last_tree ) {
227             $self->dir_last_tree($last_tree);
228             }
229              
230             return 1;
231             }
232              
233             sub _cleanup {
234             my $self = shift;
235             my $ok = shift;
236              
237             # Logfiles
238             my $rsync_logfile = $self->logfile();
239             my $logfile = $self->fs()->filename( ( $self->bank(), $self->vault(), 'daily', $self->rotation(), 'log' ) );
240              
241             # Read amount of transfered data from rsync logfile
242             if ( -r $rsync_logfile ) {
243             # DGR: the rsync logfile is probably huge, we MUST NOT slurp it into main memory
244             ## no critic (RequireBriefOpen)
245             if ( open( my $FH, '<', $rsync_logfile ) ) {
246             while ( my $line = <$FH> ) {
247             ## no critic (ProhibitComplexRegexes)
248             if ( $line =~ m/^sent (\d+) bytes\s+received (\d+) bytes\s+([\d\.]+) bytes\/sec/i ) {
249             ## use critic
250             my ( $bytes_sent, $bytes_recv, $bytes_per_sec ) = ( $1, $2, $3 );
251             File::Blarf::blarf( $logfile, 'BYTES-SENT: ' . $bytes_sent, { Append => 1, Flock => 1, Newline => 1, } );
252             File::Blarf::blarf( $logfile, 'BYTES-RECV: ' . $bytes_recv, { Append => 1, Flock => 1, Newline => 1, } );
253             File::Blarf::blarf( $logfile, 'BYTES-PER-SEC: ' . $bytes_per_sec, { Append => 1, Flock => 1, Newline => 1, } );
254             }
255             }
256             # DGR: just reading
257             ## no critic (RequireCheckedClose)
258             close($FH);
259             ## use critic
260             }
261             ## use critic
262             }
263              
264             # Move Rsync logfile into backupdir
265             my $destfile = $self->dir_daily() . q{/} . $self->rotation() . '/rsync';
266              
267             # if we sync multiple times per day the logfile may already exist, so we append instead of overwriting
268             if ( -e $destfile . '.gz' ) {
269              
270             # uncompress old logfile
271             my $cmd = 'gzip -d -f "' . $destfile . '.gz"';
272             $self->logger()->log( message => "CMD: $cmd", level => 'debug', );
273             $self->sys()->run_cmd($cmd);
274              
275             # append new log
276             $cmd = 'cat "' . $rsync_logfile . q{" >> "} . $destfile . q{"};
277             $self->logger()->log( message => "CMD: $cmd", level => 'debug', );
278             $self->sys()->run_cmd($cmd);
279              
280             # remove temp logfile
281             $cmd = 'rm -f "' . $rsync_logfile . q{"};
282             $self->logger()->log( message => "CMD: $cmd", level => 'debug', );
283             $self->sys()->run_cmd($cmd);
284             }
285             else {
286             my $cmd = 'mv '.$rsync_logfile.q{ } . $destfile;
287             $self->logger()->log( message => "CMD: $cmd", level => 'debug', );
288             if ( !$self->sys()->run_cmd($cmd) ) {
289             return;
290             }
291             }
292              
293             # Compress rsync logfile
294             my $cmd = 'gzip -f --fast ' . $destfile;
295             $self->logger()->log( message => "CMD: $cmd", level => 'debug', );
296             $self->sys()->run_cmd($cmd);
297              
298             # Create (compressed) index file
299             $cmd = 'find ' . $self->dir_daily() . q{/} . $self->rotation() . '/tree/ -ls | gzip --fast > ' . $self->dir_daily() . q{/} . $self->rotation() . '/index.gz';
300             $self->logger()->log( message => "CMD: $cmd", level => 'debug', );
301             if ( !$self->sys()->run_cmd($cmd) ) {
302             return;
303             }
304              
305             # Write timestamp to logfile
306             my $status = q{};
307             $status .= 'RUNLOOPS:' . "\n";
308             foreach my $runloop ( sort keys %{ $self->loop_status() } ) {
309             my $rv = $self->loop_status()->{$runloop}->{'rv'};
310             my $reason = $self->loop_status()->{$runloop}->{'reason'};
311             my $sev = $self->loop_status()->{$runloop}->{'severity'};
312             my $tstart = $self->loop_status()->{$runloop}->{'time_start'};
313             my $tend = $self->loop_status()->{$runloop}->{'time_finish'};
314             $status .=
315             "\tNo. " . $runloop . ' - Return-Code: ' . $rv . ' - Explaination: ' . $reason . ' - Severity: ' . $sev . ' - Starttime: '.$tstart.' - Endtime: '.$tend."\n";
316             }
317             $status .= 'BACKUP-STATUS: ';
318             if ($ok) {
319             $status .= 'OK';
320             }
321             else {
322             $status .= 'ERROR';
323             }
324             File::Blarf::blarf( $logfile, $status . "\n" . 'BACKUP-FINISHED: ' . time(), { Append => 1, Flock => 1, Newline => 1, } );
325             File::Blarf::blarf( $logfile, '# Localtime: ' . localtime(), { Append => 1, Flock => 1, Newline => 1, } );
326              
327             # Transfer the summary logfile to the host backed up
328             $self->_upload_summary_log($logfile);
329              
330             # Rotate the backup, but only on successfull backups
331             if ( $self->rotation() eq 'inprogress' && $ok ) {
332             my $arg_ref = {
333             'logger' => $self->logger(),
334             'sys' => $self->sys(),
335             'vault' => $self->fs()->filename( ( $self->bank(), $self->vault() ) ),
336             'daily' => $self->config()->get( 'Sys::RevoBackup::Rotations::Daily', { Default => 10, } ),
337             'weekly' => $self->config()->get( 'Sys::RevoBackup::Rotations::Weekly', { Default => 4, } ),
338             'monthly' => $self->config()->get( 'Sys::RevoBackup::Rotations::Monthly', { Default => 12, } ),
339             'yearly' => $self->config()->get( 'Sys::RevoBackup::Rotations::Yearly', { Default => 10, } ),
340             };
341              
342             my $common_prefix = $self->parent()->config_prefix() . q{::} . $self->_job_prefix() . q{::} . $self->name() . q{::};
343             if ( $self->config()->get( $common_prefix . 'Rotations' ) ) {
344             $arg_ref->{'daily'} = $self->config()->get( $common_prefix . 'Rotations::Daily', { Default => 10, } );
345             $arg_ref->{'weekly'} = $self->config()->get( $common_prefix . 'Rotations::Weekly', { Default => 4, } );
346             $arg_ref->{'monthly'} = $self->config()->get( $common_prefix . 'Rotations::Monthly', { Default => 12, } );
347             $arg_ref->{'yearly'} = $self->config()->get( $common_prefix . 'Rotations::Yearly', { Default => 10, } );
348             }
349              
350             my $Rotor = Sys::RotateBackup::->new($arg_ref);
351             $Rotor->rotate();
352             } else {
353             $self->logger()->log( message => 'Not rotating a failed backup!', level => 'debug', );
354             }
355              
356             return 1;
357             }
358              
359             sub _upload_summary_log {
360             my $self = shift;
361             my $logfile = shift;
362              
363             if ( $self->source() =~ m/::/ ) {
364             $self->logger()->log( message => 'Log-Upload not supported for rsyncd. Offending source: ' . $self->source(), level => 'notice', );
365             return;
366             }
367             if ( $self->source() !~ m/:/ ) {
368             $self->logger()->log( message => 'Log-Upload not supported for local backups. Offending source: ' . $self->source(), level => 'notice', );
369             return;
370             }
371             if ( $self->source() =~ m/\@/ && $self->source() !~ m/^root\@/ && !$self->sudo() ) {
372             $self->logger()
373             ->log( message => 'Log-Upload not supported for remote backups as non-root user w/o sudo. Offending source: ' . $self->source(), level => 'notice', );
374             return;
375             }
376              
377             my $destination = $self->source();
378             if ( $destination !~ m#/$# ) {
379             $destination .= q{/};
380             }
381             $destination .= '.revobackup.log';
382             my $source = $logfile;
383              
384             my ( $rsync_cmd, $rsync_opts, $dirs ) = $self->_rsync_cmd();
385             $dirs = q{ } . $source . q{ } . $destination;
386             my $cmd = $rsync_cmd . $rsync_opts . $dirs;
387              
388             my $opts = {
389             'ReturnRV' => 0,
390             'Timeout' => 60, # 1m
391             };
392              
393             my $rv;
394             if ( $self->parent()->config()->get( $self->parent()->config_prefix() . '::Dry' ) ) {
395             $self->logger()->log( message => 'Log-Upload skipped due to dry-mode.', level => 'debug', );
396             return 1;
397             }
398             else {
399             $self->logger()->log( message => 'Log-Upload to commencing: ' . $cmd, level => 'debug', );
400             if ( $self->sys()->run_cmd( $cmd, $opts ) ) {
401             $self->logger()->log( message => 'Log-Upload successful to: ' . $dirs, level => 'debug', );
402             return 1;
403             }
404             else {
405             $self->logger()->log( message => 'Log-Upload failed to: ' . $dirs, level => 'warning', );
406             }
407             }
408             return;
409             }
410              
411             # try to find the last successfull backup
412             sub _find_last_working_backup {
413             my $self = shift;
414             my $start = shift || 0;
415              
416             foreach my $rotation ( $start .. $self->config()->get( 'Sys::RevoBackup::Rotations::Daily', { Default => 10, } ) ) {
417             my $rot_dir = $self->fs()->filename( ( $self->dir_daily(), $rotation ) );
418             # return the first OK backup
419             if(Sys::RevoBackup::Utils::_backup_status_ok($rot_dir)) {
420             return $self->fs()->filename( $rot_dir, 'tree' );
421             }
422             }
423              
424             return;
425             }
426              
427             override '_rsync_cmd' => sub {
428             my $self = shift;
429              
430             $self->_init();
431              
432             my ( $cmd, $opts, $dirs ) = super();
433              
434             # Hardlink unchanged files to the files of the last rotation
435             if ( $self->dir_last_tree() && -d $self->dir_last_tree() ) {
436             $opts .= ' --link-dest=' . $self->dir_last_tree();
437             } else {
438             my $dir = $self->dir_last_tree() || '';
439             $self->logger()->log( message => 'No last rotation tree for this job found. Can not hardlink. Dir: '.$dir, level => 'warning', );
440             }
441              
442             # If we do not have root access to the target host, we can also use
443             # sudo to run rsync on the source host as root.
444             if ( $self->sudo() ) {
445             $opts .= ' --rsync-path="/usr/bin/sudo /usr/bin/rsync"';
446             }
447              
448             # Rsync after 2.6.4 supports multiple link-dest options.
449             # All given directories are searched for matching files
450             # and hardlinked if found. This may be useful for initializing
451             # large backup vaults based on another backup tool (migration).
452             if ( $self->linkdir() ) {
453             foreach my $link_dir ( @{ $self->linkdir() } ) {
454             if ( $link_dir && -d $link_dir ) {
455             $opts .= ' --link-dest='. $link_dir;
456             } else {
457             $self->logger()->log( message => 'Given linkdir not found for this job. Can not hardlink. Dir: '.$link_dir, level => 'warning', );
458             }
459             }
460             }
461              
462             # Add the last successfull backup before daily/0, too
463             my $addn_linkdir = $self->_find_last_working_backup(1);
464             if( $addn_linkdir && -d $addn_linkdir ) {
465             $opts .= ' --link-dest=' . $addn_linkdir;
466             }
467              
468             my @cmd = ( $cmd, $opts, $dirs );
469              
470             return wantarray ? @cmd : join( q{}, @cmd );
471             };
472              
473             no Moose;
474             __PACKAGE__->meta->make_immutable;
475              
476             1;
477              
478             __END__
479              
480             =pod
481              
482             =encoding UTF-8
483              
484             =head1 NAME
485              
486             Sys::RevoBackup::Worker - a Revobackup Worker, does all the work
487              
488             =head1 METHODS
489              
490             =head2 BUILD
491              
492             Initialize the configuration.
493              
494             =head1 NAME
495              
496             Sys::RevoBackup::Worker - A RevoBackup Worker
497              
498             =head1 AUTHOR
499              
500             Dominik Schulz <dominik.schulz@gauner.org>
501              
502             =head1 COPYRIGHT AND LICENSE
503              
504             This software is copyright (c) 2012 by Dominik Schulz.
505              
506             This is free software; you can redistribute it and/or modify it under
507             the same terms as the Perl 5 programming language system itself.
508              
509             =cut