Blob Blame History Raw
#!@PERL@
# Copyright (c) 2009-2012 Zmanda, Inc.  All Rights Reserved.
# Copyright (c) 2013-2016 Carbonite, Inc.  All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
#
# Contact information: Carbonite Inc., 756 N Pastoria Ave
# Sunnyvale, CA 94086, USA, or: http://www.zmanda.com

use lib '@amperldir@';
use strict;
use warnings;
use Getopt::Long;

package Amanda::Application::ampgsql;
use base qw(Amanda::Application);
use Carp;
use File::Copy;
use File::Path;
use IO::Dir;
use IO::File;
use IPC::Open3;
use POSIX;
use POSIX qw( ceil );
use Sys::Hostname;
use Symbol;
use Amanda::Constants;
use Amanda::Config qw( :init :getconf  config_dir_relative string_to_boolean );
use Amanda::Debug qw( :logging );
use Amanda::Paths;
use Amanda::Util qw( :constants :encoding quote_string );
use Amanda::MainLoop qw( :GIOCondition );

my $_DATA_DIR_TAR = "data_dir.tar";
my $_ARCHIVE_DIR_TAR = "archive_dir.tar";
my $_WAL_FILE_PAT = qr/\w{24}/;

my $_DATA_DIR_RESTORE = "data";
my $_ARCHIVE_DIR_RESTORE = "archive";

sub new {
    my $class = shift @_;
    my $args = shift @_;
    my $self = $class->SUPER::new($args->{'config'});
    $self->{'args'} = $args;
    $self->{'label-prefix'} = 'amanda';
    $self->{'runtar'}  = "$Amanda::Paths::amlibexecdir/runtar";

    # default arguments (application properties)
    $self->{'args'}->{'statedir'} ||= $Amanda::Paths::GNUTAR_LISTED_INCREMENTAL_DIR;
    $self->{'args'}->{'tmpdir'} ||= $AMANDA_TMPDIR;
    # XXX: when using runtar, this is not actually honored.
    # So, this only works for restore at the moment
    $self->{'args'}->{'gnutar-path'} ||= $Amanda::Constants::GNUTAR;

    if (!defined $self->{'args'}->{'disk'}) {
	$self->{'args'}->{'disk'} = $self->{'args'}->{'device'};
    }
    if (!defined $self->{'args'}->{'device'}) {
	$self->{'args'}->{'device'} = $self->{'args'}->{'disk'};
    }
    # default properties
    $self->{'props'} = {
        'pg-cleanupwal' => 'yes',
	'pg-max-wal-wait' => 60,
	'pg-full-wal' => 'incr',
	'pg-incremental' => 'no',
	'pg-remove-full-wal' => 'yes',
	'pg-remove-incremental-wal' => 'no',
    };

    my @PROP_NAMES = qw(pg-host pg-port pg-db pg-user pg-password pg-passfile
			psql-path pg-datadir pg-archivedir pg-cleanupwal
			pg-max-wal-wait  pg-full-wal
			pg-remove-full-wal pg-remove-incremental-wal
			pg-incremental );

    # config is loaded by Amanda::Application (and Amanda::Script_App)
    my $conf_props = getconf($CNF_PROPERTY);
    # check for properties like 'pg-host'
    foreach my $pname (@PROP_NAMES) {
        if ($conf_props->{$pname}) {
            debug("More than one value for $pname. Using the first.")
                if scalar(@{$conf_props->{$pname}->{'values'}}) > 1;
            $self->{'props'}->{$pname} = $conf_props->{$pname}->{'values'}->[0];
        }
    }

    # check for properties like 'foo-pg-host' where the diskname is 'foo'
    if ($self->{'args'}->{'disk'}) {
        foreach my $pname (@PROP_NAMES) {
            my $tmp = "$self->{'args'}->{'disk'}-$pname";
            if ($conf_props->{$tmp}) {
                debug("More than one value for $tmp. Using the first.")
                    if scalar(@{$conf_props->{$tmp}->{'values'}}) > 1;
                $self->{'props'}->{$pname} = $conf_props->{$tmp}->{'values'}->[0];
            }
        }
    }

    # overwrite with dumptype properties if they are set.
    foreach my $pname (@PROP_NAMES) {
	my $pdumpname = $pname;
	$pdumpname =~ s/^pg-//g;
	$self->{'props'}->{$pname} = $self->{'args'}->{$pdumpname}
				 if defined $self->{'args'}->{$pdumpname};
	debug("prop $pname set from dumpname $pdumpname: $self->{'args'}->{$pdumpname}")
	      if defined $self->{'args'}->{$pdumpname};
    }

    unless ($self->{'props'}->{'psql-path'}) {
        foreach my $pre (split(/:/, $ENV{PATH})) {
            my $psql = "$pre/psql";
            if (-x $psql) {
                $self->{'props'}{'psql-path'} = $psql;
                last;
            }
        }
    }

    foreach my $aname (keys %{$self->{'args'}}) {
        if (defined($self->{'args'}->{$aname})) {
            debug("app property: $aname $self->{'args'}->{$aname}");
        } else {
            debug("app property: $aname (undef)");
        }
    }

    foreach my $pname (keys %{$self->{'props'}}) {
        if (defined($self->{'props'}->{$pname})) {
            debug("client property: $pname $self->{'props'}->{$pname}");
        } else {
            debug("client property: $pname (undef)");
        }
    }

    if (!exists $self->{'props'}->{'pg-datadir'}) {
	$self->{'props'}->{'pg-datadir'} =  $self->{'args'}->{'device'};
    }

    $self->{'props'}->{'pg-full-wal'} ||= 'incr';
    $self->{'props'}->{'pg-full-wal'} = lc($self->{'props'}->{'pg-full-wal'});
    $self->{'args'}->{'verbose'} ||= "no";
    $self->{'args'}->{'verbose'} = string_to_boolean($self->{'args'}->{'verbose'});

    return $self;
}

sub command_support {
   my $self = shift;

   print <<EOF;
CONFIG YES
HOST YES
DISK YES
MAX-LEVEL 9
INDEX-LINE YES
INDEX-XML NO
MESSAGE-LINE YES
MESSAGE-XML NO
RECORD YES
COLLECTION NO
CLIENT-ESTIMATE YES
MULTI-ESTIMATE NO
CALCSIZE NO
EXCLUDE-FILE YES
EXCLUDE-LIST YES
EOF
}

