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