diff options
Diffstat (limited to 'pg_upgradecluster')
-rwxr-xr-x | pg_upgradecluster | 942 |
1 files changed, 942 insertions, 0 deletions
diff --git a/pg_upgradecluster b/pg_upgradecluster new file mode 100755 index 0000000..70a2a88 --- /dev/null +++ b/pg_upgradecluster @@ -0,0 +1,942 @@ +#!/usr/bin/perl -wT + +# Upgrade a PostgreSQL cluster to a newer major version. +# +# (C) 2005-2009 Martin Pitt <mpitt@debian.org> +# (C) 2013 Peter Eisentraut <petere@debian.org> +# (C) 2013-2020 Christoph Berg <myon@debian.org> +# +# 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. + +use strict; +use warnings; +use PgCommon; +use File::Temp qw(tempfile); +use Getopt::Long; +use POSIX qw(lchown); + +# untaint environment +$ENV{'PATH'} = '/bin:/usr/bin'; +delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; + +# global variables +my ($version, $newversion, $cluster, $newcluster); +my (%info, %newinfo); +my ($encoding, $old_lc_ctype, $old_lc_collate); # old cluster encoding +my $maintenance_db = 'template1'; + +# do not trip over cwd not being accessible to postgres superuser +chdir '/'; + +# update the new cluster's conffiles +sub adapt_conffiles { + my ($newversion, $newcluster, $configfile) = @_; + my %c = read_cluster_conf_file $newversion, $newcluster, $configfile; + + # Arguments: <ref to conf hash> <name> <comment> + my $deprecate = sub { + my ($conf, $guc, $comment) = @_; + if (defined $conf->{$guc}) { + PgCommon::disable_conf_value $newversion, $newcluster, + $configfile, $guc, $comment; + } + }; + + # Arguments: <ref to conf hash> <old name> <new name> + my $rename = sub { + my ($conf, $old, $new) = @_; + if (defined ${$conf}{$old}) { + PgCommon::replace_conf_value $newversion, $newcluster, + $configfile, $old, "deprecated in favor of $new", + $new, ${$conf}{$old}; + } + }; + + # Arguments: <config option> <value> + my $set = sub { + my ($guc, $val) = @_; + PgCommon::set_conf_value $newversion, $newcluster, $configfile, + $guc, $val; + }; + + # adapt paths to configuration files + if ($configfile eq 'postgresql.conf') { + $set->('data_directory', $newinfo{'pgdata'}); + } else { + # fix bug in pg_upgradecluster 200..202 + $deprecate->(\%c, 'data_directory', 'not valid in postgresql.auto.conf'); + } + for my $guc (qw(hba_file ident_file external_pid_file stats_temp_directory)) { + next unless (defined $c{$guc}); + my $val = $c{$guc}; + $val =~ s/\b\Q$version\E\b/$newversion/g; + $val =~ s/\b\Q$cluster\E\b/$newcluster/g if ($cluster ne $newcluster); + $set->($guc, $val); + } + + if ($newversion >= '8.2') { + # preload_libraries -> shared_preload_libraries transition + $rename->(\%c, 'preload_libraries', 'shared_preload_libraries'); + + # australian_timezones -> timezone_abbreviations transition + my $australian_timezones = config_bool $c{'australian_timezones'}; + if (defined $australian_timezones) { + PgCommon::replace_conf_value $newversion, $newcluster, $configfile, + 'australian_timezones', 'deprecated in favor of timezone_abbreviations', + 'timezone_abbreviations', ($australian_timezones ? 'Australia' : 'Default'); + } + } + + if ($newversion >= '8.3') { + $deprecate->(\%c, 'bgwriter_lru_percent', 'deprecated'); + $deprecate->(\%c, 'bgwriter_all_percent', 'deprecated'); + $deprecate->(\%c, 'bgwriter_all_maxpages', 'deprecated'); + + $rename->(\%c, 'redirect_stderr', 'logging_collector'); + + $rename->(\%c, 'stats_command_string', 'track_activities'); + $deprecate->(\%c, 'stats_start_collector', 'deprecated, always on now'); + $deprecate->(\%c, 'stats_reset_on_server_start', 'deprecated'); + + # stats_block_level and stats_row_level are merged into track_counts + if ($c{'stats_block_level'} || $c{'stats_row_level'}) { + $deprecate->(\%c, 'stats_block_level', 'deprecated in favor of track_counts'); + $deprecate->(\%c, 'stats_row_level', 'deprecated in favor of track_counts'); + $set->('track_counts', (config_bool $c{'stats_block_level'} || config_bool $c{'stats_row_level'}) ? 'on' : 'off'); + } + + # archive_command now has to be enabled explicitly + if ($c{'archive_command'}) { + $set->('archive_mode', 'on'); + } + } + + if ($newversion >= '8.4') { + $deprecate->(\%c, 'max_fsm_pages', 'not needed anymore'); + $deprecate->(\%c, 'max_fsm_relations', 'not needed anymore'); + $deprecate->(\%c, 'krb_server_hostname', 'does not exist anymore'); + $deprecate->(\%c, 'krb_realm', 'does not exist anymore'); + $rename->(\%c, 'explain_pretty_print', 'debug_pretty_print'); + } + + if ($newversion >= '9.0') { + $deprecate->(\%c, 'add_missing_from', 'does not exist anymore'); + $deprecate->(\%c, 'regex_flavor', 'does not exist anymore'); + } + + if ($newversion >= '9.2') { + $deprecate->(\%c, 'wal_sender_delay', 'does not exist anymore'); + $deprecate->(\%c, 'silent_mode', 'does not exist anymore'); + $deprecate->(\%c, 'custom_variable_classes', 'does not exist anymore'); + } + + if ($newversion >= '9.3') { + $rename->(\%c, 'unix_socket_directory', 'unix_socket_directories'); + $rename->(\%c, 'replication_timeout', 'wal_sender_timeout'); + } + + if ($newversion >= '9.4') { + $deprecate->(\%c, 'krb_srvname', 'native krb5 authentication deprecated in favor of GSSAPI'); + # grab dsmt from the new config just written by initdb + if (not $c{dynamic_shared_memory_type} and $configfile eq 'postgresql.conf') { + $set->('dynamic_shared_memory_type', ($newinfo{config}->{dynamic_shared_memory_type} || 'mmap')); + } + } + + if ($newversion >= '9.5') { + if (exists $c{checkpoint_segments}) { + my $max_wal_size = 16*$c{checkpoint_segments} . 'MB'; + $rename->(\%c, 'checkpoint_segments', 'max_wal_size'); + $set->('max_wal_size', $max_wal_size); + } + $deprecate->(\%c, 'ssl_renegotiation_limit', 'does not exist anymore'); + # adapt cluster_name + my $cluster_name = PgCommon::get_conf_value ($newversion, $newcluster, $configfile, 'cluster_name'); + if ($cluster_name) { + $cluster_name =~ s/\Q$version\E/$newversion/g; + $cluster_name =~ s/\Q$cluster\E/$newcluster/g; + $set->('cluster_name', $cluster_name); + } + } + + if ($newversion >= '10') { + $rename->(\%c, 'min_parallel_relation_size', 'min_parallel_table_scan_size'); + $deprecate->(\%c, 'sql_inheritance', 'does not exist anymore'); + } + + if ($newversion >= '11') { + $deprecate->(\%c, 'replacement_sort_tuples', 'does not exist anymore'); + } + + if ($newversion >= '13') { + if (exists $c{wal_keep_segments}) { + my $wal_keep_size = 16*$c{wal_keep_segments} . 'MB'; + $rename->(\%c, 'wal_keep_segments', 'wal_keep_size'); + $set->('wal_keep_size', $wal_keep_size); + } + } + + if ($newversion >= '14') { + $deprecate->(\%c, 'operator_precedence_warning', 'does not exist anymore'); + if ($c{password_encryption} and $c{password_encryption} =~ /^(on|off|true|false)$/) { + $deprecate->(\%c, 'password_encryption', 'password_encryption is not a boolean anymore'); + } + } +} + +# Write temporary pg_hba.conf. +# Arguments: <version> <cluster> <owner> <owneruid> +sub temp_hba_conf { + my ($fh, $hba) = tempfile("pg_hba.XXXXXX", TMPDIR => 1, SUFFIX => ".conf"); + + if ($_[0] >= '8.4') { + print $fh "local all $_[2] ident\n"; + } else { + print $fh "local all $_[2] ident sameuser\n"; + } + close $fh; + chmod 0400, $hba; + lchown $_[3], 0, $hba; + + return $hba; +} + +# Get encoding and locales of a running cluster +# Arguments: <version> <cluster> +sub get_encoding { + my ($version, $cluster) = @_; + $encoding = get_db_encoding $version, $cluster, $maintenance_db; + if ($version <= '8.3') { + ($old_lc_ctype, $old_lc_collate) = get_cluster_locales $version, $cluster; + } else { + ($old_lc_ctype, $old_lc_collate) = get_db_locales $version, $cluster, $maintenance_db; + } + unless ($encoding && $old_lc_ctype && $old_lc_collate) { + error 'could not get cluster locales'; + } +} + +# RedHat's run-parts doesn't support any options, supply a minimalistic implementation here +# BUG: we don't care about validating the filenames yet +# Arguments: <directory> <argv to pass to scripts> +sub run_parts { + my ($dir, @argv) = @_; + for my $script (<$dir/*>) { + my ($s) = $script =~ /(.*)/; # untaint + system ($s, @argv); + error "$s failed: $?" if ($?); + } +} + + +# +# Execution starts here +# + +# command line arguments + +my $newest_version = get_newest_version('postgres'); +$newversion = $newest_version; + +my $method = 'dump'; +my $link = 0; +my $clone = 0; +my $keep_port = 0; +my $start = -1; # -1 = auto + +my ($locale, $lc_collate, $lc_ctype, $lc_messages, $lc_monetary, $lc_numeric, + $lc_time, $logfile, $old_bindir, $jobs); +GetOptions ('v|version=s' => \$newversion, + 'locale=s' => \$locale, + 'lc-collate=s' => \$lc_collate, + 'lc-ctype=s' => \$lc_ctype, + 'lc-messages=s' => \$lc_messages, + 'lc-monetary=s' => \$lc_monetary, + 'lc-numeric=s' => \$lc_numeric, + 'lc-time=s' => \$lc_time, + 'logfile=s' => \$logfile, + 'm|method=s' => \$method, + 'j|jobs=s', => \$jobs, + 'k|link' => \$link, + 'clone' => \$clone, + 'keep-port' => \$keep_port, + 'rename=s' => \$newcluster, + 'old-bindir=s' => \$old_bindir, + 'maintenance-db=s' => \$maintenance_db, + 'start!' => \$start, + ) or exit 1; + +if ($method eq 'dump') { + error 'cannot use --link with --method=dump' if ($link); + error 'cannot use --clone with --method=dump' if ($clone); + error 'cannot use --jobs with --method=dump' if ($jobs); +} elsif ($method eq 'link') { + $method = 'upgrade'; + $link = 1; +} elsif ($method eq 'clone') { + $method = 'upgrade'; + $clone = 1; +} elsif ($method ne 'upgrade') { + error 'method must be "dump", "upgrade", "link", or "clone"'; +} + +# untaint +($newversion) = $newversion =~ /^(\d+\.?\d+)$/; +($locale) = $locale =~ /^([\w@._-]+)$/ if $locale; +($lc_collate) = $lc_collate =~ /^([\w@._-]+)$/ if $lc_collate; +($lc_ctype) = $lc_ctype =~ /^([\w@._-]+)$/ if $lc_ctype; +($lc_messages) = $lc_messages =~ /^([\w@._-]+)$/ if $lc_messages; +($lc_monetary) = $lc_monetary =~ /^([\w@._-]+)$/ if $lc_monetary; +($lc_numeric) = $lc_numeric =~ /^([\w@._-]+)$/ if $lc_numeric; +($lc_time) = $lc_time =~ /^([\w@._-]+)$/ if $lc_time; +($logfile) = $logfile =~ /^([^\n]+)$/ if $logfile; +($old_bindir) = $old_bindir =~ /^(\/.*)$/ if $old_bindir; +($maintenance_db) = $maintenance_db =~ /^([\w-]+)$/ if $maintenance_db; +($jobs) = $jobs =~ /^(\d+)$/ if $jobs; + +if ($#ARGV < 1) { + print "Usage: $0 [OPTIONS] <old version> <cluster name> [<new data directory>]\n"; + exit 1; +} + +($version) = $ARGV[0] =~ /^(\d+\.?\d+)$/; +($cluster) = $ARGV[1] =~ /^([-.\w]+)$/; +$newcluster ||= $cluster; # use old cluster name by default +($newcluster) = $newcluster =~ /^([-.\w]+)$/; +my $datadir; +($datadir) = $ARGV[2] =~ /(.*)/ if defined $ARGV[2]; + +error 'specified cluster does not exist' unless cluster_exists $version, $cluster; +%info = cluster_info ($version, $cluster); +error 'cluster is disabled' if $info{'start'} eq 'disabled'; + +error "cluster $version/$cluster is already on version $newversion. " . + "(The newest version installed on this system is $newest_version.)" + if ($version eq $newversion and $cluster eq $newcluster); + +if (cluster_exists $newversion, $newcluster) { + error "target cluster $newversion/$newcluster already exists"; +} + +my $oldcontrol = get_cluster_controldata ($version, $cluster); + +my $oldsocket = get_cluster_socketdir $version, $cluster; +my $owner = getpwuid $info{'owneruid'}; +error 'could not get name of cluster owner' unless $owner; +my $temp_hba_conf = temp_hba_conf $version, $cluster, $owner, $info{'owneruid'}; + +# stop old cluster +if ($info{'running'}) { + get_encoding $version, $cluster; + print "Stopping old cluster...\n"; + my @argv = ('pg_ctlcluster', $version, $cluster, 'stop'); + error "Could not stop old cluster" if system @argv; +} + +if ($method eq 'dump' or ($method eq 'upgrade' and not $info{'running'})) { + print "Restarting old cluster with restricted connections...\n"; + my @argv = ('pg_ctlcluster', + ($old_bindir ? ("--bindir=$old_bindir") : ()), + $version, $cluster, 'start', '-o', "-c hba_file=$temp_hba_conf"); + error "Could not restart old cluster" if system @argv; + + get_encoding $version, $cluster unless ($encoding); # if the cluster was not running before, get encoding now + + if ($method eq 'upgrade') { + print "Stopping old cluster...\n"; + @argv = ('pg_ctlcluster', $version, $cluster, 'stop'); + error "Could not stop old cluster" if system @argv; + } +} + +# in dump mode, old cluster is running now +# in upgrade mode, old cluster is stopped + +# create new cluster, preserving encoding and locales +my @argv = ('pg_createcluster', '-u', $info{'owneruid'}, '-g', $info{'ownergid'}, + '--socketdir', $info{'socketdir'}, $newversion, + $newcluster); + +push @argv, ('--datadir', $datadir) if $datadir; +push @argv, ('--logfile', $logfile) if $logfile; +push @argv, ('--encoding', $encoding) unless $locale or $lc_ctype; +$lc_ctype ||= $locale || $old_lc_ctype; +$lc_collate ||= $locale || $old_lc_collate; +push @argv, ('--locale', $locale) if $locale; +push @argv, ('--lc-collate', $lc_collate) if $lc_collate; +push @argv, ('--lc-ctype', $lc_ctype) if $lc_ctype; +push @argv, ('--lc-messages', $lc_messages) if $lc_messages; +push @argv, ('--lc-monetary', $lc_monetary) if $lc_monetary; +push @argv, ('--lc-numeric', $lc_numeric) if $lc_numeric; +push @argv, ('--lc-time', $lc_time) if $lc_time; +delete $ENV{'LC_ALL'}; +push @argv, ('--', '--data-checksums') if $oldcontrol->{'Data page checksum version'}; # 0 = off +error "Could not create target cluster" if system @argv; +print "\n"; + +# if pg_upgrade is used, wal_level needs to be set early so standby servers can +# be upgraded with the instructions outlined in pg_upgrade(1) +if ($method eq 'upgrade' and $info{config}->{wal_level}) { + PgCommon::set_conf_value $newversion, $newcluster, 'postgresql.conf', + 'wal_level', $info{config}->{wal_level}; + # if wal_level is minimal, max_wal_senders must be zero so the new cluster + # can start, so set max_wal_senders config option early as well + if (defined($info{config}->{max_wal_senders})) { + PgCommon::set_conf_value $newversion, $newcluster, 'postgresql.conf', + 'max_wal_senders', $info{config}->{max_wal_senders}; + } +} + +%newinfo = cluster_info($newversion, $newcluster); + +if ($method eq 'dump') { + print "Starting new cluster...\n"; + @argv = ('pg_ctlcluster', $newversion, $newcluster, 'start', '-o', "-c hba_file=$temp_hba_conf"); + error "Could not start target cluster" if system @argv; +} + +my $pg_restore = get_program_path 'pg_restore', $newversion; + +# check whether upgrade scripts exist +my $upgrade_scripts = (-d "$PgCommon::common_confdir/pg_upgradecluster.d" && + ($PgCommon::rpm ? `ls $PgCommon::common_confdir/pg_upgradecluster.d` : + `run-parts --test $PgCommon::common_confdir/pg_upgradecluster.d`)); + +# Run upgrade scripts in init phase +if ($upgrade_scripts) { + print "Running <init> phase upgrade hook scripts...\n"; + if (!fork) { + change_ugid $info{'owneruid'}, $info{'ownergid'}; + + if ($PgCommon::rpm) { + run_parts ("$PgCommon::common_confdir/pg_upgradecluster.d", + $version, $newcluster, $newversion, 'init'); + exit; + } + + @argv = ('run-parts', '--lsbsysinit', '-a', $version, '-a', $newcluster, + '-a', $newversion, '-a', 'init', + "$PgCommon::common_confdir/pg_upgradecluster.d"); + error "$PgCommon::common_confdir/pg_upgradecluster.d script failed" if system @argv; + exit 0; + } + wait; + if ($? > 0) { + print STDERR "Error during running upgrade hooks, removing new cluster\n"; + system 'pg_dropcluster', '--stop', $newversion, $newcluster; + exit 1; + } +} + +# dump cluster; drop to cluster owner privileges + +if (!fork) { + change_ugid $info{'owneruid'}, $info{'ownergid'}; + my $pg_dumpall = get_program_path 'pg_dumpall', $newversion; + my $pg_dump = get_program_path 'pg_dump', $newversion; + my $psql = get_program_path 'psql'; + my $newsocket = get_cluster_socketdir $newversion, $newcluster; + + if ($method eq 'dump') { + # get list of databases (value = datallowconn) + my %databases; + open F, '-|', $psql, '-h', $oldsocket, '-p', $info{'port'}, + '-F|', '-d', $maintenance_db, '-AXtc', + 'SELECT datname, datallowconn FROM pg_database' or + error 'Could not get pg_database list'; + while (<F>) { + chomp; + my ($n, $a) = split '\|'; + $databases{$n} = ($a eq 't'); + } + close F; + error 'could not get list of databases' if $?; + + # Temporarily enable access to all DBs, so that we can upgrade them + for my $db (keys %databases) { + next if $db eq 'template0'; + + unless ($databases{$db}) { + print "Temporarily enabling access to database $db\n"; + (system $psql, '-h', $oldsocket, '-p', $info{'port'}, '-qX', + '-d', $maintenance_db, '-c', + "BEGIN READ WRITE; UPDATE pg_database SET datallowconn = 't' WHERE datname = '$db'; COMMIT") == 0 or + error 'Could not enable access to database'; + } + } + + # dump schemas + print "Roles, databases, schemas, ACLs...\n"; + open SOURCE, '-|', $pg_dumpall, '-h', $oldsocket, '-p', $info{'port'}, + '-s', '--quote-all-identifiers' or error 'Could not execute pg_dumpall for old cluster'; + my $data = ''; + my $buffer; + while (read SOURCE, $buffer, 1048576) { + $data .= $buffer; + } + close SOURCE; + ($? == 0) or exit 1; + + # remove creation of db superuser role to avoid error message + $data =~ s/^CREATE (ROLE|USER) "\Q$owner\E";\s*$//m; + + # create global objects in target cluster + open SINK, '|-', $psql, '-h', $newsocket, '-p', $newinfo{'port'}, + '-qX', '-d', $maintenance_db or + error 'Could not execute psql for new cluster'; + + # ensure that we can upgrade tables for DBs with default read-only + # transactions + print SINK "BEGIN READ WRITE; ALTER USER $owner SET default_transaction_read_only to off; COMMIT;\n"; + + print SINK $data; + + close SINK; + ($? == 0) or exit 1; + + + # Upgrade databases + for my $db (keys %databases) { + next if $db eq 'template0'; + + print "Fixing hardcoded library paths for stored procedures...\n"; + # starting from 9.0, replace() works on strings; for earlier versions it + # works on bytea + if ($version >= '9.0') { + (system $psql, '-h', $oldsocket, '-p', $info{'port'}, '-qX', '-d', + $db, '-c', "BEGIN READ WRITE; \ + UPDATE pg_proc SET probin = replace(\ + replace(probin, '/usr/lib/postgresql/lib', '\$libdir'), \ + '/usr/lib/postgresql/$version/lib', '\$libdir'); COMMIT") == 0 or + error 'Could not fix library paths'; + } else { + (system $psql, '-h', $oldsocket, '-p', $info{'port'}, '-qX', '-d', + $db, '-c', "BEGIN READ WRITE; \ + UPDATE pg_proc SET probin = decode(replace(\ + replace(encode(probin, 'escape'), '/usr/lib/postgresql/lib', '\$libdir'), \ + '/usr/lib/postgresql/$version/lib', '\$libdir'), 'escape'); COMMIT") == 0 or + error 'Could not fix library paths'; + } + + print 'Upgrading database ', $db, "...\n"; + open SOURCE, '-|', $pg_dump, '-h', $oldsocket, '-p', $info{'port'}, + '-Fc', '--quote-all-identifiers', $db or + error 'Could not execute pg_dump for old cluster'; + + # start pg_restore and copy over everything + my @restore_argv = ($pg_restore, '-h', $newsocket, '-p', + $newinfo{'port'}, '--data-only', '-d', $db, + '--disable-triggers', '--no-data-for-failed-tables'); + open SINK, '|-', @restore_argv or + error 'Could not execute pg_restore for new cluster'; + + my $buffer; + while (read SOURCE, $buffer, 1048576) { + print SINK $buffer; + } + close SOURCE; + ($? == 0) or exit 1; + close SINK; + + # clean up + print 'Analyzing database ', $db, "...\n"; + (system $psql, '-h', $newsocket, '-p', $newinfo{'port'}, '-qX', + '-d', $db, '-c', 'ANALYZE') == 0 or + error 'Could not ANALYZE database'; + + unless ($databases{$db}) { + print "Disabling access to database $db again\n"; + (system $psql, '-h', $oldsocket, '-p', $info{'port'}, '-qX', + '-d', $maintenance_db, '-c', + "BEGIN READ WRITE; UPDATE pg_database SET datallowconn = 'f' where datname = '$db'; COMMIT") == 0 or + error 'Could not disable access to database in old cluster'; + (system $psql, '-h', $newsocket, '-p', $newinfo{'port'}, '-qX', + '-d', $maintenance_db, '-c', + "BEGIN READ WRITE; UPDATE pg_database SET datallowconn = 'f' where datname = '$db'; COMMIT") == 0 or + error 'Could not disable access to database in new cluster'; + } + } + + # reset owner specific override for default read-only transactions + (system $psql, '-h', $newsocket, '-p', $newinfo{'port'}, '-qX', $maintenance_db, '-c', + "BEGIN READ WRITE; ALTER USER $owner RESET default_transaction_read_only; COMMIT;\n") == 0 or + error 'Could not reset default_transaction_read_only value for superuser'; + } else { + # pg_upgrade + + use File::Temp qw(tempdir); + + my $pg_upgrade = get_program_path 'pg_upgrade', $newversion; + $pg_upgrade or error "pg_upgrade $newversion not found"; + my @argv = ($pg_upgrade, + '-b', ($old_bindir || "$PgCommon::binroot$version/bin"), + '-B', "$PgCommon::binroot$newversion/bin", + '-p', $info{'port'}, + '-P', $newinfo{'port'}, + ); + if ($version <= 9.1) { + push @argv, '-d', $info{pgdata}, '-o', "-D $info{configdir}"; # -o and -D configdir require $newversion >= 9.2 + } else { + push @argv, '-d', $info{configdir}; + } + push @argv, '-D', $newinfo{configdir}; + push @argv, "--link" if $link; + push @argv, "--clone" if $clone; + push @argv, '-j', $jobs if $jobs; + + # Make a directory for pg_upgrade to store its reports and log + # files. It will not be removed. + my $logdir = tempdir("/var/log/postgresql/pg_upgradecluster-$version-$newversion-$newcluster.XXXX"); + chdir $logdir; + + # Run pg_upgrade. + print "@argv\n"; + my $status = system @argv; + + # Remove the PID file of the old cluster (normally removed by + # pg_ctlcluster, but not by pg_upgrade). + unlink "/var/run/postgresql/$version-$cluster.pid"; + + $status == 0 or error "pg_upgrade run failed. Logfiles are in $logdir"; + print "pg_upgrade output scripts are in $logdir\n"; + } + + exit 0; +} + +wait; + +if ($?) { + print STDERR "Error during cluster dumping, removing new cluster\n"; + system 'pg_dropcluster', '--stop', $newversion, $newcluster; + + # Restart old cluster to allow connections again (two steps because we started without systemd) + if (system 'pg_ctlcluster', $version, $cluster, 'stop') { + error 'could not stop old cluster, please do that manually'; + } + if (system 'pg_ctlcluster', $version, $cluster, 'start') { + error 'could not start old cluster, please do that manually'; + } + exit 1; +} + +# copy configuration files +print "Copying old configuration files...\n"; +install_file $info{'configdir'}.'/postgresql.conf', $newinfo{'configdir'}, + $newinfo{'owneruid'}, $newinfo{'ownergid'}, "644"; +adapt_conffiles $newversion, $newcluster, 'postgresql.conf'; +# copy auto.conf after postgresql.conf has been updated +# (otherwise read_cluster_conf_file would also read auto.conf) +if ($version >= 9.4) { + install_file $info{'pgdata'}.'/postgresql.auto.conf', $newinfo{'pgdata'}, + $newinfo{'owneruid'}, $newinfo{'ownergid'}, "600"; + adapt_conffiles $newversion, $newcluster, 'postgresql.auto.conf'; +} +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"; +if ($version < 8.4 and $newversion >= 8.4) { + print "Removing 'ident sameuser' from pg_hba.conf...\n"; + my $hba = "$PgCommon::confroot/$newversion/$newcluster/pg_hba.conf"; + open O, $hba or error "open $hba: $!"; + open N, ">$hba.new" or error "open $hba.new: $!"; + while (<O>) { + s/ident\s+sameuser/ident/; + print N $_; + } + close O; + close N; + lchown $newinfo{'owneruid'}, $newinfo{'ownergid'}, "$hba.new"; + chmod 0640, "$hba.new"; + rename "$hba.new", $hba or error "rename: $!"; +} +if ( -e $info{'configdir'}.'/start.conf') { + print "Copying old start.conf...\n"; + install_file $info{'configdir'}.'/start.conf', $newinfo{'configdir'}, + $newinfo{'owneruid'}, $newinfo{'ownergid'}, "644"; +} +if ( -e $info{'configdir'}.'/pg_ctl.conf') { + print "Copying old pg_ctl.conf...\n"; + install_file $info{'configdir'}.'/pg_ctl.conf', $newinfo{'configdir'}, + $newinfo{'owneruid'}, $newinfo{'ownergid'}, "644"; +} + +# copy SSL files (overwriting any file that pg_createcluster put there) +for my $file (qw/server.crt server.key root.crt root.crl/) { + if ( -e "$info{'pgdata'}/$file") { + print "Copying old $file...\n"; + if (!fork) { # we don't use install_file because that converts symlinks to files + change_ugid $info{'owneruid'}, $info{'ownergid'}; + system "cp -a $info{'pgdata'}/$file $newinfo{'pgdata'}"; + exit 0; + } + wait; + } +} +if ($newversion >= 9.2) { + # SSL certificate paths have an explicit option now, older versions use + # a symlink + for my $f (['server.crt', 'ssl_cert_file'], + ['server.key', 'ssl_key_file'], + ['root.crt', 'ssl_ca_file'], + ['root.crl', 'ssl_crl_file']) { + my $file = "$newinfo{'pgdata'}/$f->[0]"; + if (-l $file) { # migrate symlink to config entry with link target + PgCommon::set_conf_value $newversion, $newcluster, 'postgresql.conf', + $f->[1], (readlink $file); + unlink $file; + } elsif (-e $file) { # plain file in data dir, put in config + PgCommon::set_conf_value $newversion, $newcluster, 'postgresql.conf', + $f->[1], $file; + } + } +} + +if ($method eq 'dump') { + print "Stopping target cluster...\n"; + @argv = ('pg_ctlcluster', $newversion, $newcluster, 'stop'); + error "Could not stop target cluster" if system @argv; + + print "Stopping old cluster...\n"; + @argv = ('pg_ctlcluster', $version, $cluster, 'stop'); + error "Could not stop old cluster" if system @argv; +} + +print "Disabling automatic startup of old cluster...\n"; +my $startconf = $info{'configdir'}.'/start.conf'; +if (open F, ">$startconf") { + print F "# This cluster was upgraded to a newer major version. The old +# cluster has been preserved for backup purposes, but is not started +# automatically. + +manual +"; + close F; +} else { + error "could not create $startconf: $!"; +} + +my $free_port = next_free_port; +if ($keep_port) { + print "Configuring new cluster to use a different port ($free_port)...\n"; + set_cluster_port $newversion, $cluster, $free_port; + $newinfo{port} = $free_port; +} else { + print "Configuring old cluster to use a different port ($free_port)...\n"; + set_cluster_port $version, $cluster, $free_port; + $newinfo{port} = $info{port}; +} + +# notify systemd that we modified the old start.conf +if (not exists $ENV{'PG_CLUSTER_CONF_ROOT'} and -d '/run/systemd/system' and $> == 0) { + system 'systemctl daemon-reload'; +} + +# start cluster if it was running before, or upgrade scripts are present +$start = ($info{running} or $upgrade_scripts) if ($start == -1); +if ($start) { + print "Starting target cluster on port $newinfo{port}...\n"; + @argv = ('pg_ctlcluster', $newversion, $newcluster, 'start'); + error "Could not start target cluster; please check configuration and log files" if system @argv; +} + +# Run upgrade scripts in finish phase +if ($upgrade_scripts) { + print "Running <finish> phase upgrade hook scripts...\n"; + if (!fork) { + change_ugid $info{'owneruid'}, $info{'ownergid'}; + + if ($PgCommon::rpm) { + run_parts ("$PgCommon::common_confdir/pg_upgradecluster.d", + $version, $newcluster, $newversion, 'finish'); + exit; + } + + @argv = ('run-parts', '--lsbsysinit', '-a', $version, '-a', $newcluster, + '-a', $newversion, '-a', 'finish', + "$PgCommon::common_confdir/pg_upgradecluster.d"); + error "$PgCommon::common_confdir/pg_upgradecluster.d script failed" if system @argv; + exit 0; + } + wait; + exit $? >> 8 if ($?); +} + +print "\nSuccess. Please check that the upgraded cluster works. If it does, +you can remove the old cluster with + pg_dropcluster $version $cluster\n\n"; + +system 'pg_lsclusters', $version, $cluster; +system 'pg_lsclusters', $newversion, $newcluster; + +__END__ + +=head1 NAME + +pg_upgradecluster - upgrade an existing PostgreSQL cluster to a new major version. + +=head1 SYNOPSIS + +B<pg_upgradecluster> [B<-v> I<newversion>] I<oldversion> I<name> [I<newdatadir>] + +=head1 DESCRIPTION + +B<pg_upgradecluster> upgrades an existing PostgreSQL server cluster (i. e. a +collection of databases served by a B<postgres> 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 and +adjusted for the new version. The new cluster is set up to use data page +checksums if the old cluster uses them. + +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 removed. After upgrading, please verify that the new cluster +indeed works as expected; if so, you should remove the old cluster with +L<pg_dropcluster(8)>. Please note that the old cluster is set to "manual" +startup mode, in order to avoid inadvertently changing it; this means that it +will not be started automatically on system boot, and you have to use +L<pg_ctlcluster(8)> to start/stop it. See section "STARTUP CONTROL" in +L<pg_createcluster(8)> for details. + +The I<newdatadir> argument can be used to specify a non-default data directory +of the upgraded cluster. It is passed to B<pg_createcluster>. If not specified, +this defaults to /var/lib/postgresql/I<newversion>/I<name>. + +=head1 OPTIONS + +=over 4 + +=item B<-v> I<newversion> + +Set the version to upgrade to (default: latest available). + +=item B<--logfile> I<filel> + +Set a custom log file path for the upgraded database cluster. + +=item B<--locale=>I<locale> + +Set the default locale for the upgraded database cluster. If this option is not +specified, the locale is inherited from the old cluster. + +When upgrading to PostgreSQL 11 or newer, this option no longer allows to +switch the encoding of individual databases. (L<pg_dumpall(1)> was changed to +retain database encodings.) + +=item B<--lc-collate=>I<locale> + +=item B<--lc-ctype=>I<locale> + +=item B<--lc-messages=>I<locale> + +=item B<--lc-monetary=>I<locale> + +=item B<--lc-numeric=>I<locale> + +=item B<--lc-time=>I<locale> + +Like B<--locale>, but only sets the locale in the specified category. + +=item B<-m>, B<--method=>B<dump>|B<upgrade>|B<link>|B<clone> + +Specify the upgrade method. B<dump> uses L<pg_dump(1)> and +L<pg_restore(1)>, B<upgrade> uses L<pg_upgrade(1)>. The default is +B<dump>. + +B<link> and B<clone> are shorthands for B<-m upgrade --link> and B<-m upgrade --clone>, +respectively. + +=item B<-k>, B<--link> + +In pg_upgrade mode, use hard links instead of copying files to the new +cluster. This option is merely passed on to pg_upgrade. See +L<pg_upgrade(1)> for details. + +=item B<--clone> + +In pg_upgrade mode, use efficient file cloning (also known as "reflinks" +on some systems) instead of copying files to the new cluster. This option +is merely passed on to pg_upgrade. See L<pg_upgrade(1)> for details. + +=item B<-j>, B<--jobs> + +In pg_upgrade mode, number of simultaneous processes to use. This +option is merely passed on to pg_upgrade. See L<pg_upgrade(1)> +for details. + +=item B<--keep-port> + +By default, the old cluster is moved to a new port, and the new cluster is +moved to the original port so clients will see the upgraded cluster. This +option disables that. + +=item B<--rename=>I<new cluster name> + +Use a different name for the upgraded cluster. + +=item B<--old-bindir=>I<directory> + +Passed to B<pg_upgrade>. + +=item B<--maintenance-db=>I<database> + +Database to connect to for maintenance queries. The default is B<template1>. + +=item B<--[no-]start> + +Start the new database cluster after upgrading. The default is to start the new +cluster if the old cluster was running, or if upgrade hook scripts are present. + +=back + +=head1 HOOK SCRIPTS + +Some PostgreSQL extensions like PostGIS need metadata in auxiliary tables which +must not be upgraded from the old version, but rather initialized for the new +version before copying the table data. For this purpose, extensions (as well as +administrators, of course) can drop upgrade hook scripts into +C</etc/postgresql-common/pg_upgradecluster.d/>. Script file names must consist +entirely of upper and lower case letters, digits, underscores, and hyphens; in +particular, dots (i. e. file extensions) are not allowed. + +Scripts in that directory will be called with the following arguments: + +<old version> <cluster name> <new version> <phase> + +Phases: + +=over + +=item B<init> + +A virgin cluster of version I<new version> has been created, i. e. this new +cluster will already have B<template1> and B<postgres>, but no user databases. Please note that +you should not create tables in this phase, since they will be overwritten by +the dump/restore or B<pg_upgrade> operation. + +=item B<finish> + +All data from the old version cluster has been dumped/reloaded into the new +one. The old cluster still exists, but is not running. + +=back + +Failing scripts will abort the upgrade. +The scripts are called as the user who owns the database. + +=head1 SEE ALSO + +L<pg_createcluster(8)>, L<pg_dropcluster(8)>, L<pg_lsclusters(1)>, L<pg_wrapper(1)> + +=head1 AUTHORS + +Martin Pitt L<E<lt>mpitt@debian.orgE<gt>>, Christoph Berg L<E<lt>myon@debian.orgE<gt>> |