sub _check {
    my ($desc, $succ_suf, $err_suf, $check, @check_args) = @_;
    my $ret = $check->(@check_args);
    my $msg = $ret? "OK $desc $succ_suf" :  "ERROR $desc $err_suf";
    debug($msg);
    print "$msg\n";
    $ret;
}

sub _check_parent_dirs {
    my ($dir) = @_;
    my $ok = 1;
    my $is_abs = substr($dir, 0, 1) eq "/";
    _check("$dir is an absolute path?", "Yes", "No. It should start with '/'",
       sub {$is_abs});

    my @parts = split('/', $dir);
    pop @parts; # don't test the last part
    my $partial_path = '';
    for my $path_part (@parts) {
        $partial_path .= $path_part . (($partial_path || $is_abs)? '/' : '');
        $ok &&=
	    _check("$partial_path is executable?", "Yes", "No",
               sub {-x $_[0]}, $partial_path);
        $ok &&=
	    _check("$partial_path is a directory?", "Yes", "No",
               sub {-d $_[0]}, $partial_path);
    }
    $ok;
}

sub _ok_passfile_perms {
    my $passfile = shift @_;
    # libpq uses stat, so we use stat
    my @fstat = stat($passfile);
    return 0 unless @fstat;
    return 0 if 077 & $fstat[2];
    return -r $passfile;
}

sub _run_psql_command {
    my ($self, $cmd) = @_;

    # n.b. deprecated, passfile recommended for better security
    my $orig_pgpassword = $ENV{'PGPASSWORD'};
   $ENV{'PGPASSWORD'} = $self->{'props'}->{'pg-password'} if $self->{'props'}->{'pg-password'};
    # n.b. supported in 8.1+
    my $orig_pgpassfile = $ENV{'PGPASSFILE'};
    $ENV{'PGPASSFILE'} = $self->{'props'}->{'pg-passfile'} if $self->{'props'}->{'pg-passfile'};

    my @cmd = ($self->{'props'}->{'psql-path'});
    push @cmd, "-X";
    push @cmd, "-h", $self->{'props'}->{'pg-host'} if ($self->{'props'}->{'pg-host'});
    push @cmd, "-p", $self->{'props'}->{'pg-port'} if ($self->{'props'}->{'pg-port'});
    push @cmd, "-U", $self->{'props'}->{'pg-user'} if ($self->{'props'}->{'pg-user'});

    if (!($cmd =~ /pg_xlogfile_name_offset/) && !$self->{'args'}->{'verbose'}) {
	push @cmd, '--quiet', '--output', '/dev/null';
    }
    push @cmd, '--command', $cmd;
    push @cmd,$self->{'props'}->{'pg-db'} if exists $self->{'props'}->{'pg-db'};
    debug("running " . join(" ", @cmd));

    my ($wtr, $rdr);
    my $err = Symbol::gensym;
    my $pid = open3($wtr, $rdr, $err, @cmd);
    close($wtr);

    my $file_to_close = 2;
    my $psql_stdout_src = Amanda::MainLoop::fd_source($rdr,
						$G_IO_IN|$G_IO_HUP|$G_IO_ERR);
    my $psql_stderr_src = Amanda::MainLoop::fd_source($err,
						$G_IO_IN|$G_IO_HUP|$G_IO_ERR);
    $psql_stdout_src->set_callback(sub {
	my $line = <$rdr>;
	if (!defined $line) {
	    $file_to_close--;
	    $psql_stdout_src->remove();
	    Amanda::MainLoop::quit() if $file_to_close == 0;
	    return;
	}
	chomp $line;
	return if $line =~ /^\s*$/;
	debug("psql stdout: $line");
	if ($cmd =~ /pg_xlogfile_name_offset/) {
	    return if $line =~ /file_name/;
	    return if $line =~ /------/;
	    return if $line =~ /\(1 row\)/;
	    if ($line =~ /^ ($_WAL_FILE_PAT)/) {
		$self->{'switch_xlog_filename'} = $1;
		return;
	    }
	}
	if ($line =~ /NOTICE: pg_stop_backup complete, all required WAL segments have been archived/) {
	} else {
	    $self->print_to_server("psql stdout: $line",
				   $Amanda::Script_App::GOOD);
	}
    });
    $psql_stderr_src->set_callback(sub {
	my $line = <$err>;
	if (!defined $line) {
	    $file_to_close--;
	    $psql_stderr_src->remove();
	    Amanda::MainLoop::quit() if $file_to_close == 0;
	    return;
	}
	chomp $line;
	debug("psql stderr: $line");
	if ($line =~ /NOTICE: pg_stop_backup complete, all required WAL segments have been archived/) {
	} elsif ($line =~ /could not connect to server/) {
	    $self->print_to_server("psql stderr: $line",
				   $Amanda::Script_App::ERROR);
	} else {
	    $self->print_to_server("psql stderr: $line",
				   $Amanda::Script_App::GOOD);
	}
    });

    Amanda::MainLoop::run();
    close($rdr);
    close($err);

    waitpid $pid, 0;
    my $status = $?;

    $ENV{'PGPASSWORD'} = $orig_pgpassword || '';
    $ENV{'PGPASSFILE'} = $orig_pgpassfile || '';

    return 0 == ($status >> 8)
}

