root/trunk/omnipitr/lib/OmniPITR/Program/Restore.pm

Revision 101, 8.8 kB (checked in by depesz, 4 years ago)

restore is nearly done. only removal of old files is left to be added

Line 
1 package OmniPITR::Program::Restore;
2 use strict;
3 use warnings;
4
5 use base qw( OmniPITR::Program );
6
7 use Carp;
8 use OmniPITR::Tools qw( :all );
9 use English qw( -no_match_vars );
10 use File::Basename;
11 use File::Spec;
12 use File::Path qw( make_path remove_tree );
13 use File::Copy;
14 use Storable;
15 use Getopt::Long;
16
17 =head1 run()
18
19 Main function, called by actual script in bin/, wraps all work done by script with the sole exception of reading and validating command line arguments.
20
21 These tasks (reading and validating arguments) are in this module, but they are called from L<OmniPITR::Program::new()>
22
23 Name of called method should be self explanatory, and if you need further information - simply check doc for the method you have questions about.
24
25 =cut
26
27 sub run {
28     my $self = shift;
29
30     $SIG{ 'USR1' } = sub {
31         $self->{ 'finish' } = 'immediate';
32         return;
33     };
34
35     while ( 1 ) {
36         $self->try_to_restore_and_exit();
37         sleep 1;
38         next if $self->{ 'finish' };
39         $self->check_for_trigger_file();
40         next if $self->{ 'finish' };
41         $self->do_some_removal();
42     }
43 }
44
45 sub do_some_removal {
46     my $self = shift;
47     return unless $self->{ 'remove-unneeded' };
48     if ( $self->{ 'removal-pause-trigger' } ) {
49         return if -e $self->{ 'removal-pause-trigger' };
50     }
51 }
52
53 sub try_to_restore_and_exit {
54     my $self = shift;
55
56     if ( $self->{ 'finish' } eq 'immediate' ) {
57         $self->log->error( 'Got immediate finish request. Dying.' );
58         $self->exit_with_status( 1 );
59     }
60
61     my $wanted_file = File::Spec->catfile( $self->{ 'source' }->{ 'path' }, $self->{ 'segment' } );
62     $wanted_file .= ext_for_compression( $self->{ 'source' }->{ 'compression' } ) if $self->{ 'source' }->{ 'compression' };
63
64     unless ( -e $wanted_file ) {
65         if ( $self->{ 'finish' } ) {
66             $self->log->error( 'Got finish request. Dying.' );
67             $self->exit_with_status( 1 );
68         }
69     }
70
71     if (   ( $self->{ 'recovery-delay' } )
72         && ( !$self->{ 'finish' } ) )
73     {
74         my @file_info  = stat( $wanted_file );
75         my $file_mtime = $file_info[ 9 ];
76         my $ok_since   = time - $self->{ 'recovery-delay' };
77         if ( $ok_since > $file_mtime ) {
78             if ( $self->{ 'verbose' } ) {
79                 unless ( $self->{ 'logged_delay' } ) {
80                     $self->log->log( 'Segment %s found, but it is too fresh (mtime = %u, accepted since %u)', $self->{ 'segment' }, $file_mtime, $ok_since );
81                     $self->{ 'logged_delay' } = 1;
82                 }
83             }
84             return;
85         }
86     }
87
88     my $full_destination = File::Spec->catfile( $self->{ 'data-dir' }, $self->{ 'segment_destination' } );
89
90     unless ( $self->{ 'source' }->{ 'compression' } ) {
91         if ( copy( $wanted_file, $full_destination ) ) {
92             $self->log->log( 'Segment %s restored, from non-compressed source.', $self->{ 'segment' } );
93             $self->exit_with_status( 0 );
94         }
95         $self->log->error( 'Segment %s restoration failed: %s.', $self->{ 'segment' }, $OS_ERROR );
96         $self->exit_with_status( 1 );
97     }
98
99     my $compression = $self->{ 'source' }->{ 'compression' };
100     my $command = sprintf '%s --decompress --stdout %s > %s', quotemeta( $self->{ "$compression-path" } ), quotemeta( $wanted_file ), quotemeta( $full_destination );
101
102     $self->prepare_temp_directory();
103
104     my $response = run_command( $self->{ 'temp-dir' }, 'bash', '-c', $command );
105
106     if ( $response->{ 'error_code' } ) {
107         $self->log->error( 'Error while decompressing with %s : %s', $compression, Dumper( $response ) );
108         $self->exit_with_status( 1 );
109     }
110
111     $self->log->log( 'Segment %s restored, from %s compressed source.', $self->{ 'segment' }, $compression, );
112     $self->exit_with_status( 0 );
113 }
114
115 sub check_for_trigger_file {
116     my $self = shift;
117
118     return unless $self->{ 'finish-trigger' };
119     return unless -e $self->{ 'finish-trigger' };
120
121     if ( open my $fh, '<', $self->{ 'finish-trigger' } ) {
122         local $INPUT_RECORD_SEPARATOR = undef;
123         my $content = <$fh>;
124         close $fh;
125
126         $self->{ 'finish' } = $content =~ m{\ANOW\n?\z} ? 'immediate' : 'smart';
127
128         $self->log->log( 'Finish trigger found, %s mode.', $self->{ 'finish' } );
129         return;
130     }
131     $self->log->fatal( 'Finish trigger (%s) exists, but cannot be open?! : %s', $self->{ 'finish-trigger' }, $OS_ERROR );
132 }
133
134 =head1 exit_with_status()
135
136 Exit function, doing cleanup (remove temp-dir), and exiting with given status.
137
138 =cut
139
140 sub exit_with_status {
141     my $self = shift;
142     my $code = shift;
143
144     remove_tree( $self->{ 'temp-dir' } ) if $self->{ 'temp-dir-prepared' };
145
146     exit( $code );
147 }
148
149 =head1 prepare_temp_directory()
150
151 Helper function, which builds path for temp directory, and creates it.
152
153 Path is generated by using given temp-dir and 'omnipitr-restore' name.
154
155 For example, for temp-dir '/tmp', actual, used temp directory would be /tmp/omnipitr-restore/.
156
157 =cut
158
159 sub prepare_temp_directory {
160     my $self = shift;
161     return if $self->{ 'temp-dir-prepared' };
162     my $full_temp_dir = File::Spec->catfile( $self->{ 'temp-dir' }, basename( $PROGRAM_NAME ) );
163     make_path( $full_temp_dir );
164     $self->{ 'temp-dir' }          = $full_temp_dir;
165     $self->{ 'temp-dir-prepared' } = 1;
166     return;
167 }
168
169 =head1 read_args()
170
171 Function which does all the parsing, and transformation of command line arguments.
172
173 It also verified base facts about passed WAL segment name, but all other validations, are being done in separate function: L<validate_args()>.
174
175 =cut
176
177 sub read_args {
178     my $self = shift;
179
180     my @argv_copy = @ARGV;
181
182     my %args = (
183         'bzip2-path'         => 'bzip2',
184         'data-dir'           => '.',
185         'gzip-path'          => 'gzip',
186         'lzma-path'          => 'lzma',
187         'pgcontroldata-path' => 'pg_controldata',
188         'temp-dir'           => $ENV{ 'TMPDIR' } || '/tmp',
189     );
190
191     croak( 'Error while reading command line arguments. Please check documentation in doc/omnipitr-archive.pod' )
192         unless GetOptions(
193         \%args,
194         'bzip2-path|bp=s',
195         'data-dir|D=s',
196         'finish-trigger|f=s',
197         'gzip-path|gp=s',
198         'log|l=s',
199         'lzma-path|lp=s',
200         'pgcontroldata-path|pp=s',
201         'pid-file=s',
202         'pre-removal-processing|h=s',
203         'recovery-delay|w=i',
204         'removal-pause-trigger|p=s',
205         'remove-unneeded|r=s',
206         'source|s=s',
207         'temp-dir|t=s',
208         'verbose|v',
209         );
210
211     croak( '--log was not provided - cannot continue.' ) unless $args{ 'log' };
212     $args{ 'log' } =~ tr/^/%/;
213
214     for my $key ( keys %args ) {
215         next if $key =~ m{ \A (?: source | log ) \z }x;    # Skip those, not needed in $self
216         $self->{ $key } = $args{ $key };
217     }
218
219     # We do it here so it will actually work for reporing problems in validation
220     $self->{ 'log_template' } = $args{ 'log' };
221     $self->{ 'log' }          = OmniPITR::Log->new( $self->{ 'log_template' } );
222
223     $self->log->fatal( 'Source path not provided!' ) unless $args{ 'source' };
224
225     if ( $args{ 'source' } =~ s/\A(gzip|bzip2|lzma)=// ) {
226         $self->{ 'source' }->{ 'compression' } = $1;
227     }
228     $self->{ 'source' }->{ 'path' } = $args{ 'source' };
229
230     # These could theoretically go into validation, but we need to check if we can get anything to put in segment* keys in $self
231     $self->log->fatal( 'WAL segment file name and/or destination have not been given' ) if 2 > scalar @ARGV;
232     $self->log->fatal( 'Too many arguments given.' ) if 2 < scalar @ARGV;
233
234     @{ $self }{ qw( segment segment_destination ) } = @ARGV;
235
236     $self->log->log( 'Called with parameters: %s', join( ' ', @argv_copy ) ) if $self->{ 'verbose' };
237
238     $self->{ 'finish' } = '';
239
240     return;
241 }
242
243 =head1 validate_args()
244
245 Does all necessary validation of given command line arguments.
246
247 One exception is for compression programs paths - technically, it could be validated in here, but benefit would be pretty limited, and code to do so relatively complex, as compression program path
248 might, but doesn't have to be actual file path - it might be just program name (without path), which is the default.
249
250 =cut
251
252 sub validate_args {
253     my $self = shift;
254
255     $self->log->fatal( 'Given data-dir (%s) is not valid', $self->{ 'data-dir' } ) unless -d $self->{ 'data-dir' } && -f File::Spec->catfile( $self->{ 'data-dir' }, 'PG_VERSION' );
256
257     $self->log->fatal( 'Given segment name is not valid (%s)', $self->{ 'segment' } ) unless $self->{ 'segment' } =~ m{\A[a-f0-9]{24}\z};
258
259     $self->log->fatal( 'Given source (%s) is not a directory', $self->{ 'source' }->{ 'path' } ) unless -d $self->{ 'source' }->{ 'path' };
260     $self->log->fatal( 'Given source (%s) is not readable',    $self->{ 'source' }->{ 'path' } ) unless -r $self->{ 'source' }->{ 'path' };
261     $self->log->fatal( 'Given source (%s) is not writable',    $self->{ 'source' }->{ 'path' } ) unless -w $self->{ 'source' }->{ 'path' };
262
263     return;
264 }
265
266 1;
Note: See TracBrowser for help on using the browser.