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 );
43 my $_DATA_DIR_TAR = "data_dir.tar";
44 my $_ARCHIVE_DIR_TAR = "archive_dir.tar";
45 my $_WAL_FILE_PAT = qr/\w{24}/;
47 my $_DATA_DIR_RESTORE = "data";
48 my $_ARCHIVE_DIR_RESTORE = "archive";
53 my $self = $class->SUPER::new($args->{'config'});
54 $self->{'args'} = $args;
55 $self->{'label-prefix'} = 'amanda';
56 $self->{'runtar'} = "$Amanda::Paths::amlibexecdir/runtar";
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;
65 if (!defined $self->{'args'}->{'disk'}) {
66 $self->{'args'}->{'disk'} = $self->{'args'}->{'device'};
68 if (!defined $self->{'args'}->{'device'}) {
69 $self->{'args'}->{'device'} = $self->{'args'}->{'disk'};
73 'pg-db' => 'template1',
74 'pg-cleanupwal' => 'yes',
75 'pg-max-wal-wait' => 60,
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
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];
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];
104 unless ($self->{'props'}->{'psql-path'}) {
105 foreach my $pre (split(/:/, $ENV{PATH})) {
106 my $psql = "$pre/psql";
108 $self->{'props'}{'psql-path'} = $psql;
114 foreach my $aname (keys %{$self->{'args'}}) {
115 if (defined($self->{'args'}->{$aname})) {
116 debug("app property: $aname $self->{'args'}->{$aname}");
118 debug("app property: $aname (undef)");
122 foreach my $pname (keys %{$self->{'props'}}) {
123 if (defined($self->{'props'}->{$pname})) {
124 debug("client property: $pname $self->{'props'}->{$pname}");
126 debug("client property: $pname (undef)");
133 sub command_support {
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";
162 sub _check_parent_dirs {
165 my $is_abs = substr($dir, 0, 1) eq "/";
166 _check("$dir is an absolute path?", "Yes", "No. It should start with '/'",
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)? '/' : '');
175 _check("$partial_path is executable?", "Yes", "No",
176 sub {-x $_[0]}, $partial_path);
178 _check("$partial_path is a directory?", "Yes", "No",
179 sub {-d $_[0]}, $partial_path);
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];
193 sub _run_psql_command {
194 my ($self, $cmd) = @_;
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'};
203 my @cmd = ($self->{'props'}->{'psql-path'});
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'});
209 push @cmd, '--quiet', '--output', '/dev/null', '--command', $cmd, $self->{'props'}->{'pg-db'};
210 debug("running " . join(" ", @cmd));
211 my $status = system(@cmd);
213 $ENV{'PGPASSWORD'} = $orig_pgpassword || '';
214 $ENV{'PGPASSFILE'} = $orig_pgpassfile || '';
216 return 0 == ($status >>8)
219 sub command_selfcheck {
222 # set up to handle errors correctly
223 $self->{'die_cb'} = sub {
230 for my $k (keys %{$self->{'args'}}) {
231 print "OK application property: $k = $self->{'args'}->{$k}\n";
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'});
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);
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'});
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'});
261 $self->{'props'}->{'pg-datadir'} = $self->{'args'}->{'device'};
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
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'});
274 if ($self->{'args'}->{'device'}) {
277 for my $k (keys %{$self->{'props'}}) {
278 print "OK client property: $k = $self->{'props'}->{$k}\n";
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'});
296 _check("Are both PG-PASSFILE and PG-PASSWORD set?",
298 "Yes. Please set only one or the other",
299 sub {!($self->{'props'}->{'pg-passfile'} and
300 $self->{'props'}->{'pg-password'})});
302 if ($self->{'props'}->{'pg-passfile'}) {
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'});
308 _check_parent_dirs($self->{'props'}->{'pg-passfile'});
311 if (_check("PSQL-PATH property", "is set", "is NOT set and psql is not in \$PATH",
312 sub { $_[0] }, $self->{'props'}->{'psql-path'})) {
314 _check("PSQL-PATH $self->{'props'}->{'psql-path'}",
315 "is executable", "is NOT executable",
316 sub {-x $_[0]}, $self->{'props'}->{'psql-path'});
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'});
322 _check_parent_dirs($self->{'props'}->{'psql-path'});
329 _check("Connecting to database server", "succeeded", "failed",
330 \&_run_psql_command, $self, '');
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()")) {
341 _check("Get info from .backup file", "succeeded", "failed",
342 sub {my ($start, $end) = _get_backup_info($self, $label); $start and $end});
348 sub _state_filename {
349 my ($self, $level) = @_;
351 my @parts = ("ampgsql", hexencode($self->{'args'}->{'host'}), hexencode($self->{'args'}->{'disk'}), $level);
352 $self->{'args'}->{'statedir'} . '/' . join("-", @parts);
355 sub _write_state_file {
356 my ($self, $end_wal) = @_;
358 my $h = new IO::File(_state_filename($self, $self->{'args'}->{'level'}), "w");
361 debug("writing state file");
362 $h->print("VERSION: 0\n");
363 $h->print("LAST WAL FILE: $end_wal\n");
368 sub _get_prev_state {
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");
377 while (my $l = <$h>) {
380 if ($l =~ /^VERSION: (\d+)/) {
385 } elsif ($l =~ /^LAST WAL FILE: ($_WAL_FILE_PAT)/) {
395 sub _make_dummy_dir {
398 my $dummydir = "$self->{'args'}->{'tmpdir'}/ampgsql-dummy-$$";
400 open(my $fh, ">$dummydir/empty-incremental");
406 sub _run_tar_totals {
407 my ($self, @other_args) = @_;
409 my @cmd = ($self->{'runtar'}, $self->{'args'}->{'config'},
410 $Amanda::Constants::GNUTAR, '--create', '--totals', @other_args);
411 debug("running " . join(" ", @cmd));
413 local (*TAR_IN, *TAR_OUT, *TAR_ERR);
414 open TAR_OUT, ">&", $self->{'out_h'};
416 eval { $pid = open3(\*TAR_IN, ">&TAR_OUT", \*TAR_ERR, @cmd); 1;} or
417 $self->{'die_cb'}->("failed to run tar. error was $@");
422 while (my $l = <TAR_ERR>) {
423 if ($l =~ /^Total bytes written: (\d+)/) {
427 $self->print_to_server($l, $Amanda::Script_App::ERROR);
428 debug("TAR_ERR: $l");
432 my $status = POSIX::WEXITSTATUS($?);
435 debug("size of generated tar file: " . (defined($size)? $size : "undef"));
437 debug("ignored non-fatal tar exit status of 1");
439 $self->{'die_cb'}->("Tar failed (exit status $status)");
444 sub command_estimate {
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");
452 $self->{'done_cb'} = sub {
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");
459 $self->{'die_cb'} = sub {
462 $self->{'done_cb'}->(-1);
465 $self->{'state_cb'} = sub {
468 $self->{'unlink_cb'} = sub {
472 if ($self->{'args'}->{'level'} > 0) {
479 sub _get_backup_info {
480 my ($self, $label) = @_;
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$/) {
490 # use runtar to read a protected file, then grep the resulting tarfile (yes,
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>) {
500 if ($l =~ /^START WAL LOCATION:.*?\(file ($_WAL_FILE_PAT)\)$/) {
502 } elsif($l =~ /^STOP WAL LOCATION:.*?\(file ($_WAL_FILE_PAT)\)$/) {
504 } elsif ($l =~ /^LABEL: (.*)$/) {
508 if ($lab and $lab eq $label) {
514 debug("logfile had non-matching label");
519 if ($start_wal and $end_wal) {
520 debug("$bfile named WALs $start_wal .. $end_wal");
522 # try to cleanup a bit, although this may fail and that's ok
523 unlink("$self->{'props'}->{'pg-archivedir'}/$bfile");
529 ($start_wal, $end_wal);
532 # return the postgres version as an integer
533 sub _get_pg_version {
538 my @cmd = ($self->{'props'}->{'psql-path'});
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>;
545 $self->{'die_cb'}->("could not run psql to determine version") if (($? >> 8) != 0);
547 my ($maj, $min, $pat) = ($lines[0] =~ / ([0-9]+)\.([0-9]+)\.([0-9]+)$/);
548 return $maj * 10000 + $min * 100 + $pat;
551 # create a large table and immediately drop it; this can help to push a WAL file out
552 sub _write_garbage_to_db {
555 debug("writing garbage to database to force a WAL archive");
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;
563 $self->{'die_cb'}->("Failed to create or drop table _ampgsql_garbage");
566 # wait up to pg-max-wal-wait seconds for a WAL file to appear
568 my ($self, $wal) = @_;
569 my $pg_version = $self->_get_pg_version();
571 my $archive_dir = $self->{'props'}->{'pg-archivedir'};
572 my $maxwait = 0+$self->{'props'}->{'pg-max-wal-wait'};
575 debug("waiting $maxwait s for WAL $wal to be archived..");
577 debug("waiting forever for WAL $wal to be archived..");
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";
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();
594 $self->{'die_cb'}->("WAL file $wal was not archived in $maxwait seconds");
600 debug("running _base_backup");
602 my $label = "$self->{'label-prefix'}-" . time();
603 my $tmp = "$self->{'args'}->{'tmpdir'}/$label";
605 -d $self->{'props'}->{'pg-archivedir'} or
606 die("WAL file archive directory does not exist (or is not a directory)");
608 # try to protect what we create
609 my $old_umask = umask();
614 eval {rmtree($tmp); 1}
616 my $old_die = $self->{'die_cb'};
617 $self->{'die_cb'} = sub {
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: $@");
625 _run_psql_command($self, "SELECT pg_start_backup('$label')") or
626 $self->{'die_cb'}->("Failed to call pg_start_backup");
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 {
634 unless(_run_psql_command($self, "SELECT pg_stop_backup()")) {
635 $msg .= " and failed to call pg_stop_backup";
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
644 $self->{'die_cb'} = $old_die_cb;
646 unless (_run_psql_command($self, "SELECT pg_stop_backup()")) {
647 $self->{'die_cb'}->("Failed to call pg_stop_backup");
650 # determine WAL files and append and create their tar file
651 my ($start_wal, $end_wal) = _get_backup_info($self, $label);
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'}");
657 $self->_wait_for_wal($end_wal);
659 # now grab all of the WAL files, *inclusive* of $start_wal
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");
675 _run_tar_totals($self, '--file', "$tmp/$_ARCHIVE_DIR_TAR",
676 '--directory', $self->{'props'}->{'pg-archivedir'}, @wal_files);
678 my $dummydir = $self->_make_dummy_dir();
679 $self->{'done_cb'}->(_run_tar_totals($self,
680 '--directory', $dummydir, "empty-incremental"));
684 # create the final tar file
685 my $size = _run_tar_totals($self, '--directory', $tmp,
686 $_ARCHIVE_DIR_TAR, $_DATA_DIR_TAR);
688 $self->{'state_cb'}->($self, $end_wal);
691 $self->{'done_cb'}->($size);
697 debug("running _incr_backup");
699 my $end_wal = _get_prev_state($self);
701 debug("previously ended at: $end_wal");
703 debug("no previous state found!");
704 return _base_backup(@_);
707 my $adir = new IO::Dir($self->{'props'}->{'pg-archivedir'});
708 $adir or $self->{'die_cb'}->("Could not open archive WAL directory");
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");
719 $self->{'state_cb'}->($self, $max_wal ? $max_wal : $end_wal);
722 $self->{'done_cb'}->(_run_tar_totals($self,
723 '--directory', $self->{'props'}->{'pg-archivedir'}, @wal_files));
725 my $dummydir = $self->_make_dummy_dir();
726 $self->{'done_cb'}->(_run_tar_totals($self,
727 '--directory', $dummydir, "empty-incremental"));
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");
742 $self->{'done_cb'} = sub {
744 debug("done. size $size");
745 $size = ceil($size/1024);
746 debug("sending size $size");
747 $msg_fd->print("sendbackup: size $size\n");
749 $self->{'index_h'}->print("/PostgreSQL-Database-$self->{'args'}->{'level'}\n");
751 $msg_fd->print("sendbackup: end\n");
753 $self->{'die_cb'} = sub {
756 $msg_fd->print("! $msg\n");
757 $self->{'done_cb'}->(0);
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");
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");
775 $self->{'unlink_cb'} = sub {
780 if ($self->{'args'}->{'level'} > 0) {
781 _incr_backup($self, \*STDOUT);
783 _base_backup($self, \*STDOUT);
787 sub command_restore {
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);
796 if (!-w $self->{'args'}->{directory}) {
797 $self->print_to_server_and_die("Directory $self->{directory}: $!",
798 $Amanda::Script_App::ERROR);
800 chdir($self->{'args'}->{directory});
802 my $cur_dir = POSIX::getcwd();
804 if (!-d $_ARCHIVE_DIR_RESTORE) {
805 mkdir($_ARCHIVE_DIR_RESTORE) or die("could not create archive WAL directory: $!");
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',
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)");
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: $!");
820 $status = system($self->{'args'}->{'gnutar-path'}, '--extract') >> 8;
821 (0 == $status) or die("Failed to extract base backup (exit status: $status)");
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);
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);
838 sub command_validate {
841 # set up to handle errors correctly
842 $self->{'die_cb'} = sub {
849 if (!defined($self->{'args'}->{'gnutar-path'}) ||
850 !-x $self->{'args'}->{'gnutar-path'}) {
851 return $self->default_validate();
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);
861 $self->print_to_server_and_die("$self->{gnutar} returned error",
862 $Amanda::Application::ERROR);
864 exit($self->{error_status});
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.
899 my $application = Amanda::Application::ampgsql->new($opts);
901 $application->do($ARGV[0]);