sub command_selfcheck {
    my $self = shift;

   # set up to handle errors correctly
   $self->{'die_cb'} = sub {
       my ($msg) = @_;
       debug("$msg");
       print "$msg\n";
       exit(1);
   };

    $self->print_to_server("disk " . quote_string($self->{args}->{disk}), $Amanda::Script_App::GOOD);

    $self->print_to_server("ampgsql version " . $Amanda::Constants::VERSION,
			   $Amanda::Script_App::GOOD);

    if ($self->{'props'}->{'pg-full-wal'} ne "no" &&
	$self->{'props'}->{'pg-full-wal'} ne "full" &&
	$self->{'props'}->{'pg-full-wal'} ne "incr") {
	$self->print_to_server("FULL-WAL property must be set to 'NO', 'FULL' or 'INCR'",
			       $Amanda::Script_App::ERROR);
    }
    for my $k (keys %{$self->{'args'}}) {
        print "OK application property: $k = $self->{'args'}->{$k}\n";
    }

    _check("GNUTAR-PATH $self->{'args'}->{'gnutar-path'}",
           "is executable", "is NOT executable",
           sub {-x $_[0]}, $self->{'args'}->{'gnutar-path'});
    _check("GNUTAR-PATH $self->{'args'}->{'gnutar-path'}",
           "is not a directory (okay)", "is a directory (it shouldn't be)",
           sub {!(-d $_[0])}, $self->{'args'}->{'gnutar-path'});
    _check_parent_dirs($self->{'args'}->{'gnutar-path'});

    _check("GNUTAR $Amanda::Constants::GNUTAR",
           "is executable", "is NOT executable",
           sub {-x $_[0]}, $Amanda::Constants::GNUTAR);
    _check("GNUTAR $Amanda::Constants::GNUTAR",
           "is not a directory (okay)", "is a directory (it shouldn't be)",
           sub {!(-d $_[0])}, $Amanda::Constants::GNUTAR);
    _check_parent_dirs($Amanda::Constants::GNUTAR);

    _check("TMPDIR '$self->{'args'}->{'tmpdir'}'",
           "is an acessible directory", "is NOT an acessible directory",
           sub {$_[0] && -d $_[0] && -r $_[0] && -w $_[0] && -x $_[0]},
           $self->{'args'}->{'tmpdir'});

    if (exists $self->{'props'}->{'pg-datadir'}) {
	_check("PG-DATADIR property is",
	       "same as diskdevice", "differrent than diskdevice",
	       sub { $_[0] eq $_[1] },
	       $self->{'props'}->{'pg-datadir'}, $self->{'args'}->{'device'});
    } else {
	$self->{'props'}->{'pg-datadir'} = $self->{'args'}->{'device'};
    }

    _check("PG-DATADIR property", "is set", "is NOT set",
	   sub { $_[0] }, $self->{'props'}->{'pg-datadir'});
       # note that the backup user need not be able ot read this dir

    _check("STATEDIR '$self->{'args'}->{'statedir'}'",
           "is an acessible directory", "is NOT an acessible directory",
           sub {$_[0] && -d $_[0] && -r $_[0] && -w $_[0] && -x $_[0]},
           $self->{'args'}->{'statedir'});
    _check_parent_dirs($self->{'args'}->{'statedir'});

    if ($self->{'args'}->{'device'}) {
	my $try_connect = 1;

        for my $k (keys %{$self->{'props'}}) {
            print "OK client property: $k = $self->{'props'}->{$k}\n";
        }

        if (_check("PG-ARCHIVEDIR property", "is set", "is NOT set",
               sub { $_[0] }, $self->{'props'}->{'pg-archivedir'})) {
	    _check("PG-ARCHIVEDIR $self->{'props'}->{'pg-archivedir'}",
		   "is a directory", "is NOT a directory",
		   sub {-d $_[0]}, $self->{'props'}->{'pg-archivedir'});
	    _check("PG-ARCHIVEDIR $self->{'props'}->{'pg-archivedir'}",
		   "is readable", "is NOT readable",
		   sub {-r $_[0]}, $self->{'props'}->{'pg-archivedir'});
	    _check("PG-ARCHIVEDIR $self->{'props'}->{'pg-archivedir'}",
		   "is executable", "is NOT executable",
		   sub {-x $_[0]}, $self->{'props'}->{'pg-archivedir'});
	    _check_parent_dirs($self->{'props'}->{'pg-archivedir'});
	}

	if (defined $self->{'props'}->{'pg-datadir'} and
	    defined $self->{'props'}->{'pg-archivedir'}) {
	    _check("ARCHIVEDIR", "is not inside DATADIR", "is inside DATADIR",
	           sub {$self->{'props'}->{'pg-datadir'} ne substr($self->{'props'}->{'pg-archivedir'}, 0, length($self->{'props'}->{'pg-datadir'}))});
	}

	$try_connect &&=
	    _check("Are both PG-PASSFILE and PG-PASSWORD set?",
		   "No (okay)",
		   "Yes. Please set only one or the other",
		   sub {!($self->{'props'}->{'pg-passfile'} and
			  $self->{'props'}->{'pg-password'})});

        if ($self->{'props'}->{'pg-passfile'}) {
	    $try_connect &&=
		_check("PG-PASSFILE $self->{'props'}->{'pg-passfile'}",
                   "has correct permissions", "does not have correct permissions",
                   \&_ok_passfile_perms, $self->{'props'}->{'pg-passfile'});
	    $try_connect &&=
		_check_parent_dirs($self->{'props'}->{'pg-passfile'});
        }

        if (_check("PSQL-PATH property", "is set", "is NOT set and psql is not in \$PATH",
               sub { $_[0] }, $self->{'props'}->{'psql-path'})) {
	    $try_connect &&=
		_check("PSQL-PATH $self->{'props'}->{'psql-path'}",
		       "is executable", "is NOT executable",
		       sub {-x $_[0]}, $self->{'props'}->{'psql-path'});
	    $try_connect &&=
		_check("PSQL-PATH $self->{'props'}->{'psql-path'}",
		       "is not a directory (okay)", "is a directory (it shouldn't be)",
		       sub {!(-d $_[0])}, $self->{'props'}->{'psql-path'});
	    $try_connect &&=
		_check_parent_dirs($self->{'props'}->{'psql-path'});
	} else {
	    $try_connect = 0;
	}

	if ($try_connect) {
	    my @pv = `$self->{'props'}->{'psql-path'} --version`;
	    if ($? >> 8 == 0) {
		$pv[0] =~ /^[^0-9]*([0-9.]*)[^0-9]*$/;
		my $pv = $1;
		$self->print_to_server("ampgsql psql-version $pv",
				       $Amanda::Script_App::GOOD);
	    } else {
		$self->print_to_server(
		"[Can't get " . $self->{'props'}->{'psql-path'} . " version]\n",
		$Amanda::Script_App::ERROR);
	    }
	}

	if ($try_connect) {
	    $try_connect &&=
		_check("Connecting to database server", "succeeded", "failed",
		   \&_run_psql_command, $self, '');
	}

	{
	    my @gv = `$self->{'args'}->{'gnutar-path'} --version`;
	    if ($? >> 8 == 0) {
		$gv[0] =~ /^[^0-9]*([0-9.]*)[^0-9]*$/;
		my $gv = $1;
		$self->print_to_server("ampgsql gtar-version $gv",
				       $Amanda::Script_App::GOOD);
	    } else {
		$self->print_to_server(
		"[Can't get " . $self->{'props'}->{'gnutar-path'} . " version]\n",
		$Amanda::Script_App::ERROR);
	    }
	}
    }
}

