#!/usr/bin/perl -w

# Upgrade a PostgreSQL cluster to a newer major version.
#
# (C) 2005 Martin Pitt <mpitt@debian.org>

use lib '/usr/share/postgresql-common';
use PgCommon;
use Getopt::Long;
use POSIX;

sub adapt_conffiles {
    # tcpip_socket transition
    my $tcpip_socket = config_bool (PgCommon::get_conf_value $newversion, $cluster,
        'postgresql.conf', 'tcpip_socket');
    my $virtual_host = PgCommon::get_conf_value $newversion, $cluster,
            'postgresql.conf', 'virtual_host';
    if (defined $virtual_host) {
        PgCommon::disable_conf_value $newversion, $cluster, 'postgresql.conf',
                'virtual_host', 'deprecated in favor of listen_addresses';
    }
    if (defined $tcpip_socket) {
        PgCommon::replace_conf_value $newversion, $cluster, 'postgresql.conf',
                'tcpip_socket', 'deprecated in favor of listen_addresses', 
                'listen_addresses', ($tcpip_socket ? ($virtual_host || '*') : '');
    }

    # syslog transition
    my $syslog = PgCommon::get_conf_value $newversion, $cluster,
        'postgresql.conf', 'syslog';
    if (defined $syslog) {
        my %syslog_values = (0 => 'stderr', 1 => 'stderr,syslog', 2 => 'syslog');
        PgCommon::replace_conf_value $newversion, $cluster, 'postgresql.conf',
            'syslog', 'deprecated in favor of log_destination',
            'log_destination', $syslog_values{$syslog};
    }

    # log_statement transition
    my $log_statement = config_bool (PgCommon::get_conf_value $newversion,
        $cluster, 'postgresql.conf', 'log_statement');
    if (defined $log_statement) {
        PgCommon::set_conf_value $newversion, $cluster, 'postgresql.conf',
            'log_statement', ($log_statement ? 'all' : 'none');
    }

    # log_* transition
    my $log_line_prefix = '';

    my $log_timestamp = PgCommon::get_conf_value $newversion, $cluster,
            'postgresql.conf', 'log_timestamp';
    if (defined $log_timestamp) {
        PgCommon::disable_conf_value $newversion, $cluster, 'postgresql.conf',
                'log_timestamp', 'deprecated in favor of log_line_prefix';
    }
    my $log_pid = PgCommon::get_conf_value $newversion, $cluster,
            'postgresql.conf', 'log_pid';
    if (defined $log_pid) {
        PgCommon::disable_conf_value $newversion, $cluster, 'postgresql.conf',
                'log_pid', 'deprecated in favor of log_line_prefix';
    }
    my $log_hostname = PgCommon::get_conf_value $newversion, $cluster,
            'postgresql.conf', 'log_hostname';
    if (defined $log_hostname) {
        PgCommon::disable_conf_value $newversion, $cluster, 'postgresql.conf',
                'log_hostname', 'deprecated in favor of log_line_prefix';
    }
    my $log_source_port = PgCommon::get_conf_value $newversion, $cluster,
            'postgresql.conf', 'log_source_port';
    if (defined $log_source_port) {
        PgCommon::disable_conf_value $newversion, $cluster, 'postgresql.conf',
                'log_source_port', 'deprecated in favor of log_line_prefix';
    }
    $log_line_prefix .= '%t ' if $log_timestamp;
    $log_line_prefix .= '[%p] ' if $log_pid;
    $log_line_prefix .= '%r ' if $log_hostname || $log_source_port;
    chop $log_line_prefix if (substr $log_line_prefix, -1) eq ' ';
    if ($log_line_prefix) {
        PgCommon::set_conf_value $newversion, $cluster, 'postgresql.conf',
            'log_line_prefix', $log_line_prefix;
    }

    # obsolete max_expr_depth
    my $max_expr_depth = PgCommon::get_conf_value $newversion, $cluster,
            'postgresql.conf', 'max_expr_depth';
    if (defined $max_expr_depth) {
        PgCommon::disable_conf_value $newversion, $cluster, 'postgresql.conf',
                'max_expr_depth',
                'does not exist any more, look at max_stack_depth';
    }
}

#
# Execution starts here
#

# command line arguments

$newversion = get_newest_version;

exit 1 unless GetOptions ('v|version=s' => \$newversion);

if ($#ARGV < 1) {
    print "Usage: $0 [-v <newversion>] <version> <cluster name> [<data directory>]\n";
    exit 1;
}

($version, $cluster, $datadir) = @ARGV;
%info = cluster_info ($version, $cluster);
error 'specified cluster does not exist' unless $info{'pgdata'};
error 'specified cluster is not running' unless $info{'running'};

