2 # Copyright (c) 2009-2012 Zmanda, Inc. All Rights Reserved.
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
19 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
21 use lib '@amperldir@';
26 package Amanda::Application::ampgsql;
27 use base qw(Amanda::Application);
38 use Amanda::Constants;
39 use Amanda::Config qw( :init :getconf config_dir_relative string_to_boolean );
40 use Amanda::Debug qw( :logging );
42 use Amanda::Util qw( :constants :encoding quote_string );
43 use Amanda::MainLoop qw( :GIOCondition );
45 my $_DATA_DIR_TAR = "data_dir.tar";
46 my $_ARCHIVE_DIR_TAR = "archive_dir.tar";
47 my $_WAL_FILE_PAT = qr/\w{24}/;
49 my $_DATA_DIR_RESTORE = "data";
50 my $_ARCHIVE_DIR_RESTORE = "archive";
55 my $self = $class->SUPER::new($args->{'config'});
56 $self->{'args'} = $args;
57 $self->{'label-prefix'} = 'amanda';
58 $self->{'runtar'} = "$Amanda::Paths::amlibexecdir/runtar";
60 # default arguments (application properties)
61 $self->{'args'}->{'statedir'} ||= $Amanda::Paths::GNUTAR_LISTED_INCREMENTAL_DIR;
62 $self->{'args'}->{'tmpdir'} ||= $AMANDA_TMPDIR;
63 # XXX: when using runtar, this is not actually honored.
64 # So, this only works for restore at the moment
65 $self->{'args'}->{'gnutar-path'} ||= $Amanda::Constants::GNUTAR;
67 if (!defined $self->{'args'}->{'disk'}) {
68 $self->{'args'}->{'disk'} = $self->{'args'}->{'device'};
70 if (!defined $self->{'args'}->{'device'}) {
71 $self->{'args'}->{'device'} = $self->{'args'}->{'disk'};
75 'pg-db' => 'template1',
76 'pg-cleanupwal' => 'yes',
77 'pg-max-wal-wait' => 60,
80 my @PROP_NAMES = qw(pg-host pg-port pg-db pg-user pg-password pg-passfile
81 psql-path pg-datadir pg-archivedir pg-cleanupwal
84 # config is loaded by Amanda::Application (and Amanda::Script_App)
85 my $conf_props = getconf($CNF_PROPERTY);
86 # check for properties like 'pg-host'
87 foreach my $pname (@PROP_NAMES) {
88 if ($conf_props->{$pname}) {
89 debug("More than one value for $pname. Using the first.")
90 if scalar(@{$conf_props->{$pname}->{'values'}}) > 1;
91 $self->{'props'}->{$pname} = $conf_props->{$pname}->{'values'}->[0];
95 # check for properties like 'foo-pg-host' where the diskname is 'foo'
96 if ($self->{'args'}->{'disk'}) {
97 foreach my $pname (@PROP_NAMES) {
98 my $tmp = "$self->{'args'}->{'disk'}-$pname";
99 if ($conf_props->{$tmp}) {
100 debug("More than one value for $tmp. Using the first.")
101 if scalar(@{$conf_props->{$tmp}->{'values'}}) > 1;
102 $self->{'props'}->{$pname} = $conf_props->{$tmp}->{'values'}->[0];
107 # overwrite with dumptype properties if they are set.
108 foreach my $pname (@PROP_NAMES) {
109 my $pdumpname = $pname;
110 $pdumpname =~ s/^pg-//g;
111 $self->{'props'}->{$pname} = $self->{'args'}->{$pdumpname}
112 if defined $self->{'args'}->{$pdumpname};
113 debug("prop $pname set from dumpname $pdumpname: $self->{'args'}->{$pdumpname}")
114 if defined $self->{'args'}->{$pdumpname};
117 unless ($self->{'props'}->{'psql-path'}) {
118 foreach my $pre (split(/:/, $ENV{PATH})) {
119 my $psql = "$pre/psql";
121 $self->{'props'}{'psql-path'} = $psql;
127 foreach my $aname (keys %{$self->{'args'}}) {
128 if (defined($self->{'args'}->{$aname})) {
129 debug("app property: $aname $self->{'args'}->{$aname}");
131 debug("app property: $aname (undef)");
135 foreach my $pname (keys %{$self->{'props'}}) {
136 if (defined($self->{'props'}->{$pname})) {
137 debug("client property: $pname $self->{'props'}->{$pname}");
139 debug("client property: $pname (undef)");
143 if (!exists $self->{'props'}->{'pg-datadir'}) {
144 $self->{'props'}->{'pg-datadir'} = $self->{'args'}->{'device'};
150 sub command_support {
171 my ($desc, $succ_suf, $err_suf, $check, @check_args) = @_;
172 my $ret = $check->(@check_args);
173 my $msg = $ret? "OK $desc $succ_suf" : "ERROR $desc $err_suf";
179 sub _check_parent_dirs {
182 my $is_abs = substr($dir, 0, 1) eq "/";
183 _check("$dir is an absolute path?", "Yes", "No. It should start with '/'",
186 my @parts = split('/', $dir);
187 pop @parts; # don't test the last part
188 my $partial_path = '';
189 for my $path_part (@parts) {
190 $partial_path .= $path_part . (($partial_path || $is_abs)? '/' : '');
192 _check("$partial_path is executable?", "Yes", "No",
193 sub {-x $_[0]}, $partial_path);
195 _check("$partial_path is a directory?", "Yes", "No",
196 sub {-d $_[0]}, $partial_path);
201 sub _ok_passfile_perms {
202 my $passfile = shift @_;
203 # libpq uses stat, so we use stat
204 my @fstat = stat($passfile);
205 return 0 unless @fstat;
206 return 0 if 077 & $fstat[2];
210 sub _run_psql_command {
211 my ($self, $cmd) = @_;
213 # n.b. deprecated, passfile recommended for better security
214 my $orig_pgpassword = $ENV{'PGPASSWORD'};
215 $ENV{'PGPASSWORD'} = $self->{'props'}->{'pg-password'} if $self->{'props'}->{'pg-password'};
216 # n.b. supported in 8.1+
217 my $orig_pgpassfile = $ENV{'PGPASSFILE'};
218 $ENV{'PGPASSFILE'} = $self->{'props'}->{'pg-passfile'} if $self->{'props'}->{'pg-passfile'};
220 my @cmd = ($self->{'props'}->{'psql-path'});
222 push @cmd, "-h", $self->{'props'}->{'pg-host'} if ($self->{'props'}->{'pg-host'});
223 push @cmd, "-p", $self->{'props'}->{'pg-port'} if ($self->{'props'}->{'pg-port'});
224 push @cmd, "-U", $self->{'props'}->{'pg-user'} if ($self->{'props'}->{'pg-user'});
226 push @cmd, '--quiet', '--output', '/dev/null' if (!($cmd =~ /pg_xlogfile_name_offset/));
227 push @cmd, '--command', $cmd, $self->{'props'}->{'pg-db'};
228 debug("running " . join(" ", @cmd));
231 my $err = Symbol::gensym;
232 my $pid = open3($wtr, $rdr, $err, @cmd);
235 my $file_to_close = 2;
236 my $psql_stdout_src = Amanda::MainLoop::fd_source($rdr,
237 $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
238 my $psql_stderr_src = Amanda::MainLoop::fd_source($err,
239 $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
240 $psql_stdout_src->set_callback(sub {
242 if (!defined $line) {
244 $psql_stdout_src->remove();
245 Amanda::MainLoop::quit() if $file_to_close == 0;
249 return if $line =~ /^\s*$/;
250 debug("psql stdout: $line");
251 if ($cmd =~ /pg_xlogfile_name_offset/) {
252 return if $line =~ /file_name/;
253 return if $line =~ /------/;
254 return if $line =~ /\(1 row\)/;
255 if ($line =~ /^ ($_WAL_FILE_PAT)/) {
256 $self->{'switch_xlog_filename'} = $1;
260 if ($line =~ /NOTICE: pg_stop_backup complete, all required WAL segments have been archived/) {
262 $self->print_to_server("psql stdout: $line",
263 $Amanda::Script_App::GOOD);
266 $psql_stderr_src->set_callback(sub {
268 if (!defined $line) {
270 $psql_stderr_src->remove();
271 Amanda::MainLoop::quit() if $file_to_close == 0;
275 debug("psql stderr: $line");
276 if ($line =~ /NOTICE: pg_stop_backup complete, all required WAL segments have been archived/) {
277 } elsif ($line =~ /could not connect to server/) {
278 $self->print_to_server("psql stderr: $line",
279 $Amanda::Script_App::ERROR);
281 $self->print_to_server("psql stderr: $line",
282 $Amanda::Script_App::GOOD);
287 Amanda::MainLoop::run();
294 $ENV{'PGPASSWORD'} = $orig_pgpassword || '';
295 $ENV{'PGPASSFILE'} = $orig_pgpassfile || '';
297 return 0 == ($status >> 8)
300 sub command_selfcheck {
303 # set up to handle errors correctly
304 $self->{'die_cb'} = sub {
311 $self->print_to_server("disk " . quote_string($self->{args}->{disk}));
313 $self->print_to_server("ampgsql version " . $Amanda::Constants::VERSION,
314 $Amanda::Script_App::GOOD);
316 for my $k (keys %{$self->{'args'}}) {
317 print "OK application property: $k = $self->{'args'}->{$k}\n";
320 _check("GNUTAR-PATH $self->{'args'}->{'gnutar-path'}",
321 "is executable", "is NOT executable",
322 sub {-x $_[0]}, $self->{'args'}->{'gnutar-path'});
323 _check("GNUTAR-PATH $self->{'args'}->{'gnutar-path'}",
324 "is not a directory (okay)", "is a directory (it shouldn't be)",
325 sub {!(-d $_[0])}, $self->{'args'}->{'gnutar-path'});
326 _check_parent_dirs($self->{'args'}->{'gnutar-path'});
328 _check("GNUTAR $Amanda::Constants::GNUTAR",
329 "is executable", "is NOT executable",
330 sub {-x $_[0]}, $Amanda::Constants::GNUTAR);
331 _check("GNUTAR $Amanda::Constants::GNUTAR",
332 "is not a directory (okay)", "is a directory (it shouldn't be)",
333 sub {!(-d $_[0])}, $Amanda::Constants::GNUTAR);
334 _check_parent_dirs($Amanda::Constants::GNUTAR);
336 _check("TMPDIR '$self->{'args'}->{'tmpdir'}'",
337 "is an acessible directory", "is NOT an acessible directory",
338 sub {$_[0] && -d $_[0] && -r $_[0] && -w $_[0] && -x $_[0]},
339 $self->{'args'}->{'tmpdir'});
341 if (exists $self->{'props'}->{'pg-datadir'}) {
342 _check("PG-DATADIR property is",
343 "same as diskdevice", "differrent than diskdevice",
344 sub { $_[0] eq $_[1] },
345 $self->{'props'}->{'pg-datadir'}, $self->{'args'}->{'device'});
347 $self->{'props'}->{'pg-datadir'} = $self->{'args'}->{'device'};
350 _check("PG-DATADIR property", "is set", "is NOT set",
351 sub { $_[0] }, $self->{'props'}->{'pg-datadir'});
352 # note that the backup user need not be able ot read this dir
354 _check("STATEDIR '$self->{'args'}->{'statedir'}'",
355 "is an acessible directory", "is NOT an acessible directory",
356 sub {$_[0] && -d $_[0] && -r $_[0] && -w $_[0] && -x $_[0]},
357 $self->{'args'}->{'statedir'});
358 _check_parent_dirs($self->{'args'}->{'statedir'});
360 if ($self->{'args'}->{'device'}) {
363 for my $k (keys %{$self->{'props'}}) {
364 print "OK client property: $k = $self->{'props'}->{$k}\n";
367 if (_check("PG-ARCHIVEDIR property", "is set", "is NOT set",
368 sub { $_[0] }, $self->{'props'}->{'pg-archivedir'})) {
369 _check("PG-ARCHIVEDIR $self->{'props'}->{'pg-archivedir'}",
370 "is a directory", "is NOT a directory",
371 sub {-d $_[0]}, $self->{'props'}->{'pg-archivedir'});
372 _check("PG-ARCHIVEDIR $self->{'props'}->{'pg-archivedir'}",
373 "is readable", "is NOT readable",
374 sub {-r $_[0]}, $self->{'props'}->{'pg-archivedir'});
375 _check("PG-ARCHIVEDIR $self->{'props'}->{'pg-archivedir'}",
376 "is executable", "is NOT executable",
377 sub {-x $_[0]}, $self->{'props'}->{'pg-archivedir'});
378 _check_parent_dirs($self->{'props'}->{'pg-archivedir'});
382 _check("Are both PG-PASSFILE and PG-PASSWORD set?",
384 "Yes. Please set only one or the other",
385 sub {!($self->{'props'}->{'pg-passfile'} and
386 $self->{'props'}->{'pg-password'})});
388 if ($self->{'props'}->{'pg-passfile'}) {
390 _check("PG-PASSFILE $self->{'props'}->{'pg-passfile'}",
391 "has correct permissions", "does not have correct permissions",
392 \&_ok_passfile_perms, $self->{'props'}->{'pg-passfile'});
394 _check_parent_dirs($self->{'props'}->{'pg-passfile'});
397 if (_check("PSQL-PATH property", "is set", "is NOT set and psql is not in \$PATH",
398 sub { $_[0] }, $self->{'props'}->{'psql-path'})) {
400 _check("PSQL-PATH $self->{'props'}->{'psql-path'}",
401 "is executable", "is NOT executable",
402 sub {-x $_[0]}, $self->{'props'}->{'psql-path'});
404 _check("PSQL-PATH $self->{'props'}->{'psql-path'}",
405 "is not a directory (okay)", "is a directory (it shouldn't be)",
406 sub {!(-d $_[0])}, $self->{'props'}->{'psql-path'});
408 _check_parent_dirs($self->{'props'}->{'psql-path'});
414 my @pv = `$self->{'props'}->{'psql-path'} --version`;
416 $pv[0] =~ /^[^0-9]*([0-9.]*)[^0-9]*$/;
418 $self->print_to_server("ampgsql psql-version $pv",
419 $Amanda::Script_App::GOOD);
421 $self->print_to_server(
422 "[Can't get " . $self->{'props'}->{'psql-path'} . " version]\n",
423 $Amanda::Script_App::ERROR);
429 _check("Connecting to database server", "succeeded", "failed",
430 \&_run_psql_command, $self, '');
434 my @gv = `$self->{'args'}->{'gnutar-path'} --version`;
436 $gv[0] =~ /^[^0-9]*([0-9.]*)[^0-9]*$/;
438 $self->print_to_server("ampgsql gtar-version $gv",
439 $Amanda::Script_App::GOOD);
441 $self->print_to_server(
442 "[Can't get " . $self->{'props'}->{'gnutar-path'} . " version]\n",
443 $Amanda::Script_App::ERROR);
449 sub _state_filename {
450 my ($self, $level) = @_;
452 my @parts = ("ampgsql", hexencode($self->{'args'}->{'host'}), hexencode($self->{'args'}->{'disk'}), $level);
453 my $statefile = $self->{'args'}->{'statedir'} . '/' . join("-", @parts);
454 debug("statefile: $statefile");
458 sub _write_state_file {
459 my ($self, $end_wal) = @_;
461 my $h = new IO::File(_state_filename($self, $self->{'args'}->{'level'}), "w");
464 debug("writing state file");
465 $h->print("VERSION: 0\n");
466 $h->print("LAST WAL FILE: $end_wal\n");
471 sub _get_prev_state {
473 my $initial_level = shift;
474 $initial_level = $self->{'args'}->{'level'} - 1 if !defined $initial_level;
477 for (my $level = $initial_level; $level >= 0; $level--) {
478 my $fn = _state_filename($self, $level);
479 debug("reading state file: $fn");
480 my $h = new IO::File($fn, "r");
482 while (my $l = <$h>) {
485 if ($l =~ /^VERSION: (\d+)/) {
490 } elsif ($l =~ /^LAST WAL FILE: ($_WAL_FILE_PAT)/) {
500 sub _make_dummy_dir_base {
503 my $dummydir = "$self->{'args'}->{'tmpdir'}/ampgsql-dummy-$$";
504 mkpath("$dummydir/$_ARCHIVE_DIR_RESTORE");
509 sub _make_dummy_dir {
512 my $dummydir = "$self->{'args'}->{'tmpdir'}/ampgsql-dummy-$$";
514 open(my $fh, ">$dummydir/empty-incremental");
520 sub _run_tar_totals {
521 my ($self, @other_args) = @_;
524 @cmd = ($self->{'runtar'}, $self->{'args'}->{'config'},
525 $Amanda::Constants::GNUTAR, '--create', '--totals', @other_args);
526 debug("running: " . join(" ", @cmd));
528 local (*TAR_IN, *TAR_OUT, *TAR_ERR);
529 open TAR_OUT, ">&", $self->{'out_h'};
531 eval { $pid = open3(\*TAR_IN, ">&TAR_OUT", \*TAR_ERR, @cmd); 1;} or
532 $self->{'die_cb'}->("failed to run tar. error was $@");
537 while (my $l = <TAR_ERR>) {
538 if ($l =~ /^Total bytes written: (\d+)/) {
542 $self->print_to_server($l, $Amanda::Script_App::ERROR);
543 debug("TAR_ERR: $l");
547 my $status = POSIX::WEXITSTATUS($?);
550 debug("size of generated tar file: " . (defined($size)? $size : "undef"));
552 debug("ignored non-fatal tar exit status of 1");
554 $self->{'die_cb'}->("Tar failed (exit status $status)");
559 sub command_estimate {
562 $self->{'out_h'} = new IO::File("/dev/null", "w");
563 $self->{'out_h'} or die("Could not open /dev/null");
564 $self->{'index_h'} = new IO::File("/dev/null", "w");
565 $self->{'index_h'} or die("Could not open /dev/null");
567 $self->{'done_cb'} = sub {
569 debug("done. size $size");
570 $size = ceil($size/1024);
571 debug("sending $self->{'args'}->{'level'} $size 1");
572 print("$self->{'args'}->{'level'} $size 1\n");
574 $self->{'die_cb'} = sub {
577 $self->{'done_cb'}->(-1);
580 $self->{'state_cb'} = sub {
583 $self->{'unlink_cb'} = sub {
587 if ($self->{'args'}->{'level'} > 0) {
594 sub _get_backup_info {
595 my ($self, $label) = @_;
597 my ($fname, $bfile, $start_wal, $end_wal);
598 # wait up to 60s for the .backup file to be copied
599 for (my $count = 0; $count < 60; $count++) {
600 my $adir = new IO::Dir($self->{'props'}->{'pg-archivedir'});
601 $adir or $self->{'die_cb'}->("Could not open archive WAL directory");
602 while (defined($fname = $adir->read())) {
603 if ($fname =~ /\.backup$/) {
605 # use runtar to read a protected file, then grep the resulting tarfile (yes,
608 my $conf = $self->{'args'}->{'config'} || 'NOCONFIG';
609 my $cmd = "$self->{'runtar'} $conf $Amanda::Constants::GNUTAR --create --file - --directory $self->{'props'}->{'pg-archivedir'} $fname | $Amanda::Constants::GNUTAR --file - --extract --to-stdout";
610 debug("running: $cmd");
611 open(TAROUT, "$cmd |");
612 my ($start, $end, $lab);
613 while (my $l = <TAROUT>) {
615 if ($l =~ /^START WAL LOCATION:.*?\(file ($_WAL_FILE_PAT)\)$/) {
617 } elsif($l =~ /^STOP WAL LOCATION:.*?\(file ($_WAL_FILE_PAT)\)$/) {
619 } elsif ($l =~ /^LABEL: (.*)$/) {
624 if ($lab and $lab eq $label) {
630 debug("logfile had non-matching label");
635 if ($start_wal and $end_wal) {
636 debug("$bfile named WALs $start_wal .. $end_wal");
638 # try to cleanup a bit, although this may fail and that's ok
639 my $filename = "$self->{'props'}->{'pg-archivedir'}/$bfile";
640 if (unlink($filename) == 0) {
641 debug("Failed to unlink '$filename': $!");
642 $self->print_to_server("Failed to unlink '$filename': $!",
643 $Amanda::Script_App::ERROR);
650 ($start_wal, $end_wal);
653 # return the postgres version as an integer
654 sub _get_pg_version {
659 my @cmd = ($self->{'props'}->{'psql-path'});
661 push @cmd, "--version";
662 my $pid = open3('>&STDIN', \*VERSOUT, '>&STDERR', @cmd)
663 or $self->{'die_cb'}->("could not open psql to determine version");
664 my @lines = <VERSOUT>;
666 $self->{'die_cb'}->("could not run psql to determine version") if (($? >> 8) != 0);
668 my ($maj, $min, $pat) = ($lines[0] =~ / ([0-9]+)\.([0-9]+)\.([0-9]+)$/);
669 return $maj * 10000 + $min * 100 + $pat;
672 # create a large table and immediately drop it; this can help to push a WAL file out
673 sub _write_garbage_to_db {
676 debug("writing garbage to database to force a WAL archive");
678 # note: lest ye be tempted to add "if exists" to the drop table here, note that
679 # the clause was not supported in 8.1
680 _run_psql_command($self, <<EOF) or
681 CREATE TABLE _ampgsql_garbage AS SELECT * FROM GENERATE_SERIES(1, 500000);
682 DROP TABLE _ampgsql_garbage;
684 $self->{'die_cb'}->("Failed to create or drop table _ampgsql_garbage");
687 # wait up to pg-max-wal-wait seconds for a WAL file to appear
689 my ($self, $wal) = @_;
690 my $pg_version = $self->_get_pg_version();
692 my $archive_dir = $self->{'props'}->{'pg-archivedir'};
693 my $maxwait = 0+$self->{'props'}->{'pg-max-wal-wait'};
696 debug("waiting $maxwait s for WAL $wal to be archived..");
698 debug("waiting forever for WAL $wal to be archived..");
701 my $count = 0; # try at least 4 cycles
702 my $stoptime = time() + $maxwait;
703 while ($maxwait == 0 || time < $stoptime || $count++ < 4) {
704 if (-f "$archive_dir/$wal") {
709 # for versions 8.0 or 8.1, the only way to "force" a WAL archive is to write
710 # garbage to the database.
711 if ($pg_version < 80200) {
712 $self->_write_garbage_to_db();
718 $self->{'die_cb'}->("WAL file $wal was not archived in $maxwait seconds");
724 debug("running _base_backup");
726 my $label = "$self->{'label-prefix'}-" . time();
728 -d $self->{'props'}->{'pg-archivedir'} or
729 die("WAL file archive directory does not exist (or is not a directory)");
731 if ($self->{'action'} eq 'backup') {
732 _run_psql_command($self, "SELECT pg_start_backup('$label')") or
733 $self->{'die_cb'}->("Failed to call pg_start_backup");
736 # tar data dir, using symlink to prefix
737 # XXX: tablespaces and their symlinks?
738 # See: http://www.postgresql.org/docs/8.0/static/manage-ag-tablespaces.html
739 my $old_die_cb = $self->{'die_cb'};
740 $self->{'die_cb'} = sub {
742 if ($self->{'action'} eq 'backup') {
743 unless(_run_psql_command($self, "SELECT pg_stop_backup()")) {
744 $msg .= " and failed to call pg_stop_backup";
749 my $size = _run_tar_totals($self, '--file', "-",
750 '--directory', $self->{'props'}->{'pg-datadir'},
751 '--exclude', 'postmaster.pid',
752 '--exclude', 'pg_xlog/*', # contains WAL files; will be handled below
753 '--transform', "s,^,$_DATA_DIR_RESTORE/,S",
755 $self->{'die_cb'} = $old_die_cb;
757 if ($self->{'action'} eq 'backup') {
758 unless (_run_psql_command($self, "SELECT pg_stop_backup()")) {
759 $self->{'die_cb'}->("Failed to call pg_stop_backup");
763 # determine WAL files and append and create their tar file
767 if ($self->{'action'} eq 'backup') {
768 ($start_wal, $end_wal) = _get_backup_info($self, $label);
769 ($start_wal and $end_wal)
770 or $self->{'die_cb'}->("A .backup file was never found in the archive "
771 . "dir $self->{'props'}->{'pg-archivedir'}");
772 $self->_wait_for_wal($end_wal);
775 $end_wal = _get_prev_state($self, 0);
778 # now grab all of the WAL files, *inclusive* of $start_wal
780 my $adir = new IO::Dir($self->{'props'}->{'pg-archivedir'});
781 while (defined(my $fname = $adir->read())) {
782 if ($fname =~ /^$_WAL_FILE_PAT$/) {
783 if (!defined $end_wal ||
784 (!defined $start_wal and ($fname le $end_wal)) ||
785 (defined $start_wal and ($fname ge $start_wal) and
786 ($fname le $end_wal))) {
787 push @wal_files, $fname;
788 debug("will store: $fname");
789 } elsif (defined $start_wal and $fname lt $start_wal) {
790 $self->{'unlink_cb'}->("$self->{'props'}->{'pg-archivedir'}/$fname");
797 $size += _run_tar_totals($self, '--file', "-",
798 '--directory', $self->{'props'}->{'pg-archivedir'},
799 '--transform', "s,^,$_ARCHIVE_DIR_RESTORE/,S",
802 my $dummydir = $self->_make_dummy_dir_base();
803 $self->{'done_cb'}->(_run_tar_totals($self, '--file', '-',
804 '--directory', $dummydir, "$_ARCHIVE_DIR_RESTORE"));
808 $self->{'state_cb'}->($self, $end_wal);
810 $self->{'done_cb'}->($size);
816 debug("running _incr_backup");
818 if ($self->{'action'} eq 'backup') {
819 _run_psql_command($self, "SELECT file_name from pg_xlogfile_name_offset(pg_switch_xlog())");
820 if (defined($self->{'switch_xlog_filename'})) {
821 $self->_wait_for_wal($self->{'switch_xlog_filename'});
825 my $end_wal = _get_prev_state($self);
827 debug("previously ended at: $end_wal");
829 debug("no previous state found!");
830 return _base_backup(@_);
833 my $adir = new IO::Dir($self->{'props'}->{'pg-archivedir'});
834 $adir or $self->{'die_cb'}->("Could not open archive WAL directory");
836 my ($fname, @wal_files);
837 while (defined($fname = $adir->read())) {
838 if (($fname =~ /^$_WAL_FILE_PAT$/) and ($fname gt $end_wal)) {
839 $max_wal = $fname if $fname gt $max_wal;
840 push @wal_files, $fname;
841 debug("will store: $fname");
845 $self->{'state_cb'}->($self, $max_wal ? $max_wal : $end_wal);
848 $self->{'done_cb'}->(_run_tar_totals($self, '--file', '-',
849 '--directory', $self->{'props'}->{'pg-archivedir'}, @wal_files));
851 my $dummydir = $self->_make_dummy_dir();
852 $self->{'done_cb'}->(_run_tar_totals($self, '--file', '-',
853 '--directory', $dummydir, "empty-incremental"));
861 $self->{'out_h'} = IO::Handle->new_from_fd(1, 'w');
862 $self->{'out_h'} or die("Could not open data fd");
863 my $msg_fd = IO::Handle->new_from_fd(3, 'w');
864 $msg_fd or die("Could not open message fd");
865 $self->{'index_h'} = IO::Handle->new_from_fd(4, 'w');
866 $self->{'index_h'} or die("Could not open index fd");
868 $self->{'done_cb'} = sub {
870 debug("done. size $size");
871 $size = ceil($size/1024);
872 debug("sending size $size");
873 $msg_fd->print("sendbackup: size $size\n");
875 $self->{'index_h'}->print("/PostgreSQL-Database-$self->{'args'}->{'level'}\n");
877 $msg_fd->print("sendbackup: end\n");
879 $self->{'die_cb'} = sub {
882 $msg_fd->print("! $msg\n");
883 $self->{'done_cb'}->(0);
886 $self->{'state_cb'} = sub {
887 my ($self, $end_wal) = @_;
888 _write_state_file($self, $end_wal) or $self->{'die_cb'}->("Failed to write state file");
890 my $cleanup_wal_val = $self->{'props'}->{'pg-cleanupwal'} || 'yes';
891 my $cleanup_wal = string_to_boolean($cleanup_wal_val);
892 if (!defined($cleanup_wal)) {
893 $self->{'die_cb'}->("couldn't interpret PG-CLEANUPWAL value '$cleanup_wal_val' as a boolean");
894 } elsif ($cleanup_wal) {
895 $self->{'unlink_cb'} = sub {
896 my $filename = shift @_;
897 debug("unlinking WAL file $filename");
898 if (unlink($filename) == 0) {
899 debug("Failed to unlink '$filename': $!");
900 $self->print_to_server("Failed to unlink '$filename': $!",
901 $Amanda::Script_App::ERROR);
905 $self->{'unlink_cb'} = sub {
910 if ($self->{'args'}->{'level'} > 0) {
911 _incr_backup($self, \*STDOUT);
913 _base_backup($self, \*STDOUT);
917 sub command_restore {
920 chdir(Amanda::Util::get_original_cwd());
921 if (defined $self->{'args'}->{directory}) {
922 if (!-d $self->{'args'}->{directory}) {
923 $self->print_to_server_and_die("Directory $self->{directory}: $!",
924 $Amanda::Script_App::ERROR);
926 if (!-w $self->{'args'}->{directory}) {
927 $self->print_to_server_and_die("Directory $self->{directory}: $!",
928 $Amanda::Script_App::ERROR);
930 chdir($self->{'args'}->{directory});
932 my $cur_dir = POSIX::getcwd();
934 if (!-d $_ARCHIVE_DIR_RESTORE) {
935 mkdir($_ARCHIVE_DIR_RESTORE) or die("could not create archive WAL directory: $!");
938 if ($self->{'args'}->{'level'} > 0) {
939 debug("extracting incremental backup to $cur_dir/$_ARCHIVE_DIR_RESTORE");
940 $status = system($self->{'args'}->{'gnutar-path'},
944 '--exclude', 'empty-incremental',
945 '--directory', $_ARCHIVE_DIR_RESTORE) >> 8;
946 (0 == $status) or die("Failed to extract level $self->{'args'}->{'level'} backup (exit status: $status)");
948 debug("extracting base of full backup to $cur_dir/$_DATA_DIR_RESTORE");
949 debug("extracting archive dir to $cur_dir/$_ARCHIVE_DIR_RESTORE");
950 if (!-d $_DATA_DIR_RESTORE) {
951 mkdir($_DATA_DIR_RESTORE) or die("could not create archive WAL directory: $!");
953 my @cmd = ($self->{'args'}->{'gnutar-path'}, '--extract',
956 '--transform', "s,^DATA/,$_DATA_DIR_RESTORE/,S",
957 '--transform', "s,^WAL/,$_ARCHIVE_DIR_RESTORE/,S");
958 debug("run: " . join ' ',@cmd);
959 $status = system(@cmd) >> 8;
960 (0 == $status) or die("Failed to extract base backup (exit status: $status)");
962 if (-f $_ARCHIVE_DIR_TAR) {
963 debug("extracting archive dir to $cur_dir/$_ARCHIVE_DIR_RESTORE");
964 my @cmd = ($self->{'args'}->{'gnutar-path'}, '--extract',
965 '--exclude', 'empty-incremental',
966 '--file', $_ARCHIVE_DIR_TAR, '--directory',
967 $_ARCHIVE_DIR_RESTORE);
968 debug("run: " . join ' ',@cmd);
969 $status = system(@cmd) >> 8;
970 (0 == $status) or die("Failed to extract archived WAL files from base backup (exit status: $status)");
971 if (unlink($_ARCHIVE_DIR_TAR) == 0) {
972 debug("Failed to unlink '$_ARCHIVE_DIR_TAR': $!");
973 $self->print_to_server(
974 "Failed to unlink '$_ARCHIVE_DIR_TAR': $!",
975 $Amanda::Script_App::ERROR);
979 if (-f $_DATA_DIR_TAR) {
980 debug("extracting data dir to $cur_dir/$_DATA_DIR_RESTORE");
981 my @cmd = ($self->{'args'}->{'gnutar-path'}, '--extract',
982 '--file', $_DATA_DIR_TAR,
983 '--directory', $_DATA_DIR_RESTORE);
984 debug("run: " . join ' ',@cmd);
985 $status = system(@cmd) >> 8;
986 (0 == $status) or die("Failed to extract data directory from base backup (exit status: $status)");
987 if (unlink($_DATA_DIR_TAR) == 0) {
988 debug("Failed to unlink '$_DATA_DIR_TAR': $!");
989 $self->print_to_server("Failed to unlink '$_DATA_DIR_TAR': $!",
990 $Amanda::Script_App::ERROR);
996 sub command_validate {
999 # set up to handle errors correctly
1000 $self->{'die_cb'} = sub {
1007 if (!defined($self->{'args'}->{'gnutar-path'}) ||
1008 !-x $self->{'args'}->{'gnutar-path'}) {
1009 return $self->default_validate();
1012 my(@cmd) = ($self->{'args'}->{'gnutar-path'}, "--ignore-zeros", "-tf", "-");
1013 debug("cmd:" . join(" ", @cmd));
1014 my $pid = open3('>&STDIN', '>&STDOUT', '>&STDERR', @cmd) ||
1015 $self->print_to_server_and_die("Unable to run @cmd",
1016 $Amanda::Application::ERROR);
1019 $self->print_to_server_and_die("$self->{gnutar} returned error",
1020 $Amanda::Application::ERROR);
1022 exit($self->{error_status});
1029 Usage: ampgsql <command> --config=<config> --host=<host> --disk=<disk> --device=<device> --level=<level> --index=<yes|no> --message=<text> --collection=<no> --record=<yes|no> --calcsize.
1039 'version' => \$opt_version,
1068 if (defined $opt_version) {
1069 print "ampgsql-" . $Amanda::Constants::VERSION , "\n";
1073 my $application = Amanda::Application::ampgsql->new($opts);
1075 $application->do($ARGV[0]);