sub _filename_prefix {
    my $self = shift;

    my @parts = ("ampgsql", hexencode($self->{'args'}->{'host'}), hexencode($self->{'args'}->{'disk'}));
    return $self->{'args'}->{'statedir'} . '/'  . join("-", @parts)
}

sub _state_filename {
    my ($self, $level) = @_;

    my $prefix = $self->_filename_prefix();
    my $statefile = $prefix . "-$level";
    debug("statefile: $statefile");
    return $statefile;
}

sub _last_backup_filename {
    my $self = shift;

    my $prefix = $self->_filename_prefix();
    my $lastfile = $prefix . "-last_backup";
    debug("lastfile $lastfile");
    return $lastfile
}

sub _get_last_backup {
    my $self = shift;

    my $h = new IO::File($self->_last_backup_filename(), "r");
    $h or return undef;

    my $line = <$h>;
    $line =~ /LEVEL: (\d*)/;
    my $level = $1;
    $h->close();

    return $level;
}

sub _write_last_backup {
    my $self = shift;
    my $level = shift;

    my $h = new IO::File($self->_last_backup_filename(), "w");
    $h or return undef;

    debug("writing last file");
    $h->print("LEVEL: $level\n");
    $h->close;
}

sub _write_state_file {
    my ($self, $end_wal) = @_;

    my $h = new IO::File(_state_filename($self, $self->{'args'}->{'level'}), "w");
    $h or return undef;

    debug("writing state file");
    $h->print("VERSION: 0\n");
    $h->print("LAST WAL FILE: $end_wal\n");
    $h->close();
    1;
}

sub _get_prev_state {
    my $self = shift @_;
    my $initial_level = shift;
    $initial_level = $self->{'args'}->{'level'} - 1 if !defined $initial_level;

    my $end_wal;
    for (my $level = $initial_level; $level >= 0; $level--) {
        my $fn = _state_filename($self, $level);
        debug("reading state file: $fn");
        my $h = new IO::File($fn, "r");
        next unless $h;
        while (my $l = <$h>) {
	    chomp $l;
	    debug("  $l");
            if ($l =~ /^VERSION: (\d+)/) {
                unless (0 == $1) {
                    $end_wal = undef;
                    last;
                }
            } elsif ($l =~ /^LAST WAL FILE: ($_WAL_FILE_PAT)/) {
                $end_wal = $1;
            }
        }
        $h->close();
        last if $end_wal;
    }
    $end_wal;
}

sub _make_dummy_dir_base {
    my ($self) = @_;

   my $dummydir = "$self->{'args'}->{'tmpdir'}/ampgsql-dummy-$$";
   mkpath("$dummydir/$_ARCHIVE_DIR_RESTORE");

   return $dummydir;
}

sub _make_dummy_dir {
    my ($self) = @_;

   my $dummydir = "$self->{'args'}->{'tmpdir'}/ampgsql-dummy-$$";
   mkpath($dummydir);
   open(my $fh, ">$dummydir/empty-incremental");
   close($fh);

   return $dummydir;
}

sub _run_tar_totals {
    my ($self, @other_args) = @_;

    my @cmd;
    @cmd = ($self->{'runtar'}, $self->{'args'}->{'config'},
        $Amanda::Constants::GNUTAR, '--create', '--totals',
				    '--blocking-factor', '64',
				    @other_args);
    debug("running: " . join(" ", @cmd));

    local (*TAR_IN, *TAR_OUT, *TAR_ERR);
    open TAR_OUT, ">&", $self->{'out_h'};
    my $pid;
    eval { $pid = open3(\*TAR_IN, ">&TAR_OUT", \*TAR_ERR, @cmd); 1;} or
        $self->{'die_cb'}->("failed to run tar. error was $@");
    close(TAR_IN);

    # read stderr
    my $size;
    while (my $l = <TAR_ERR>) {
        if ($l =~ /^Total bytes written: (\d+)/) {
            $size = $1;
        } elsif ($l =~ /file changed as we read it/ ||
		 $l =~ /File removed before we read it/ ||
		 $l =~ /File shrank by \d* bytes; padding with zeros/) {
	    chomp $l;
	    $self->print_to_server($l, $Amanda::Script_App::GOOD);
	    debug("TAR_ERR: $l");
        } else {
	    chomp $l;
	    $self->print_to_server($l, $Amanda::Script_App::ERROR);
	    debug("TAR_ERR: $l");
	}
    }
    waitpid($pid, 0);
    my $status = POSIX::WEXITSTATUS($?);

    close(TAR_ERR);
    debug("size of generated tar file: " . (defined($size)? $size : "undef"));
    if ($status == 1) {
	debug("ignored non-fatal tar exit status of 1");
    } elsif ($status) {
	$self->{'die_cb'}->("Tar failed (exit status $status)");
    }
    $size;
}

sub command_estimate {
   my $self = shift;

   $self->{'out_h'} = new IO::File("/dev/null", "w");
   $self->{'out_h'} or die("Could not open /dev/null");
   $self->{'index_h'} = new IO::File("/dev/null", "w");
   $self->{'index_h'} or die("Could not open /dev/null");

   $self->{'done_cb'} = sub {
       my $size = shift @_;
       debug("done. size $size");
       $size = ceil($size/1024);
       debug("sending $self->{'args'}->{'level'} $size 1");
       print("$self->{'args'}->{'level'} $size 1\n");
   };
   $self->{'die_cb'} = sub {
       my $msg = shift @_;
       debug("$msg");
       $self->{'done_cb'}->(-1);
       die($msg);
   };
   $self->{'state_cb'} = sub {
       # do nothing
   };
   $self->{'unlink_cb'} = sub {
       # do nothing
   };

   if ($self->{'args'}->{'level'} > 0) {
       _incr_backup($self);
   } else {
       _base_backup($self);
   }
}

