2 # Copyright (c) 2009, 2010 Zmanda, Inc. All Rights Reserved.
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.
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
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
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
20 use lib '@amperldir@';
25 package Amanda::Application::ampgsql;
26 use base qw(Amanda::Application);
37 use Amanda::Constants;
38 use Amanda::Config qw( :init :getconf config_dir_relative string_to_boolean );
39 use Amanda::Debug qw( :logging );
41 use Amanda::Util qw( :constants :encoding );
42 use Amanda::MainLoop qw( :GIOCondition );
44 my $_DATA_DIR_TAR = "data_dir.tar";
45 my $_ARCHIVE_DIR_TAR = "archive_dir.tar";
46 my $_WAL_FILE_PAT = qr/\w{24}/;
48 my $_DATA_DIR_RESTORE = "data";
49 my $_ARCHIVE_DIR_RESTORE = "archive";
54 my $self = $class->SUPER::new($args->{'config'});
55 $self->{'args'} = $args;
56 $self->{'label-prefix'} = 'amanda';
57 $self->{'runtar'} = "$Amanda::Paths::amlibexecdir/runtar";
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;
66 if (!defined $self->{'args'}->{'disk'}) {
67 $self->{'args'}->{'disk'} = $self->{'args'}->{'device'};
69 if (!defined $self->{'args'}->{'device'}) {
70 $self->{'args'}->{'device'} = $self->{'args'}->{'disk'};
74 'pg-db' => 'template1',
75 'pg-cleanupwal' => 'yes',
76 'pg-max-wal-wait' => 60,
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
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];
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];
105 unless ($self->{'props'}->{'psql-path'}) {
106 foreach my $pre (split(/:/, $ENV{PATH})) {
107 my $psql = "$pre/psql";
109 $self->{'props'}{'psql-path'} = $psql;
115 foreach my $aname (keys %{$self->{'args'}}) {
116 if (defined($self->{'args'}->{$aname})) {
117 debug("app property: $aname $self->{'args'}->{$aname}");
119 debug("app property: $aname (undef)");
123 foreach my $pname (keys %{$self->{'props'}}) {
124 if (defined($self->{'props'}->{$pname})) {
125 debug("client property: $pname $self->{'props'}->{$pname}");
127 debug("client property: $pname (undef)");
134 sub command_support {
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";
163 sub _check_parent_dirs {
166 my $is_abs = substr($dir, 0, 1) eq "/";
167 _check("$dir is an absolute path?", "Yes", "No. It should start with '/'",
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)? '/' : '');
176 _check("$partial_path is executable?", "Yes", "No",
177 sub {-x $_[0]}, $partial_path);
179 _check("$partial_path is a directory?", "Yes", "No",
180 sub {-d $_[0]}, $partial_path);
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];
194 sub _run_psql_command {
195 my ($self, $cmd) = @_;
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'};
204 my @cmd = ($self->{'props'}->{'psql-path'});
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'});
210 push @cmd, '--quiet', '--output', '/dev/null', '--command', $cmd, $self->{'props'}->{'pg-db'};
211 debug("running " . join(" ", @cmd));
214 my $err = Symbol::gensym;
215 my $pid = open3($wtr, $rdr, $err, @cmd);
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 {
225 if (!defined $line) {
227 $psql_stdout_src->remove();
228 Amanda::MainLoop::quit() if $file_to_close == 0;
232 debug("psql stdout: $line");
233 if ($line =~ /NOTICE: pg_stop_backup complete, all required WAL segments have been archived/) {
235 $self->print_to_server("psql stdout: $line",
236 $Amanda::Script_App::GOOD);
239 $psql_stderr_src->set_callback(sub {
241 if (!defined $line) {
243 $psql_stderr_src->remove();
244 Amanda::MainLoop::quit() if $file_to_close == 0;
248 debug("psql stderr: $line");
249 if ($line =~ /NOTICE: pg_stop_backup complete, all required WAL segments have been archived/) {
251 $self->print_to_server("psql stderr: $line",
252 $Amanda::Script_App::GOOD);
257 Amanda::MainLoop::run();
264 $ENV{'PGPASSWORD'} = $orig_pgpassword || '';
265 $ENV{'PGPASSFILE'} = $orig_pgpassfile || '';
267 return 0 == ($status >> 8)
270 sub command_selfcheck {
273 # set up to handle errors correctly
274 $self->{'die_cb'} = sub {
281 for my $k (keys %{$self->{'args'}}) {
282 print "OK application property: $k = $self->{'args'}->{$k}\n";
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'});
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);
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'});
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'});
312 $self->{'props'}->{'pg-datadir'} = $self->{'args'}->{'device'};
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
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'});
325 if ($self->{'args'}->{'device'}) {
328 for my $k (keys %{$self->{'props'}}) {
329 print "OK client property: $k = $self->{'props'}->{$k}\n";
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'});
347 _check("Are both PG-PASSFILE and PG-PASSWORD set?",
349 "Yes. Please set only one or the other",
350 sub {!($self->{'props'}->{'pg-passfile'} and
351 $self->{'props'}->{'pg-password'})});
353 if ($self->{'props'}->{'pg-passfile'}) {
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'});
359 _check_parent_dirs($self->{'props'}->{'pg-passfile'});
362 if (_check("PSQL-PATH property", "is set", "is NOT set and psql is not in \$PATH",
363 sub { $_[0] }, $self->{'props'}->{'psql-path'})) {
365 _check("PSQL-PATH $self->{'props'}->{'psql-path'}",
366 "is executable", "is NOT executable",
367 sub {-x $_[0]}, $self->{'props'}->{'psql-path'});
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'});
373 _check_parent_dirs($self->{'props'}->{'psql-path'});
380 _check("Connecting to database server", "succeeded", "failed",
381 \&_run_psql_command, $self, '');
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()")) {
392 _check("Get info from .backup file", "succeeded", "failed",
393 sub {my ($start, $end) = _get_backup_info($self, $label); $start and $end});
399 sub _state_filename {
400 my ($self, $level) = @_;
402 my @parts = ("ampgsql", hexencode($self->{'args'}->{'host'}), hexencode($self->{'args'}->{'disk'}), $level);
403 $self->{'args'}->{'statedir'} . '/' . join("-", @parts);
406 sub _write_state_file {
407 my ($self, $end_wal) = @_;
409 my $h = new IO::File(_state_filename($self, $self->{'args'}->{'level'}), "w");
412 debug("writing state file");
413 $h->print("VERSION: 0\n");
414 $h->print("LAST WAL FILE: $end_wal\n");
419 sub _get_prev_state {
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");
428 while (my $l = <$h>) {
431 if ($l =~ /^VERSION: (\d+)/) {
436 } elsif ($l =~ /^LAST WAL FILE: ($_WAL_FILE_PAT)/) {
446 sub _make_dummy_dir {
449 my $dummydir = "$self->{'args'}->{'tmpdir'}/ampgsql-dummy-$$";
451 open(my $fh, ">$dummydir/empty-incremental");
457 sub _run_tar_totals {
458 my ($self, @other_args) = @_;
460 my @cmd = ($self->{'runtar'}, $self->{'args'}->{'config'},
461 $Amanda::Constants::GNUTAR, '--create', '--totals', @other_args);
462 debug("running " . join(" ", @cmd));
464 local (*TAR_IN, *TAR_OUT, *TAR_ERR);
465 open TAR_OUT, ">&", $self->{'out_h'};
467 eval { $pid = open3(\*TAR_IN, ">&TAR_OUT", \*TAR_ERR, @cmd); 1;} or
468 $self->{'die_cb'}->("failed to run tar. error was $@");
473 while (my $l = <TAR_ERR>) {
474 if ($l =~ /^Total bytes written: (\d+)/) {
478 $self->print_to_server($l, $Amanda::Script_App::ERROR);
479 debug("TAR_ERR: $l");
483 my $status = POSIX::WEXITSTATUS($?);
486 debug("size of generated tar file: " . (defined($size)? $size : "undef"));
488 debug("ignored non-fatal tar exit status of 1");
490 $self->{'die_cb'}->("Tar failed (exit status $status)");
495 sub command_estimate {
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");
503 $self->{'done_cb'} = sub {
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");
510 $self->{'die_cb'} = sub {
513 $self->{'done_cb'}->(-1);
516 $self->{'state_cb'} = sub {
519 $self->{'unlink_cb'} = sub {
523 if ($self->{'args'}->{'level'} > 0) {
530 sub _get_backup_info {
531 my ($self, $label) = @_;
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$/) {
541 # use runtar to read a protected file, then grep the resulting tarfile (yes,
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>) {
551 if ($l =~ /^START WAL LOCATION:.*?\(file ($_WAL_FILE_PAT)\)$/) {
553 } elsif($l =~ /^STOP WAL LOCATION:.*?\(file ($_WAL_FILE_PAT)\)$/) {
555 } elsif ($l =~ /^LABEL: (.*)$/) {
559 if ($lab and $lab eq $label) {
565 debug("logfile had non-matching label");
570 if ($start_wal and $end_wal) {
571 debug("$bfile named WALs $start_wal .. $end_wal");
573 # try to cleanup a bit, although this may fail and that's ok
574 unlink("$self->{'props'}->{'pg-archivedir'}/$bfile");
580 ($start_wal, $end_wal);
583 # return the postgres version as an integer
584 sub _get_pg_version {
589 my @cmd = ($self->{'props'}->{'psql-path'});
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>;
596 $self->{'die_cb'}->("could not run psql to determine version") if (($? >> 8) != 0);
598 my ($maj, $min, $pat) = ($lines[0] =~ / ([0-9]+)\.([0-9]+)\.([0-9]+)$/);
599 return $maj * 10000 + $min * 100 + $pat;
602 # create a large table and immediately drop it; this can help to push a WAL file out
603 sub _write_garbage_to_db {
606 debug("writing garbage to database to force a WAL archive");
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;
614 $self->{'die_cb'}->("Failed to create or drop table _ampgsql_garbage");
617 # wait up to pg-max-wal-wait seconds for a WAL file to appear
619 my ($self, $wal) = @_;
620 my $pg_version = $self->_get_pg_version();
622 my $archive_dir = $self->{'props'}->{'pg-archivedir'};
623 my $maxwait = 0+$self->{'props'}->{'pg-max-wal-wait'};
626 debug("waiting $maxwait s for WAL $wal to be archived..");
628 debug("waiting forever for WAL $wal to be archived..");
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";
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();
645 $self->{'die_cb'}->("WAL file $wal was not archived in $maxwait seconds");
651 debug("running _base_backup");
653 my $label = "$self->{'label-prefix'}-" . time();
654 my $tmp = "$self->{'args'}->{'tmpdir'}/$label";
656 -d $self->{'props'}->{'pg-archivedir'} or
657 die("WAL file archive directory does not exist (or is not a directory)");
659 # try to protect what we create
660 my $old_umask = umask();
665 eval {rmtree($tmp); 1}
667 my $old_die = $self->{'die_cb'};
668 $self->{'die_cb'} = sub {
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: $@");
676 _run_psql_command($self, "SELECT pg_start_backup('$label')") or
677 $self->{'die_cb'}->("Failed to call pg_start_backup");
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 {
685 unless(_run_psql_command($self, "SELECT pg_stop_backup()")) {
686 $msg .= " and failed to call pg_stop_backup";
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
695 $self->{'die_cb'} = $old_die_cb;
697 unless (_run_psql_command($self, "SELECT pg_stop_backup()")) {
698 $self->{'die_cb'}->("Failed to call pg_stop_backup");
701 # determine WAL files and append and create their tar file
702 my ($start_wal, $end_wal) = _get_backup_info($self, $label);
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'}");
708 $self->_wait_for_wal($end_wal);
710 # now grab all of the WAL files, *inclusive* of $start_wal
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");
726 _run_tar_totals($self, '--file', "$tmp/$_ARCHIVE_DIR_TAR",
727 '--directory', $self->{'props'}->{'pg-archivedir'}, @wal_files);
729 my $dummydir = $self->_make_dummy_dir();
730 $self->{'done_cb'}->(_run_tar_totals($self, '--file', '-',
731 '--directory', $dummydir, "empty-incremental"));
735 # create the final tar file
736 my $size = _run_tar_totals($self, '--directory', $tmp, '--file', '-',
737 $_ARCHIVE_DIR_TAR, $_DATA_DIR_TAR);
739 $self->{'state_cb'}->($self, $end_wal);
742 $self->{'done_cb'}->($size);
748 debug("running _incr_backup");
750 my $end_wal = _get_prev_state($self);
752 debug("previously ended at: $end_wal");
754 debug("no previous state found!");
755 return _base_backup(@_);
758 my $adir = new IO::Dir($self->{'props'}->{'pg-archivedir'});
759 $adir or $self->{'die_cb'}->("Could not open archive WAL directory");
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");
770 $self->{'state_cb'}->($self, $max_wal ? $max_wal : $end_wal);
773 $self->{'done_cb'}->(_run_tar_totals($self, '--file', '-',
774 '--directory', $self->{'props'}->{'pg-archivedir'}, @wal_files));
776 my $dummydir = $self->_make_dummy_dir();
777 $self->{'done_cb'}->(_run_tar_totals($self, '--file', '-',
778 '--directory', $dummydir, "empty-incremental"));
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");
793 $self->{'done_cb'} = sub {
795 debug("done. size $size");
796 $size = ceil($size/1024);
797 debug("sending size $size");
798 $msg_fd->print("sendbackup: size $size\n");
800 $self->{'index_h'}->print("/PostgreSQL-Database-$self->{'args'}->{'level'}\n");
802 $msg_fd->print("sendbackup: end\n");
804 $self->{'die_cb'} = sub {
807 $msg_fd->print("! $msg\n");
808 $self->{'done_cb'}->(0);
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");
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");
826 $self->{'unlink_cb'} = sub {
831 if ($self->{'args'}->{'level'} > 0) {
832 _incr_backup($self, \*STDOUT);
834 _base_backup($self, \*STDOUT);
838 sub command_restore {
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);
847 if (!-w $self->{'args'}->{directory}) {
848 $self->print_to_server_and_die("Directory $self->{directory}: $!",
849 $Amanda::Script_App::ERROR);
851 chdir($self->{'args'}->{directory});
853 my $cur_dir = POSIX::getcwd();
855 if (!-d $_ARCHIVE_DIR_RESTORE) {
856 mkdir($_ARCHIVE_DIR_RESTORE) or die("could not create archive WAL directory: $!");
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',
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)");
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: $!");
872 $status = system($self->{'args'}->{'gnutar-path'}, '--extract', '--file', '-',) >> 8;
873 (0 == $status) or die("Failed to extract base backup (exit status: $status)");
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);
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);
890 sub command_validate {
893 # set up to handle errors correctly
894 $self->{'die_cb'} = sub {
901 if (!defined($self->{'args'}->{'gnutar-path'}) ||
902 !-x $self->{'args'}->{'gnutar-path'}) {
903 return $self->default_validate();
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);
913 $self->print_to_server_and_die("$self->{gnutar} returned error",
914 $Amanda::Application::ERROR);
916 exit($self->{error_status});
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.
951 my $application = Amanda::Application::ampgsql->new($opts);
953 $application->do($ARGV[0]);