$encoding = get_db_encoding $version, $cluster, 'template1';
error 'could not get cluster default encoding' unless $encoding;
($lc_ctype, $lc_collate) = get_cluster_locales $version, $cluster;
error 'could not get cluster locale' unless $lc_ctype;
error 'could not get cluster collating locale' unless $lc_ctype;

if (PgCommon::cluster_data_directory $newversion, $cluster) {
    error "target cluster $newversion/$cluster already exists";
}

# create new cluster, preserving encoding and locales

@argv = ('pg_createcluster', '-u', $info{'owneruid'}, '-g', $info{'ownergid'},
    '--socketdir', $info{'socketdir'}, '--encoding', $encoding, $newversion,
    $cluster);
push @argv, $datadir if $datadir;

delete $ENV{'LC_ALL'};
$ENV{'LC_CTYPE'} = $lc_ctype;
$ENV{'LC_COLLATE'} = $lc_collate;
error "Could not create target cluster" if system @argv;

@argv = ('pg_ctlcluster', '-s', $newversion, $cluster, 'start');
error "Could not start target cluster" if system @argv;

sleep(4);

%newinfo = cluster_info($newversion, $cluster);

# dump cluster; drop to cluster owner privileges

print "Dumping the old cluster into the new one...\n";

if (!fork) {
    change_ugid $info{'owneruid'}, $info{'ownergid'};
    $pg_dumpall = get_program_path 'pg_dumpall', $newversion;
    $psql = get_program_path 'psql', $newversion;
    $oldsocket = get_cluster_socketdir $version, $cluster;
    $newsocket = get_cluster_socketdir $newversion, $cluster;

    open SOURCE, '-|', $pg_dumpall, '-h', $oldsocket, '-p', $info{'port'} or 
	error 'Could not execute pg_dumpall for old cluster';
    open SINK, '|-', $psql, '-h', $newsocket, '-q', '-p', $newinfo{'port'},
        '-d', 'template1' or 
        error 'Could not execute psql for new cluster';
    while (read SOURCE, $buffer, 16384) {
	print SINK $buffer;
    }
    close SOURCE;
    ($? == 0) or exit 1;
    close SINK;
    ($? == 0) or exit 1;
    exit 0;
}

wait;
if ($?) {
    print STDERR "Error during cluster dumping, removing new cluster\n";
    system 'pg_dropcluster', '--stop-server', $newversion, $cluster;
    exit 1;
}

# copy configuration files
print "Copying old configuration files...\n";
install_file $info{'configdir'}.'/postgresql.conf', $newinfo{'configdir'},
    $newinfo{'owneruid'}, $newinfo{'ownergid'}, "644";
install_file $info{'configdir'}.'/pg_ident.conf', $newinfo{'configdir'},
    $newinfo{'owneruid'}, $newinfo{'ownergid'}, "640";
install_file $info{'configdir'}.'/pg_hba.conf', $newinfo{'configdir'},
    $newinfo{'owneruid'}, $newinfo{'ownergid'}, "640";

adapt_conffiles;

print "Stopping target cluster...\n";
@argv = ('pg_ctlcluster', '-s', $newversion, $cluster, 'stop');
error "Could not stop target cluster" if system @argv;

print "Stopping old cluster...\n";
@argv = ('pg_ctlcluster', '-s', $version, $cluster, 'stop');
error "Could not stop old cluster" if system @argv;

$oldport = next_free_port;
print "Configuring old cluster to use a different port ($oldport)...\n";
set_cluster_port $version, $cluster, $oldport;

print "Starting target cluster on the original port...\n";
@argv = ('pg_ctlcluster', '-s', $newversion, $cluster, 'start');
error "Could not start target cluster; please check configuration and log files" if system @argv;

print "Success. Please check that the upgraded cluster works. If it does,
you can remove the old cluster with

  pg_dropcluster $version $cluster
"

__END__

=head1 NAME

pg_upgradecluster - upgrade a new PostgreSQL cluster to a new major version.

=head1 SYNOPSIS

B<pg_upgradecluster> [B<-v> I<newversion>] I<version> I<name> [I<data dir>]

=head1 DESCRIPTION

B<pg_upgradecluster> upgrades an existing PostgreSQL server cluster (i. e. a
collection of databases served by a B<postmaster> instance) to a new version
specified by I<newversion> (default: latest available version).  The
configuration files of the old version are copied to the new cluster.

The cluster of the old version will be configured to use a previously unused
port since the upgraded one will use the original port. The old cluster is not
automatically be removed. After upgrade, please verify that the new cluster
indeed works as expected; if so, you should remove the old cluster with
L<pg_dropcluster(8)>.

=head1 SEE ALSO

L<pg_createcluster(8)>, L<pg_dropcluster(8)>, L<pg_lsclusters(1)>, L<pg_wrapper(1)>

=head1 AUTHOR

Martin Pitt L<E<lt>mpitt@debian.orgE<gt>>