sub _get_backup_info {
    my ($self, $label) = @_;

   my ($fname, $bfile, $start_wal, $end_wal);
   # wait up to 60s for the .backup file to be copied
   for (my $count = 0; $count < 60; $count++) {
       my $adir = new IO::Dir($self->{'props'}->{'pg-archivedir'});
       $adir or $self->{'die_cb'}->("Could not open archive WAL directory");
       while (defined($fname = $adir->read())) {
           if ($fname =~ /\.backup$/) {
               my $blabel;
               # use runtar to read a protected file, then grep the resulting tarfile (yes,
	       # this works!)
               local *TAROUT;
               my $conf = $self->{'args'}->{'config'} || 'NOCONFIG';
               my $cmd = "$self->{'runtar'} $conf $Amanda::Constants::GNUTAR --create --dereference --file - --directory $self->{'props'}->{'pg-archivedir'} $fname | $Amanda::Constants::GNUTAR --file - --extract --to-stdout";
               debug("running: $cmd");
               open(TAROUT, "$cmd |");
               my ($start, $end, $lab);
               while (my $l = <TAROUT>) {
                   chomp($l);
                   if ($l =~ /^START WAL LOCATION:.*?\(file ($_WAL_FILE_PAT)\)$/) {
                       $start = $1;
                   } elsif($l =~ /^STOP WAL LOCATION:.*?\(file ($_WAL_FILE_PAT)\)$/) {
                       $end = $1;
                   } elsif ($l =~ /^LABEL: (.*)$/) {
                       $lab = $1;
                   }
               }
	       close TAROUT;
               if ($lab and $lab eq $label) {
                   $start_wal = $start;
                   $end_wal = $end;
                   $bfile = $fname;
                   last;
               } else {
		   debug("logfile had non-matching label");
	       }
           }
       }
       $adir->close();
       if ($start_wal and $end_wal) {
	   debug("$bfile named WALs $start_wal .. $end_wal");

           # try to cleanup a bit, although this may fail and that's ok
	   my $filename = "$self->{'props'}->{'pg-archivedir'}/$bfile";
           if (unlink($filename) == 0) {
               debug("Failed to unlink '$filename': $!");
               $self->print_to_server("Failed to unlink '$filename': $!",
				      $Amanda::Script_App::ERROR);
	   }
           last;
       }
       sleep(1);
   }

   ($start_wal, $end_wal);
}

# return the postgres version as an integer
sub _get_pg_version {
    my $self = shift;

    local *VERSOUT;

    my @cmd = ($self->{'props'}->{'psql-path'});
    push @cmd, "-X";
    push @cmd, "--version";
    my $pid = open3('>STDIN', \*VERSOUT, '>STDERR', @cmd)
	or $self->{'die_cb'}->("could not open psql to determine version");
    my @lines = <VERSOUT>;
    waitpid($pid, 0);
    $self->{'die_cb'}->("could not run psql to determine version") if (($? >> 8) != 0);

    my ($maj, $min, $pat);
    if ($lines[0] =~ /([0-9]+)\.([0-9]+)\.([0-9]+)/) {
        $maj = $1;
        $min = $2;
        $pat = $3;
    } elsif ($lines[0] =~ /([0-9]+)\.([0-9]+)/) {
        $maj = $1;
        $min = $2;
        $pat = 0;
    } elsif ($lines[0] =~ /([0-9]+)/) {
        $maj = $1;
        $min = 0;
        $pat = 0;
    }

    return $maj * 10000 + $min * 100 + $pat;
}

# create a large table and immediately drop it; this can help to push a WAL file out
sub _write_garbage_to_db {
    my $self = shift;

    debug("writing garbage to database to force a WAL archive");

    # note: lest ye be tempted to add "if exists" to the drop table here, note that
    # the clause was not supported in 8.1
    _run_psql_command($self, <<EOF) or
CREATE TABLE _ampgsql_garbage AS SELECT * FROM GENERATE_SERIES(1, 500000);
DROP TABLE _ampgsql_garbage;
EOF
        $self->{'die_cb'}->("Failed to create or drop table _ampgsql_garbage");
}

# wait up to pg-max-wal-wait seconds for a WAL file to appear
sub _wait_for_wal {
    my ($self, $wal) = @_;
    my $pg_version = $self->_get_pg_version();

    my $archive_dir = $self->{'props'}->{'pg-archivedir'};
    my $maxwait = 0+$self->{'props'}->{'pg-max-wal-wait'};

    if ($maxwait) {
	debug("waiting $maxwait s for WAL $wal to be archived..");
    } else {
	debug("waiting forever for WAL $wal to be archived..");
    }

    my $count = 0; # try at least 4 cycles
    my $stoptime = time() + $maxwait;
    while ($maxwait == 0 || time < $stoptime || $count++ < 4) {
	if (-f "$archive_dir/$wal") {
	    sleep(1);
	    return;
	}

	# for versions 8.0 or 8.1, the only way to "force" a WAL archive is to write
	# garbage to the database.
	if ($pg_version < 80200) {
	    $self->_write_garbage_to_db();
	} else {
	    sleep(1);
	}
    }

    $self->{'die_cb'}->("WAL file $wal was not archived in $maxwait seconds");
}

sub _remove_wal_files {
    my $self = shift;
    my $start_wal = shift;
    my $last_end_wal = shift;

    if (defined $start_wal || defined $last_end_wal) {
	my $adir = new IO::Dir($self->{'props'}->{'pg-archivedir'});
	$adir or $self->{'die_cb'}->("Could not open archive WAL directory");
	while (defined(my $fname = $adir->read())) {
	    if ($fname =~ /^$_WAL_FILE_PAT$/) {
		if ((defined $start_wal and $fname lt $start_wal) ||
		    (defined $last_end_wal and $fname lt $last_end_wal)) {
		    $self->{'unlink_cb'}->("$self->{'props'}->{'pg-archivedir'}/$fname");
		}
            }
	}
	$adir->close();
    }
}

