Imported Upstream version 3.3.0
[debian/amanda] / application-src / ampgsql.pl
1 #!@PERL@
2 # Copyright (c) 2009, 2010 Zmanda, Inc.  All Rights Reserved.
3 #
4 # This program is free software; you can redistribute it and/or modify it
5 # under the terms of the GNU General Public License version 2 as published
6 # by the Free Software Foundation.
7 #
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11 # for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
16 #
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
19
20 use lib '@amperldir@';
21 use strict;
22 use warnings;
23 use Getopt::Long;
24
25 package Amanda::Application::ampgsql;
26 use base qw(Amanda::Application);
27 use Carp;
28 use File::Copy;
29 use File::Path;
30 use IO::Dir;
31 use IO::File;
32 use IPC::Open3;
33 use POSIX;
34 use POSIX qw( ceil );
35 use Sys::Hostname;
36 use Symbol;
37 use Amanda::Constants;
38 use Amanda::Config qw( :init :getconf  config_dir_relative string_to_boolean );
39 use Amanda::Debug qw( :logging );
40 use Amanda::Paths;
41 use Amanda::Util qw( :constants :encoding );
42 use Amanda::MainLoop qw( :GIOCondition );
43
44 my $_DATA_DIR_TAR = "data_dir.tar";
45 my $_ARCHIVE_DIR_TAR = "archive_dir.tar";
46 my $_WAL_FILE_PAT = qr/\w{24}/;
47
48 my $_DATA_DIR_RESTORE = "data";
49 my $_ARCHIVE_DIR_RESTORE = "archive";
50
51 sub new {
52     my $class = shift @_;
53     my $args = shift @_;
54     my $self = $class->SUPER::new($args->{'config'});
55     $self->{'args'} = $args;
56     $self->{'label-prefix'} = 'amanda';
57     $self->{'runtar'}  = "$Amanda::Paths::amlibexecdir/runtar";
58
59     # default arguments (application properties)
60     $self->{'args'}->{'statedir'} ||= $Amanda::Paths::GNUTAR_LISTED_INCREMENTAL_DIR;
61     $self->{'args'}->{'tmpdir'} ||= $AMANDA_TMPDIR;
62     # XXX: when using runtar, this is not actually honored.
63     # So, this only works for restore at the moment
64     $self->{'args'}->{'gnutar-path'} ||= $Amanda::Constants::GNUTAR;
65
66     if (!defined $self->{'args'}->{'disk'}) {
67         $self->{'args'}->{'disk'} = $self->{'args'}->{'device'};
68     }
69     if (!defined $self->{'args'}->{'device'}) {
70         $self->{'args'}->{'device'} = $self->{'args'}->{'disk'};
71     }
72     # default properties
73     $self->{'props'} = {
74         'pg-db' => 'template1',
75         'pg-cleanupwal' => 'yes',
76         'pg-max-wal-wait' => 60,
77     };
78
79     my @PROP_NAMES = qw(pg-host pg-port pg-db pg-user pg-password pg-passfile
80                         psql-path pg-datadir pg-archivedir pg-cleanupwal
81                         pg-max-wal-wait);
82
83     # config is loaded by Amanda::Application (and Amanda::Script_App)
84     my $conf_props = getconf($CNF_PROPERTY);
85     # check for properties like 'pg-host'
86     foreach my $pname (@PROP_NAMES) {
87         if ($conf_props->{$pname}) {
88             debug("More than one value for $pname. Using the first.")
89                 if scalar(@{$conf_props->{$pname}->{'values'}}) > 1;
90             $self->{'props'}->{$pname} = $conf_props->{$pname}->{'values'}->[0];
91         }
92     }
93     # check for properties like 'foo-pg-host' where the diskname is 'foo'
94     if ($self->{'args'}->{'disk'}) {
95         foreach my $pname (@PROP_NAMES) {
96             my $tmp = "$self->{'args'}->{'disk'}-$pname";
97             if ($conf_props->{$tmp}) {
98                 debug("More than one value for $tmp. Using the first.")
99                     if scalar(@{$conf_props->{$tmp}->{'values'}}) > 1;
100                 $self->{'props'}->{$pname} = $conf_props->{$tmp}->{'values'}->[0];
101             }
102         }
103     }
104
105     unless ($self->{'props'}->{'psql-path'}) {
106         foreach my $pre (split(/:/, $ENV{PATH})) {
107             my $psql = "$pre/psql";
108             if (-x $psql) {
109                 $self->{'props'}{'psql-path'} = $psql;
110                 last;
111             }
112         }
113     }
114
115     foreach my $aname (keys %{$self->{'args'}}) {
116         if (defined($self->{'args'}->{$aname})) {
117             debug("app property: $aname $self->{'args'}->{$aname}");
118         } else {
119             debug("app property: $aname (undef)");
120         }
121     }
122
123     foreach my $pname (keys %{$self->{'props'}}) {
124         if (defined($self->{'props'}->{$pname})) {
125             debug("client property: $pname $self->{'props'}->{$pname}");
126         } else {
127             debug("client property: $pname (undef)");
128         }
129     }
130
131     return $self;
132 }
133
134 sub command_support {
135    my $self = shift;
136
137    print <<EOF;
138 CONFIG YES
139 HOST YES
140 DISK YES
141 MAX-LEVEL 9
142 INDEX-LINE YES
143 INDEX-XML NO
144 MESSAGE-LINE YES
145 MESSAGE-XML NO
146 RECORD YES
147 COLLECTION NO
148 CLIENT-ESTIMATE YES
149 MULTI-ESTIMATE NO
150 CALCSIZE NO
151 EOF
152 }
153
154 sub _check {
155     my ($desc, $succ_suf, $err_suf, $check, @check_args) = @_;
156     my $ret = $check->(@check_args);
157     my $msg = $ret? "OK $desc $succ_suf" :  "ERROR $desc $err_suf";
158     debug($msg);
159     print "$msg\n";
160     $ret;
161 }
162
163 sub _check_parent_dirs {
164     my ($dir) = @_;
165     my $ok = 1;
166     my $is_abs = substr($dir, 0, 1) eq "/";
167     _check("$dir is an absolute path?", "Yes", "No. It should start with '/'",
168        sub {$is_abs});
169
170     my @parts = split('/', $dir);
171     pop @parts; # don't test the last part
172     my $partial_path = '';
173     for my $path_part (@parts) {
174         $partial_path .= $path_part . (($partial_path || $is_abs)? '/' : '');
175         $ok &&=
176             _check("$partial_path is executable?", "Yes", "No",
177                sub {-x $_[0]}, $partial_path);
178         $ok &&=
179             _check("$partial_path is a directory?", "Yes", "No",
180                sub {-d $_[0]}, $partial_path);
181     }
182     $ok;
183 }
184
185 sub _ok_passfile_perms {
186     my $passfile = shift @_;
187     # libpq uses stat, so we use stat
188     my @fstat = stat($passfile);
189     return 0 unless @fstat;
190     return 0 if 077 & $fstat[2];
191     return -r $passfile;
192 }
193
194 sub _run_psql_command {
195     my ($self, $cmd) = @_;
196
197     # n.b. deprecated, passfile recommended for better security
198     my $orig_pgpassword = $ENV{'PGPASSWORD'};
199    $ENV{'PGPASSWORD'} = $self->{'props'}->{'pg-password'} if $self->{'props'}->{'pg-password'};
200     # n.b. supported in 8.1+
201     my $orig_pgpassfile = $ENV{'PGPASSFILE'};
202     $ENV{'PGPASSFILE'} = $self->{'props'}->{'pg-passfile'} if $self->{'props'}->{'pg-passfile'};
203
204     my @cmd = ($self->{'props'}->{'psql-path'});
205     push @cmd, "-X";
206     push @cmd, "-h", $self->{'props'}->{'pg-host'} if ($self->{'props'}->{'pg-host'});
207     push @cmd, "-p", $self->{'props'}->{'pg-port'} if ($self->{'props'}->{'pg-port'});
208     push @cmd, "-U", $self->{'props'}->{'pg-user'} if ($self->{'props'}->{'pg-user'});
209
210     push @cmd, '--quiet', '--output', '/dev/null', '--command', $cmd, $self->{'props'}->{'pg-db'};
211     debug("running " . join(" ", @cmd));
212
213     my ($wtr, $rdr);
214     my $err = Symbol::gensym;
215     my $pid = open3($wtr, $rdr, $err, @cmd);
216     close($wtr);
217
218     my $file_to_close = 2;
219     my $psql_stdout_src = Amanda::MainLoop::fd_source($rdr,
220                                                 $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
221     my $psql_stderr_src = Amanda::MainLoop::fd_source($err,
222                                                 $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
223     $psql_stdout_src->set_callback(sub {
224         my $line = <$rdr>;
225         if (!defined $line) {
226             $file_to_close--;
227             $psql_stdout_src->remove();
228             Amanda::MainLoop::quit() if $file_to_close == 0;
229             return;
230         }
231         chomp $line;
232         debug("psql stdout: $line");
233         if ($line =~ /NOTICE: pg_stop_backup complete, all required WAL segments have been archived/) {
234         } else {
235             $self->print_to_server("psql stdout: $line",
236                                    $Amanda::Script_App::GOOD);
237         }
238     });
239     $psql_stderr_src->set_callback(sub {
240         my $line = <$err>;
241         if (!defined $line) {
242             $file_to_close--;
243             $psql_stderr_src->remove();
244             Amanda::MainLoop::quit() if $file_to_close == 0;
245             return;
246         }
247         chomp $line;
248         debug("psql stderr: $line");
249         if ($line =~ /NOTICE: pg_stop_backup complete, all required WAL segments have been archived/) {
250         } else {
251             $self->print_to_server("psql stderr: $line",
252                                    $Amanda::Script_App::GOOD);
253         }
254     });
255
256     close($wtr);
257     Amanda::MainLoop::run();
258     close($rdr);
259     close($err);
260
261     waitpid $pid, 0;
262     my $status = $?;
263
264     $ENV{'PGPASSWORD'} = $orig_pgpassword || '';
265     $ENV{'PGPASSFILE'} = $orig_pgpassfile || '';
266
267     return 0 == ($status >> 8)
268 }
269
270 sub command_selfcheck {
271     my $self = shift;
272
273    # set up to handle errors correctly
274    $self->{'die_cb'} = sub {
275        my ($msg) = @_;
276        debug("$msg");
277        print "$msg\n";
278        exit(1);
279    };
280
281     for my $k (keys %{$self->{'args'}}) {
282         print "OK application property: $k = $self->{'args'}->{$k}\n";
283     }
284
285     _check("GNUTAR-PATH $self->{'args'}->{'gnutar-path'}",
286            "is executable", "is NOT executable",
287            sub {-x $_[0]}, $self->{'args'}->{'gnutar-path'});
288     _check("GNUTAR-PATH $self->{'args'}->{'gnutar-path'}",
289            "is not a directory (okay)", "is a directory (it shouldn't be)",
290            sub {!(-d $_[0])}, $self->{'args'}->{'gnutar-path'});
291     _check_parent_dirs($self->{'args'}->{'gnutar-path'});
292
293     _check("GNUTAR $Amanda::Constants::GNUTAR",
294            "is executable", "is NOT executable",
295            sub {-x $_[0]}, $Amanda::Constants::GNUTAR);
296     _check("GNUTAR $Amanda::Constants::GNUTAR",
297            "is not a directory (okay)", "is a directory (it shouldn't be)",
298            sub {!(-d $_[0])}, $Amanda::Constants::GNUTAR);
299     _check_parent_dirs($Amanda::Constants::GNUTAR);
300
301     _check("TMPDIR '$self->{'args'}->{'tmpdir'}'",
302            "is an acessible directory", "is NOT an acessible directory",
303            sub {$_[0] && -d $_[0] && -r $_[0] && -w $_[0] && -x $_[0]},
304            $self->{'args'}->{'tmpdir'});
305
306     if (exists $self->{'props'}->{'pg-datadir'}) {
307         _check("PG-DATADIR property is",
308                "same as diskdevice", "differrent than diskdevice",
309                sub { $_[0] eq $_[1] },
310                $self->{'props'}->{'pg-datadir'}, $self->{'args'}->{'device'});
311     } else {
312         $self->{'props'}->{'pg-datadir'} = $self->{'args'}->{'device'};
313     }
314
315     _check("PG-DATADIR property", "is set", "is NOT set",
316            sub { $_[0] }, $self->{'props'}->{'pg-datadir'});
317        # note that the backup user need not be able ot read this dir
318
319     _check("STATEDIR '$self->{'args'}->{'statedir'}'",
320            "is an acessible directory", "is NOT an acessible directory",
321            sub {$_[0] && -d $_[0] && -r $_[0] && -w $_[0] && -x $_[0]},
322            $self->{'args'}->{'statedir'});
323     _check_parent_dirs($self->{'args'}->{'statedir'});
324
325     if ($self->{'args'}->{'device'}) {
326         my $try_connect = 1;
327
328         for my $k (keys %{$self->{'props'}}) {
329             print "OK client property: $k = $self->{'props'}->{$k}\n";
330         }
331
332         if (_check("PG-ARCHIVEDIR property", "is set", "is NOT set",
333                sub { $_[0] }, $self->{'props'}->{'pg-archivedir'})) {
334             _check("PG-ARCHIVEDIR $self->{'props'}->{'pg-archivedir'}",
335                    "is a directory", "is NOT a directory",
336                    sub {-d $_[0]}, $self->{'props'}->{'pg-archivedir'});
337             _check("PG-ARCHIVEDIR $self->{'props'}->{'pg-archivedir'}",
338                    "is readable", "is NOT readable",
339                    sub {-r $_[0]}, $self->{'props'}->{'pg-archivedir'});
340             _check("PG-ARCHIVEDIR $self->{'props'}->{'pg-archivedir'}",
341                    "is executable", "is NOT executable",
342                    sub {-x $_[0]}, $self->{'props'}->{'pg-archivedir'});
343             _check_parent_dirs($self->{'props'}->{'pg-archivedir'});
344         }
345
346         $try_connect &&=
347             _check("Are both PG-PASSFILE and PG-PASSWORD set?",
348                    "No (okay)",
349                    "Yes. Please set only one or the other",
350                    sub {!($self->{'props'}->{'pg-passfile'} and
351                           $self->{'props'}->{'pg-password'})});
352
353         if ($self->{'props'}->{'pg-passfile'}) {
354             $try_connect &&=
355                 _check("PG-PASSFILE $self->{'props'}->{'pg-passfile'}",
356                    "has correct permissions", "does not have correct permissions",
357                    \&_ok_passfile_perms, $self->{'props'}->{'pg-passfile'});
358             $try_connect &&=
359                 _check_parent_dirs($self->{'props'}->{'pg-passfile'});
360         }
361
362         if (_check("PSQL-PATH property", "is set", "is NOT set and psql is not in \$PATH",
363                sub { $_[0] }, $self->{'props'}->{'psql-path'})) {
364             $try_connect &&=
365                 _check("PSQL-PATH $self->{'props'}->{'psql-path'}",
366                        "is executable", "is NOT executable",
367                        sub {-x $_[0]}, $self->{'props'}->{'psql-path'});
368             $try_connect &&=
369                 _check("PSQL-PATH $self->{'props'}->{'psql-path'}",
370                        "is not a directory (okay)", "is a directory (it shouldn't be)",
371                        sub {!(-d $_[0])}, $self->{'props'}->{'psql-path'});
372             $try_connect &&=
373                 _check_parent_dirs($self->{'props'}->{'psql-path'});
374         } else {
375             $try_connect = 0;
376         }
377
378         if ($try_connect) {
379             $try_connect &&=
380                 _check("Connecting to database server", "succeeded", "failed",
381                    \&_run_psql_command, $self, '');
382         }
383         
384         if ($try_connect) {
385             my $label = "$self->{'label-prefix'}-selfcheck-" . time();
386             if (_check("Call pg_start_backup", "succeeded",
387                        "failed (is another backup running?)",
388                        \&_run_psql_command, $self, "SELECT pg_start_backup('$label')")
389                 and _check("Call pg_stop_backup", "succeeded", "failed",
390                            \&_run_psql_command, $self, "SELECT pg_stop_backup()")) {
391
392                 _check("Get info from .backup file", "succeeded", "failed",
393                        sub {my ($start, $end) = _get_backup_info($self, $label); $start and $end});
394             }
395         }
396     }
397 }
398
399 sub _state_filename {
400     my ($self, $level) = @_;
401
402     my @parts = ("ampgsql", hexencode($self->{'args'}->{'host'}), hexencode($self->{'args'}->{'disk'}), $level);
403     $self->{'args'}->{'statedir'} . '/'  . join("-", @parts);
404 }
405
406 sub _write_state_file {
407     my ($self, $end_wal) = @_;
408
409     my $h = new IO::File(_state_filename($self, $self->{'args'}->{'level'}), "w");
410     $h or return undef;
411
412     debug("writing state file");
413     $h->print("VERSION: 0\n");
414     $h->print("LAST WAL FILE: $end_wal\n");
415     $h->close();
416     1;
417 }
418
419 sub _get_prev_state {
420     my $self = shift @_;
421
422     my $end_wal;
423     for (my $level = $self->{'args'}->{'level'} - 1; $level >= 0; $level--) {
424         my $fn = _state_filename($self, $level);
425         debug("reading state file: $fn");
426         my $h = new IO::File($fn, "r");
427         next unless $h;
428         while (my $l = <$h>) {
429             chomp $l;
430             debug("  $l");
431             if ($l =~ /^VERSION: (\d+)/) {
432                 unless (0 == $1) {
433                     $end_wal = undef;
434                     last;
435                 }
436             } elsif ($l =~ /^LAST WAL FILE: ($_WAL_FILE_PAT)/) {
437                 $end_wal = $1;
438             }
439         }
440         $h->close();
441         last if $end_wal;
442     }
443     $end_wal;
444 }
445
446 sub _make_dummy_dir {
447     my ($self) = @_;
448
449    my $dummydir = "$self->{'args'}->{'tmpdir'}/ampgsql-dummy-$$";
450    mkpath($dummydir);
451    open(my $fh, ">$dummydir/empty-incremental");
452    close($fh);
453
454    return $dummydir;
455 }
456
457 sub _run_tar_totals {
458     my ($self, @other_args) = @_;
459
460     my @cmd = ($self->{'runtar'}, $self->{'args'}->{'config'},
461         $Amanda::Constants::GNUTAR, '--create', '--totals', @other_args);
462     debug("running " . join(" ", @cmd));
463
464     local (*TAR_IN, *TAR_OUT, *TAR_ERR);
465     open TAR_OUT, ">&", $self->{'out_h'};
466     my $pid;
467     eval { $pid = open3(\*TAR_IN, ">&TAR_OUT", \*TAR_ERR, @cmd); 1;} or
468         $self->{'die_cb'}->("failed to run tar. error was $@");
469     close(TAR_IN);
470
471     # read stderr
472     my $size;
473     while (my $l = <TAR_ERR>) {
474         if ($l =~ /^Total bytes written: (\d+)/) {
475             $size = $1;
476         } else {
477             chomp $l;
478             $self->print_to_server($l, $Amanda::Script_App::ERROR);
479             debug("TAR_ERR: $l");
480         }
481     }
482     waitpid($pid, 0);
483     my $status = POSIX::WEXITSTATUS($?);
484
485     close(TAR_ERR);
486     debug("size of generated tar file: " . (defined($size)? $size : "undef"));
487     if ($status == 1) {
488         debug("ignored non-fatal tar exit status of 1");
489     } elsif ($status) {
490         $self->{'die_cb'}->("Tar failed (exit status $status)");
491     }
492     $size;
493 }
494
495 sub command_estimate {
496    my $self = shift;
497
498    $self->{'out_h'} = new IO::File("/dev/null", "w");
499    $self->{'out_h'} or die("Could not open /dev/null");
500    $self->{'index_h'} = new IO::File("/dev/null", "w");
501    $self->{'index_h'} or die("Could not open /dev/null");
502
503    $self->{'done_cb'} = sub {
504        my $size = shift @_;
505        debug("done. size $size");
506        $size = ceil($size/1024);
507        debug("sending $self->{'args'}->{'level'} $size 1");
508        print("$self->{'args'}->{'level'} $size 1\n");
509    };
510    $self->{'die_cb'} = sub {
511        my $msg = shift @_;
512        debug("$msg");
513        $self->{'done_cb'}->(-1);
514        die($msg);
515    };
516    $self->{'state_cb'} = sub {
517        # do nothing
518    };
519    $self->{'unlink_cb'} = sub {
520        # do nothing
521    };
522
523    if ($self->{'args'}->{'level'} > 0) {
524        _incr_backup($self);
525    } else {
526        _base_backup($self);
527    }
528 }
529
530 sub _get_backup_info {
531     my ($self, $label) = @_;
532
533    my ($fname, $bfile, $start_wal, $end_wal);
534    # wait up to 60s for the .backup file to be copied
535    for (my $count = 0; $count < 60; $count++) {
536        my $adir = new IO::Dir($self->{'props'}->{'pg-archivedir'});
537        $adir or $self->{'die_cb'}->("Could not open archive WAL directory");
538        while (defined($fname = $adir->read())) {
539            if ($fname =~ /\.backup$/) {
540                my $blabel;
541                # use runtar to read a protected file, then grep the resulting tarfile (yes,
542                # this works!)
543                local *TAROUT;
544                my $conf = $self->{'args'}->{'config'} || 'NOCONFIG';
545                my $cmd = "$self->{'runtar'} $conf $Amanda::Constants::GNUTAR --create --file - --directory $self->{'props'}->{'pg-archivedir'} $fname | $Amanda::Constants::GNUTAR --file - --extract --to-stdout";
546                debug("running: $cmd");
547                open(TAROUT, "$cmd |");
548                my ($start, $end, $lab);
549                while (my $l = <TAROUT>) {
550                    chomp($l);
551                    if ($l =~ /^START WAL LOCATION:.*?\(file ($_WAL_FILE_PAT)\)$/) {
552                        $start = $1;
553                    } elsif($l =~ /^STOP WAL LOCATION:.*?\(file ($_WAL_FILE_PAT)\)$/) {
554                        $end = $1;
555                    } elsif ($l =~ /^LABEL: (.*)$/) {
556                        $lab = $1;
557                    }
558                }
559                if ($lab and $lab eq $label) {
560                    $start_wal = $start;
561                    $end_wal = $end;
562                    $bfile = $fname;
563                    last;
564                } else {
565                    debug("logfile had non-matching label");
566                }
567            }
568        }
569        $adir->close();
570        if ($start_wal and $end_wal) {
571            debug("$bfile named WALs $start_wal .. $end_wal");
572
573            # try to cleanup a bit, although this may fail and that's ok
574            unlink("$self->{'props'}->{'pg-archivedir'}/$bfile");
575            last;
576        }
577        sleep(1);
578    }
579
580    ($start_wal, $end_wal);
581 }
582
583 # return the postgres version as an integer
584 sub _get_pg_version {
585     my $self = shift;
586
587     local *VERSOUT;
588
589     my @cmd = ($self->{'props'}->{'psql-path'});
590     push @cmd, "-X";
591     push @cmd, "--version";
592     my $pid = open3('>&STDIN', \*VERSOUT, '>&STDERR', @cmd)
593         or $self->{'die_cb'}->("could not open psql to determine version");
594     my @lines = <VERSOUT>;
595     waitpid($pid, 0);
596     $self->{'die_cb'}->("could not run psql to determine version") if (($? >> 8) != 0);
597
598     my ($maj, $min, $pat) = ($lines[0] =~ / ([0-9]+)\.([0-9]+)\.([0-9]+)$/);
599     return $maj * 10000 + $min * 100 + $pat;
600 }
601
602 # create a large table and immediately drop it; this can help to push a WAL file out
603 sub _write_garbage_to_db {
604     my $self = shift;
605
606     debug("writing garbage to database to force a WAL archive");
607
608     # note: lest ye be tempted to add "if exists" to the drop table here, note that
609     # the clause was not supported in 8.1
610     _run_psql_command($self, <<EOF) or
611 CREATE TABLE _ampgsql_garbage AS SELECT * FROM GENERATE_SERIES(1, 500000);
612 DROP TABLE _ampgsql_garbage;
613 EOF
614         $self->{'die_cb'}->("Failed to create or drop table _ampgsql_garbage");
615 }
616
617 # wait up to pg-max-wal-wait seconds for a WAL file to appear
618 sub _wait_for_wal {
619     my ($self, $wal) = @_;
620     my $pg_version = $self->_get_pg_version();
621
622     my $archive_dir = $self->{'props'}->{'pg-archivedir'};
623     my $maxwait = 0+$self->{'props'}->{'pg-max-wal-wait'};
624
625     if ($maxwait) {
626         debug("waiting $maxwait s for WAL $wal to be archived..");
627     } else {
628         debug("waiting forever for WAL $wal to be archived..");
629     }
630
631     my $count = 0; # try at least 4 cycles
632     my $stoptime = time() + $maxwait;
633     while ($maxwait == 0 || time < $stoptime || $count++ < 4) {
634         return if -f "$archive_dir/$wal";
635         
636         # for versions 8.0 or 8.1, the only way to "force" a WAL archive is to write
637         # garbage to the database.
638         if ($pg_version < 802000) {
639             $self->_write_garbage_to_db();
640         } else {
641             sleep(1);
642         }
643     }
644
645     $self->{'die_cb'}->("WAL file $wal was not archived in $maxwait seconds");
646 }
647
648 sub _base_backup {
649    my ($self) = @_;
650
651    debug("running _base_backup");
652
653    my $label = "$self->{'label-prefix'}-" . time();
654    my $tmp = "$self->{'args'}->{'tmpdir'}/$label";
655
656    -d $self->{'props'}->{'pg-archivedir'} or
657         die("WAL file archive directory does not exist (or is not a directory)");
658
659    # try to protect what we create
660    my $old_umask = umask();
661    umask(077);
662
663    my $cleanup = sub {
664        umask($old_umask);
665        eval {rmtree($tmp); 1}
666    };
667    my $old_die = $self->{'die_cb'};
668    $self->{'die_cb'} = sub {
669        my $msg = shift @_;
670        $cleanup->();
671        $old_die->($msg);
672    };
673    eval {rmtree($tmp,{'keep_root' => 1}); 1} or $self->{'die_cb'}->("Failed to clear tmp directory: $@");
674    eval {mkpath($tmp, 0, 0700); 1} or $self->{'die_cb'}->("Failed to create tmp directory: $@");
675
676    _run_psql_command($self, "SELECT pg_start_backup('$label')") or
677        $self->{'die_cb'}->("Failed to call pg_start_backup");
678
679    # tar data dir, using symlink to prefix
680    # XXX: tablespaces and their symlinks?
681    # See: http://www.postgresql.org/docs/8.0/static/manage-ag-tablespaces.html
682    my $old_die_cb = $self->{'die_cb'};
683    $self->{'die_cb'} = sub {
684        my $msg = shift @_;
685        unless(_run_psql_command($self, "SELECT pg_stop_backup()")) {
686            $msg .= " and failed to call pg_stop_backup";
687        }
688        $old_die_cb->($msg);
689    };
690    _run_tar_totals($self, '--file', "$tmp/$_DATA_DIR_TAR",
691        '--directory', $self->{'props'}->{'pg-datadir'},
692        '--exclude', 'postmaster.pid',
693        '--exclude', 'pg_xlog/*', # contains WAL files; will be handled below
694        ".");
695    $self->{'die_cb'} = $old_die_cb;
696
697    unless (_run_psql_command($self, "SELECT pg_stop_backup()")) {
698        $self->{'die_cb'}->("Failed to call pg_stop_backup");
699    }
700
701    # determine WAL files and append and create their tar file
702    my ($start_wal, $end_wal) = _get_backup_info($self, $label);
703
704    ($start_wal and $end_wal)
705        or $self->{'die_cb'}->("A .backup file was never found in the archive "
706                             . "dir $self->{'props'}->{'pg-archivedir'}");
707
708    $self->_wait_for_wal($end_wal);
709
710    # now grab all of the WAL files, *inclusive* of $start_wal
711    my @wal_files;
712    my $adir = new IO::Dir($self->{'props'}->{'pg-archivedir'});
713    while (defined(my $fname = $adir->read())) {
714        if ($fname =~ /^$_WAL_FILE_PAT$/) {
715            if (($fname ge $start_wal) and ($fname le $end_wal)) {
716                push @wal_files, $fname;
717                debug("will store: $fname");
718            } elsif ($fname lt $start_wal) {
719                $self->{'unlink_cb'}->("$self->{'props'}->{'pg-archivedir'}/$fname");
720            }
721        }
722    }
723    $adir->close();
724
725    if (@wal_files) {
726        _run_tar_totals($self, '--file', "$tmp/$_ARCHIVE_DIR_TAR",
727            '--directory', $self->{'props'}->{'pg-archivedir'}, @wal_files);
728    } else {
729        my $dummydir = $self->_make_dummy_dir();
730        $self->{'done_cb'}->(_run_tar_totals($self, '--file', '-',
731            '--directory', $dummydir, "empty-incremental"));
732        rmtree($dummydir);
733    }
734
735    # create the final tar file
736    my $size = _run_tar_totals($self, '--directory', $tmp, '--file', '-',
737        $_ARCHIVE_DIR_TAR, $_DATA_DIR_TAR);
738
739    $self->{'state_cb'}->($self, $end_wal);
740
741    $cleanup->();
742    $self->{'done_cb'}->($size);
743 }
744
745 sub _incr_backup {
746    my ($self) = @_;
747
748    debug("running _incr_backup");
749
750    my $end_wal = _get_prev_state($self);
751    if ($end_wal) {
752        debug("previously ended at: $end_wal");
753    } else {
754        debug("no previous state found!");
755        return _base_backup(@_);
756    }
757
758    my $adir = new IO::Dir($self->{'props'}->{'pg-archivedir'});
759    $adir or $self->{'die_cb'}->("Could not open archive WAL directory");
760    my $max_wal = "";
761    my ($fname, @wal_files);
762    while (defined($fname = $adir->read())) {
763        if (($fname =~ /^$_WAL_FILE_PAT$/) and ($fname gt $end_wal)) {
764            $max_wal = $fname if $fname gt $max_wal;
765            push @wal_files, $fname;
766            debug("will store: $fname");
767        }
768    }
769
770    $self->{'state_cb'}->($self, $max_wal ? $max_wal : $end_wal);
771
772    if (@wal_files) {
773        $self->{'done_cb'}->(_run_tar_totals($self, '--file', '-',
774            '--directory', $self->{'props'}->{'pg-archivedir'}, @wal_files));
775    } else {
776        my $dummydir = $self->_make_dummy_dir();
777        $self->{'done_cb'}->(_run_tar_totals($self, '--file', '-',
778            '--directory', $dummydir, "empty-incremental"));
779        rmtree($dummydir);
780    }
781 }
782
783 sub command_backup {
784    my $self = shift;
785
786    $self->{'out_h'} = IO::Handle->new_from_fd(1, 'w');
787    $self->{'out_h'} or die("Could not open data fd");
788    my $msg_fd = IO::Handle->new_from_fd(3, 'w');
789    $msg_fd or die("Could not open message fd");
790    $self->{'index_h'} = IO::Handle->new_from_fd(4, 'w');
791    $self->{'index_h'} or die("Could not open index fd");
792
793    $self->{'done_cb'} = sub {
794        my $size = shift @_;
795        debug("done. size $size");
796        $size = ceil($size/1024);
797        debug("sending size $size");
798        $msg_fd->print("sendbackup: size $size\n");
799
800        $self->{'index_h'}->print("/PostgreSQL-Database-$self->{'args'}->{'level'}\n");
801
802        $msg_fd->print("sendbackup: end\n");
803    };
804    $self->{'die_cb'} = sub {
805        my $msg = shift @_;
806        debug("$msg");
807        $msg_fd->print("! $msg\n");
808        $self->{'done_cb'}->(0);
809        exit(1);
810    };
811    $self->{'state_cb'} = sub {
812        my ($self, $end_wal) = @_;
813        _write_state_file($self, $end_wal) or $self->{'die_cb'}->("Failed to write state file");
814    };
815    my $cleanup_wal_val = $self->{'props'}->{'pg-cleanupwal'} || 'yes';
816    my $cleanup_wal = string_to_boolean($cleanup_wal_val);
817    if (!defined($cleanup_wal)) {
818        $self->{'die_cb'}->("couldn't interpret PG-CLEANUPWAL value '$cleanup_wal_val' as a boolean");
819    } elsif ($cleanup_wal) {
820        $self->{'unlink_cb'} = sub {
821            my $filename = shift @_;
822            debug("unlinking WAL file $filename");
823            unlink($filename);
824        };
825    } else {
826        $self->{'unlink_cb'} = sub {
827            # do nothing
828        };
829    }
830
831    if ($self->{'args'}->{'level'} > 0) {
832        _incr_backup($self, \*STDOUT);
833    } else {
834        _base_backup($self, \*STDOUT);
835    }
836 }
837
838 sub command_restore {
839    my $self = shift;
840
841    chdir(Amanda::Util::get_original_cwd());
842    if (defined $self->{'args'}->{directory}) {
843       if (!-d $self->{'args'}->{directory}) {
844          $self->print_to_server_and_die("Directory $self->{directory}: $!",
845                                         $Amanda::Script_App::ERROR);
846       }
847       if (!-w $self->{'args'}->{directory}) {
848          $self->print_to_server_and_die("Directory $self->{directory}: $!",
849                                         $Amanda::Script_App::ERROR);
850       }
851       chdir($self->{'args'}->{directory});
852    }
853    my $cur_dir = POSIX::getcwd();
854
855    if (!-d $_ARCHIVE_DIR_RESTORE) {
856        mkdir($_ARCHIVE_DIR_RESTORE) or die("could not create archive WAL directory: $!");
857    }
858    my $status;
859    if ($self->{'args'}->{'level'} > 0) {
860        debug("extracting incremental backup to $cur_dir/$_ARCHIVE_DIR_RESTORE");
861        $status = system($self->{'args'}->{'gnutar-path'}, '--extract',
862            '--file', '-',
863            '--ignore-zeros',
864            '--exclude', 'empty-incremental',
865            '--directory', $_ARCHIVE_DIR_RESTORE) >> 8;
866        (0 == $status) or die("Failed to extract level $self->{'args'}->{'level'} backup (exit status: $status)");
867    } else {
868        debug("extracting base of full backup");
869        if (!-d $_DATA_DIR_RESTORE) {
870            mkdir($_DATA_DIR_RESTORE) or die("could not create archive WAL directory: $!");
871        }
872        $status = system($self->{'args'}->{'gnutar-path'}, '--extract', '--file', '-',) >> 8;
873        (0 == $status) or die("Failed to extract base backup (exit status: $status)");
874
875        debug("extracting archive dir to $cur_dir/$_ARCHIVE_DIR_RESTORE");
876        $status = system($self->{'args'}->{'gnutar-path'}, '--extract',
877           '--exclude', 'empty-incremental',
878           '--file', $_ARCHIVE_DIR_TAR, '--directory', $_ARCHIVE_DIR_RESTORE) >> 8;
879        (0 == $status) or die("Failed to extract archived WAL files from base backup (exit status: $status)");
880        unlink($_ARCHIVE_DIR_TAR);
881
882        debug("extracting data dir to $cur_dir/$_DATA_DIR_RESTORE");
883        $status = system($self->{'args'}->{'gnutar-path'}, '--extract',
884           '--file', $_DATA_DIR_TAR, '--directory', $_DATA_DIR_RESTORE) >> 8;
885        (0 == $status) or die("Failed to extract data directory from base backup (exit status: $status)");
886        unlink($_DATA_DIR_TAR);
887    }
888 }
889
890 sub command_validate {
891    my $self = shift;
892
893    # set up to handle errors correctly
894    $self->{'die_cb'} = sub {
895        my ($msg) = @_;
896        debug("$msg");
897        print "$msg\n";
898        exit(1);
899    };
900
901    if (!defined($self->{'args'}->{'gnutar-path'}) ||
902        !-x $self->{'args'}->{'gnutar-path'}) {
903       return $self->default_validate();
904    }
905
906    my(@cmd) = ($self->{'args'}->{'gnutar-path'}, "--ignore-zeros", "-tf", "-");
907    debug("cmd:" . join(" ", @cmd));
908    my $pid = open3('>&STDIN', '>&STDOUT', '>&STDERR', @cmd) ||
909       $self->print_to_server_and_die("Unable to run @cmd",
910                                      $Amanda::Application::ERROR);
911    waitpid $pid, 0;
912    if ($? != 0){
913        $self->print_to_server_and_die("$self->{gnutar} returned error",
914                                       $Amanda::Application::ERROR);
915    }
916    exit($self->{error_status});
917 }
918
919 package main;
920
921 sub usage {
922     print <<EOF;
923 Usage: ampgsql <command> --config=<config> --host=<host> --disk=<disk> --device=<device> --level=<level> --index=<yes|no> --message=<text> --collection=<no> --record=<yes|no> --calcsize.
924 EOF
925     exit(1);
926 }
927
928 my $opts = {};
929
930 GetOptions(
931     $opts,
932     'config=s',
933     'host=s',
934     'disk=s',
935     'device=s',
936     'level=s',
937     'index=s',
938     'message=s',
939     'collection=s',
940     'record',
941     'calcsize',
942     'exclude-list=s@',
943     'include-list=s@',
944     'directory=s',
945     # ampgsql-specific
946     'statedir=s',
947     'tmpdir=s',
948     'gnutar-path=s',
949 ) or usage();
950
951 my $application = Amanda::Application::ampgsql->new($opts);
952
953 $application->do($ARGV[0]);