root/branches/omnipitr-with-slave-backups-calling-master/lib/OmniPITR/Program/Backup/Slave.pm

Revision 233, 22.9 kB (checked in by depesz, 3 years ago)

alpha version

Line 
1 package OmniPITR::Program::Backup::Slave;
2 use strict;
3 use warnings;
4
5 use base qw( OmniPITR::Program::Backup );
6
7 use File::Spec;
8 use File::Basename;
9 use English qw( -no_match_vars );
10 use File::Copy;
11 use File::Path;
12 use Getopt::Long;
13 use Carp;
14 use POSIX qw( strftime );
15 use Sys::Hostname;
16 use OmniPITR::Tools qw( run_command ext_for_compression );
17
18 =head1 make_data_archive()
19
20 Wraps all work necessary to make local .tar files (optionally compressed)
21 with content of PGDATA
22
23 =cut
24
25 sub make_data_archive {
26     my $self = shift;
27     $self->pause_xlog_removal();
28     $self->make_backup_label_temp_file();
29     $self->compress_pgdata();
30     $self->finish_pgdata_backup();
31     return;
32 }
33
34 =head1 finish_pgdata_backup()
35
36 Calls pg_stop_backup on master (if necessary), and waits for xlogs to be
37 ready
38
39 =cut
40
41 sub finish_pgdata_backup {
42     my $self = shift;
43     return unless $self->{ 'call-master' };
44
45     my $stop_backup_output = $self->psql( 'SELECT pg_stop_backup()' );
46
47     $stop_backup_output =~ s/\s*\z//;
48     $self->log->log( q{pg_start_backup() returned %s.}, $stop_backup_output );
49     $self->log->fatal( 'Output from pg_stop_backup is not parseable?!' ) unless $stop_backup_output =~ m{\A([0-9A-F]+)/([0-9A-F]{1,8})\z};
50
51     my ( $part_1, $part_2 ) = ( $1, $2 );
52     $part_2 =~ s/(.{1,6})\z//;
53     my $part_3 = $1;
54
55     my $expected_filename_suffix = sprintf '%08s%08s.%08s.backup', $part_1, $part_2, $part_3;
56
57     if ( 'none' ne $self->{ 'source' }->{ 'compression' } ) {
58         my $extension = ext_for_compression( $self->{ 'source' }->{ 'compression' } );
59         $expected_filename_suffix .= $extension;
60     }
61
62     my $backup_filename_re = qr{\A[0-9A-F]{8}\Q$expected_filename_suffix\E\z};
63
64     $self->{ 'stop_backup_filename_re' } = $backup_filename_re;
65
66     return;
67
68 }
69
70 =head1 make_xlog_archive()
71
72 Wraps all work necessary to make local .tar files (optionally compressed)
73 with xlogs required to start PostgreSQL from backup.
74
75 =cut
76
77 sub make_xlog_archive {
78     my $self = shift;
79     $self->wait_for_xlog_archive_to_be_ready();
80     $self->compress_xlogs();
81     $self->unpause_xlog_removal();
82     return;
83 }
84
85 =head1 wait_for_xlog_archive_to_be_ready()
86
87 Waits till all necessary xlogs will be in archive, or (in case --call-master
88 was not given) - for checkpoint on slave.
89
90 =cut
91
92 sub wait_for_xlog_archive_to_be_ready {
93     my $self = shift;
94     return $self->wait_for_checkpoint_location_change() unless $self->{ 'call-master' };
95     $self->wait_for_file( $self->{ 'source' }->{ 'path' }, $self->{ 'stop_backup_filename_re' } );
96     return;
97 }
98
99 =head1 compress_xlogs()
100
101 Wrapper function which encapsulates all work required to compress xlog
102 segments that accumulated during backup of data directory.
103
104 =cut
105
106 sub compress_xlogs {
107     my $self = shift;
108
109     $self->make_dot_backup_file();
110     $self->uncompress_wal_archive_segments();
111
112     $self->log->time_start( 'Compressing xlogs' ) if $self->verbose;
113     $self->start_writers( 'xlog' );
114
115     my $source_transform_from = basename( $self->{ 'source' }->{ 'path' } );
116     $source_transform_from =~ s{^/*}{};
117     $source_transform_from =~ s{/*$}{};
118
119     my $dot_backup_transform_from = $self->{ 'temp-dir' };
120     $dot_backup_transform_from =~ s{^/*}{};
121     $dot_backup_transform_from =~ s{/*$}{};
122
123     my $transform_to = basename( $self->{ 'data-dir' } ) . '/pg_xlog';
124     my $transform_command = sprintf 's#^\(%s\|%s\)#%s#', $source_transform_from, $dot_backup_transform_from, $transform_to;
125
126     my @stuff_to_compress = ( basename( $self->{ 'source' }->{ 'path' } ) );
127     push @stuff_to_compress, File::Spec->catfile( $self->{ 'temp-dir' }, $self->{ 'dot_backup_filename' } ) if $self->{ 'dot_backup_filename' };
128
129     $self->tar_and_compress(
130         'work_dir'  => dirname( $self->{ 'source' }->{ 'path' } ),
131         'tar_dir'   => \@stuff_to_compress,
132         'transform' => $transform_command,
133     );
134
135     $self->log->time_finish( 'Compressing xlogs' ) if $self->verbose;
136
137     return;
138 }
139
140 =head1 uncompress_wal_archive_segments()
141
142 In case walarchive (--source option) is compressed, L<omnipitr-backup-slave>
143 needs to uncompress files to temp directory before making archive - so that
144 the archive will be easier to use.
145
146 This work is being done in this function.
147
148 =cut
149
150 sub uncompress_wal_archive_segments {
151     my $self = shift;
152     return if 'none' eq $self->{ 'source' }->{ 'compression' };
153
154     my $old_source = $self->{ 'source' }->{ 'path' };
155     my $new_source = File::Spec->catfile( $self->{ 'temp-dir' }, 'uncompresses_pg_xlogs' );
156     $self->{ 'source' }->{ 'path' } = $new_source;
157
158     mkpath( [ $new_source ], 0, oct( "755" ) );
159
160     opendir my $dir, $old_source or $self->log->fatal( 'Cannot open wal-archive (%s) : %s', $old_source, $OS_ERROR );
161     my $extension = ext_for_compression( $self->{ 'source' }->{ 'compression' } );
162     my @wal_segments = sort grep { -f File::Spec->catfile( $old_source, $_ ) && /\Q$extension\E\z/ } readdir( $dir );
163     close $dir;
164
165     $self->log->log( '%s wal segments have to be uncompressed', scalar @wal_segments );
166
167     for my $segment ( @wal_segments ) {
168         my $old_file = File::Spec->catfile( $old_source, $segment );
169         my $new_file = File::Spec->catfile( $new_source, $segment );
170         copy( $old_file, $new_file ) or $self->log->fatal( 'Cannot copy %s to %s: %s', $old_file, $new_file, $OS_ERROR );
171         $self->log->log( 'File copied: %s -> %s', $old_file, $new_file );
172         my @uncompress = ( $self->{ $self->{ 'source' }->{ 'compression' } . '-path' }, '-d', $new_file );
173         unshift @uncompress, $self->{ 'nice-path' } unless $self->{ 'not-nice' };
174         my $response = run_command( $self->{ 'temp-dir' }, @uncompress );
175         if ( $response->{ 'error_code' } ) {
176             $self->log->fatal( 'Error while uncompressing wal segment %s: %s', $new_file, $response );
177         }
178     }
179     return;
180 }
181
182 =head make_dot_backup_file()
183
184 Make I<SEGMENT>.I<OFFSET>.backup file that will be included in xlog archive.
185
186 This file contains vital information like start and end position of WAL
187 reply that is required to get consistent state.
188
189 =cut
190
191 sub make_dot_backup_file {
192     my $self = shift;
193
194     return if $self->{ 'call-master' };
195
196     my $redo_location = $self->{ 'CONTROL' }->{ 'initial' }->{ "Latest checkpoint's REDO location" };
197     my $timeline      = $self->{ 'CONTROL' }->{ 'initial' }->{ "Latest checkpoint's TimeLineID" };
198
199     my $final_location = $self->{ 'CONTROL' }->{ 'final' }->{ "Latest checkpoint location" };
200     if (   ( defined $self->{ 'CONTROL' }->{ 'final' }->{ 'Minimum recovery ending location' } )
201         && ( $self->{ 'CONTROL' }->{ 'final' }->{ 'Minimum recovery ending location' } =~ m{\A[a-f0-9]+/[a-f0-9]+\z}i )
202         && ( '0/0' ne $self->{ 'CONTROL' }->{ 'final' }->{ 'Minimum recovery ending location' } ) )
203     {
204         $final_location = $self->{ 'CONTROL' }->{ 'final' }->{ 'Minimum recovery ending location' };
205     }
206     my $final_wal_filename = $self->convert_wal_location_and_timeline_to_filename( $final_location, $timeline );
207
208     my $final_wal_filename_re = qr{\A$final_wal_filename};
209     $self->wait_for_file( $self->{ 'source' }->{ 'path' }, $final_wal_filename_re );
210
211     my $offset = $redo_location;
212     $offset =~ s#.*/##;
213     $offset =~ s/^.*?(.{0,6})$/$1/;
214
215     my $output_filename = sprintf '%s.%08s.backup', $self->convert_wal_location_and_timeline_to_filename( $redo_location, $timeline ), $offset;
216
217     my @content_lines = @{ $self->{ 'backup_file_data' } };
218     splice( @content_lines, 1, 0, sprintf 'STOP WAL LOCATION: %s (file %s)', $final_location, $final_wal_filename );
219     splice( @content_lines, 4, 0, sprintf 'STOP TIME: %s', strftime( '%Y-%m-%d %H:%M:%S %Z', localtime time ) );
220
221     my $content = join( "\n", @content_lines ) . "\n";
222
223     my $filename = File::Spec->catfile( $self->{ 'temp-dir' }, $output_filename );
224     if ( open my $fh, '>', $filename ) {
225         print $fh $content;
226         close $fh;
227         $self->{ 'dot_backup_filename' } = $output_filename;
228         return;
229     }
230     $self->log->fatal( 'Cannot write .backup file file %s : %s', $output_filename, $OS_ERROR );
231 }
232
233 =head1 wait_for_checkpoint_location_change()
234
235 Just like the name suggests - this function periodically (every 5 seconds,
236 hardcoded, as there is not much sense in parametrizing it) checks
237 pg_controldata of PGDATA, and finishes if value in B<Latest checkpoint
238 location> will change.
239
240 =cut
241
242 sub wait_for_checkpoint_location_change {
243     my $self     = shift;
244     my $pre_wait = $self->get_control_data()->{ 'Latest checkpoint location' };
245     $self->log->log( 'Waiting for checkpoint' ) if $self->verbose;
246     while ( 1 ) {
247         sleep 5;
248         $self->{ 'CONTROL' }->{ 'final' } = $self->get_control_data();
249         last if $self->{ 'CONTROL' }->{ 'final' }->{ 'Latest checkpoint location' } ne $pre_wait;
250     }
251     $self->log->log( 'Checkpoint .' ) if $self->verbose;
252     return;
253 }
254
255 =head1 make_backup_label_temp_file()
256
257 Normal hot backup contains file named 'backup_label' in PGDATA archive.
258
259 Since this is not normal hot backup - PostgreSQL will not create this file,
260 and it has to be created separately by I<omnipitr-backup-slave>.
261
262 This file is created in temp directory (it is B<not> created in PGDATA), and
263 is included in tar in such a way that, on uncompressing, it will get to
264 unarchived PGDATA.
265
266 If --call-master was given, it will run pg_start_backup() on master, and
267 retrieve generated backup_label file.
268
269 =cut
270
271 sub make_backup_label_temp_file {
272     my $self = shift;
273
274     $self->{ 'CONTROL' }->{ 'initial' } = $self->get_control_data();
275
276     if ( $self->{ 'call-master' } ) {
277         $self->get_backup_label_from_master();
278     }
279     else {
280         my $redo_location = $self->{ 'CONTROL' }->{ 'initial' }->{ "Latest checkpoint's REDO location" };
281         my $last_location = $self->{ 'CONTROL' }->{ 'initial' }->{ "Latest checkpoint location" };
282         my $timeline      = $self->{ 'CONTROL' }->{ 'initial' }->{ "Latest checkpoint's TimeLineID" };
283
284         my @content_lines = ();
285         push @content_lines, sprintf 'START WAL LOCATION: %s (file %s)', $redo_location, $self->convert_wal_location_and_timeline_to_filename( $redo_location, $timeline );
286         push @content_lines, sprintf 'CHECKPOINT LOCATION: %s', $last_location;
287         push @content_lines, sprintf 'START TIME: %s', strftime( '%Y-%m-%d %H:%M:%S %Z', localtime time );
288         push @content_lines, 'LABEL: OmniPITR_Slave_Hot_Backup';
289
290         $self->{ 'backup_file_data' } = \@content_lines;
291     }
292     my $content = join( "\n", @{ $self->{ 'backup_file_data' } } ) . "\n";
293
294     my $filename = File::Spec->catfile( $self->{ 'temp-dir' }, 'backup_label' );
295     if ( open my $fh, '>', $filename ) {
296         print $fh $content;
297         close $fh;
298         return;
299     }
300     $self->log->fatal( 'Cannot write backup_label file %s : %s', $filename, $OS_ERROR );
301 }
302
303 =head1 get_backup_label_from_master()
304
305 Wraps logic required to call pg_start_backup(), get response, and
306 backup_label file content .
307
308 =cut
309
310 sub get_backup_label_from_master {
311     my $self = shift;
312
313     my $start_backup_output = $self->psql( "SELECT pg_start_backup('omnipitr_slave_backup_with_master_callback')" );
314
315     $start_backup_output =~ s/\s*\z//;
316     $self->log->log( q{pg_start_backup('omnipitr') returned %s.}, $start_backup_output );
317     $self->log->fatal( 'Output from pg_start_backup is not parseable?!' ) unless $start_backup_output =~ m{\A([0-9A-F]+)/([0-9A-F]{1,8})\z};
318
319     my $backup_label_content = $self->psql(
320         "select pg_read_file( 'backup_label', 0, ( pg_stat_file( 'backup_label' ) ).size )",
321     );
322
323     $self->{ 'backup_file_data' } = [ split( /\n/, $backup_label_content ) ];
324
325     $self->wait_for_checkpoint_from_backup_label();
326
327     return;
328 }
329
330 =head1 wait_for_checkpoint_from_backup_label()
331
332 Waits till slave will do checkpoint in at least the same location as master
333 did when pg_start_backup() was called.
334
335 =cut
336
337 sub wait_for_checkpoint_from_backup_label {
338     my $self = shift;
339
340     my @checkpoint_lines = grep { m{\ACHECKPOINT\s+LOCATION:\s+[a-f0-9]+/[0-9a-f]{8}\s*\z} } @{ $self->{ 'backup_file_data' } };
341
342     $self->log->fatal( 'Cannot get checkpoint lines from: %s', $self->{ 'backup_file_data' } ) if 1 != scalar @checkpoint_lines;
343
344     my ( $major, $minor ) = $checkpoint_lines[ 0 ] =~ m{ \s+ ( [a-f0-9]+ ) / ( [a-f0-9]{8} ) \s* \z }xms;
345     $major = hex $major;
346     $minor = hex $minor;
347
348     $self->log->log( 'Waiting for checkpoint (based on backup_label from master)' ) if $self->verbose;
349     while ( 1 ) {
350         my $temp = $self->get_control_data();
351
352         my ( $c_major, $c_minor ) = $temp->{ 'Latest checkpoint location' } =~ m{ \s+ ( [a-f0-9]+ ) / ( [a-f0-9]{8} ) \s* \z }xms;
353         $c_major = hex $c_major;
354         $c_minor = hex $c_minor;
355
356         last if $c_major > $major;
357         last if ( $c_major == $major ) && ( $c_minor >= $minor );
358
359         sleep 5;
360     }
361     $self->log->log( 'Checkpoint .' ) if $self->verbose;
362     return;
363 }
364
365 =head1 convert_wal_location_and_timeline_to_filename()
366
367 Helper function which converts WAL location and timeline number into
368 filename that given location will be in.
369
370 =cut
371
372 sub convert_wal_location_and_timeline_to_filename {
373     my $self = shift;
374     my ( $location, $timeline ) = @_;
375
376     my ( $series, $offset ) = split m{/}, $location;
377
378     $offset =~ s/.{0,6}$//;
379
380     my $location_filename = sprintf '%08s%08s%08s', $timeline, $series, $offset;
381
382     return $location_filename;
383 }
384
385 =head1 compress_pgdata()
386
387 Wrapper function which encapsulates all work required to compress data
388 directory.
389
390 =cut
391
392 sub compress_pgdata {
393     my $self = shift;
394
395     $self->log->time_start( 'Compressing $PGDATA' ) if $self->verbose;
396     $self->start_writers( 'data' );
397
398     my $transform_from = $self->{ 'temp-dir' };
399     $transform_from =~ s{^/*}{};
400     $transform_from =~ s{/*$}{};
401     my $transform_to = basename( $self->{ 'data-dir' } );
402     my $transform_command = sprintf 's#^%s/#%s/#', $transform_from, $transform_to;
403
404     my @excludes = qw( pg_log/* pg_xlog/0* pg_xlog/archive_status/* recovery.conf postmaster.pid );
405     for my $dir ( qw( pg_log pg_xlog ) ) {
406         push @excludes, $dir if -l File::Spec->catfile( $self->{ 'data-dir' }, $dir );
407     }
408
409     my ( $tablespaces, $transforms ) = $self->get_tablespaces_and_transforms();
410     push @{ $tablespaces }, basename( $self->{ 'data-dir' } ), File::Spec->catfile( $self->{ 'temp-dir' }, 'backup_label' );
411     push @{ $transforms }, $transform_command;
412
413     $self->tar_and_compress(
414         'work_dir'  => dirname( $self->{ 'data-dir' } ),
415         'tar_dir'   => $tablespaces,
416         'excludes'  => [ map { sprintf( '%s/%s', basename( $self->{ 'data-dir' } ), $_ ) } @excludes ],
417         'transform' => $transforms,
418     );
419
420     $self->log->time_finish( 'Compressing $PGDATA' ) if $self->verbose;
421     return;
422 }
423
424 =head1 pause_xlog_removal()
425
426 Creates trigger file that will pause removal of old segments by
427 I<omnipitr-restore>.
428
429 =cut
430
431 sub pause_xlog_removal {
432     my $self = shift;
433
434     if ( open my $fh, '>', $self->{ 'removal-pause-trigger' } ) {
435         print $fh $PROCESS_ID, "\n";
436         close $fh;
437         $self->{ 'removal-pause-trigger-created' } = 1;
438         return;
439     }
440     $self->log->fatal(
441         'Cannot create/write to removal pause trigger (%s) : %S',
442         $self->{ 'removal-pause-trigger' },
443         $OS_ERROR
444     );
445 }
446
447 =head1 unpause_xlog_removal()
448
449 Removed trigger file, effectively unpausing removal of old, obsolete log
450 segments in I<omnipitr-restore>.
451
452 =cut
453
454 sub unpause_xlog_removal {
455     my $self = shift;
456     unlink( $self->{ 'removal-pause-trigger' } );
457     delete $self->{ 'removal-pause-trigger-created' };
458     return;
459 }
460
461 =head1 DESTROY()
462
463 Destructor for object - removes created pause trigger;
464
465 =cut
466
467 sub DESTROY {
468     my $self = shift;
469     unlink( $self->{ 'removal-pause-trigger' } ) if $self->{ 'removal-pause-trigger-created' };
470     $self->SUPER::DESTROY();
471     return;
472 }
473
474 =head1 read_args()
475
476 Function which does all the parsing, and transformation of command line
477 arguments.
478
479 =cut
480
481 sub read_args {
482     my $self = shift;
483
484     my @argv_copy = @ARGV;
485
486     my %args = (
487         'temp-dir' => $ENV{ 'TMPDIR' } || '/tmp',
488         'gzip-path'          => 'gzip',
489         'bzip2-path'         => 'bzip2',
490         'lzma-path'          => 'lzma',
491         'tar-path'           => 'tar',
492         'nice-path'          => 'nice',
493         'psql-path'          => 'psql',
494         'rsync-path'         => 'rsync',
495         'pgcontroldata-path' => 'pg_controldata',
496         'filename-template'  => '__HOSTNAME__-__FILETYPE__-^Y-^m-^d.tar__CEXT__',
497     );
498
499     croak( 'Error while reading command line arguments. Please check documentation in doc/omnipitr-backup-slave.pod' )
500         unless GetOptions(
501         \%args,
502         'database|d=s',
503         'host|h=s',
504         'port|P=i',
505         'username|U=s',
506         'data-dir|D=s',
507         'source|s=s',
508         'dst-local|dl=s@',
509         'dst-remote|dr=s@',
510         'temp-dir|t=s',
511         'log|l=s',
512         'filename-template|f=s',
513         'removal-pause-trigger|p=s',
514         'pid-file=s',
515         'verbose|v',
516         'gzip-path|gp=s',
517         'bzip2-path|bp=s',
518         'lzma-path|lp=s',
519         'nice-path|np=s',
520         'psql-path|pp=s',
521         'tar-path|tp=s',
522         'rsync-path|rp=s',
523         'pgcontroldata-path|pp=s',
524         'not-nice|nn',
525         'call-master|cm',
526         );
527
528     croak( '--log was not provided - cannot continue.' ) unless $args{ 'log' };
529     for my $key ( qw( log filename-template ) ) {
530         $args{ $key } =~ tr/^/%/;
531     }
532
533     for my $key ( grep { !/^dst-(?:local|remote)$/ } keys %args ) {
534         $self->{ $key } = $args{ $key };
535     }
536
537     for my $type ( qw( local remote ) ) {
538         my $D = [];
539         $self->{ 'destination' }->{ $type } = $D;
540
541         next unless defined $args{ 'dst-' . $type };
542
543         my %temp_for_uniq = ();
544         my @items = grep { !$temp_for_uniq{ $_ }++ } @{ $args{ 'dst-' . $type } };
545
546         for my $item ( @items ) {
547             my $current = { 'compression' => 'none', };
548             if ( $item =~ s/\A(gzip|bzip2|lzma)=// ) {
549                 $current->{ 'compression' } = $1;
550             }
551             $current->{ 'path' } = $item;
552             push @{ $D }, $current;
553         }
554     }
555
556     if ( defined $args{ 'source' } && $args{ 'source' } =~ s/\A(gzip|bzip2|lzma)=// ) {
557         $self->{ 'source' } = {
558             'compression' => $1,
559             'path'        => $args{ 'source' },
560         };
561     }
562     else {
563         $self->{ 'source' } = {
564             'compression' => 'none',
565             'path'        => $args{ 'source' },
566         };
567     }
568
569     $self->{ 'filename-template' } = strftime( $self->{ 'filename-template' }, localtime time() );
570     $self->{ 'filename-template' } =~ s/__HOSTNAME__/hostname()/ge;
571
572     # We do it here so it will actually work for reporing problems in validation
573     $self->{ 'log_template' } = $args{ 'log' };
574     $self->{ 'log' }          = OmniPITR::Log->new( $self->{ 'log_template' } );
575
576     my @psql = ();
577     push @psql, $self->{ 'psql-path' };
578     push @psql, '-qAtX';
579     push @psql, ( '-U', $self->{ 'username' } ) if $self->{ 'username' };
580     push @psql, ( '-d', $self->{ 'database' } ) if $self->{ 'database' };
581     push @psql, ( '-h', $self->{ 'host' } )     if $self->{ 'host' };
582     push @psql, ( '-p', $self->{ 'port' } )     if $self->{ 'port' };
583     push @psql, '-c';
584     $self->{ 'psql' } = \@psql;
585
586     $self->log->log( 'Called with parameters: %s', join( ' ', @argv_copy ) ) if $self->verbose;
587
588     return;
589 }
590
591 =head1 validate_args()
592
593 Does all necessary validation of given command line arguments.
594
595 One exception is for compression programs paths - technically, it could be
596 validated in here, but benefit would be pretty limited, and code to do so
597 relatively complex, as compression program path might, but doesn't have to
598 be actual file path - it might be just program name (without path), which is
599 the default.
600
601 =cut
602
603 sub validate_args {
604     my $self = shift;
605
606     $self->log->fatal( 'Data-dir was not provided!' ) unless defined $self->{ 'data-dir' };
607     $self->log->fatal( 'Provided data-dir (%s) does not exist!',   $self->{ 'data-dir' } ) unless -e $self->{ 'data-dir' };
608     $self->log->fatal( 'Provided data-dir (%s) is not directory!', $self->{ 'data-dir' } ) unless -d $self->{ 'data-dir' };
609     $self->log->fatal( 'Provided data-dir (%s) is not readable!',  $self->{ 'data-dir' } ) unless -r $self->{ 'data-dir' };
610
611     my $dst_count = scalar( @{ $self->{ 'destination' }->{ 'local' } } ) + scalar( @{ $self->{ 'destination' }->{ 'remote' } } );
612     $self->log->fatal( "No --dst-* has been provided!" ) if 0 == $dst_count;
613
614     $self->log->fatal( "Filename template does not contain __FILETYPE__ placeholder!" ) unless $self->{ 'filename-template' } =~ /__FILETYPE__/;
615     $self->log->fatal( "Filename template cannot contain / or \\ characters!" ) if $self->{ 'filename-template' } =~ m{[/\\]};
616
617     $self->log->fatal( 'Source of WAL files was not provided!' ) unless defined $self->{ 'source' }->{ 'path' };
618     $self->log->fatal( 'Provided source of wal files (%s) does not exist!',   $self->{ 'source' }->{ 'path' } ) unless -e $self->{ 'source' }->{ 'path' };
619     $self->log->fatal( 'Provided source of wal files (%s) is not directory!', $self->{ 'source' }->{ 'path' } ) unless -d $self->{ 'source' }->{ 'path' };
620     $self->log->fatal( 'Provided source of wal files (%s) is not readable!',  $self->{ 'source' }->{ 'path' } ) unless -r $self->{ 'source' }->{ 'path' };
621
622     $self->log->fatal( 'Temp-dir was not provided!' ) unless defined $self->{ 'temp-dir' };
623     $self->log->fatal( 'Provided temp-dir (%s) does not exist!',   $self->{ 'temp-dir' } ) unless -e $self->{ 'temp-dir' };
624     $self->log->fatal( 'Provided temp-dir (%s) is not directory!', $self->{ 'temp-dir' } ) unless -d $self->{ 'temp-dir' };
625     $self->log->fatal( 'Provided temp-dir (%s) is not writable!',  $self->{ 'temp-dir' } ) unless -w $self->{ 'temp-dir' };
626     $self->log->fatal( 'Provided temp-dir (%s) contains # character!', $self->{ 'temp-dir' } ) if $self->{ 'temp-dir' } =~ /#/;
627
628     $self->log->fatal( 'Removal pause trigger name was not provided!' ) unless defined $self->{ 'removal-pause-trigger' };
629     $self->log->fatal( 'Provided removal pause trigger file (%s) already exists!', $self->{ 'removal-pause-trigger' } ) if -e $self->{ 'removal-pause-trigger' };
630
631     $self->log->fatal( 'Directory for provided removal pause trigger (%s) does not exist!',   $self->{ 'removal-pause-trigger' } ) unless -e dirname( $self->{ 'removal-pause-trigger' } );
632     $self->log->fatal( 'Directory for provided removal pause trigger (%s) is not directory!', $self->{ 'removal-pause-trigger' } ) unless -d dirname( $self->{ 'removal-pause-trigger' } );
633     $self->log->fatal( 'Directory for provided removal pause trigger (%s) is not writable!',  $self->{ 'removal-pause-trigger' } ) unless -w dirname( $self->{ 'removal-pause-trigger' } );
634
635     return unless $self->{ 'destination' }->{ 'local' };
636
637     for my $d ( @{ $self->{ 'destination' }->{ 'local' } } ) {
638         my $dir = $d->{ 'path' };
639         $self->log->fatal( 'Choosen local destination dir (%s) does not exist. Cannot continue.',   $dir ) unless -e $dir;
640         $self->log->fatal( 'Choosen local destination dir (%s) is not directory. Cannot continue.', $dir ) unless -d $dir;
641         $self->log->fatal( 'Choosen local destination dir (%s) is not writable. Cannot continue.',  $dir ) unless -w $dir;
642     }
643
644     return;
645 }
646
647 1;
Note: See TracBrowser for help on using the browser.