sub _base_backup {
   my ($self) = @_;

   debug("running _base_backup");

   my $label = "$self->{'label-prefix'}-" . time();

   -d $self->{'props'}->{'pg-archivedir'} or
	die("WAL file archive directory does not exist (or is not a directory)");

   if ($self->{'action'} eq 'backup') {
       _run_psql_command($self, "SELECT pg_start_backup('$label')") or
           $self->{'die_cb'}->("Failed to call pg_start_backup");
   }

   # tar data dir, using symlink to prefix
   # XXX: tablespaces and their symlinks?
   # See: http://www.postgresql.org/docs/8.0/static/manage-ag-tablespaces.html
   my $old_die_cb = $self->{'die_cb'};
   $self->{'die_cb'} = sub {
       my $msg = shift @_;
       if ($self->{'action'} eq 'backup') {
           unless(_run_psql_command($self, "SELECT pg_stop_backup()")) {
               $msg .= " and failed to call pg_stop_backup";
	   }
       }
       $old_die_cb->($msg);
   };

   my @args = ('--dereference', '--file', "-",
       '--directory', $self->{'props'}->{'pg-datadir'},
       '--exclude', 'postmaster.pid',
       '--exclude', 'pg_xlog/*', # contains WAL files; will be handled below
       '--exclude', 'postmaster.pid',
       '--exclude', 'postmaster.opts',
       '--exclude', 'pg_replslot/*');
   if (defined $self->{args}->{'exclude-file'}) {
       foreach my $excl (@{$self->{args}->{'exclude-file'}}) {
           push @args, '--exclude', $excl;
       }
   }
   if (defined $self->{args}->{'exclude-list'}) {
       foreach my $excl (@{$self->{args}->{'exclude-list'}}) {
           push @args, '--exclude-from', $excl;
       }
   }
   push @args, '--transform', "s,^./pg_tblspc,./pg_tblspc_data,S;s,^,$_DATA_DIR_RESTORE/,S",
               '.';

   my $size = _run_tar_totals($self,@args);

   # tar the symlink in pg_tblspc
   $size += _run_tar_totals($self, '--file', "-",
		'--directory', "$self->{'props'}->{'pg-datadir'}",
		'--transform', "s,^,$_DATA_DIR_RESTORE/,S",
                "./pg_tblspc");

   if ($self->{'action'} eq 'backup') {
       unless (_run_psql_command($self, "SELECT pg_stop_backup()")) {
           $self->{'die_cb'}->("Failed to call pg_stop_backup");
       }
   }
   $self->{'die_cb'} = $old_die_cb;

   # determine WAL files and append and create their tar file
   my $last_end_wal;
   my $start_wal;
   my $end_wal;

   if ($self->{'action'} eq 'backup') {
	($start_wal, $end_wal)  = _get_backup_info($self, $label);
	($start_wal and $end_wal)
		or $self->{'die_cb'}->("A .backup file was never found in the archive "
				    . "dir $self->{'props'}->{'pg-archivedir'}");
	$self->_wait_for_wal($end_wal);
	if ($self->{'props'}->{'pg-full-wal'} eq "full") {
	    $last_end_wal = _get_prev_state($self, 0);
	} elsif ($self->{'props'}->{'pg-full-wal'} eq "incr") {
	    my $level = $self->_get_last_backup();
	    $last_end_wal = _get_prev_state($self, $level);
	}
   } else {
	$start_wal = undef;
	$end_wal = _get_prev_state($self, 0);
   }

   # now grab all of the WAL files, *inclusive* of $start_wal
   my $cleanup_wal_val = $self->{'props'}->{'pg-cleanupwal'} || 'yes';
   my $cleanup_wal = string_to_boolean($cleanup_wal_val);
   my @wal_files;
   my $adir = new IO::Dir($self->{'props'}->{'pg-archivedir'});
   $adir or $self->{'die_cb'}->("Could not open archive WAL directory");
   while (defined(my $fname = $adir->read())) {
       if ($fname =~ /^$_WAL_FILE_PAT$/) {
           if (!defined $end_wal ||
	       (!defined $start_wal and ($fname le $end_wal)) ||
	       (defined $start_wal and ($fname ge $start_wal) and
		($fname le $end_wal)) ||
	       (defined $last_end_wal and ($fname gt $last_end_wal) and ($fname le $end_wal))) {
               push @wal_files, $fname;
               debug("will store: $fname");
           } elsif (defined $start_wal and $fname lt $start_wal) {
               if ($cleanup_wal) {
                   $self->{'unlink_cb'}->("$self->{'props'}->{'pg-archivedir'}/$fname");
               }
           }
       }
   }
   $adir->close();

   if (@wal_files) {
       $size += _run_tar_totals($self, '--dereference', '--file', "-",
	   '--directory', $self->{'props'}->{'pg-archivedir'},
	   '--transform', "s,^,$_ARCHIVE_DIR_RESTORE/,S",
	   @wal_files);
       #take the directory itself
       $size += _run_tar_totals($self, '--dereference', '--file', "-",
	   '--directory', $self->{'props'}->{'pg-archivedir'},
	   '--transform', "s,^,$_ARCHIVE_DIR_RESTORE/,S",
	   '--no-recursion', '.');
   } else {
       my $dummydir = $self->_make_dummy_dir_base();
       $self->{'done_cb'}->(_run_tar_totals($self, '--dereference', '--file', '-',
           '--directory', $dummydir, "$_ARCHIVE_DIR_RESTORE"));
       rmtree($dummydir);
   }

    # should be done only on confirmation the backup succeeded from the server
    $self->{'state_cb'}->($self, $end_wal);

    # remove older WAL files.
    my $remove_full_wal_val = $self->{'props'}->{'pg-remove-full-wal'} || 'yes';
    my $remove_full_wal = string_to_boolean($remove_full_wal_val);
    # should be done only on confirmation the backup succeeded from the server
    if ($remove_full_wal) {
	$self->_remove_wal_files($start_wal, $last_end_wal);
    }

    $self->{'done_cb'}->($size);
}

sub _incr_backup {
   my ($self) = @_;

   debug("running _incr_backup");

   my $incremental_val = $self->{'props'}->{'pg-incremental'} || 'yes';
   my $incremental = string_to_boolean($incremental_val);

   if ($self->{'action'} eq 'backup') {
      _run_psql_command($self, "SELECT file_name from pg_xlogfile_name_offset(pg_switch_xlog())");
      if (defined($self->{'switch_xlog_filename'})) {
	 $self->_wait_for_wal($self->{'switch_xlog_filename'});
      }
   }

   my $end_wal;
   if ($incremental) {
	$end_wal = _get_prev_state($self, $self->{'args'}->{'level'});
	for (my $level = $self->{'args'}->{'level'}-1; $level >= 0; $level--) {
	    my $xend_wal = _get_prev_state($self, $level);
	    if (!$end_wal || $xend_wal lt $end_wal) {
		$end_wal = $xend_wal;
	    }
	}
   } else {
	$end_wal = _get_prev_state($self);
   }
   if ($end_wal) {
       debug("previously ended at: $end_wal");
   } else {
       debug("no previous state found!");
       return _base_backup(@_);
   }

   my $adir = new IO::Dir($self->{'props'}->{'pg-archivedir'});
   $adir or $self->{'die_cb'}->("Could not open archive WAL directory");
   my $max_wal = "";
   my ($fname, @wal_files);
   while (defined($fname = $adir->read())) {
       if (($fname =~ /^$_WAL_FILE_PAT$/) and ($fname gt $end_wal)) {
           $max_wal = $fname if $fname gt $max_wal;
           push @wal_files, $fname;
           debug("will store: $fname");
       }
   }

   my $size;
   if (@wal_files) {
       $size = _run_tar_totals($self, '--dereference', '--file', '-',
           '--directory', $self->{'props'}->{'pg-archivedir'}, @wal_files);
   } else {
       my $dummydir = $self->_make_dummy_dir();
       $size = _run_tar_totals($self, '--dereference', '--file', '-',
           '--directory', $dummydir, "empty-incremental");
       rmtree($dummydir);
   }

   # should be done only on confirmation the backup succeeded from the server
   $self->{'state_cb'}->($self, $max_wal ? $max_wal : $end_wal);

   my $remove_incremental_wal_val = $self->{'props'}->{'pg-remove-incremental-wal'} || 'no';
   my $remove_incremental_wal = string_to_boolean($remove_incremental_wal_val);
   # should be done only on confirmation the backup succeeded from the server
   if ($remove_incremental_wal) {
	my $end_wal = pop @wal_files;   # Do not unlink the latest wal files
	foreach my $wal_file (@wal_files) {
	    $end_wal = $wal_file if $wal_file gt $end_wal;
	}
	$self->_remove_wal_files(undef, $end_wal);
   }

   $self->{'done_cb'}->($size);
}

sub command_backup {
   my $self = shift;

   $self->{'out_h'} = IO::Handle->new_from_fd(1, 'w');
   $self->{'out_h'} or die("Could not open data fd");
   my $msg_fd = IO::Handle->new_from_fd(3, 'w');
   $msg_fd or die("Could not open message fd");
   $self->{'index_h'} = IO::Handle->new_from_fd(4, 'w');
   $self->{'index_h'} or die("Could not open index fd");

   $self->{'done_cb'} = sub {
       my $size = shift @_;
       debug("done. size $size");
       $size = ceil($size/1024);
       debug("sending size $size");
       $msg_fd->print("sendbackup: size $size\n");

       $self->{'index_h'}->print("/PostgreSQL-Database-$self->{'args'}->{'level'}\n");

   };
   $self->{'die_cb'} = sub {
       my $msg = shift @_;
       debug("$msg");
       $msg_fd->print("! $msg\n");
       $self->{'done_cb'}->(0);
       exit(1);
   };
   $self->{'state_cb'} = sub {
       my ($self, $end_wal) = @_;
       _write_state_file($self, $end_wal) or $self->{'die_cb'}->("Failed to write state file");
       $self->_write_last_backup($self->{'args'}->{'level'});
   };
   my $cleanup_wal_val = $self->{'props'}->{'pg-cleanupwal'} || 'yes';
   my $cleanup_wal = string_to_boolean($cleanup_wal_val);
   if (!defined($cleanup_wal)) {
       $self->{'die_cb'}->("couldn't interpret PG-CLEANUPWAL value '$cleanup_wal_val' as a boolean");
   }

   $self->{'unlink_cb'} = sub {
       my $filename = shift @_;
       debug("unlinking WAL file $filename");
       if (unlink($filename) == 0) {
           debug("Failed to unlink '$filename': $!");
           $self->print_to_server("Failed to unlink '$filename': $!",
                                  $Amanda::Script_App::ERROR);
       }
   };

   if ($self->{'args'}->{'level'} > 0) {
       _incr_backup($self, \*STDOUT);
   } else {
       _base_backup($self, \*STDOUT);
   }
}

sub command_restore {
    my $self = shift;

    chdir(Amanda::Util::get_original_cwd());
    if (defined $self->{'args'}->{directory}) {
	if (!-d $self->{'args'}->{directory}) {
	    $self->print_to_server_and_die("Directory $self->{directory}: $!",
					   $Amanda::Script_App::ERROR);
	}
	if (!-w $self->{'args'}->{directory}) {
	    $self->print_to_server_and_die("Directory $self->{directory}: $!",
					   $Amanda::Script_App::ERROR);
	}
	chdir($self->{'args'}->{directory});
    }
    my $cur_dir = POSIX::getcwd();

    if (!-d $_ARCHIVE_DIR_RESTORE) {
	mkdir($_ARCHIVE_DIR_RESTORE) or die("could not create archive WAL directory: $!");
    }
    my $status;
    if ($self->{'args'}->{'level'} > 0) {
	debug("extracting incremental backup to $cur_dir/$_ARCHIVE_DIR_RESTORE");
	$status = system($self->{'args'}->{'gnutar-path'},
		'--extract',
		'--dereference',
		'--file', '-',
		'--ignore-zeros',
		'--exclude', 'empty-incremental',
		'--directory', $_ARCHIVE_DIR_RESTORE) >> 8;
	my $uid = (stat $_ARCHIVE_DIR_RESTORE)[4];
	if ($uid == 0) {
	    opendir(my $dh, $_ARCHIVE_DIR_RESTORE) || die;
	    while (my $name = readdir $dh) {
		next if $name eq '.';
		next if $name eq '..';
		my $uid = (stat "$_ARCHIVE_DIR_RESTORE/$name")[4];
		my $gid = (stat "$_ARCHIVE_DIR_RESTORE/$name")[5];
		chown $uid, $gid, $_ARCHIVE_DIR_RESTORE;
		last;
	    }
	    closedir $dh;
	}
	(0 == $status) or die("Failed to extract level $self->{'args'}->{'level'} backup (exit status: $status)");
    } else {
	debug("extracting base of full backup to $cur_dir/$_DATA_DIR_RESTORE");
	debug("extracting archive dir to $cur_dir/$_ARCHIVE_DIR_RESTORE");
	if (!-d $_DATA_DIR_RESTORE) {
	    mkdir($_DATA_DIR_RESTORE) or die("could not create archive WAL directory: $!");
	}
	my @cmd = ($self->{'args'}->{'gnutar-path'}, '--extract',
		'--dereference',
		'--file', '-',
		'--ignore-zero',
		'--transform', "s,^DATA/,$_DATA_DIR_RESTORE/,S",
		'--transform', "s,^WAL/,$_ARCHIVE_DIR_RESTORE/,S");
	debug("run: " . join ' ',@cmd);
	$status = system(@cmd) >> 8;
	(0 == $status) or die("Failed to extract base backup (exit status: $status)");

	if (-f $_ARCHIVE_DIR_TAR) {
	    debug("extracting archive dir to $cur_dir/$_ARCHIVE_DIR_RESTORE");
	    my @cmd = ($self->{'args'}->{'gnutar-path'}, '--extract',
		'--dereference',
		'--exclude', 'empty-incremental',
		'--file', $_ARCHIVE_DIR_TAR, '--directory',
		$_ARCHIVE_DIR_RESTORE);
	    debug("run: " . join ' ',@cmd);
	    $status = system(@cmd) >> 8;
	    (0 == $status) or die("Failed to extract archived WAL files from base backup (exit status: $status)");
	    if (unlink($_ARCHIVE_DIR_TAR) == 0) {
		debug("Failed to unlink '$_ARCHIVE_DIR_TAR': $!");
		$self->print_to_server(
				"Failed to unlink '$_ARCHIVE_DIR_TAR': $!",
				$Amanda::Script_App::ERROR);
	    }
	}

	if (-f $_DATA_DIR_TAR) {
	    debug("extracting data dir to $cur_dir/$_DATA_DIR_RESTORE");
	    my @cmd = ($self->{'args'}->{'gnutar-path'}, '--extract',
		'--dereference',
		'--file', $_DATA_DIR_TAR,
		'--directory', $_DATA_DIR_RESTORE);
	    debug("run: " . join ' ',@cmd);
	    $status = system(@cmd) >> 8;
	    (0 == $status) or die("Failed to extract data directory from base backup (exit status: $status)");
	    if (unlink($_DATA_DIR_TAR) == 0) {
		debug("Failed to unlink '$_DATA_DIR_TAR': $!");
		$self->print_to_server("Failed to unlink '$_DATA_DIR_TAR': $!",
				$Amanda::Script_App::ERROR);
	    }
	}
    }
}

sub command_validate {
   my $self = shift;

   # set up to handle errors correctly
   $self->{'die_cb'} = sub {
       my ($msg) = @_;
       debug("$msg");
       print "$msg\n";
       exit(1);
   };

   if (!defined($self->{'args'}->{'gnutar-path'}) ||
       !-x $self->{'args'}->{'gnutar-path'}) {
      return $self->default_validate();
   }

   my(@cmd) = ($self->{'args'}->{'gnutar-path'}, "--ignore-zeros", "-tf", "-");
   debug("cmd:" . join(" ", @cmd));
   my $pid = open3('>&STDIN', '>&STDOUT', '>&STDERR', @cmd) ||
      $self->print_to_server_and_die("Unable to run @cmd",
				     $Amanda::Application::ERROR);
   waitpid $pid, 0;
   if ($? != 0){
       $self->print_to_server_and_die("$self->{gnutar} returned error",
				      $Amanda::Application::ERROR);
   }
}

sub command_index {
    my $self = shift;
    my $buffer;

    print "/PostgreSQL-Database-$self->{'args'}->{'level'}\n";

    do {
	sysread STDIN, $buffer, 1048576;
    } while (defined $buffer and length($buffer) > 0);
}

package main;

sub usage {
    print <<EOF;
Usage: ampgsql <command> --config=<config> --host=<host> --disk=<disk> --device=<device> --level=<level> --index=<yes|no> --message=<text> --collection=<no> --record=<yes|no> --calcsize.
EOF
    exit(1);
}

my @orig_argv = @ARGV;

my $opts = {};
my $opt_version;

GetOptions(
    $opts,
    'version' => \$opt_version,
    'config=s',
    'host=s',
    'disk=s',
    'device=s',
    'level=s',
    'index=s',
    'message=s',
    'collection=s',
    'record',
    'calcsize',
    'exclude-file=s@',
    'exclude-list=s@',
    'include-list=s@',
    'directory|target=s',
    # ampgsql-specific
    'statedir=s',
    'tmpdir=s',
    'gnutar-path=s',
    'cleanupwal=s',
    'archivedir=s',
    'db=s',
    'full-wal=s',
    'host=s',
    'max-wal-wait=s',
    'passfile=s',
    'port=s',
    'user=s',
    'psql-path=s',
    'verbose=s',
    'incremental=s',
    'remove-full-wal=s',
    'remove-incremental-wal=s'
) or usage();

if (defined $opt_version) {
    print "ampgsql-" . $Amanda::Constants::VERSION , "\n";
    exit(0);
}

my $application = Amanda::Application::ampgsql->new($opts);

Amanda::Debug::debug("Arguments: " . join(' ', @orig_argv));

$application->do($ARGV[0]);
# NOTREACHED