summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.vimrc3
-rw-r--r--Makefile64
-rw-r--r--PgCommon.pm1677
-rw-r--r--README.md114
-rw-r--r--TODO39
-rwxr-xr-xcleanpg24
-rw-r--r--createcluster.conf41
-rw-r--r--debhelper/Debian/Debhelper/Buildsystem/pgxs.pm58
-rw-r--r--debhelper/Debian/Debhelper/Buildsystem/pgxs_loop.pm33
-rw-r--r--debhelper/Debian/Debhelper/Sequence/pgxs.pm22
-rw-r--r--debhelper/Debian/Debhelper/Sequence/pgxs_loop.pm23
-rw-r--r--debhelper/Debian/Debhelper/pgxs.pm38
-rwxr-xr-xdebhelper/dh_pgxs_test11
-rw-r--r--debhelper/dh_pgxs_test.pod44
-rw-r--r--dh_make_pgxs/debian/control.in23
-rw-r--r--dh_make_pgxs/debian/copyright24
-rw-r--r--dh_make_pgxs/debian/gitlab-ci.yml1
-rw-r--r--dh_make_pgxs/debian/pgversions1
-rwxr-xr-xdh_make_pgxs/debian/rules36
-rw-r--r--dh_make_pgxs/debian/source/format1
-rw-r--r--dh_make_pgxs/debian/tests/control5
-rwxr-xr-xdh_make_pgxs/debian/tests/installcheck3
-rw-r--r--dh_make_pgxs/debian/watch2
-rwxr-xr-xdh_make_pgxs/dh_make_pgxs136
-rw-r--r--dh_make_pgxs/dh_make_pgxs.pod43
-rw-r--r--doc/dependencies.diabin0 -> 3144 bytes
-rw-r--r--doc/dependencies.svg206
-rw-r--r--doc/postgresql-debian-packaging.md741
-rw-r--r--gitlab/gitlab-ci.yml13
-rwxr-xr-xpg_backupcluster659
-rwxr-xr-xpg_buildext503
-rw-r--r--pg_buildext.pod358
-rwxr-xr-xpg_checksystem59
-rwxr-xr-xpg_config33
-rwxr-xr-xpg_conftool233
-rwxr-xr-xpg_createcluster952
-rwxr-xr-xpg_ctlcluster649
-rwxr-xr-xpg_dropcluster226
-rwxr-xr-xpg_getwal96
-rwxr-xr-xpg_hba144
-rwxr-xr-xpg_lsclusters184
-rwxr-xr-xpg_renamecluster176
-rwxr-xr-xpg_restorecluster436
-rwxr-xr-xpg_updateaptconfig41
-rwxr-xr-xpg_updatedicts139
-rwxr-xr-xpg_upgradecluster966
-rwxr-xr-xpg_upgradecluster.d/analyze33
-rwxr-xr-xpg_virtualenv280
-rw-r--r--pg_virtualenv.pod122
-rwxr-xr-xpg_wrapper329
-rw-r--r--pgdg/Makefile2
-rw-r--r--pgdg/apt.postgresql.org.asc77
-rw-r--r--pgdg/apt.postgresql.org.gpgbin0 -> 3494 bytes
-rwxr-xr-xpgdg/apt.postgresql.org.sh291
-rwxr-xr-xpgdg/update12
-rw-r--r--pgxs_debian_control.mk13
-rw-r--r--postgresqlrc.542
-rw-r--r--rpm/README52
-rw-r--r--rpm/init-functions-compat12
-rw-r--r--rpm/postgresql-common.spec139
-rw-r--r--server/README2
-rwxr-xr-xserver/catversion17
-rwxr-xr-xserver/pg_config.pl76
-rw-r--r--server/postgresql.mk294
-rw-r--r--server/test-with-jit.conf8
-rw-r--r--systemd/README.systemd55
-rwxr-xr-xsystemd/system-generators/postgresql-generator38
-rw-r--r--systemd/system/pg_basebackup@.service14
-rw-r--r--systemd/system/pg_basebackup@.timer12
-rw-r--r--systemd/system/pg_compresswal@.service9
-rw-r--r--systemd/system/pg_compresswal@.timer12
-rw-r--r--systemd/system/pg_dump@.service14
-rw-r--r--systemd/system/pg_dump@.timer12
-rw-r--r--systemd/system/pg_receivewal@.service21
-rw-r--r--systemd/system/postgresql.service18
-rw-r--r--systemd/system/postgresql@.service40
-rw-r--r--t/001_packages.t87
-rw-r--r--t/002_existing_clusters.t11
-rw-r--r--t/003_alternatives.t37
-rw-r--r--t/005_PgCommon.t311
-rw-r--r--t/006_next_free_port.t49
-rw-r--r--t/007_pg_conftool.t85
-rw-r--r--t/010_defaultport_cluster.t33
-rw-r--r--t/015_start_stop.t174
-rw-r--r--t/020_create_sql_remove.t452
-rw-r--r--t/021_pg_renamecluster.t43
-rw-r--r--t/022_recovery.t54
-rw-r--r--t/025_logging.t99
-rw-r--r--t/030_errors.t336
-rw-r--r--t/031_errors_disk_full.t86
-rw-r--r--t/032_ssl_key_permissions.t60
-rw-r--r--t/040_upgrade.t272
-rw-r--r--t/041_upgrade_custompaths.t51
-rw-r--r--t/042_upgrade_rename.t27
-rw-r--r--t/043_upgrade_ssl_cert.t79
-rw-r--r--t/045_backup.t169
-rw-r--r--t/050_encodings.t117
-rw-r--r--t/052_upgrade_encodings.t98
-rw-r--r--t/060_obsolete_confparams.t83
-rw-r--r--t/070_non_postgres_clusters.t116
-rw-r--r--t/080_start.conf.t145
-rw-r--r--t/085_pg_ctl.conf.t51
-rw-r--r--t/090_multicluster.t286
-rw-r--r--t/110_integrate_cluster.t44
-rw-r--r--t/120_pg_upgradecluster_scripts.t114
-rw-r--r--t/130_nonroot_admin.t50
-rw-r--r--t/135_pg_virtualenv.t35
-rw-r--r--t/140_pg_config.t89
-rw-r--r--t/150_tsearch_stemming.t108
-rw-r--r--t/160_alternate_confroot.t57
-rw-r--r--t/170_extensions.t87
-rw-r--r--t/180_ecpg.t56
-rw-r--r--t/190_pg_buildext.t104
-rw-r--r--t/200_maintscripts.t46
-rw-r--r--t/TestLib.pm270
-rw-r--r--t/bar/debian/control.in8
-rw-r--r--t/bar/debian/pgversions3
-rw-r--r--t/foo/Makefile12
-rw-r--r--t/foo/foo-123/Makefile10
-rw-r--r--t/foo/foo-123/README.md5
-rw-r--r--t/foo/foo-123/expected/foo.out8
-rw-r--r--t/foo/foo-123/expected/upgrade.out14
-rw-r--r--t/foo/foo-123/foo--100--123.sql3
-rw-r--r--t/foo/foo-123/foo--100.sql3
-rw-r--r--t/foo/foo-123/foo--123.sql3
-rw-r--r--t/foo/foo-123/foo.c13
-rw-r--r--t/foo/foo-123/foo.control2
-rw-r--r--t/foo/foo-123/sql/foo.sql5
-rw-r--r--t/foo/foo-123/sql/upgrade.sql7
-rw-r--r--t/template26
-rwxr-xr-xtestsuite222
-rw-r--r--user_clusters22
-rw-r--r--user_clusters.563
133 files changed, 16226 insertions, 0 deletions
diff --git a/.vimrc b/.vimrc
new file mode 100644
index 0000000..1853616
--- /dev/null
+++ b/.vimrc
@@ -0,0 +1,3 @@
+set expandtab
+set shiftwidth=4
+set smarttab
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..07a50bd
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,64 @@
+POD2MAN=pod2man --center "Debian PostgreSQL infrastructure" -r "Debian"
+POD1PROGS = pg_backupcluster.1 \
+ pg_conftool.1 \
+ pg_createcluster.1 \
+ pg_ctlcluster.1 \
+ pg_dropcluster.1 \
+ pg_getwal.1 \
+ pg_lsclusters.1 \
+ pg_renamecluster.1 \
+ pg_restorecluster.1 \
+ pg_upgradecluster.1 \
+ pg_wrapper.1
+POD1PROGS_POD = pg_buildext.1 \
+ pg_virtualenv.1 \
+ debhelper/dh_pgxs_test.1 \
+ dh_make_pgxs/dh_make_pgxs.1
+POD8PROGS = pg_updatedicts.8
+
+all: man sub-pgdg
+
+man: $(POD1PROGS) $(POD1PROGS_POD) $(POD8PROGS)
+
+%.1: %.pod
+ $(POD2MAN) --quotes=none --section 1 $< $@
+
+%.1: %
+ $(POD2MAN) --quotes=none --section 1 $< $@
+
+%.8: %
+ $(POD2MAN) --quotes=none --section 8 $< $@
+
+clean:
+ rm -f *.1 *.8 debhelper/*.1 dh_make_pgxs/*.1
+
+sub-pgdg:
+ $(MAKE) -C pgdg
+
+# rpm
+
+DPKG_VERSION=$(shell sed -ne '1s/.*(//; 1s/).*//p' debian/changelog)
+RPMDIR=$(CURDIR)/rpm
+TARNAME=postgresql-common_$(DPKG_VERSION).tar.xz
+TARBALL=$(RPMDIR)/SOURCES/$(TARNAME)
+
+rpmbuild: $(TARBALL)
+ rpmbuild -D"%_topdir $(RPMDIR)" --define='version $(DPKG_VERSION)' -ba rpm/postgresql-common.spec
+
+$(TARBALL):
+ mkdir -p $(dir $(TARBALL))
+ if test -f ../$(TARNAME); then \
+ cp -v ../$(TARNAME) $(TARBALL); \
+ else \
+ git archive --prefix=postgresql-common-$(DPKG_VERSION)/ HEAD | xz > $(TARBALL); \
+ fi
+
+rpminstall:
+ sudo yum install -y perl-JSON
+ sudo rpm --upgrade --replacefiles --replacepkgs -v $(RPMDIR)/RPMS/noarch/*-$(DPKG_VERSION)-*.rpm
+
+rpmremove:
+ -sudo rpm -e postgresql-common postgresql-client-common postgresql-server-dev-all
+
+rpmclean:
+ rm -rf $(RPMDIR)/*/
diff --git a/PgCommon.pm b/PgCommon.pm
new file mode 100644
index 0000000..df4d060
--- /dev/null
+++ b/PgCommon.pm
@@ -0,0 +1,1677 @@
+=head1 NAME
+
+PgCommon - Common functions for the postgresql-common framework
+
+=head1 COPYRIGHT AND LICENSE
+
+ (C) 2008-2009 Martin Pitt <mpitt@debian.org>
+ (C) 2012-2023 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
+L<version 2 of the License|https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>,
+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.
+
+=cut
+
+package PgCommon;
+use strict;
+use IPC::Open3;
+use Socket;
+use POSIX;
+
+use Exporter;
+our $VERSION = 1.00;
+our @ISA = ('Exporter');
+our @EXPORT = qw/error user_cluster_map get_cluster_port set_cluster_port
+ get_cluster_socketdir set_cluster_socketdir cluster_port_running
+ get_cluster_start_conf set_cluster_start_conf set_cluster_pg_ctl_conf
+ get_program_path cluster_info validate_cluster_owner get_versions get_newest_version version_exists
+ get_version_clusters next_free_port cluster_exists install_file
+ change_ugid system_or_error config_bool replace_v_c
+ get_db_encoding get_db_locales get_cluster_locales get_cluster_controldata
+ get_cluster_databases cluster_conf_filename read_cluster_conf_file
+ read_pg_hba read_pidfile valid_hba_method/;
+our @EXPORT_OK = qw/$confroot $binroot $rpm $have_python2
+ quote_conf_value read_conf_file get_conf_value
+ set_conf_value set_conffile_value disable_conffile_value disable_conf_value
+ replace_conf_value cluster_data_directory get_file_device
+ check_pidfile_running/;
+
+
+=head1 CONTENTS
+
+=head2 error
+
+ Print an error message to stderr and die with exit status 1
+
+=cut
+
+sub error {
+ $! = 1; # force exit code 1
+ die "Error: $_[0]\n";
+}
+
+our $confroot = '/etc/postgresql';
+if ($ENV{'PG_CLUSTER_CONF_ROOT'}) {
+ ($confroot) = $ENV{'PG_CLUSTER_CONF_ROOT'} =~ /(.*)/; # untaint
+}
+our $common_confdir = "/etc/postgresql-common";
+if ($ENV{'PGSYSCONFDIR'}) {
+ ($common_confdir) = $ENV{'PGSYSCONFDIR'} =~ /(.*)/; # untaint
+}
+my $mapfile = "$common_confdir/user_clusters";
+our $binroot = "/usr/lib/postgresql/";
+#redhat# $binroot = "/usr/pgsql-";
+our $rpm = 0;
+#redhat# $rpm = 1;
+our $defaultport = 5432;
+our $have_python2 = 0; # python2 removed in bullseye+
+#py2#$have_python2 = 1;
+
+=head2 prepare_exec, restore_exec
+
+ Untaint the environment for executing an external program
+
+ Optional arguments: list of additional variables
+
+=cut
+
+{
+ my %saved_env;
+
+ # untaint the environment for executing an external program
+ sub prepare_exec {
+ my @cleanvars = qw/PATH IFS ENV BASH_ENV CDPATH/;
+ push @cleanvars, @_;
+ %saved_env = ();
+
+ foreach (@cleanvars) {
+ $saved_env{$_} = $ENV{$_};
+ delete $ENV{$_};
+ }
+
+ $ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
+ }
+
+ # restore the environment after prepare_exec()
+ sub restore_exec {
+ foreach (keys %saved_env) {
+ if (defined $saved_env{$_}) {
+ $ENV{$_} = $saved_env{$_};
+ } else {
+ delete $ENV{$_};
+ }
+ }
+ }
+}
+
+
+=head2 config_bool
+
+ returns '1' if the argument is a configuration file value that stands for
+ true (ON, TRUE, YES, or 1, case insensitive), '0' if the argument represents
+ a false value (OFF, FALSE, NO, or 0, case insensitive), or undef otherwise.
+
+=cut
+
+sub config_bool {
+ return undef unless defined($_[0]);
+ return 1 if ($_[0] =~ /^(on|true|yes|1)$/i);
+ return 0 if ($_[0] =~ /^(off|false|no|0)$/i);
+ return undef;
+}
+
+
+=head2 quote_conf_value
+
+ Quotes a value with single quotes
+
+ Arguments: <value>
+ Returns: quoted string
+
+=cut
+
+sub quote_conf_value ($) {
+ my $value = shift;
+ return $value if ($value =~ /^-?[\d.]+$/); # integer or float
+ return $value if ($value =~ /^\w+$/); # plain word
+ $value =~ s/'/''/g; # else quote it
+ return "'$value'";
+}
+
+
+=head2 replace_v_c
+
+ Replaces %v and %c placeholders
+
+ Arguments: <string> <version> <cluster>
+ Returns: string
+
+=cut
+
+sub replace_v_c ($$$) {
+ my ($str, $version, $cluster) = @_;
+ $str =~ s/%([vc%])/$1 eq 'v' ? $version :
+ $1 eq 'c' ? $cluster : '%'/eg;
+ return $str;
+}
+
+
+=head2 read_conf_file
+
+ Read a 'var = value' style configuration file and return a hash with the
+ values. Error out if the file cannot be read.
+
+ If the file name ends with '.conf', the keys will be normalized to
+ lower case (suitable for e.g. postgresql.conf), otherwise kept intact
+ (suitable for environment).
+
+ Arguments: <path>
+ Returns: hash (empty if file does not exist)
+
+=cut
+
+sub read_conf_file {
+ my ($config_path) = @_;
+ my %conf;
+ local (*F);
+
+ sub get_absolute_path {
+ my ($path, $parent_path) = @_;
+ return $path if ($path =~ m!^/!); # path is absolute
+ # else strip filename component from parent path
+ $parent_path =~ s!/[^/]*$!!;
+ return "$parent_path/$path";
+ }
+
+ if (open F, $config_path) {
+ while (<F>) {
+ if (/^\s*(?:#.*)?$/) {
+ next;
+ } elsif(/^\s*include_dir\s*=?\s*'([^']+)'\s*(?:#.*)?$/i) {
+ # read included configuration directory and merge into %conf
+ # files in the directory will be read in ascending order
+ my $path = $1;
+ my $absolute_path = get_absolute_path($path, $config_path);
+ next unless -e $absolute_path && -d $absolute_path;
+ my $dir;
+ opendir($dir, $absolute_path) or next;
+ foreach my $filename (sort readdir($dir) ) {
+ next if ($filename =~ m/^\./ or not $filename =~/\.conf$/ );
+ my %include_conf = read_conf_file("$absolute_path/$filename");
+ while ( my ($k, $v) = each(%include_conf) ) {
+ $conf{$k} = $v;
+ }
+ }
+ closedir($dir);
+ } elsif (/^\s*include(?:_if_exists)?\s*=?\s*'([^']+)'\s*(?:#.*)?$/i) {
+ # read included file and merge into %conf
+ my $path = $1;
+ my $absolute_path = get_absolute_path($path, $config_path);
+ my %include_conf = read_conf_file($absolute_path);
+ while ( my ($k, $v) = each(%include_conf) ) {
+ $conf{$k} = $v;
+ }
+ } elsif (/^\s*([a-zA-Z0-9_.-]+)\s*(?:=|\s)\s*'((?:[^']|''|(?:(?<=\\)'))*)'\s*(?:#.*)?$/i) {
+ # string value
+ my $v = $2;
+ my $k = $1;
+ $k = lc $k if $config_path =~ /\.conf$/;
+ $v =~ s/\\(.)/$1/g;
+ $v =~ s/''/'/g;
+ $conf{$k} = $v;
+ } elsif (m{^\s*([a-zA-Z0-9_.-]+)\s*(?:=|\s)\s*(-?[[:alnum:]][[:alnum:]._:/+-]*)\s*(?:\#.*)?$}i) {
+ # simple value (string/float)
+ my $v = $2;
+ my $k = $1;
+ $k = lc $k if $config_path =~ /\.conf$/;
+ $conf{$k} = $v;
+ } else {
+ chomp;
+ error "invalid line $. in $config_path: $_";
+ }
+ }
+ close F;
+ }
+
+ return %conf;
+}
+
+=head2 cluster_conf_filename
+
+ Returns path to cluster config file from a cluster configuration
+ directory (with /etc/postgresql-common/<file name> as fallback)
+ and return a hash with the values. Error out if the file cannot be read.
+ If config file name is postgresql.auto.conf, read from PGDATA
+
+ Arguments: <version> <cluster> <config file name>
+ Returns: hash (empty if the file does not exist)
+
+=cut
+
+sub cluster_conf_filename {
+ my ($version, $cluster, $configfile) = @_;
+ if ($configfile eq 'postgresql.auto.conf') {
+ my $data_directory = cluster_data_directory($version, $cluster);
+ return "$data_directory/$configfile";
+ }
+ my $fname = "$confroot/$version/$cluster/$configfile";
+ -e $fname or $fname = "$common_confdir/$configfile";
+ return $fname;
+}
+
+
+=head2 read_cluster_conf_file
+
+Read a 'var = value' style configuration file from a cluster configuration
+
+Arguments: <version> <cluster> <config file name>
+Returns: hash (empty if the file does not exist)
+
+=cut
+
+sub read_cluster_conf_file {
+ my ($version, $cluster, $configfile) = @_;
+ my %conf = read_conf_file(cluster_conf_filename($version, $cluster, $configfile));
+
+ if ($version >= 9.4 and $configfile eq 'postgresql.conf') { # merge settings changed by ALTER SYSTEM
+ # data_directory cannot be changed by ALTER SYSTEM
+ my $data_directory = cluster_data_directory($version, $cluster, \%conf);
+ my %auto_conf = read_conf_file "$data_directory/postgresql.auto.conf";
+ foreach my $guc (keys %auto_conf) {
+ next if ($guc eq 'data_directory'); # defend against pg_upgradecluster bug in 200..202
+ $conf{$guc} = $auto_conf{$guc};
+ }
+ }
+
+ return %conf;
+}
+
+
+=head2 get_conf_value
+
+ Return parameter from a PostgreSQL configuration file,
+ or undef if the parameter does not exist.
+
+ Arguments: <version> <cluster> <config file name> <parameter name>
+
+=cut
+
+sub get_conf_value {
+ my %conf = (read_cluster_conf_file $_[0], $_[1], $_[2]);
+ return $conf{$_[3]};
+}
+
+
+=head2 set_conffile_value
+
+ Set parameter of a PostgreSQL configuration file.
+
+ Arguments: <config file name> <parameter name> <value>
+
+=cut
+
+sub set_conffile_value {
+ my ($fname, $key, $value) = ($_[0], $_[1], quote_conf_value($_[2]));
+ my @lines;
+
+ # read configuration file lines
+ open (F, $fname) or die "Error: could not open $fname for reading";
+ push @lines, $_ while (<F>);
+ close F;
+
+ my $found = 0;
+ # first, search for an uncommented setting
+ for (my $i=0; $i <= $#lines; ++$i) {
+ if ($lines[$i] =~ /^\s*($key)(\s*(?:=|\s)\s*)\w+\b((?:\s*#.*)?)/i or
+ $lines[$i] =~ /^\s*($key)(\s*(?:=|\s)\s*)'[^']*'((?:\s*#.*)?)/i) {
+ $lines[$i] = "$1$2$value$3\n";
+ $found = 1;
+ last;
+ }
+ }
+
+ # now check if the setting exists as a comment; if so, change that instead
+ # of appending
+ if (!$found) {
+ for (my $i=0; $i <= $#lines; ++$i) {
+ if ($lines[$i] =~ /^\s*#\s*($key)(\s*(?:=|\s)\s*)\w+\b((?:\s*#.*)?)$/i or
+ $lines[$i] =~ /^\s*#\s*($key)(\s*(?:=|\s)\s*)'[^']*'((?:\s*#.*)?)$/i) {
+ $lines[$i] = "$1$2$value$3\n";
+ $found = 1;
+ last;
+ }
+ }
+ }
+
+ # not found anywhere, append it
+ push (@lines, "$key = $value\n") unless $found;
+
+ # write configuration file lines
+ open (F, ">$fname.new") or die "Error: could not open $fname.new for writing";
+ foreach (@lines) {
+ print F $_ or die "writing $fname.new: $!";
+ }
+ close F;
+
+ # copy permissions
+ my @st = stat $fname or die "stat: $!";
+ chown $st[4], $st[5], "$fname.new"; # might fail as non-root
+ chmod $st[2], "$fname.new" or die "chmod: $!";
+
+ rename "$fname.new", "$fname" or die "rename $fname.new $fname: $!";
+}
+
+
+=head2 set_conf_value
+
+ Set parameter of a PostgreSQL cluster configuration file.
+
+ Arguments: <version> <cluster> <config file name> <parameter name> <value>
+
+=cut
+
+sub set_conf_value {
+ return set_conffile_value(cluster_conf_filename($_[0], $_[1], $_[2]), $_[3], $_[4]);
+}
+
+
+=head2 disable_conffile_value
+
+ Disable a parameter in a PostgreSQL configuration file by prepending it
+ with a '#'. Appends an optional explanatory comment <reason> if given.
+
+ Arguments: <config file name> <parameter name> <reason>
+
+=cut
+
+sub disable_conffile_value {
+ my ($fname, $key, $reason) = @_;
+ my @lines;
+
+ # read configuration file lines
+ open (F, $fname) or die "Error: could not open $fname for reading";
+ push @lines, $_ while (<F>);
+ close F;
+
+ my $changed = 0;
+ for (my $i=0; $i <= $#lines; ++$i) {
+ if ($lines[$i] =~ /^\s*$key\s*(?:=|\s)/i) {
+ $lines[$i] =~ s/^/#/;
+ $lines[$i] =~ s/$/ #$reason/ if $reason;
+ $changed = 1;
+ last;
+ }
+ }
+
+ # write configuration file lines
+ if ($changed) {
+ open (F, ">$fname.new") or die "Error: could not open $fname.new for writing";
+ foreach (@lines) {
+ print F $_ or die "writing $fname.new: $!";
+ }
+ close F;
+
+ # copy permissions
+ my @st = stat $fname or die "stat: $!";
+ chown $st[4], $st[5], "$fname.new"; # might fail as non-root
+ chmod $st[2], "$fname.new" or die "chmod: $1";
+
+ rename "$fname.new", "$fname";
+ }
+}
+
+
+=head2 disable_conf_value
+
+ Disable a parameter in a PostgreSQL cluster configuration file by prepending
+ it with a '#'. Appends an optional explanatory comment <reason> if given.
+
+ Arguments: <version> <cluster> <config file name> <parameter name> <reason>
+
+=cut
+
+sub disable_conf_value {
+ return disable_conffile_value(cluster_conf_filename($_[0], $_[1], $_[2]), $_[3], $_[4]);
+}
+
+
+=head2 replace_conf_value
+
+ Replace a parameter in a PostgreSQL configuration file. The old parameter
+ is prepended with a '#' and gets an optional explanatory comment <reason>
+ appended, if given. The new parameter is inserted directly after the old one.
+
+ Arguments: <version> <cluster> <config file name> <old parameter name>
+ <reason> <new parameter name> <new value>
+
+=cut
+
+sub replace_conf_value {
+ my ($version, $cluster, $configfile, $oldparam, $reason, $newparam, $val) = @_;
+ my $fname = cluster_conf_filename($version, $cluster, $configfile);
+ my @lines;
+
+ # quote $val if necessary
+ unless ($val =~ /^\w+$/) {
+ $val = "'$val'";
+ }
+
+ # read configuration file lines
+ open (F, $fname) or die "Error: could not open $fname for reading";
+ push @lines, $_ while (<F>);
+ close F;
+
+ my $found = 0;
+ for (my $i = 0; $i <= $#lines; ++$i) {
+ if ($lines[$i] =~ /^\s*$oldparam\s*(?:=|\s)/i) {
+ $lines[$i] = '#'.$lines[$i];
+ chomp $lines[$i];
+ $lines[$i] .= ' #'.$reason."\n" if $reason;
+
+ # insert the new param
+ splice @lines, $i+1, 0, "$newparam = $val\n";
+ ++$i;
+
+ $found = 1;
+ last;
+ }
+ }
+
+ return if !$found;
+
+ # write configuration file lines
+ open (F, ">$fname.new") or die "Error: could not open $fname.new for writing";
+ foreach (@lines) {
+ print F $_ or die "writing $fname.new: $!";
+ }
+ close F;
+
+ # copy permissions
+ my @st = stat $fname or die "stat: $!";
+ chown $st[4], $st[5], "$fname.new"; # might fail as non-root
+ chmod $st[2], "$fname.new" or die "chmod: $1";
+
+ rename "$fname.new", "$fname";
+}
+
+
+=head2 get_cluster_port
+
+ Return the port of a particular cluster
+
+ Arguments: <version> <cluster>
+
+=cut
+
+sub get_cluster_port {
+ return get_conf_value($_[0], $_[1], 'postgresql.conf', 'port') || $defaultport;
+}
+
+
+=head2 set_cluster_port
+
+ Set the port of a particular cluster.
+
+ Arguments: <version> <cluster> <port>
+
+=cut
+
+sub set_cluster_port {
+ set_conf_value $_[0], $_[1], 'postgresql.conf', 'port', $_[2];
+}
+
+
+=head2 cluster_data_directory
+
+ Return cluster data directory.
+
+ Arguments: <version> <cluster name> [<config_hash>]
+
+=cut
+
+sub cluster_data_directory {
+ my $d;
+ if ($_[2]) {
+ $d = ${$_[2]}{'data_directory'};
+ } else {
+ $d = get_conf_value($_[0], $_[1], 'postgresql.conf', 'data_directory');
+ }
+ my $confdir = "$confroot/$_[0]/$_[1]";
+ if (!$d) {
+ # fall back to /pgdata symlink (supported by earlier p-common releases)
+ $d = readlink "$confdir/pgdata";
+ }
+ if (!$d and -l $confdir and -f "$confdir/PG_VERSION") { # symlink from /etc/postgresql
+ $d = readlink $confdir;
+ }
+ if (!$d and -f "$confdir/PG_VERSION") { # PGDATA in /etc/postgresql
+ $d = $confdir;
+ }
+ ($d) = $d =~ /(.*)/ if defined $d; #untaint
+ return $d;
+}
+
+
+=head2 get_cluster_socketdir
+
+ Return the socket directory of a particular cluster
+ or undef if the cluster does not exist.
+
+ Arguments: <version> <cluster>
+
+=cut
+
+sub get_cluster_socketdir {
+ # if it is explicitly configured, just return it
+ my $socketdir = get_conf_value($_[0], $_[1], 'postgresql.conf',
+ $_[0] >= 9.3 ? 'unix_socket_directories' : 'unix_socket_directory');
+ $socketdir =~ s/\s*,.*// if ($socketdir); # ignore additional directories for now
+ return $socketdir if $socketdir;
+
+ #redhat# return '/tmp'; # RedHat PGDG packages default to /tmp
+ # try to determine whether this is a postgres owned cluster and we default
+ # to /var/run/postgresql
+ $socketdir = '/var/run/postgresql';
+ my @socketdirstat = stat $socketdir;
+
+ error "Cannot stat $socketdir" unless @socketdirstat;
+
+ if ($_[0] && $_[1]) {
+ my $datadir = cluster_data_directory $_[0], $_[1];
+ error "Invalid data directory for cluster $_[0] $_[1]" unless $datadir;
+ my @datadirstat = stat $datadir;
+ unless (@datadirstat) {
+ my @p = split '/', $datadir;
+ my $parent = join '/', @p[0..($#p-1)];
+ error "$datadir is not accessible; please fix the directory permissions ($parent/ should be world readable)" unless @datadirstat;
+ }
+
+ $socketdir = '/tmp' if $socketdirstat[4] != $datadirstat[4];
+ }
+
+ return $socketdir;
+}
+
+
+=head2 set_cluster_socketdir
+
+ Set the socket directory of a particular cluster.
+
+ Arguments: <version> <cluster> <directory>
+
+=cut
+
+sub set_cluster_socketdir {
+ set_conf_value $_[0], $_[1], 'postgresql.conf',
+ $_[0] >= 9.3 ? 'unix_socket_directories' : 'unix_socket_directory',
+ $_[2];
+}
+
+
+=head2 get_program_path
+
+ Return the path of a program of a particular version.
+
+ Arguments: <program name> [<version>]
+
+=cut
+
+sub get_program_path {
+ my ($program, $version) = @_;
+ return '' unless defined $program;
+ $version //= get_newest_version($program);
+ my $path = "$binroot$version/bin/$program";
+ ($path) = $path =~ /(.*)/; #untaint
+ return $path if -x $path;
+ return '';
+}
+
+
+=head2 cluster_port_running
+
+ Check whether a postgres server is running at the specified port.
+
+ Arguments: <version> <cluster> <port>
+
+=cut
+
+sub cluster_port_running {
+ die "port_running: invalid port $_[2]" if $_[2] !~ /\d+/;
+ my $socketdir = get_cluster_socketdir $_[0], $_[1];
+ my $socketpath = "$socketdir/.s.PGSQL.$_[2]";
+ return 0 unless -S $socketpath;
+
+ socket(SRV, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+ my $running = connect(SRV, sockaddr_un($socketpath));
+ close SRV;
+ return $running ? 1 : 0;
+}
+
+
+=head2 get_cluster_start_conf
+
+ Read, verify, and return the current start.conf setting.
+
+ Arguments: <version> <cluster>
+ Returns: auto | manual | disabled
+
+=cut
+
+sub get_cluster_start_conf {
+ my $start_conf = "$confroot/$_[0]/$_[1]/start.conf";
+ if (-e $start_conf) {
+ open F, $start_conf or error "Could not open $start_conf: $!";
+ while (<F>) {
+ s/#.*$//;
+ s/^\s*//;
+ s/\s*$//;
+ next unless $_;
+ close F;
+ return $1 if (/^(auto|manual|disabled)/);
+ error "Invalid mode in $start_conf, must be one of auto, manual, disabled";
+ }
+ close F;
+ }
+ return 'auto'; # default
+}
+
+
+=head2 set_cluster_start_conf
+
+ Change start.conf setting.
+
+ Arguments: <version> <cluster> <value>
+ <value> = auto | manual | disabled
+
+=cut
+
+sub set_cluster_start_conf {
+ my ($v, $c, $val) = @_;
+
+ error "Invalid mode: '$val'" unless $val eq 'auto' ||
+ $val eq 'manual' || $val eq 'disabled';
+
+ my $perms = 0644;
+
+ # start.conf setting
+ my $start_conf = "$confroot/$_[0]/$_[1]/start.conf";
+ my $text;
+ if (-e $start_conf) {
+ open F, $start_conf or error "Could not open $start_conf: $!";
+ while (<F>) {
+ if (/^\s*(?:auto|manual|disabled)\b(.*$)/) {
+ $text .= $val . $1 . "\n";
+ } else {
+ $text .= $_;
+ }
+ }
+
+ # preserve permissions if it already exists
+ $perms = (stat F)[2];
+ error "Could not get permissions of $start_conf: $!" unless $perms;
+ close F;
+ } else {
+ $text = "# Automatic startup configuration
+# auto: automatically start the cluster
+# manual: manual startup with pg_ctlcluster/postgresql@.service only
+# disabled: refuse to start cluster
+# See pg_createcluster(1) for details. When running from systemd,
+# invoke 'systemctl daemon-reload' after editing this file.
+
+$val
+";
+ }
+
+ open F, '>' . $start_conf or error "Could not open $start_conf for writing: $!";
+ chmod $perms, $start_conf;
+ print F $text;
+ close F;
+}
+
+
+=head2 set_cluster_pg_ctl_conf
+
+ Change pg_ctl.conf setting.
+
+ Arguments: <version> <cluster> <options>
+ <options> = options passed to pg_ctl(1)
+
+=cut
+
+sub set_cluster_pg_ctl_conf {
+ my ($v, $c, $opts) = @_;
+ my $perms = 0644;
+
+ # pg_ctl.conf setting
+ my $pg_ctl_conf = "$confroot/$v/$c/pg_ctl.conf";
+ my $text = "# Automatic pg_ctl configuration
+# This configuration file contains cluster specific options to be passed to
+# pg_ctl(1).
+
+pg_ctl_options = '$opts'
+";
+
+ open F, '>' . $pg_ctl_conf or error "Could not open $pg_ctl_conf for writing: $!";
+ chmod $perms, $pg_ctl_conf;
+ print F $text;
+ close F;
+}
+
+
+=head2 read_pidfile
+
+ Return the PID from an existing PID file or undef if it does not exist.
+
+ Arguments: <pid file path>
+
+=cut
+
+sub read_pidfile {
+ return undef unless -e $_[0];
+
+ if (open PIDFILE, $_[0]) {
+ my $pid = <PIDFILE>;
+ close PIDFILE;
+ return undef unless ($pid);
+ chomp $pid;
+ ($pid) = $pid =~ /^(\d+)\s*$/; # untaint
+ return $pid;
+ } else {
+ return undef;
+ }
+}
+
+
+=head2 check_pidfile_running
+
+ Check whether a pid file is present and belongs to a running postgres.
+ Returns undef if it cannot be determined
+
+ Arguments: <pid file path>
+
+ postgres does not clean up the PID file when it stops, and it is
+ not world readable, so only its absence is a definitive result;
+ if it is present, we need to read it and check the PID, which will
+ only work as root
+
+=cut
+
+sub check_pidfile_running {
+ return 0 if ! -e $_[0];
+
+ my $pid = read_pidfile $_[0];
+ if (defined $pid and open CL, "/proc/$pid/cmdline") {
+ my $cmdline = <CL>;
+ close CL;
+ if ($cmdline and $cmdline =~ /\bpostgres\b/) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ return undef;
+}
+
+
+=head2 cluster_supervisor
+
+ Determine if a cluster is managed by a supervisor (pacemaker, patroni).
+ Returns undef if it cannot be determined
+
+ Arguments: <pid file path>
+
+ postgres does not clean up the PID file when it stops, and it is
+ not world readable, so only its absence is a definitive result; if it
+ is present, we need to read it and check the PID, which will only
+ work as root
+
+=cut
+
+sub cluster_supervisor {
+ return undef if ! -e $_[0];
+
+ my $pid = read_pidfile $_[0];
+ if (defined $pid and open(CG, "/proc/$pid/cgroup")) {
+ local $/; # enable localized slurp mode
+ my $cgroup = <CG>;
+ close CG;
+ if ($cgroup and $cgroup =~ /\b(pacemaker|patroni)\b/) {
+ return $1;
+ }
+ }
+ return undef;
+}
+
+
+=head2 cluster_info
+
+ Return a hash with information about a specific cluster (which needs to exist).
+
+ Arguments: <version> <cluster name>
+ Returns: information hash (keys: pgdata, port, running, logfile [unless it
+ has a custom one], configdir, owneruid, ownergid, waldir, socketdir,
+ config->postgresql.conf)
+
+=cut
+
+sub cluster_info {
+ my ($v, $c) = @_;
+ error 'cluster_info must be called with <version> <cluster> arguments' unless ($v and $c);
+
+ my %result;
+ $result{'configdir'} = "$confroot/$v/$c";
+ $result{'configuid'} = (stat "$result{configdir}/postgresql.conf")[4];
+
+ my %postgresql_conf = read_cluster_conf_file $v, $c, 'postgresql.conf';
+ $result{'config'} = \%postgresql_conf;
+ $result{'pgdata'} = cluster_data_directory $v, $c, \%postgresql_conf;
+ return %result unless (keys %postgresql_conf);
+ $result{'port'} = $postgresql_conf{'port'} || $defaultport;
+ $result{'socketdir'} = get_cluster_socketdir $v, $c;
+
+ # if we can determine the running status with the pid file, prefer that
+ if ($postgresql_conf{'external_pid_file'} &&
+ $postgresql_conf{'external_pid_file'} ne '(none)') {
+ $result{'running'} = check_pidfile_running $postgresql_conf{'external_pid_file'};
+ my $supervisor = cluster_supervisor($postgresql_conf{'external_pid_file'});
+ $result{supervisor} = $supervisor if ($supervisor);
+ }
+
+ # otherwise fall back to probing the port; this is unreliable if the port
+ # was changed in the configuration file in the meantime
+ if (!defined ($result{'running'})) {
+ $result{'running'} = cluster_port_running ($v, $c, $result{'port'});
+ }
+
+ if ($result{'pgdata'}) {
+ ($result{'owneruid'}, $result{'ownergid'}) =
+ (stat $result{'pgdata'})[4,5];
+ if ($v >= 12) {
+ $result{'recovery'} = 1 if (-e "$result{'pgdata'}/recovery.signal"
+ or -e "$result{'pgdata'}/standby.signal");
+ } else {
+ $result{'recovery'} = 1 if (-e "$result{'pgdata'}/recovery.conf");
+ }
+ my $waldirname = $v >= 10 ? 'pg_wal' : 'pg_xlog';
+ if (-l "$result{pgdata}/$waldirname") { # custom wal directory
+ ($result{waldir}) = readlink("$result{pgdata}/$waldirname") =~ /(.*)/; # untaint
+ }
+ }
+ $result{'start'} = get_cluster_start_conf $v, $c;
+
+ # default log file (possibly used only for early startup messages)
+ my $log_symlink = $result{'configdir'} . "/log";
+ if (-l $log_symlink) {
+ ($result{'logfile'}) = readlink ($log_symlink) =~ /(.*)/; # untaint
+ } else {
+ $result{'logfile'} = "/var/log/postgresql/postgresql-$v-$c.log";
+ }
+
+ return %result;
+}
+
+
+=head2 validate_cluster_owner
+
+ Checks if the owner of a cluster is valid, and the owner of the config matches
+ the owner of the data directory.
+
+ Arguments: cluster_info hash reference
+
+=cut
+
+sub validate_cluster_owner($) {
+ my $info = shift;
+
+ unless ($info->{pgdata}) {
+ error "Cluster data directory is unknown";
+ }
+ unless (-d $info->{pgdata}) {
+ error "$info->{pgdata} is not accessible or does not exist";
+ }
+ unless (defined $info->{owneruid}) {
+ error "Could not determine owner of $info->{pgdata}";
+ }
+ if ($info->{owneruid} == 0) {
+ error "Data directory $info->{pgdata} must not be owned by root";
+ }
+ unless (getpwuid $info->{owneruid}) {
+ error "The cluster is owned by user id $info->{owneruid} which does not exist";
+ }
+ unless (getgrgid $info->{ownergid}) {
+ error "The cluster is owned by group id $info->{ownergid} which does not exist";
+ }
+ # owneruid and configuid need to match, unless configuid is root
+ if (($< == 0 or $> == 0) and $info->{configuid} != 0 and
+ $info->{configuid} != $info->{owneruid}) {
+ my $configowner = (getpwuid $info->{configuid})[0] || "(unknown)";
+ my $dataowner = (getpwuid $info->{owneruid})[0];
+ error "Config owner ($configowner:$info->{configuid}) and data owner ($dataowner:$info->{owneruid}) do not match, and config owner is not root";
+ }
+}
+
+
+=head2 get_versions
+
+ Return an array of all available versions (by binaries and postgresql.conf files)
+
+ Arguments: binary to scan for (optional, defaults to postgres), maximum acceptable version (optional)
+
+=cut
+
+sub get_versions {
+ my $program = shift // 'postgres';
+ my $max_version = shift;
+ my %versions = ();
+
+ # enumerate psql versions from /usr/lib/postgresql/* (or /usr/pgsql-*)
+ my $dir = $binroot;
+ #redhat# $dir = '/usr';
+ if (opendir (D, $dir)) {
+ my $entry;
+ while (defined ($entry = readdir D)) {
+ next if $entry eq '.' || $entry eq '..';
+ my $pfx = '';
+ #redhat# $pfx = "pgsql-";
+ my $version;
+ ($version) = $entry =~ /^$pfx(\d+\.?\d+)$/; # untaint
+ next if ($max_version and $version > $max_version);
+ $versions{$version} = 1 if $version and get_program_path ($program, $version);
+ }
+ closedir D;
+ }
+
+ # enumerate server versions from /etc/postgresql/*
+ if ($program eq 'postgres' and opendir (D, $confroot)) {
+ my $v;
+ while (defined ($v = readdir D)) {
+ next if $v eq '.' || $v eq '..';
+ ($v) = $v =~ /^(\d+\.?\d+)$/; # untaint
+ next unless ($v);
+ next if ($max_version and $v > $max_version);
+
+ if (opendir (C, "$confroot/$v")) {
+ my $c;
+ while (defined ($c = readdir C)) {
+ if (-e "$confroot/$v/$c/postgresql.conf") {
+ $versions{$v} = 1;
+ last;
+ }
+ }
+ closedir C;
+ }
+ }
+ closedir D;
+ }
+
+ return sort { $a <=> $b } keys %versions;
+}
+
+
+=head2 get_newest_version
+
+ Return the newest available version
+
+ Arguments: binary to scan for (optional), maximum acceptable version (optional)
+
+=cut
+
+sub get_newest_version {
+ my $program = shift;
+ my $max_version = shift;
+ my @versions = get_versions($program, $max_version);
+ return undef unless (@versions);
+ return $versions[-1];
+}
+
+=head2 version_exists
+
+ Check whether a version exists
+
+=cut
+
+sub version_exists {
+ my ($version) = @_;
+ return get_program_path ('psql', $version);
+}
+
+
+=head2 get_version_clusters
+
+ Return an array of all available clusters of given version
+
+ Arguments: <version>
+
+=cut
+
+sub get_version_clusters {
+ my $vdir = $confroot.'/'.$_[0].'/';
+ my @clusters = ();
+ if (opendir (D, $vdir)) {
+ my $entry;
+ while (defined ($entry = readdir D)) {
+ next if $entry eq '.' || $entry eq '..';
+ ($entry) = $entry =~ /^(.*)$/; # untaint
+ my $conf = "$vdir$entry/postgresql.conf";
+ if (-e $conf or -l $conf) { # existing file, or dead symlink
+ push @clusters, $entry;
+ }
+ }
+ closedir D;
+ }
+ return sort @clusters;
+}
+
+
+=head2 cluster_exists
+
+ Check if a cluster exists.
+
+ Arguments: <version> <cluster>
+
+=cut
+
+sub cluster_exists {
+ for my $c (get_version_clusters $_[0]) {
+ return 1 if $c eq $_[1];
+ }
+ return 0;
+}
+
+
+=head2 next_free_port
+
+ Return the next free PostgreSQL port.
+
+=cut
+
+sub next_free_port {
+ # create list of already used ports
+ my %ports;
+ for my $v (get_versions) {
+ for my $c (get_version_clusters $v) {
+ $ports{ get_cluster_port ($v, $c) } = 1;
+ }
+ }
+
+ my $port;
+ for ($port = $defaultport; $port < 65536; ++$port) {
+ # port in use by existing cluster
+ next if (exists $ports{$port});
+
+ # IPv4 port in use
+ my ($have_ip4, $have_ip6);
+ if (socket (SOCK, PF_INET, SOCK_STREAM, getprotobyname('tcp'))) {
+ $have_ip4 = 1;
+ setsockopt(SOCK, Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1) or error "setsockopt: $!";
+ my $res4 = bind (SOCK, sockaddr_in($port, INADDR_ANY)) and listen (SOCK, 0);
+ my $err = $!;
+ close SOCK;
+ next unless ($res4);
+ }
+
+ # IPv6 port in use
+ if (exists $Socket::{"IN6ADDR_ANY"}) {
+ if (socket (SOCK, PF_INET6, SOCK_STREAM, getprotobyname('tcp'))) {
+ $have_ip6 = 1;
+ setsockopt(SOCK, Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1) or error "setsockopt: $!";
+ my $res6 = bind (SOCK, sockaddr_in6($port, Socket::IN6ADDR_ANY)) and listen (SOCK, 0);
+ my $err = $!;
+ close SOCK;
+ next unless ($res6);
+ }
+ }
+
+ unless ($have_ip4 or $have_ip6) {
+ # require at least one protocol to work (PostgreSQL needs it anyway
+ # for the stats collector)
+ die "could not create socket: $!";
+ }
+
+ close SOCK;
+ # return port if it is available on all supported protocols
+ return $port;
+ }
+
+ die "no free port found";
+}
+
+
+=head2 user_cluster_map
+
+ Return the PostgreSQL version, cluster, and database to connect to.
+
+ Version is always set (defaulting to the version of the default port
+ if no matching entry is found, or finally to the latest installed version
+ if there are no clusters at all), cluster and database may be 'undef'.
+ If only one cluster exists, and no matching entry is found in the map files,
+ that cluster is returned.
+
+=cut
+
+sub user_cluster_map {
+ my ($user, $pwd, $uid, $gid) = getpwuid $>;
+ my $group = (getgrgid $gid)[0];
+
+ # check per-user configuration file
+ my $home = $ENV{"HOME"} || (getpwuid $>)[7];
+ my $homemapfile = $home . '/.postgresqlrc';
+ if (open MAP, $homemapfile) {
+ while (<MAP>) {
+ s/#.*//;
+ next if /^\s*$/;
+ my ($v,$c,$db) = split;
+ if (!version_exists $v) {
+ print "Warning: $homemapfile line $.: version $v does not exist\n";
+ next;
+ }
+ if (!cluster_exists $v, $c and $c !~ /^(\S+):(\d*)$/) {
+ print "Warning: $homemapfile line $.: cluster $v/$c does not exist\n";
+ next;
+ }
+ if ($db) {
+ close MAP;
+ return ($v, $c, ($db eq "*") ? undef : $db);
+ } else {
+ print "Warning: ignoring invalid line $. in $homemapfile\n";
+ next;
+ }
+ }
+ close MAP;
+ }
+
+ # check global map file
+ if (open MAP, $mapfile) {
+ while (<MAP>) {
+ s/#.*//;
+ next if /^\s*$/;
+ my ($u,$g,$v,$c,$db) = split;
+ if (!$db) {
+ print "Warning: ignoring invalid line $. in $mapfile\n";
+ next;
+ }
+ if (!version_exists $v) {
+ print "Warning: $mapfile line $.: version $v does not exist\n";
+ next;
+ }
+ if (!cluster_exists $v, $c and $c !~ /^(\S+):(\d*)$/) {
+ print "Warning: $mapfile line $.: cluster $v/$c does not exist\n";
+ next;
+ }
+ if (($u eq "*" || $u eq $user) && ($g eq "*" || $g eq $group)) {
+ close MAP;
+ return ($v,$c, ($db eq "*") ? undef : $db);
+ }
+ }
+ close MAP;
+ }
+
+ # if only one cluster exists, use that
+ my $count = 0;
+ my ($last_version, $last_cluster, $defaultport_version, $defaultport_cluster);
+ for my $v (get_versions) {
+ for my $c (get_version_clusters $v) {
+ my $port = get_cluster_port ($v, $c);
+ $last_version = $v;
+ $last_cluster = $c;
+ if ($port == $defaultport) {
+ $defaultport_version = $v;
+ $defaultport_cluster = $c;
+ }
+ ++$count;
+ }
+ }
+ return ($last_version, $last_cluster, undef) if $count == 1;
+
+ if ($count == 0) {
+ # if there are no local clusters, use latest clients for accessing
+ # network clusters
+ return (get_newest_version('psql'), undef, undef);
+ }
+
+ # more than one cluster exists, return cluster at default port
+ return ($defaultport_version, $defaultport_cluster, undef);
+}
+
+
+=head2 install_file
+
+ Copy a file to a destination and setup permissions
+
+ Arguments: <source file> <destination file or dir> <uid> <gid> <permissions>
+
+=cut
+
+sub install_file {
+ my ($source, $dest, $uid, $gid, $perm) = @_;
+
+ if (system 'install', '-o', $uid, '-g', $gid, '-m', $perm, $source, $dest) {
+ error "install_file: could not install $source to $dest";
+ }
+}
+
+
+=head2 change_ugid
+
+ Change effective and real user and group id. Also activates all auxiliary
+ groups the user is in. Exits with an error message if user/group ID cannot
+ be changed.
+
+ Arguments: <user id> <group id>
+
+=cut
+
+sub change_ugid {
+ my ($uid, $gid) = @_;
+
+ # auxiliary groups
+ my $uname = (getpwuid $uid)[0];
+ prepare_exec;
+ my $groups = "$gid " . `/usr/bin/id -G $uname`;
+ restore_exec;
+
+ $) = $groups;
+ $( = $gid;
+ $> = $< = $uid;
+ error 'Could not change user id' if $< != $uid;
+ error 'Could not change group id' if $( != $gid;
+}
+
+
+=head2 system_or_error
+
+ Run a command and error out if it exits with a non-zero status.
+
+ Arguments: <command ...>
+
+=cut
+
+sub system_or_error {
+ my $ret = system @_;
+ if ($ret) {
+ my $message = "@_ failed with exit code $ret";
+ $message .= ": $!" if ($!);
+ error $message;
+ }
+}
+
+
+=head2 get_db_encoding
+
+ Return the encoding of a particular database in a cluster.
+
+ This requires access privileges to that database, so this
+ function should be called as the cluster owner.
+
+ Arguments: <version> <cluster> <database>
+ Returns: Encoding or undef if it cannot be determined.
+
+=cut
+
+sub get_db_encoding {
+ my ($version, $cluster, $db) = @_;
+ my $port = get_cluster_port $version, $cluster;
+ my $socketdir = get_cluster_socketdir $version, $cluster;
+ my $psql = get_program_path 'psql', $version;
+ return undef unless ($port && $socketdir && $psql);
+
+ # try to swich to cluster owner
+ prepare_exec 'LC_ALL';
+ $ENV{'LC_ALL'} = 'C';
+ my $orig_euid = $>;
+ $> = (stat (cluster_data_directory $version, $cluster))[4];
+ open PSQL, '-|', $psql, '-h', $socketdir, '-p', $port, '-AXtc',
+ 'select getdatabaseencoding()', $db or
+ die "Internal error: could not call $psql to determine db encoding: $!";
+ my $out = <PSQL>;
+ close PSQL;
+ $> = $orig_euid;
+ restore_exec;
+ return undef if $?;
+ chomp $out;
+ ($out) = $out =~ /^([\w.-]+)$/; # untaint
+ return $out;
+}
+
+
+=head2 get_db_locales
+
+ Return locale of a particular database in a cluster. This requires access
+ privileges to that database, so this function should be called as the cluster
+ owner. (For versions >= 8.4; for older versions use get_cluster_locales()).
+
+ Arguments: <version> <cluster> <database>
+ Returns: (LC_CTYPE, LC_COLLATE) or (undef,undef) if it cannot be determined.
+ PG15 adds locale provider and icu locale to the returned values
+ PG16 adds icu rules
+
+=cut
+
+sub get_db_locales {
+ my ($version, $cluster, $db) = @_;
+ my $port = get_cluster_port $version, $cluster;
+ my $socketdir = get_cluster_socketdir $version, $cluster;
+ my $psql = get_program_path 'psql', $version;
+ return undef unless ($port && $socketdir && $psql);
+ my ($ctype, $collate, $locale_provider, $icu_locale, $icu_rules);
+
+ # try to switch to cluster owner
+ prepare_exec 'LC_ALL';
+ $ENV{'LC_ALL'} = 'C';
+ my $orig_euid = $>;
+ $> = (stat (cluster_data_directory $version, $cluster))[4];
+
+ open PSQL, '-|', $psql, '-h', $socketdir, '-p', $port, '-AXtc',
+ "SELECT datctype, datcollate FROM pg_database where datname = current_database()", $db or
+ die "Internal error: could not call $psql to determine datctype and datcollate: $!";
+ my $out = <PSQL> // error 'could not determine datctype and datcollate';
+ close PSQL;
+ ($out) = $out =~ /^(.*)$/; # untaint
+ ($ctype, $collate) = split /\|/, $out;
+
+ if ($version >= 15) {
+ open PSQL, '-|', $psql, '-h', $socketdir, '-p', $port, '-AXtc',
+ "SELECT CASE datlocprovider::text WHEN 'c' THEN 'libc' WHEN 'i' THEN 'icu' END, daticulocale" .
+ ($version >= 16 ? ", daticurules" : "") .
+ " FROM pg_database where datname = current_database()", $db or
+ die "Internal error: could not call $psql to determine datlocprovider: $!";
+ $out = <PSQL> // error 'could not determine datlocprovider';
+ close PSQL;
+ ($out) = $out =~ /^(.*)$/; # untaint
+ ($locale_provider, $icu_locale, $icu_rules) = split /\|/, $out;
+ }
+
+ $> = $orig_euid;
+ restore_exec;
+ chomp $ctype;
+ chomp $collate;
+ return ($ctype, $collate, $locale_provider, $icu_locale, $icu_rules) unless $?;
+ return (undef, undef);
+}
+
+
+=head2 get_cluster_locales
+
+ Return the CTYPE and COLLATE locales of a cluster.
+
+ This needs to be called as root or as the cluster owner.
+ (For versions <= 8.3; for >= 8.4, use get_db_locales()).
+
+ Arguments: <version> <cluster>
+ Returns: (LC_CTYPE, LC_COLLATE) or (undef,undef) if it cannot be determined.
+
+=cut
+
+sub get_cluster_locales {
+ my ($version, $cluster) = @_;
+ my ($lc_ctype, $lc_collate) = (undef, undef);
+
+ if ($version >= '8.4') {
+ print STDERR "Error: get_cluster_locales() does not work for 8.4+\n";
+ exit 1;
+ }
+
+ my $pg_controldata = get_program_path 'pg_controldata', $version;
+ if (! -e $pg_controldata) {
+ print STDERR "Error: pg_controldata not found, please install postgresql-$version\n";
+ exit 1;
+ }
+ prepare_exec ('LC_ALL', 'LANG', 'LANGUAGE');
+ $ENV{'LC_ALL'} = 'C';
+ my $result = open (CTRL, '-|', $pg_controldata, (cluster_data_directory $version, $cluster));
+ restore_exec;
+ return (undef, undef) unless defined $result;
+ while (<CTRL>) {
+ if (/^LC_CTYPE\W*(\S+)\s*$/) {
+ $lc_ctype = $1;
+ } elsif (/^LC_COLLATE\W*(\S+)\s*$/) {
+ $lc_collate = $1;
+ }
+ }
+ close CTRL;
+ return ($lc_ctype, $lc_collate);
+}
+
+
+=head2 get_cluster_controldata
+
+ Return the pg_control data for a cluster
+
+ Arguments: <version> <cluster>
+ Returns: hashref
+
+=cut
+
+sub get_cluster_controldata {
+ my ($version, $cluster) = @_;
+
+ my $pg_controldata = get_program_path 'pg_controldata', $version;
+ if (! -e $pg_controldata) {
+ print STDERR "Error: pg_controldata not found, please install postgresql-$version\n";
+ exit 1;
+ }
+ prepare_exec ('LC_ALL', 'LANG', 'LANGUAGE');
+ $ENV{'LC_ALL'} = 'C';
+ my $result = open (CTRL, '-|', $pg_controldata, (cluster_data_directory $version, $cluster));
+ restore_exec;
+ return undef unless defined $result;
+ my $data = {};
+ while (<CTRL>) {
+ if (/^(.+?):\s*(.*)/) {
+ $data->{$1} = $2;
+ } else {
+ error "Invalid pg_controldata output: $_";
+ }
+ }
+ close CTRL;
+ return $data;
+}
+
+
+=head2 get_cluster_databases
+
+ Return an array with all databases of a cluster.
+
+ This requires connection privileges to template1, so
+ this function should be called as the cluster owner.
+
+ Arguments: <version> <cluster>
+ Returns: array of database names or undef on error.
+
+=cut
+
+sub get_cluster_databases {
+ my ($version, $cluster) = @_;
+ my $port = get_cluster_port $version, $cluster;
+ my $socketdir = get_cluster_socketdir $version, $cluster;
+ my $psql = get_program_path 'psql', $version;
+ return undef unless ($port && $socketdir && $psql);
+
+ # try to swich to cluster owner
+ prepare_exec 'LC_ALL';
+ $ENV{'LC_ALL'} = 'C';
+ my $orig_euid = $>;
+ $> = (stat (cluster_data_directory $version, $cluster))[4];
+
+ my @dbs;
+ my @fields;
+ if (open PSQL, '-|', $psql, '-h', $socketdir, '-p', $port, '-AXtl') {
+ while (<PSQL>) {
+ chomp;
+ @fields = split '\|';
+ next if $#fields < 2; # remove access privs which get line broken
+ push (@dbs, $fields[0]);
+ }
+ close PSQL;
+ }
+
+ $> = $orig_euid;
+ restore_exec;
+
+ return $? ? undef : @dbs;
+}
+
+
+=head2 get_file_device
+
+ Return the device name a file is stored at.
+
+ Arguments: <file path>
+ Returns: device name, or '' if it cannot be determined.
+
+=cut
+
+sub get_file_device {
+ my $dev = '';
+ prepare_exec;
+ my $pid = open3(\*CHLD_IN, \*CHLD_OUT, \*CHLD_ERR, '/bin/df', $_[0]);
+ waitpid $pid, 0; # we simply ignore exit code and stderr
+ while (<CHLD_OUT>) {
+ if (/^\/dev/) {
+ $dev = (split)[0];
+ }
+ }
+ restore_exec;
+ close CHLD_IN;
+ close CHLD_OUT;
+ close CHLD_ERR;
+ return $dev;
+}
+
+
+=head2 parse_hba_line
+
+ Parse a single pg_hba.conf line.
+
+ Arguments: <line>
+ Returns: Hash reference (or only line and type==undef for invalid lines)
+
+=over 4
+
+=item *
+
+line -> the verbatim pg_hba line
+
+=item *
+
+type -> comment, local, host, hostssl, hostnossl, undef
+
+=item *
+
+db -> database name
+
+=item *
+
+user -> user name
+
+=item *
+
+method -> trust, reject, md5, crypt, password, krb5, ident, pam
+
+=item *
+
+ip -> ip address
+
+=item *
+
+mask -> network mask (either a single number as number of bits, or bit mask)
+
+=back
+
+=cut
+
+sub parse_hba_line {
+ my $l = $_[0];
+ chomp $l;
+
+ # comment line?
+ return { 'type' => 'comment', 'line' => $l } if ($l =~ /^\s*($|#)/);
+
+ my $res = { 'line' => $l };
+ my @tok = split /\s+/, $l;
+ goto error if $#tok < 3;
+
+ $$res{'type'} = shift @tok;
+ $$res{'db'} = shift @tok;
+ $$res{'user'} = shift @tok;
+
+ # local connection?
+ if ($$res{'type'} eq 'local') {
+ goto error if $#tok > 1;
+ goto error unless valid_hba_method($tok[0]);
+ $$res{'method'} = join (' ', @tok);
+ return $res;
+ }
+
+ # host connection?
+ if ($$res{'type'} =~ /^host((no)?ssl)?$/) {
+ my ($i, $c) = split '/', (shift @tok);
+ goto error unless $i;
+ $$res{'ip'} = $i;
+
+ # CIDR mask given?
+ if (defined $c) {
+ goto error if $c !~ /^(\d+)$/;
+ $$res{'mask'} = $c;
+ } else {
+ $$res{'mask'} = shift @tok;
+ }
+
+ goto error if $#tok > 1;
+ goto error unless valid_hba_method($tok[0]);
+ $$res{'method'} = join (' ', @tok);
+ return $res;
+ }
+
+error:
+ $$res{'type'} = undef;
+ return $res;
+}
+
+
+=head2 read_pg_hba
+
+ Parse given pg_hba.conf file.
+
+ Arguments: <pg_hba.conf path>
+ Returns: Array with hash refs; for hash contents, see parse_hba_line().
+
+=cut
+
+sub read_pg_hba {
+ open HBA, $_[0] or return undef;
+ my @hba;
+ while (<HBA>) {
+ my $r = parse_hba_line $_;
+ push @hba, $r;
+ }
+ close HBA;
+ return @hba;
+}
+
+
+=head2 valid_hba_method
+
+ Check if hba method is known
+
+ Argument: hba method
+ Returns: True if method is valid
+
+=cut
+
+sub valid_hba_method {
+ my $method = $_[0];
+
+ my %valid_methods = qw/trust 1 reject 1 md5 1 crypt 1 password 1 krb5 1 ident 1 pam 1/;
+
+ return exists($valid_methods{$method});
+}
+
+1;
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5e48b28
--- /dev/null
+++ b/README.md
@@ -0,0 +1,114 @@
+Multi-Version/Multi-Cluster PostgreSQL architecture
+===================================================
+2004, Oliver Elphick, Martin Pitt
+
+Solving a problem
+-----------------
+
+When a new major version of PostgreSQL is released, it is necessary to dump and
+reload the database. The old software must be used for the dump, and the new
+software for the reload.
+
+This was a major problem for RedHat and Debian, because a dump and reload was
+not required by every upgrade and by the time the need for a dump is realised,
+the old software might have been deleted. Debian had certain rather unreliable
+procedures to save the old software and use it to do a dump, but these
+procedures often went wrong. RedHat's installation environment is so rigid that
+it is not practicable for the RedHat packages to attempt an automatic upgrade.
+Debian offered a debconf choice for whether to attempt automatic upgrading; if
+it failed or was not allowed, a manual upgrade had to be done, either from a
+pre-existing dump or by manual invocation of the postgresql-dump script.
+
+It is possible to run different versions of PostgreSQL simultaneously, and
+indeed to run the same version on separate database clusters simultaneously. To
+do so, each postgres instance must listen on a different port, so each client
+must specify the correct port. By having two separate versions of the
+PostgreSQL packages installed simultaneously, it is simple to do database
+upgrades by dumping from the old version and uploading to the new. The
+PostgreSQL client wrapper is designed to permit this.
+
+General Architecture idea
+-------------------------
+
+The Debian packaging has been changed to create a new package for each major
+version. The criterion for creating a new package is that initdb is required
+when upgrading from the previous version. Thus, there are now source packages
+`postgresql-8.1` and `postgresql-8.3` (and similarly for all the binary
+packages).
+
+The legacy postgresql and the other existing binary package names have become
+dummy packages depending on one of the versioned equivalents. Their only
+purpose is now to ensure a smooth upgrade and to register the existing database
+cluster to the new architecture. These packages will be removed from the
+archive as soon as the next Debian release after Sarge (Etch) is released.
+
+Each versioned package installs into `/usr/lib/postgresql/version`. In order
+to allow users easily to select the right version and cluster when working, the
+`postgresql-common` package provides the `pg_wrapper` program, which reads the
+per-user and system wide configuration file and forks the correct executable
+with the correct library versions according to those preferences. `/usr/bin`
+provides executables soft-linked to `pg_wrapper`.
+
+This architecture also allows separate database clusters to be maintained for
+the use of different groups of users; these clusters need not all be of the
+same major version. This allows much greater flexibility for those people who
+need to make application software changes consequent on a PostgreSQL upgrade.
+
+Detailed structure
+------------------
+
+### Configuration hierarchy
+
+* `/etc/postgresql-common/user_clusters`: maps users against clusters and
+ default databases
+
+* `$HOME/.postgresqlrc`: per-user preferences for default version/cluster and
+ database; overrides `/etc/postgresql-common/user_clusters`
+
+* `/etc/postgresql/version/clustername`: cluster-specific configuration files:
+
+ * `postgresql.conf`, `pg_hba.conf`, `pg_ident.conf`
+ * optionally `start.conf`: startup mode of the cluster: `auto` (start/stop in
+ init script), `manual` (do not start/stop in init script, but manual
+ control with `pg_ctlcluster` is possible), `disabled` (`pg_ctlcluster`
+ is not allowed).
+ * optionally `pg_ctl.conf`: options to be passed to `pg_ctl`.
+ * optionally a symbolic link `log` which points to the postgres log file.
+ Defaults to `/var/log/postgresql/postgresql-version-cluster.conf`.
+ Explicitly setting `log_directory` and/or `log_filename` in
+ `postgresql.conf` overrides this.
+
+### Per-version files and programs
+
+* `/usr/lib/postgresql/version`
+* `/usr/share/postgresql/version`
+* `/usr/share/doc/postgresql/postgresql-doc-version`:
+version specific program and data files
+
+### Common programs
+
+* `/usr/share/postgresql-common/pg_wrapper`: environment chooser and program selector
+* `/usr/bin/program`: symbolic links to pg_wrapper, for all client programs
+* `/usr/bin/pg_lsclusters`: list all available clusters with their status and configuration
+* `/usr/bin/pg_createcluster: wrapper for `initdb`, sets up the necessary configuration structure
+* `/usr/bin/pg_ctlcluster`: wrapper for `pg_ctl`, control the cluster postgres server
+* `/usr/bin/pg_upgradecluster`: upgrade a cluster to a newer major version
+* `/usr/bin/pg_dropcluster`: remove a cluster and its configuration
+
+### /etc/init.d/postgresql
+
+This script handles the postgres server processes for each version and all
+their clusters. However, most of the actual work is done by the new
+`pg_ctlcluster` program.
+
+### pg_upgradecluster
+
+This program replaces postgresql-dump (a Debian specific program).
+
+It is used to migrate a cluster from one major version to another.
+
+Usage: `pg_upgradecluster [-v newversion] version name [data_dir]`
+
+`-v`: specifies the version to upgrade to; defaults to the newest available version.
+
+ -- The Debian PostgreSQL maintainers
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..140f6f0
--- /dev/null
+++ b/TODO
@@ -0,0 +1,39 @@
+postgresql TODO
+===============
+
+Bugs:
+- pg_createcluster with existing cluster: respect symlinks to shared
+ postgresql.conf files, never remove symlinks in already existing
+ cluster dirs
+- Clean up at purging if /etc/ is there without /var, or vice versa
+
+Transition bugs:
+
+Missing selftest:
+- --force option for pg_ctlcluster
+
+Important features:
+
+Wishlist:
+- Add pg_conf to change startup and possibly other things
+- add program for web applications, which configure pg_hba.conf:
+
+ pg_hba add|remove|test [options] yourwebappdb yourwebappuser
+
+ Options:
+
+ --cluster: self-explanatory, defaults to default cluster
+ --ip: IP and netmask for host socket; if not given, defaults to Unix
+ socket (local)
+ --method: defaults to "md5" for TCP connections, and "ident sameuser" for
+ Unix socket connections
+ --force-ssl: If given, create a "hostssl" entry, otherwise a "host"
+ entry
+
+ For remove, only --cluster is allowed; it will remove all hba
+ entries that refer to the given db/user pair. test checks whether the
+ given connection is allowed; if so, it exits with 0, otherwise it prints the
+ required pg_hba.conf line to stdout and exits with 1. If pg_hba.conf has a
+ scrambled format that cannot be parsed by pg_*_hba, the scripts exit with 2.
+
+ Add libnet-cidr-perl dependency!
diff --git a/cleanpg b/cleanpg
new file mode 100755
index 0000000..22ef371
--- /dev/null
+++ b/cleanpg
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+# Clean all traces of PostgreSQL data/configuration from the current system.
+# Use with caution, for development/testing only! (This is not installed into
+# packages.)
+#
+# (C) 2008-2009 Martin Pitt <mpitt@debian.org>
+# (C) 2018 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.
+
+set -ux
+
+/etc/init.d/postgresql stop
+killall postgres
+rm -rf /etc/postgresql/* /var/lib/postgresql/* /var/log/postgresql/* /tmp/postgresql-testsuite/ /var/run/postgresql/*
diff --git a/createcluster.conf b/createcluster.conf
new file mode 100644
index 0000000..5e66693
--- /dev/null
+++ b/createcluster.conf
@@ -0,0 +1,41 @@
+# Default values for pg_createcluster(8)
+# Occurrences of '%v' are replaced by the major version number,
+# and '%c' by the cluster name. Use '%%' for a literal '%'.
+
+# Create a "main" cluster when a new postgresql-x.y server package is installed
+#create_main_cluster = true
+
+# Default start.conf value, must be one of "auto", "manual", and "disabled".
+# See pg_createcluster(8) for more documentation.
+#start_conf = 'auto'
+
+# Default data directory.
+#data_directory = '/var/lib/postgresql/%v/%c'
+
+# Default directory for transaction logs
+# Unset by default, i.e. transaction logs remain in the data directory.
+#waldir = '/var/lib/postgresql/wal/%v/%c/pg_wal'
+
+# Options to pass to initdb.
+#initdb_options = ''
+
+# The following options are copied into the new cluster's postgresql.conf:
+
+# Enable SSL by default (using the "snakeoil" certificates installed by the
+# ssl-cert package, unless configured otherwise here)
+ssl = on
+
+# Show cluster name in process title
+cluster_name = '%v/%c'
+
+# Put stats_temp_directory on tmpfs (PG <= 14)
+stats_temp_directory = '/var/run/postgresql/%v-%c.pg_stat_tmp'
+
+# Add prefix to log lines
+log_line_prefix = '%%m [%%p] %%q%%u@%%d '
+
+# Add "include_dir" in postgresql.conf
+add_include_dir = 'conf.d'
+
+# Directory for additional createcluster config
+include_dir '/etc/postgresql-common/createcluster.d'
diff --git a/debhelper/Debian/Debhelper/Buildsystem/pgxs.pm b/debhelper/Debian/Debhelper/Buildsystem/pgxs.pm
new file mode 100644
index 0000000..f1270ea
--- /dev/null
+++ b/debhelper/Debian/Debhelper/Buildsystem/pgxs.pm
@@ -0,0 +1,58 @@
+# A debhelper build system class for building PostgreSQL extension modules using PGXS
+#
+# Per PostgreSQL major version, a `build-$version` subdirectory is created.
+#
+# Copyright: © 2020 Christoph Berg
+# License: GPL-2+
+
+package Debian::Debhelper::Buildsystem::pgxs;
+
+use strict;
+use warnings;
+use parent qw(Debian::Debhelper::Buildsystem);
+use Cwd;
+use Debian::Debhelper::Dh_Lib;
+use Debian::Debhelper::pgxs;
+
+sub DESCRIPTION {
+ "PGXS (PostgreSQL extensions), building in subdirectory per PostgreSQL version"
+}
+
+sub check_auto_buildable {
+ my $this=shift;
+ unless (-e $this->get_sourcepath("debian/pgversions")) {
+ error("debian/pgversions is required to build with PGXS");
+ }
+ return (-e $this->get_sourcepath("Makefile")) ? 1 : 0;
+}
+
+sub new {
+ my $class=shift;
+ my $this=$class->SUPER::new(@_);
+ $this->enforce_in_source_building();
+ return $this;
+}
+
+sub build {
+ my $this=shift;
+ $this->doit_in_sourcedir(qw(pg_buildext build build-%v));
+}
+
+sub install {
+ my $this=shift;
+ my $pattern = package_pattern();
+ $this->doit_in_sourcedir(qw(pg_buildext install build-%v), $pattern);
+}
+
+sub test {
+ my $this=shift;
+ verbose_print("Postponing tests to install stage");
+}
+
+sub clean {
+ my $this=shift;
+ my $pattern = package_pattern();
+ $this->doit_in_sourcedir(qw(pg_buildext clean build-%v), $pattern);
+}
+
+1;
diff --git a/debhelper/Debian/Debhelper/Buildsystem/pgxs_loop.pm b/debhelper/Debian/Debhelper/Buildsystem/pgxs_loop.pm
new file mode 100644
index 0000000..716bcbf
--- /dev/null
+++ b/debhelper/Debian/Debhelper/Buildsystem/pgxs_loop.pm
@@ -0,0 +1,33 @@
+# A debhelper build system class for building PostgreSQL extension modules using PGXS
+#
+# For packages not supporting building in subdirectories, the pgxs_loop variant builds
+# for each PostgreSQL major version in turn in the top-level directory.
+#
+# Copyright: © 2020 Christoph Berg
+# License: GPL-2+
+
+package Debian::Debhelper::Buildsystem::pgxs_loop;
+
+use strict;
+use warnings;
+use parent qw(Debian::Debhelper::Buildsystem::pgxs);
+use Cwd;
+use Debian::Debhelper::Dh_Lib;
+use Debian::Debhelper::pgxs;
+
+sub DESCRIPTION {
+ "PGXS (PostgreSQL extensions), building for each PostgreSQL version in top level directory"
+}
+
+sub build {
+ my $this=shift;
+ verbose_print("Postponing build to install stage; if this package supports out-of-tree builds, replace --buildsystem=pgxs_loop by --buildsystem=pgxs to build in the build stage");
+}
+
+sub install {
+ my $this=shift;
+ my $pattern = package_pattern();
+ $this->doit_in_sourcedir(qw(pg_buildext loop), $pattern);
+}
+
+1;
diff --git a/debhelper/Debian/Debhelper/Sequence/pgxs.pm b/debhelper/Debian/Debhelper/Sequence/pgxs.pm
new file mode 100644
index 0000000..45a1ff1
--- /dev/null
+++ b/debhelper/Debian/Debhelper/Sequence/pgxs.pm
@@ -0,0 +1,22 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+use Debian::Debhelper::Dh_Lib;
+
+# check if debian/control needs updating from debian/control.in
+insert_after("dh_clean", "pg_buildext");
+add_command_options("pg_buildext", "checkcontrol");
+
+# use PGXS for clean, build, and install
+add_command_options("dh_auto_clean", "--buildsystem=pgxs");
+add_command_options("dh_auto_build", "--buildsystem=pgxs");
+add_command_options("dh_auto_install", "--buildsystem=pgxs");
+
+# move tests from dh_auto_test to dh_pgxs_test
+remove_command("dh_auto_test");
+if (! get_buildoption("nocheck")) {
+ insert_after("dh_link", "dh_pgxs_test");
+}
+
+1;
diff --git a/debhelper/Debian/Debhelper/Sequence/pgxs_loop.pm b/debhelper/Debian/Debhelper/Sequence/pgxs_loop.pm
new file mode 100644
index 0000000..314085b
--- /dev/null
+++ b/debhelper/Debian/Debhelper/Sequence/pgxs_loop.pm
@@ -0,0 +1,23 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+use Debian::Debhelper::Dh_Lib;
+
+# check if debian/control needs updating from debian/control.in
+insert_after("dh_clean", "pg_buildext");
+add_command_options("pg_buildext", "checkcontrol");
+
+# use PGXS for clean, build, and install
+add_command_options("dh_auto_clean", "--buildsystem=pgxs_loop");
+add_command_options("dh_auto_build", "--buildsystem=pgxs_loop");
+add_command_options("dh_auto_install", "--buildsystem=pgxs_loop");
+
+# move tests from dh_auto_test to dh_pgxs_test
+remove_command("dh_auto_test");
+if (! get_buildoption("nocheck")) {
+ insert_after("dh_link", "dh_pgxs_test");
+ add_command_options("dh_pgxs_test", "loop");
+}
+
+1;
diff --git a/debhelper/Debian/Debhelper/pgxs.pm b/debhelper/Debian/Debhelper/pgxs.pm
new file mode 100644
index 0000000..e3d86b2
--- /dev/null
+++ b/debhelper/Debian/Debhelper/pgxs.pm
@@ -0,0 +1,38 @@
+# A debhelper build system class for building PostgreSQL extension modules using PGXS
+#
+# Copyright: © 2020 Christoph Berg
+# License: GPL-2+
+
+package Debian::Debhelper::pgxs;
+
+use strict;
+use warnings;
+use Exporter 'import';
+our @EXPORT = qw(package_pattern);
+
+=head1 package_pattern()
+
+From C<debian/control.in>, look for the package name containing the
+B<PGVERSION> placeholder, and return it in the format suitable for passing to
+B<pg_buildext>, i.e. with B<PGVERSION> replaced by B<%v>.
+
+For B<Package: postgresql-PGVERSION-unit> it will return B<postgresql-%v-unit>.
+
+Errors out if more than one package with the B<PGVERSION> placeholder is found.
+
+=cut
+
+sub package_pattern () {
+ open F, "debian/control.in" or die "debian/control.in: $!";
+ my $pattern;
+ while (<F>) {
+ if (/^Package: (.*)PGVERSION(.*)/) {
+ die "More than one Package with PGVERSION placeholder found in debian/control.in, cannot build with dh --buildsystem=pgxs. Use pg_buildext manually." if ($pattern);
+ $pattern = "$1%v$2";
+ }
+ }
+ close F;
+ return $pattern;
+}
+
+1;
diff --git a/debhelper/dh_pgxs_test b/debhelper/dh_pgxs_test
new file mode 100755
index 0000000..c12bacd
--- /dev/null
+++ b/debhelper/dh_pgxs_test
@@ -0,0 +1,11 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+use Debian::Debhelper::Dh_Lib;
+use Debian::Debhelper::pgxs;
+
+my $target = (@ARGV and $ARGV[0] eq 'loop') ? "." : "build-%v";
+my $pattern = package_pattern();
+
+print_and_doit(qw(pg_buildext installcheck .), $target, $pattern);
diff --git a/debhelper/dh_pgxs_test.pod b/debhelper/dh_pgxs_test.pod
new file mode 100644
index 0000000..5b24e4b
--- /dev/null
+++ b/debhelper/dh_pgxs_test.pod
@@ -0,0 +1,44 @@
+=head1 NAME
+
+dh_pgxs_test - Run testsuite during a PGXS PostgreSQL extension build
+
+=head1 SYNOPSIS
+
+B<dh_pgxs_test> [B<loop>]
+
+=head1 DESCRIPTION
+
+B<PostgreSQL> extensions need to be installed before they can be tested and
+hence the usual B<debhelper> way of invoking tests from dh_auto_test(1) does
+not work.
+
+B<dh_pgxs_test> is a dh(1) sequence point created by the B<pgxs> and
+B<pgxs_loop> B<debhelper> extensions that is executed after dh_auto_install(1).
+It calls B<pg_buildext installcheck> after a B<PostgreSQL> extension module has
+been built and installed into the C<debian/>I<packagename/> directory.
+
+Users wishing to change the action called by B<dh_pgxs_test> should call
+B<pg_buildext> or similar commands.
+
+ override_dh_pgxs_test:
+ echo "CREATE EXTENSION foo" | pg_buildext psql . . postgresql-%v-foo
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<loop>
+
+B<dh --with pgxs> builds packages in C<build-%v> subdirectories. The B<loop>
+options corresponds to B<dh --with pgxs_loop> and builds in the top-level
+directory.
+
+=back
+
+=head1 SEE ALSO
+
+debhelper(7), dh(1), dh_make_pgxs(1), pg_buildext(1).
+
+=head1 AUTHOR
+
+Christoph Berg L<E<lt>myon@debian.orgE<gt>>
diff --git a/dh_make_pgxs/debian/control.in b/dh_make_pgxs/debian/control.in
new file mode 100644
index 0000000..090ac72
--- /dev/null
+++ b/dh_make_pgxs/debian/control.in
@@ -0,0 +1,23 @@
+Source: @SOURCE@
+Section: database
+Priority: optional
+Maintainer: Debian PostgreSQL Maintainers <team+postgresql@tracker.debian.org>
+Uploaders:
+ @MAINTAINER_NAME@ <@DEBEMAIL@>,
+Build-Depends:
+ debhelper-compat (= @COMPAT@),
+ postgresql-all (>= 217~),
+Standards-Version: @STANDARDS_VERSION@
+Rules-Requires-Root: no
+Homepage: @URL@
+Vcs-Browser: https://salsa.debian.org/postgresql/@SOURCE@
+Vcs-Git: https://salsa.debian.org/postgresql/@SOURCE@.git
+
+Package: postgresql-PGVERSION-@EXTNAME@
+Architecture: any
+Depends:
+ ${misc:Depends},
+ ${postgresql:Depends},
+ ${shlibs:Depends},
+Description: FIXME PostgreSQL extension @SOURCE@
+ FIXME long description here
diff --git a/dh_make_pgxs/debian/copyright b/dh_make_pgxs/debian/copyright
new file mode 100644
index 0000000..60ca5f0
--- /dev/null
+++ b/dh_make_pgxs/debian/copyright
@@ -0,0 +1,24 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: @NAME@
+Source: @URL@
+
+Files: *
+Copyright: Portions Copyright (c) 1996-@YEAR@, PostgreSQL Global Development Group
+ Portions Copyright (c) 1994, The Regents of the University of California
+License: PostgreSQL
+ Permission to use, copy, modify, and distribute this software and its
+ documentation for any purpose, without fee, and without a written agreement
+ is hereby granted, provided that the above copyright notice and this
+ paragraph and the following two paragraphs appear in all copies.
+ .
+ IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR
+ DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING
+ LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
+ DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+ .
+ THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
+ ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO
+ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
diff --git a/dh_make_pgxs/debian/gitlab-ci.yml b/dh_make_pgxs/debian/gitlab-ci.yml
new file mode 100644
index 0000000..67e4816
--- /dev/null
+++ b/dh_make_pgxs/debian/gitlab-ci.yml
@@ -0,0 +1 @@
+include: https://salsa.debian.org/postgresql/postgresql-common/raw/master/gitlab/gitlab-ci.yml
diff --git a/dh_make_pgxs/debian/pgversions b/dh_make_pgxs/debian/pgversions
new file mode 100644
index 0000000..0702cb5
--- /dev/null
+++ b/dh_make_pgxs/debian/pgversions
@@ -0,0 +1 @@
+all
diff --git a/dh_make_pgxs/debian/rules b/dh_make_pgxs/debian/rules
new file mode 100755
index 0000000..5c0ff40
--- /dev/null
+++ b/dh_make_pgxs/debian/rules
@@ -0,0 +1,36 @@
+#!/usr/bin/make -f
+
+%:
+ dh $@ --with pgxs
+
+override_dh_installdocs:
+ dh_installdocs --all README.*
+
+# if the package does not support building from subdirectories, use
+# `--with pgxs_loop` above.
+#
+# change the way tests are run:
+# override_dh_pgxs_test:
+# +pg_buildext installcheck . . postgresql-%v-@EXTNAME@
+
+# classic `pg_buildext` interface:
+
+#include /usr/share/postgresql-common/pgxs_debian_control.mk
+#
+#override_dh_auto_build:
+# +pg_buildext build build-%v
+#
+#override_dh_auto_test:
+# # nothing to do here, see debian/tests/* instead
+#
+#override_dh_auto_install:
+# +pg_buildext install build-%v postgresql-%v-@EXTNAME@
+#
+#override_dh_installdocs:
+# dh_installdocs --all README.*
+#
+#override_dh_auto_clean:
+# +pg_buildext clean build-%v
+#
+#%:
+# dh $@
diff --git a/dh_make_pgxs/debian/source/format b/dh_make_pgxs/debian/source/format
new file mode 100644
index 0000000..163aaf8
--- /dev/null
+++ b/dh_make_pgxs/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/dh_make_pgxs/debian/tests/control b/dh_make_pgxs/debian/tests/control
new file mode 100644
index 0000000..74b0464
--- /dev/null
+++ b/dh_make_pgxs/debian/tests/control
@@ -0,0 +1,5 @@
+Depends:
+ make,
+ @,
+Tests: installcheck
+Restrictions: allow-stderr
diff --git a/dh_make_pgxs/debian/tests/installcheck b/dh_make_pgxs/debian/tests/installcheck
new file mode 100755
index 0000000..5a20e78
--- /dev/null
+++ b/dh_make_pgxs/debian/tests/installcheck
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+pg_buildext installcheck
diff --git a/dh_make_pgxs/debian/watch b/dh_make_pgxs/debian/watch
new file mode 100644
index 0000000..acfb2a8
--- /dev/null
+++ b/dh_make_pgxs/debian/watch
@@ -0,0 +1,2 @@
+version=4
+@URL@/tags .*/(.*).tar.gz
diff --git a/dh_make_pgxs/dh_make_pgxs b/dh_make_pgxs/dh_make_pgxs
new file mode 100755
index 0000000..6c59b8a
--- /dev/null
+++ b/dh_make_pgxs/dh_make_pgxs
@@ -0,0 +1,136 @@
+#!/bin/bash
+
+# (C) 2015-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.
+
+set -eu
+
+# basic variables
+
+template_dir="/usr/share/postgresql-common/dh_make_pgxs/debian"
+
+DIRECTORY="$(basename $PWD)"
+NAME="${DIRECTORY%-*}" # Upstream name
+VERSION="${DIRECTORY##*-}"
+URL="https://FIXME/$NAME"
+
+# options
+
+while getopts "fh:n:v:" opt ; do
+ case $opt in
+ f) FORCE=yes ;;
+ h) URL="$OPTARG" ;;
+ n) NAME="$OPTARG" ;;
+ v) VERSION="$OPTARG" ;;
+ *) exit 5 ;;
+ esac
+done
+shift $((OPTIND - 1)) # shift away args
+
+# more variables
+
+SOURCE="${NAME//_/-}" # Debian name
+EXTNAME="$(echo $SOURCE | sed -e 's/^\(postgresql\|pgsql\|pg\)-//')" # binary package name suffix
+DHVERSION="$(dpkg-query --showformat '${Version}' --show debhelper)"
+COMPAT="${DHVERSION%%.*}"
+STANDARDS_VERSION="$(apt-cache show debian-policy | sed -n 's/Version: \(.*\)\..*/\1/p' | head -n1)"
+USERNAME=${LOGNAME:-${USER:-root}}
+MAINTAINER_NAME=$(getent passwd $USERNAME | cut -d : -f 5 | sed -e 's/,.*//')
+: ${DEBEMAIL:=$USERNAME@localhost}
+YEAR=$(date +%Y)
+
+echo "Upstream (-n): $NAME"
+echo "Version (-v): $VERSION"
+echo "Source: $SOURCE ($VERSION-1)"
+echo "Binaries: postgresql-PGVERSION-$EXTNAME ($VERSION-1)"
+echo "Uploader: $MAINTAINER_NAME <$DEBEMAIL>"
+echo "Homepage (-h): $URL"
+echo
+if [ -t 0 ]; then
+ echo -n "Press Enter to continue, ^C to abort "
+ read
+fi
+
+# install files
+
+install_dir ()
+{
+ local directory="debian/$1"
+ #[ -z "$directory" ] && return
+ [ -d "$directory" ] && return
+ echo "Creating $directory/"
+ mkdir "$directory"
+}
+
+install_template ()
+{
+ local template="$1"
+
+ if [ "${FORCE:-}" ] || ! [ -e debian/$template ]; then
+ echo "Installing debian/$template"
+ sed -e "s/@COMPAT@/$COMPAT/g" \
+ -e "s/@EXTNAME@/$EXTNAME/g" \
+ -e "s/@NAME@/$NAME/g" \
+ -e "s/@STANDARDS_VERSION@/$STANDARDS_VERSION/g" \
+ -e "s/@SOURCE@/$SOURCE/g" \
+ -e "s/@MAINTAINER_NAME@/$MAINTAINER_NAME/g" \
+ -e "s/@DEBEMAIL@/$DEBEMAIL/g" \
+ -e "s;@URL@;$URL;g" \
+ -e "s/@YEAR@/$YEAR/g" \
+ "$template_dir/$template" > "debian/$template"
+ if [ -x $template_dir/$template ]; then
+ chmod +x "debian/$template"
+ fi
+ else
+ echo "Keeping existing debian/$template"
+ fi
+}
+
+mkdir -p debian
+
+for template in $(find $template_dir -mindepth 1 | sort); do
+ case $template in
+ *.swp|*~) continue ;; # skip vim stuff
+ esac
+ basename=${template##$template_dir/}
+ if [ -d $template ]; then
+ install_dir "$basename"
+ else
+ install_template "$basename"
+ fi
+done
+
+if [ "$COMPAT" -lt "12" ]; then
+ sed -i -e "s/debhelper-compat[^,]*/debhelper (>= $COMPAT)/" debian/control*
+ echo "$COMPAT" > debian/compat
+fi
+
+echo "Updating debian/control from debian/control.in"
+pg_buildext updatecontrol
+
+if [ "${FORCE:-}" ] || ! [ -e debian/changelog ]; then
+ rm -f debian/changelog
+ echo "Creating debian/changelog"
+ if [ -x /usr/bin/dch ]; then
+ dch --create --package "$SOURCE" --newversion "$VERSION-1"
+ else
+ cat > debian/changelog <<-EOT
+ $SOURCE ($VERSION-1) UNRELEASED; urgency=medium
+
+ * Initial release. (Closes: #XXXXXX)
+
+ -- $MAINTAINER_NAME <$DEBEMAIL> $(date -R)
+ EOT
+ fi
+else
+ echo "Keeping existing debian/changelog"
+fi
diff --git a/dh_make_pgxs/dh_make_pgxs.pod b/dh_make_pgxs/dh_make_pgxs.pod
new file mode 100644
index 0000000..8efd172
--- /dev/null
+++ b/dh_make_pgxs/dh_make_pgxs.pod
@@ -0,0 +1,43 @@
+=head1 NAME
+
+dh_make_pgxs - Create a new Debian source package for a PGXS PostgreSQL extension
+
+=head1 SYNOPSIS
+
+B<dh_make_pgxs> [B<-f>] [B<-h> I<URL>] [B<-n> I<name>] [B<-v> I<version>]
+
+=head1 DESCRIPTION
+
+B<dh_make_pgxs> creates a F<debian/> directory tree for PostgreSQL extension
+packages using the PGXS build system. The B<pg_buildext> tool is used for the
+build process.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<-f>
+
+Overwrite existing files.
+
+=item B<-h> I<URL>
+
+Package upstream homepage.
+
+=item B<-n> I<name>
+
+Package name to use. Default is to extract it from the current directory's name.
+
+=item B<-v> I<version>
+
+Package version to use. Default is to extract it from the current directory's name.
+
+=back
+
+=head1 SEE ALSO
+
+dh_make(1), pg_buildext(1).
+
+=head1 AUTHOR
+
+Christoph Berg L<E<lt>myon@debian.orgE<gt>>
diff --git a/doc/dependencies.dia b/doc/dependencies.dia
new file mode 100644
index 0000000..ba23097
--- /dev/null
+++ b/doc/dependencies.dia
Binary files differ
diff --git a/doc/dependencies.svg b/doc/dependencies.svg
new file mode 100644
index 0000000..b4fd8a4
--- /dev/null
+++ b/doc/dependencies.svg
@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd">
+<svg width="65cm" height="24cm" viewBox="40 80 1300 480" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs/>
+ <g id="Hintergrund">
+ <rect style="fill: #d8e5e5; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2.35099e-37; stroke: #000000" x="40" y="80" width="1300" height="480" rx="0" ry="0"/>
+ <rect style="fill: #90ee90; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2.35099e-37; stroke: #000000" x="60" y="260" width="1260" height="200" rx="0" ry="0"/>
+ <g>
+ <rect style="fill: #84bef6; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="660" y="180" width="140" height="54" rx="0" ry="0"/>
+ <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="730" y="202.881">
+ <tspan x="730" y="202.881">postgresql:all</tspan>
+ <tspan x="730" y="218.881">M-A:none</tspan>
+ </text>
+ </g>
+ <g>
+ <rect style="fill: #ffa500; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="380" y="180" width="180" height="54" rx="0" ry="0"/>
+ <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="470" y="202.881">
+ <tspan x="470" y="202.881">postgresql-client:all</tspan>
+ <tspan x="470" y="218.881">M-A:foreign</tspan>
+ </text>
+ </g>
+ <g>
+ <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="470" y1="234.988" x2="470" y2="268.007"/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="465,268.007 470,278.007 475,268.007 "/>
+ </g>
+ <g>
+ <rect style="fill: #84bef6; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="620" y="280" width="224.85" height="70" rx="0" ry="0"/>
+ <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="732.425" y="302.881">
+ <tspan x="732.425" y="302.881">postgresql-NN:any</tspan>
+ <tspan x="732.425" y="318.881">M-A:none</tspan>
+ <tspan x="732.425" y="334.881">Provides: postgresql-contrib-NN</tspan>
+ </text>
+ </g>
+ <g>
+ <rect style="fill: #ffa500; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="360" y="280" width="220" height="60" rx="0" ry="0"/>
+ <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="470" y="305.881">
+ <tspan x="470" y="305.881">postgresql-client-NN:any</tspan>
+ <tspan x="470" y="321.881">M-A:foreign</tspan>
+ </text>
+ </g>
+ <g>
+ <rect style="fill: #ffa500; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="640" y="480" width="180" height="54" rx="0" ry="0"/>
+ <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="730" y="502.881">
+ <tspan x="730" y="502.881">postgresql-common:all</tspan>
+ <tspan x="730" y="518.881">M-A:foreign</tspan>
+ </text>
+ </g>
+ <g>
+ <rect style="fill: #ffa500; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="360" y="480" width="220" height="54" rx="0" ry="0"/>
+ <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="470" y="502.881">
+ <tspan x="470" y="502.881">postgresql-client-common:all</tspan>
+ <tspan x="470" y="518.881">M-A:foreign</tspan>
+ </text>
+ </g>
+ <g>
+ <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="730.628" y1="234.976" x2="731.37" y2="268.012"/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="726.371,268.124 731.594,278.009 736.369,267.899 "/>
+ </g>
+ <g>
+ <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="731.97" y1="351.006" x2="730.493" y2="468.005"/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="725.493,467.942 730.366,478.004 735.492,468.068 "/>
+ </g>
+ <g>
+ <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="470" y1="341.004" x2="470" y2="467.996"/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="465,467.996 470,477.996 475,467.996 "/>
+ </g>
+ <g>
+ <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="619.008" y1="312.839" x2="592.005" y2="312.325"/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="592.1,307.325 582.007,312.134 591.91,317.324 "/>
+ </g>
+ <g>
+ <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="638.998" y1="507" x2="591.997" y2="507"/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="591.997,502 581.997,507 591.997,512 "/>
+ </g>
+ <g>
+ <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="680.616" y1="350.994" x2="649.334" y2="372.726"/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="646.481,368.62 641.121,378.432 652.186,376.833 "/>
+ </g>
+ <g>
+ <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="511.553" y1="341.005" x2="553.653" y2="372.418"/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="550.663,376.426 561.668,378.398 556.643,368.411 "/>
+ </g>
+ <g>
+ <rect style="fill: #ffff00; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="540" y="380" width="120" height="54" rx="0" ry="0"/>
+ <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="600" y="402.881">
+ <tspan x="600" y="402.881">libpq5:amd64</tspan>
+ <tspan x="600" y="418.881">M-A:same</tspan>
+ </text>
+ </g>
+ <g>
+ <rect style="fill: #84bef6; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="140" y="380" width="140" height="54" rx="0" ry="0"/>
+ <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="210" y="402.881">
+ <tspan x="210" y="402.881">libpq-dev:amd64</tspan>
+ <tspan x="210" y="418.881">M-A:none</tspan>
+ </text>
+ </g>
+ <g>
+ <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="280.995" y1="407" x2="528.003" y2="407"/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="528.003,412 538.003,407 528.003,402 "/>
+ </g>
+ <g>
+ <rect style="fill: #ffa500; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="1140" y="280" width="160.85" height="60" rx="0" ry="0"/>
+ <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="1220.42" y="305.881">
+ <tspan x="1220.42" y="305.881">postgresql-doc-NN:all</tspan>
+ <tspan x="1220.42" y="321.881">M-A:foreign</tspan>
+ </text>
+ </g>
+ <g>
+ <rect style="fill: #ffa500; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="1140" y="180" width="160" height="54" rx="0" ry="0"/>
+ <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="1220" y="202.881">
+ <tspan x="1220" y="202.881">postgresql-doc:all</tspan>
+ <tspan x="1220" y="218.881">M-A:foreign</tspan>
+ </text>
+ </g>
+ <g>
+ <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="1220.12" y1="234.988" x2="1220.25" y2="268.007"/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="1215.25,268.028 1220.29,278.007 1225.25,267.986 "/>
+ </g>
+ <g>
+ <rect style="fill: #84bef6; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="840" y="180" width="180" height="54" rx="0" ry="0"/>
+ <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="930" y="202.881">
+ <tspan x="930" y="202.881">postgresql-contrib:all</tspan>
+ <tspan x="930" y="218.881">M-A:none</tspan>
+ </text>
+ </g>
+ <g>
+ <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="878.822" y1="234.976" x2="807.919" y2="273.733"/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="805.521,269.345 799.145,278.529 810.318,278.12 "/>
+ </g>
+ <g>
+ <rect style="fill: #84bef6; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="100" y="280" width="220" height="60" rx="0" ry="0"/>
+ <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="210" y="305.881">
+ <tspan x="210" y="305.881">postgresql-server-dev-NN:any</tspan>
+ <tspan x="210" y="321.881">M-A:none</tspan>
+ </text>
+ </g>
+ <g>
+ <rect style="fill: #ffff00; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="100" y="180" width="220" height="54" rx="0" ry="0"/>
+ <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="210" y="202.881">
+ <tspan x="210" y="202.881">postgresql-server-dev-all:any</tspan>
+ <tspan x="210" y="218.881">M-A:same</tspan>
+ </text>
+ </g>
+ <g>
+ <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="210" y1="234.988" x2="210" y2="268.007"/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="205,268.007 210,278.007 215,268.007 "/>
+ </g>
+ <g>
+ <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="210" y1="341.005" x2="210" y2="367.996"/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="205,367.996 210,377.996 215,367.996 "/>
+ </g>
+ <text font-size="19.1234" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:700" x="60" y="540">
+ <tspan x="60" y="540">postgresql-common</tspan>
+ </text>
+ <text font-size="19.1234" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:end;font-family:sans-serif;font-style:normal;font-weight:700" x="1200" y="440">
+ <tspan x="1200" y="440">postgresql-NN</tspan>
+ </text>
+ <text font-size="12.7998" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="210" y="207">
+ <tspan x="210" y="207"></tspan>
+ </text>
+ <g>
+ <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="320" y1="310" x2="349.256" y2="310"/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="356.756,310 346.756,315 349.256,310 346.756,305 "/>
+ </g>
+ <text font-size="12.7998" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="210" y="310">
+ <tspan x="210" y="310"></tspan>
+ </text>
+ <text font-size="12.7998" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="210" y="310">
+ <tspan x="210" y="310"></tspan>
+ </text>
+ <text font-size="12.7998" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="690" y="360">
+ <tspan x="690" y="360"></tspan>
+ </text>
+ <g>
+ <rect style="fill: #84bef6; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="440" y="100" width="140" height="54" rx="0" ry="0"/>
+ <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="510" y="122.881">
+ <tspan x="510" y="122.881">postgresql-all:all</tspan>
+ <tspan x="510" y="138.881">M-A:none</tspan>
+ </text>
+ </g>
+ <g>
+ <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="439.01" y1="145.931" x2="324.363" y2="176.503"/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="317.117,178.436 325.491,171.028 324.363,176.503 328.067,180.69 "/>
+ </g>
+ <g>
+ <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="543.132" y1="155.004" x2="682.402" y2="272.719"/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="688.13,277.561 677.265,274.924 682.402,272.719 683.72,267.287 "/>
+ </g>
+ <g>
+ <rect style="fill: #84bef6; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x="880" y="280" width="224.85" height="70" rx="0" ry="0"/>
+ <text font-size="12.8" style="fill: #000000; fill-opacity: 1; stroke: none;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="992.425" y="302.881">
+ <tspan x="992.425" y="302.881">postgresql-plXX:any</tspan>
+ <tspan x="992.425" y="318.881">M-A:none</tspan>
+ <tspan x="992.425" y="334.881">Provides: postgresql-plXX</tspan>
+ </text>
+ </g>
+ <g>
+ <line style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" x1="879.024" y1="315" x2="855.562" y2="315"/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="848.062,315 858.062,310 855.562,315 858.062,320 "/>
+ </g>
+ <g>
+ <polyline style="fill: none; stroke-opacity: 1; stroke-width: 2; stroke: #000000" points="580,127 1040,127 1040,270.264 "/>
+ <polygon style="fill: #000000; fill-opacity: 1; stroke-opacity: 1; stroke-width: 2; stroke: #000000" fill-rule="evenodd" points="1040,277.764 1035,267.764 1040,270.264 1045,267.764 "/>
+ </g>
+ </g>
+</svg>
diff --git a/doc/postgresql-debian-packaging.md b/doc/postgresql-debian-packaging.md
new file mode 100644
index 0000000..4edd9ca
--- /dev/null
+++ b/doc/postgresql-debian-packaging.md
@@ -0,0 +1,741 @@
+Packaging PostgreSQL Extensions for Debian
+==========================================
+January 2024, Christoph Berg <myon@debian.org>
+
+Debian ships many PostgreSQL applications and extensions as packages. I often
+get asked by developers how they would get their programs packaged, and ended
+up writing the same reply over and over. This is a write-up intended as a more
+thorough answer.
+
+# Anatomy of Debian packages
+
+Debian knows two sorts of packages: "source" packages and "binary" packages.
+The latter type is the `.deb` files that get installed using apt or dpkg. The
+first type is what this article is mostly about. For both sorts of packages,
+there is an unpacked form and a packed form.
+
+## Binary packages
+
+Packed binary packages are a single `.deb` file:
+
+```
+$ ls -al postgresql-16-unit_7.7-1_amd64.deb
+-rw-r--r-- 1 myon myon 136740 Jan 12 14:13 postgresql-16-unit_7.7-1_amd64.deb
+
+$ dpkg-deb -I postgresql-16-unit_7.7-1_amd64.deb
+ new Debian package, version 2.0.
+ size 136740 bytes: control archive=1332 bytes.
+ 643 bytes, 15 lines control
+ 2046 bytes, 25 lines md5sums
+ Package: postgresql-16-unit
+ Source: postgresql-unit
+ Version: 7.7-1
+ Architecture: amd64
+ Maintainer: Christoph Berg <myon@debian.org>
+ Installed-Size: 500
+ Depends: postgresql-16, postgresql-16-jit-llvm (>= 16), libc6 (>= 2.14)
+ Section: database
+ Priority: optional
+ Homepage: https://github.com/df7cb/postgresql-unit
+ Description: SI Units for PostgreSQL
+ postgresql-unit implements a PostgreSQL datatype for SI units, plus byte. The
+ base units can be combined to named and unnamed derived units using operators
+ defined in the PostgreSQL type system. SI prefixes are used for input and
+ output, and quantities can be converted to arbitrary scale.
+
+$ dpkg-deb -c postgresql-16-unit_7.7-1_amd64.deb
+drwxr-xr-x root/root 0 2023-01-06 16:34 ./
+drwxr-xr-x root/root 0 2023-01-06 16:34 ./usr/
+drwxr-xr-x root/root 0 2023-01-06 16:34 ./usr/share/
+drwxr-xr-x root/root 0 2023-01-06 16:34 ./usr/share/doc/
+drwxr-xr-x root/root 0 2023-01-06 16:34 ./usr/share/doc/postgresql-16-unit/
+-rw-r--r-- root/root 267 2023-01-06 16:34 ./usr/share/doc/postgresql-16-unit/NEWS.Debian.gz
+-rw-r--r-- root/root 7202 2023-01-06 16:34 ./usr/share/doc/postgresql-16-unit/README.md.gz
+-rw-r--r-- root/root 977 2023-01-06 16:34 ./usr/share/doc/postgresql-16-unit/changelog.Debian.gz
+-rw-r--r-- root/root 868 2023-01-06 16:34 ./usr/share/doc/postgresql-16-unit/copyright
+drwxr-xr-x root/root 0 2023-01-06 16:34 ./usr/share/postgresql/
+drwxr-xr-x root/root 0 2023-01-06 16:34 ./usr/share/postgresql/16/
+drwxr-xr-x root/root 0 2023-01-06 16:34 ./usr/share/postgresql/16/extension/
+-rw-r--r-- root/root 19259 2023-01-06 16:34 ./usr/share/postgresql/16/extension/unit--7.sql
+-rw-r--r-- root/root 244 2023-01-06 16:34 ./usr/share/postgresql/16/extension/unit.control
+...
+```
+
+The unpacked binary package is of course the files being installed on the
+system.
+
+## Source packages
+
+Unpacked source packages consist of the original upstream source code, with a `debian/` directory added.
+
+```
+postgresql-unit/
+├── debian/
+│   ├── changelog
+│   ├── control
+│   ├── control.in
+│   ├── copyright
+│   ├── gitlab-ci.yml
+│   ├── NEWS
+│   ├── pgversions
+│   ├── rules*
+│   ├── source/
+│   │   └── format
+│   ├── tests/
+│   │   ├── control
+│   │   └── installcheck*
+│   ├── upstream/
+│   │   └── metadata
+│   └── watch
+├── Makefile
+├── NEWS.md
+├── README.md
+├── unit--7.sql.in
+├── unit.c
+└── unit.control
+```
+
+Most packages are maintained in Git, which tracks this unpacked source package
+form. (Either with or without the original upstream source, more on that
+later.)
+
+Packed source packages are actually a set of files:
+
+```
+-rw-rw-r-- 1 myon myon 3848 Jan 12 14:31 postgresql-unit_7.7-1.debian.tar.xz
+-rw-rw-r-- 1 myon myon 1100 Jan 12 14:31 postgresql-unit_7.7-1.dsc
+-rw-rw-r-- 1 myon myon 414114 Jan 12 14:12 postgresql-unit_7.7.orig.tar.gz
+```
+
+This transport format is used for uploading to the Debian archive and for
+retrieving the source code using `apt source`. (It is not stored in Git.)
+
+The `.orig.tar.gz` is the original source tarball as distributed by upstream,
+either as download from the upstream homepage, or for projects hosted on
+GitHub, often a tarball automatically generated by GitHub from an upstream Git
+tag.
+
+The `.debiar.tar.xz` file contains the `debian/` directory.
+
+The `.dsc` file is a descriptor that contains pointers to the other files in
+the source package, and more meta information.
+
+```
+$ cat postgresql-unit_7.7-1.dsc
+Format: 3.0 (quilt)
+Source: postgresql-unit
+Binary: postgresql-16-unit
+Architecture: any
+Version: 7.7-1
+Maintainer: Christoph Berg <myon@debian.org>
+Homepage: https://github.com/df7cb/postgresql-unit
+Standards-Version: 4.6.1
+Vcs-Browser: https://github.com/df7cb/postgresql-unit
+Vcs-Git: https://github.com/df7cb/postgresql-unit.git
+Testsuite: autopkgtest
+Testsuite-Triggers: make
+Build-Depends: bison, debhelper-compat (= 13), flex, postgresql-server-dev-all (>= 217~)
+Package-List:
+ postgresql-16-unit deb database optional arch=any
+Checksums-Sha1:
+ c2f81968bfbe83fed49b084b737e3aba423bf15a 414114 postgresql-unit_7.7.orig.tar.gz
+ b8a1917ddecb99b1441218bc74a0b7cb30752235 3848 postgresql-unit_7.7-1.debian.tar.xz
+Checksums-Sha256:
+ 411d05beeb97e5a4abf17572bfcfbb5a68d98d1018918feff995f6ee3bb03e79 414114 postgresql-unit_7.7.orig.tar.gz
+ 36e89c762e50ddf997b079703200c0df6967b4fe911bde8e9482d8e82dcb6a98 3848 postgresql-unit_7.7-1.debian.tar.xz
+Files:
+ 33a22586c8b81564ba7e9c05f430ad40 414114 postgresql-unit_7.7.orig.tar.gz
+ a0b31860b86c12c7173a78d6ecd525cb 3848 postgresql-unit_7.7-1.debian.tar.xz
+```
+
+## Building the source package
+
+To get started with working with a Debian package, get the unpacked source
+package. This could mean invoking `apt source`, but most often checking out the
+packaging Git repository is the better option as it might contain changes that
+have not been uploaded yet. It also makes contributing changes easier. For
+packages, that are already part of Debian, the `debcheckout` tool can automate
+that (it uses the `Vcs-Git` field in the metadata).
+
+To build the packed source packed from the unpacked one, enter the package
+directory, and invoke `dpkg-buildpackage -S --no-sign`:
+
+```
+postgresql-unit $ dpkg-buildpackage -S --no-sign
+dpkg-buildpackage: info: source package postgresql-unit
+dpkg-buildpackage: info: source version 7.7-1
+dpkg-buildpackage: info: source distribution unstable
+dpkg-buildpackage: info: source changed by Christoph Berg <myon@debian.org>
+ dpkg-source --before-build .
+ debian/rules clean
+dh clean --with pgxs
+...
+ dpkg-source -b .
+dpkg-source: info: using source format '3.0 (quilt)'
+dpkg-source: info: building postgresql-unit using existing ./postgresql-unit_7.7.orig.tar.gz
+dpkg-source: info: building postgresql-unit in postgresql-unit_7.7-1.debian.tar.xz
+dpkg-source: info: building postgresql-unit in postgresql-unit_7.7-1.dsc
+ dpkg-genbuildinfo --build=source -O../postgresql-unit_7.7-1_source.buildinfo
+ dpkg-genchanges --build=source -O../postgresql-unit_7.7-1_source.changes
+dpkg-genchanges: info: including full source code in upload
+ dpkg-source --after-build .
+dpkg-buildpackage: info: source-only upload (original source is included)
+```
+
+Note that the artifacts produced by `dpkg-buildpackage` are always in the *parent* directory of the working directory.
+To make room for that, I always create *two* levels of directory for my working copies, so a typical case looks like this:
+
+```
+postgresql-unit/
+├── postgresql-unit/ <--- most commands are invoked from here
+│   ├── debian/
+│   │   ├── changelog
+│   │   ├── control
+│   │   ├── control.in
+│   │   ├── copyright
+│   │   ├── files
+│   │   ├── gitlab-ci.yml
+│   │   ├── NEWS
+│   │   ├── pgversions
+│   │   ├── rules*
+│   │   ├── source/
+│   │   │   └── format
+│   │   ├── tests/
+│   │   │   ├── control
+│   │   │   └── installcheck*
+│   │   ├── upstream/
+│   │   │   └── metadata
+│   │   └── watch
+│   ├── Makefile
+│   ├── NEWS.md
+│   ├── powers.c
+│   ├── powers.h
+│   ├── README.md
+│   ├── unit--7.sql.in
+│   ├── unit.c
+│   └── unit.control
+├── postgresql-16-unit_7.7-1_amd64.deb
+├── postgresql-16-unit-dbgsym_7.7-1_amd64.deb
+├── postgresql-unit_7.7-1_amd64.changes
+├── postgresql-unit_7.7-1.debian.tar.xz
+├── postgresql-unit_7.7-1.dsc
+└── postgresql-unit_7.7.orig.tar.gz
+```
+
+## Building binary packages
+
+Binary packages are built using `dpkg-buildpackage --no-sign`.
+
+```
+ dpkg-buildpackage --no-sign
+dpkg-buildpackage: info: source package postgresql-unit
+dpkg-buildpackage: info: source version 7.7-1
+dpkg-buildpackage: info: source distribution unstable
+dpkg-buildpackage: info: source changed by Christoph Berg <myon@debian.org>
+dpkg-buildpackage: info: host architecture amd64
+ dpkg-source --before-build .
+ debian/rules clean
+dh clean --with pgxs
+...
+ dpkg-source -b .
+dpkg-source: info: using source format '3.0 (quilt)'
+dpkg-source: info: building postgresql-unit using existing ./postgresql-unit_7.7.orig.tar.gz
+dpkg-source: info: building postgresql-unit in postgresql-unit_7.7-1.debian.tar.xz
+dpkg-source: info: building postgresql-unit in postgresql-unit_7.7-1.dsc
+ debian/rules binary
+dh binary --with pgxs
+ dh_update_autotools_config
+ dh_autoreconf
+ dh_auto_configure
+ dh_auto_build --buildsystem=pgxs
+ pg_buildext build build-%v
+### PostgreSQL 16 build ###
+make[1]: Entering directory '/home/myon/projects/postgresql/postgresql-unit/postgresql-unit/build-16'
+gcc -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Werror=vla -Wendif-labels -Wmissing-format-attribute -Wimplicit-fallthrough=3 -Wcast-function-type -Wshadow=compatible-local -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -Wno-format-truncation -Wno-stringop-truncation -g -g -O2 -fstack-protector-strong -fstack-clash-protection -Wformat -Werror=format-security -fcf-protection -fno-omit-frame-pointer -g -O2 -ffile-prefix-map=/home/myon/projects/postgresql/postgresql-unit/postgresql-unit=. -fstack-protector-strong -fstack-clash-protection -Wformat -Werror=format-security -fcf-protection -fPIC -fvisibility=hidden -ffp-contract=off -I. -I/home/myon/postgresql/postgresql-unit/postgresql-unit -I/usr/include/postgresql/16/server -I/usr/include/postgresql/internal -Wdate-time -D_FORTIFY_SOURCE=2 -D_GNU_SOURCE -I/usr/include/libxml2 -c -o unit.o /home/myon/postgresql/postgresql-unit/postgresql-unit/unit.c
+...
+### End 16 build ###
+ create-stamp debian/debhelper-build-stamp
+ dh_prep
+ dh_auto_install --buildsystem=pgxs --destdir=debian/postgresql-16-unit/
+ pg_buildext install build-%v postgresql-%v-unit
+### PostgreSQL 16 install ###
+make[1]: Entering directory '/home/myon/projects/postgresql/postgresql-unit/postgresql-unit/build-16'
+/usr/bin/install -c -m 755 unit.so '/home/myon/postgresql/postgresql-unit/postgresql-unit/debian/postgresql-16-unit/usr/lib/postgresql/16/lib/unit.so'
+...
+### End 16 install ###
+ debian/rules override_dh_installdocs
+make[1]: Entering directory '/home/myon/projects/postgresql/postgresql-unit/postgresql-unit'
+dh_installdocs --all README.*
+make[1]: Leaving directory '/home/myon/projects/postgresql/postgresql-unit/postgresql-unit'
+ dh_installchangelogs
+ dh_perl
+ dh_link
+ debian/rules override_dh_pgxs_test
+make[1]: Entering directory '/home/myon/projects/postgresql/postgresql-unit/postgresql-unit'
+# defer testing to autopkgtest, the data tables are not in /usr/share/postgresql yet
+make[1]: Leaving directory '/home/myon/projects/postgresql/postgresql-unit/postgresql-unit'
+ dh_strip_nondeterminism
+ dh_compress
+ dh_fixperms
+ dh_missing
+ dh_dwz -a
+ dh_strip -a
+ dh_makeshlibs -a
+ dh_shlibdeps -a
+ dh_installdeb
+ dh_gencontrol
+ dh_md5sums
+ dh_builddeb
+dpkg-deb: building package 'postgresql-16-unit' in '../postgresql-16-unit_7.7-1_amd64.deb'.
+dpkg-deb: building package 'postgresql-16-unit-dbgsym' in '../postgresql-16-unit-dbgsym_7.7-1_amd64.deb'.
+ dpkg-genbuildinfo -O../postgresql-unit_7.7-1_amd64.buildinfo
+ dpkg-genchanges -O../postgresql-unit_7.7-1_amd64.changes
+dpkg-genchanges: info: including full source code in upload
+ dpkg-source --after-build .
+dpkg-buildpackage: info: full upload (original source is included)
+```
+
+Again, the resulting `.deb` files are placed in the parent directory.
+
+By default, this also builds the source. If this fails (often when there are
+files that differ from the tarball version, more on patches later), use `-b` to
+skip building the source: `dpkg-buildpackage -b --no-sign`.
+
+If building fails because of missing dependencies, install them using
+`apt build-dep .`.
+
+There are various front-end utilities to automate these building steps better
+(git-buildpackage, sbuild, pbuilder, debuild), but dpkg-buildpackage is just
+fine.
+
+## The debian/ directory
+
+The `debian/` directory contains metadata and build instructions for the
+package. "Creating a Debian package" really means editing the files in this
+directory to make the package behave as desired.
+
+### debian/control
+
+The control file contains one section for the source package, followed by one
+or more sections for binary packages.
+
+```
+$ cat debian/control
+Source: postgresql-unit
+Section: database
+Priority: optional
+Maintainer: Christoph Berg <myon@debian.org>
+Build-Depends:
+ bison,
+ debhelper-compat (= 13),
+ flex,
+ postgresql-server-dev-all (>= 217~),
+Standards-Version: 4.6.1
+Rules-Requires-Root: no
+Vcs-Git: https://github.com/df7cb/postgresql-unit.git
+Vcs-Browser: https://github.com/df7cb/postgresql-unit
+Homepage: https://github.com/df7cb/postgresql-unit
+
+Package: postgresql-16-unit
+Architecture: any
+Depends: ${misc:Depends}, ${shlibs:Depends}, postgresql-16
+Description: SI Units for PostgreSQL
+ postgresql-unit implements a PostgreSQL datatype for SI units, plus byte. The
+ base units can be combined to named and unnamed derived units using operators
+ defined in the PostgreSQL type system. SI prefixes are used for input and
+ output, and quantities can be converted to arbitrary scale.
+```
+
+(In the case of PostgreSQL extension packages, the control file is
+automatically generated from `debian/control.in`, see below. Normal packages do
+not have a control.in file.)
+
+### debian/rules
+
+The rules file handles the actual binary package building steps, in Makefile
+syntax.
+
+Almost all packages today use a helper system called `debhelper` which consists
+of various building blocks called `dh_*` that handle the build steps.
+Historically, rules files would consist of long lists of `dh_*` steps (similar
+to the build output above), but there is a "sequencer" command `dh` that knows
+how to invoke the basic steps.
+
+Many packages that don't need any tweaking at that level have a very short
+`debian/rules` file. (Make sure the indentation is a tab, not spaces!)
+
+```
+#!/usr/bin/make -f
+
+%:
+ dh $@
+```
+
+Or in the case of PostgreSQL extensions:
+
+```
+#!/usr/bin/make -f
+
+%:
+ dh $@ --with pgxs
+```
+
+If any of the `dh_*` steps need changes, we can override them in the rules file
+by adding a `override_dh_*` target:
+
+```
+#!/usr/bin/make -f
+
+override_dh_installdocs:
+ dh_installdocs --all README.*
+
+override_dh_pgxs_test:
+ # defer testing to autopkgtest, the data tables are not in /usr/share/postgresql yet
+
+%:
+ dh $@ --with pgxs
+```
+
+### Build steps
+
+The most interesting build steps to hook into are:
+
+#### `dh_auto_configure`
+
+Autodetects the build system and runs `./configure`, `cmake` and the like with
+a set of default options.
+
+To add options, do:
+
+```
+override_dh_auto_configure:
+ dh_auto_configure -- -DEXTRA_OPTION=foo --with-bar
+```
+
+#### `dh_auto_build`
+
+The main build step. If the automatically run command is wrong, just override it.
+
+```
+override_dh_auto_build:
+ $(MAKE) -C some_sub_dir world
+```
+
+#### `dh_auto_install`
+
+Runs upstreams' `make install` or equivalent. The files are installed into
+`DESTDIR=debian/foo` (single-binary package) or `DESTDIR=debian/tmp`
+(multi-binary package).
+
+#### `dh_install` and `debian/foo.install`
+
+If there is more than one binary package, the files installed in `debian/tmp`
+need to be distributed to the individual binary packages. This is done by
+directory/file lists in `debian/*.install`.
+
+### debian/source/format
+
+Should be verbatim this:
+
+```
+3.0 (quilt)
+```
+
+(More on quilt and patches below.)
+
+### debian/copyright
+
+Debian wants copyright information on *all* files in a package. For private
+package, this file can be omitted, but for anything official, this file has to
+list all the copyright holders, along with any copyright terms and license
+texts.
+
+### debian/watch
+
+To get informed about new upstream versions, Debian runs a "watch" system that
+polls upstream download locations for new package versions. The `debian/watch`
+file tells the `uscan` tool where to look and (optionally) how to transform
+upstream's version naming scheme into a Debian-compatible one.
+
+```
+version=4
+https://github.com/df7cb/postgresql-unit/tags .*/([0-9.]*).tar.gz
+```
+
+This is a simple example where `uscan` looks at some GitHub "tags" URL, parses
+the HTML, and recognizes all links pointing to .tar.gz files as new versions.
+The regexp part of the URL in parentheses is used as the version number.
+
+The `uscan` tool is part of the `devscripts` package which holds a bunch of
+utilities useful for packaging tasks.
+
+## Patching the upstream source
+
+Debian packages are based on upstream tarballs, and `dpkg-buildpackage` does
+not like changed (or new) files (except in the `debian/` directory). Changes
+need to be done using patch files stored in the `debian/patches/` directory,
+and applied in the order listed in `debian/patches/series`.
+
+At package build time, support is built-in in dpkg-buildpackage. Patches can
+edited with whatever tool, with `quilt` being the easiest solution. (It's an
+optional package that likely isn't preinstalled.) `quilt` needs some
+configuration in `~/.quiltrc`:
+
+```
+QUILT_DIFF_OPTS="-p"
+QUILT_REFRESH_ARGS="-p ab --no-timestamps --no-index"
+QUILT_PATCHES=debian/patches
+```
+
+Some useful commands:
+
+* Apply all patches: `quilt push -a`
+* Unapply all patches: `quilt pop -a`
+* Create a new patch: `quilt new foo`
+* Add a file to a patch: `quilt add bar` (do this *before* changing the file!)
+* Add a file and edit it: `quilt edit bar`
+* Show current diff: `quilt diff`
+* Save a patch after editing: `quilt refresh`
+
+Suppose we want to fix something in the `unit.c` file:
+
+```
+$ quilt new unit-dllexport
+Patch unit-dllexport is now on top
+
+$ quilt add unit.c
+File unit.c added to patch unit-dllexport
+
+$ vi unit.c
+
+$ quilt diff | cat
+Index: postgresql-unit/unit.c
+===================================================================
+--- postgresql-unit.orig/unit.c 2023-11-08 20:51:04.343207806 +0100
++++ postgresql-unit/unit.c 2024-01-12 16:10:23.143509535 +0100
+@@ -136,7 +136,7 @@ unit_get_definitions(void)
+
+ PG_MODULE_MAGIC;
+
+-void _PG_init(void);
++void PGDLLEXPORT _PG_init(void);
+
+ void
+ _PG_init(void)
+
+$ quilt refresh
+Refreshed patch unit-dllexport
+```
+
+This creates the `debian/patches/` directory:
+
+```
+postgresql-unit/debian/patches/
+├── series
+└── unit-dllexport
+```
+
+At build-time, `dpkg-buildpackage` will then automatically apply and un-apply
+patches as needed.
+
+## Tests
+
+Package tests are run in two flavors: at build-time and later independently on
+packages installed on some system.
+
+### Build-time package tests
+
+If an upstream project has a test suite, it is recommended to run it at build
+time. In many cases, `dh_auto_test` will guess how to do that and no extra
+configuration is needed. If it guesses incorrectly, use `override_dh_auto_test`
+to provide a better command.
+
+### Install-time package tests
+
+Next to tests at build-time, we can also run tests when the binary packages are
+installed on some actual system. This has the advantage that it runs when all
+files are in their final location, and can also interface with other packages
+and services that might not be available at build time. It can also run
+periodically to spot regression, while build-time tests would usually not be
+repeated.
+
+Debian's system to run these tests is `autopkgtest`. The TL;DR version of the
+documentation is this: In the `debian/tests/` directory, provide a command
+(usually a shell script) that exercises some package smoke test or more complex
+scenario. Register that test in `debian/tests/control`.
+
+```
+postgresql-unit/debian/tests/
+├── control
+└── installcheck*
+
+$ cat debian/tests/control
+Depends: @, make
+Tests: installcheck
+Restrictions: allow-stderr
+
+$ cat debian/tests/installcheck
+#!/bin/sh
+pg_buildext -i '--locale=C.UTF-8' installcheck
+```
+
+In `Depends` the `@` is a shorthand for all binary packages built from this
+source. Other packages that the tests needs (and that the binaries don't depend
+on) can be listed here. `Restrictions` declare tests properties. Examples are
+`allow-stderr` (don't consider stderr output to be a test failure) and
+`root-needed` (run test as root instead of an unprivileged user).
+
+In Debian, these tests are run automatically for QA, see
+https://ci.debian.net/. More details are in the autopkgtest package
+documentation in /usr/share/doc/autopkgtest/.
+
+# PostgreSQL extension packages
+
+Packages for PostgreSQL extensions work as described above, but since
+extensions have to be compiled for each PostgreSQL major version separately,
+things are a bit more complex.
+
+For Debian, the process is changed to still have one source package per
+upstream project, but to build separate binary packages for each PostgreSQL
+major version. The naming scheme is `postgresql-NN-foo`. (In case the upstream
+project is called `pg_foo`, make a judgment call if `postgresql-NN-pg-foo` or
+`postgresql-NN-foo` is better.)
+
+In Debian, only one PostgreSQL major version is supported at a time, but in the
+https://apt.postgresql.org/ repository, many major versions are supported in
+parallel (currently PostgreSQL 10 everything newer, even when 10 is already
+EOL).
+
+## `debian/control.in`
+
+In order not to have to edit the list of binary packages built when a new
+PostgreSQL major version comes out, or when a source package is built both for
+Debian and for apt.postgresql.org, the `debian/control` file is generated from
+a template in `debian/control.in`.
+
+```
+$ cat debian/control.in
+Source: postgresql-unit
+Section: database
+Priority: optional
+Maintainer: Christoph Berg <myon@debian.org>
+Build-Depends:
+ bison,
+ debhelper-compat (= 13),
+ flex,
+ postgresql-server-dev-all (>= 217~),
+Standards-Version: 4.6.2
+Rules-Requires-Root: no
+Vcs-Git: https://github.com/df7cb/postgresql-unit.git
+Vcs-Browser: https://github.com/df7cb/postgresql-unit
+Homepage: https://github.com/df7cb/postgresql-unit
+
+Package: postgresql-PGVERSION-unit
+Architecture: any
+Depends: ${misc:Depends}, ${shlibs:Depends}, ${postgresql:Depends}
+Description: SI Units for PostgreSQL
+ postgresql-unit implements a PostgreSQL datatype for SI units, plus byte. The
+ base units can be combined to named and unnamed derived units using operators
+ defined in the PostgreSQL type system. SI prefixes are used for input and
+ output, and quantities can be converted to arbitrary scale.
+```
+
+The section with the `PGVERSION` token is duplicated for each major version
+supported, with the version number filled in.
+
+## `debian/pgversions`
+
+Not every package supports all PostgreSQL major versions. The
+`debian/pgversions` file is used to mark which versions are actually supported,
+so apt.postgresql.org can skip building the other version.
+
+Unless we know the exact versions supported, we should use `all`:
+
+```
+$ cat debian/pgversions
+all
+```
+
+If 14 or newer is supported:
+
+```
+$ cat debian/pgversions
+14+
+```
+
+### Supported versions
+
+`debian/pgversions` lists the versions supported *by the package*. The other
+half of that system is the set of versions supported *by the system*. This list
+is configured in `/etc/postgresql-common/supported_versions` or by setting the
+`PG_SUPPORTED_VERSIONS` environment variable. (Set this variable to test
+building for other major versions.) The actual build process will use the
+intersection of these two lists.
+
+## `pg_buildext`
+
+The process of building PostgreSQL extensions for several major versions is
+automated by the `pg_buildext` utility. It provides commands for the most
+common tasks.
+
+Package building:
+
+* `pg_buildext supported-versions` - print list of supported versions.
+ Use this to loop over versions in `debian/rules`.
+* `pg_buildext build build-%v` - build in `build-%v` directory
+* `pg_buildext install build-%v postgresql-%v-unit` - invoke `make install`
+* `pg_buildext installcheck build-%v postgresql-%v-unit` - invoke
+ `make installcheck` for build-time testing
+* `pg_buildext loop postgresql-%v-unit` - use instead of
+ `build/install/installcheck` if the package doesn't support out-of-tree
+ builds in subdirectories
+* `pg_buildext updatecontrol` - rebuild `debian/control` from
+ `debian/control.in`. Run this manually when the set of supported versions has
+ changed. This is not run automatically because the Debian packaging policy
+ forbids changing the set of binary packages at build time. (In environments
+ where this is not an issue, set `PG_UPDATECONTROL=yes`.)
+
+Package testing:
+
+* `pg_buildext installed-versions` - print list of installed versions.
+ Use this to loop over versions in `debian/tests/*`.
+* `pg_buildext installcheck` - invoke `make installcheck` on installed packages
+
+## `dh --with pgxs`
+
+A debhelper extension `pgxs` is provided that adds builds steps to the `dh`
+build sequence.
+
+```
+$ cat debian/rules
+#!/usr/bin/make -f
+
+%:
+ dh $@ --with pgxs
+```
+
+If the package doesn't support out-of-tree builds, use
+`dh $@ --with pgxs_loop`.
+
+```
+ dh_auto_build --buildsystem=pgxs
+ pg_buildext build build-%v
+ dh_auto_install --buildsystem=pgxs
+ pg_buildext install build-%v postgresql-%v-unit
+ dh_pgxs_test
+ pg_buildext installcheck . build-%v postgresql-%v-unit
+```
+
+To override any of these steps, use `override_dh_auto_*` in `debian/rules`.
+
+## Package template
+
+To get started with a new package, the `dh_make_pgxs` tool can generate a
+skeleton `debian/` directory:
+
+```
+$ dh_make_pgxs
+```
+
+If the auto-detected values are wrong, hit `^C` and add more command line
+parameters.
diff --git a/gitlab/gitlab-ci.yml b/gitlab/gitlab-ci.yml
new file mode 100644
index 0000000..5c640aa
--- /dev/null
+++ b/gitlab/gitlab-ci.yml
@@ -0,0 +1,13 @@
+# gitlab-ci.yml file for the Debian PostgreSQL packages
+# to be used in debian/gitlab-ci.yml:
+# include: https://salsa.debian.org/postgresql/postgresql-common/raw/master/gitlab/gitlab-ci.yml
+
+include:
+ - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml
+ - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml
+
+variables:
+ PG_UPDATECONTROL: 'yes' # tell pg_buildext to automatically generate debian/control from debian/control.in (TODO: doesn't work this way yet)
+ SALSA_CI_DISABLE_REPROTEST: 'yes' # too many packages fail because reprotest runs the build as root, disable for now
+ SALSA_CI_REPROTEST_ENABLE_DIFFOSCOPE: 'yes'
+ SALSA_CI_DISABLE_CROSSBUILD_ARM64: 'yes' # clang isn't ready for cross-building yet
diff --git a/pg_backupcluster b/pg_backupcluster
new file mode 100755
index 0000000..2959056
--- /dev/null
+++ b/pg_backupcluster
@@ -0,0 +1,659 @@
+#!/usr/bin/perl -wT
+
+# simple pg_basebackup front-end
+#
+# Copyright (C) 2021-2023 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 Fcntl qw(LOCK_EX);
+use Getopt::Long;
+use JSON;
+use PgCommon;
+use POSIX qw(strftime);
+
+my ($version, $cluster);
+
+# untaint environment
+$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
+delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
+chdir '/';
+umask 027;
+
+sub help () {
+ print "Syntax: $0 [options] <version> <cluster> <action>
+Actions:
+ createdirectory Create /var/backups/version-cluster
+ basebackup Backup using pg_basebackup
+ dump Backup using pg_dump
+ expiredumps <N> Remove all but last N dumps
+ expirebasebackups <N> Remove all but last N basebackups
+ receivewal Launch pg_receivewal
+ compresswal Compress WAL files in archive
+ archivecleanup Remove obsolete WAL files from archive
+ list Show dumps, basebackups, and WAL
+Options:
+ -c --checkpoint <spread|fast> Passed to pg_basebackup
+ -k --keep-on-error Keep faulty backup directory on error
+ -v --verbose Verbose output
+";
+}
+
+my $checkpoint = 'spread';
+my $keep_on_error;
+my $verbose;
+
+exit 1 unless GetOptions (
+ 'c|checkpoint=s' => sub { $checkpoint = $_[1] =~ /^f/ ? "fast" : "spread" },
+ 'k|keep-on-error' => \$keep_on_error,
+ 'v|verbose' => \$verbose,
+);
+
+$verbose = 1 if (-t 1); # verbose output when running on terminal
+
+# accept both "version cluster action" and "version[-/]cluster action"
+if (@ARGV >= 2 and $ARGV[0] =~ m!^(\d+\.?\d)[-/]([^/]+)$!) {
+ ($version, $cluster) = ($1, $2);
+ shift @ARGV;
+} elsif (@ARGV >= 3 and $ARGV[0] =~ /^(\d+\.?\d)$/) {
+ $version = $1;
+ ($cluster) = ($ARGV[1]) =~ m!^([^/]+)$!;
+ shift @ARGV;
+ shift @ARGV;
+} else {
+ help();
+ exit 1;
+}
+my $action = $ARGV[0];
+
+error "specified cluster $version $cluster does not exist" unless $version && $cluster && cluster_exists $version, $cluster;
+
+my %info = cluster_info($version, $cluster);
+validate_cluster_owner \%info;
+
+my $rootdir = "/var/backups/postgresql";
+my $clusterdir = "$rootdir/$version-$cluster";
+my $waldir = "$clusterdir/wal";
+
+# functions to be run as root
+
+sub create_directory() {
+ if (! -d $rootdir) {
+ my ($pg_uid, $pg_gid) = (getpwnam 'postgres')[2,3];
+ ($pg_uid and $pg_gid) or error "getpwnam postgres: $!";
+ mkdir $rootdir, 0755 or error "mkdir $rootdir: $!";
+ chown $pg_uid, $pg_gid, $rootdir or error "chown $rootdir: $!";
+ }
+ if (! -d $clusterdir) {
+ mkdir($clusterdir, 0750) or error "mkdir $clusterdir: $!";
+ chown $info{owneruid}, $info{ownergid}, $clusterdir or error "chown $clusterdir: $!";
+ }
+}
+
+sub switch_to_cluster_owner() {
+ change_ugid $info{owneruid}, $info{ownergid};
+}
+
+# backup functions
+
+sub get_backupdir($$) {
+ my ($starttime, $suffix) = @_;
+ my $timestamp = strftime("%FT%H%M%SZ", gmtime($starttime));
+ my $backupdir = "$clusterdir/$timestamp.$suffix";
+ error "$backupdir already exists" if (-d $backupdir);
+ print "Creating $suffix in $backupdir\n" if ($verbose);
+ return $backupdir;
+}
+
+sub remove_backup_on_error($) {
+ my $backupdir = shift;
+ if ($keep_on_error) {
+ print "Not removing $backupdir (--keep-on-error)\n";
+ } else {
+ print "Removing $backupdir ...\n" if ($verbose);
+ system_or_error "rm", "-rf", $backupdir;
+ }
+}
+
+sub create_basebackup($) {
+ my $backupdir = shift;
+
+ system_or_error "pg_basebackup",
+ "--cluster", "$version/$cluster",
+ "--verbose",
+ (-t 2 ? "--progress" : ()),
+ "--checkpoint=$checkpoint",
+ "--format", "tar", "--gzip",
+ ($version < 10 ? "--xlog" : ()),
+ "-D", $backupdir;
+}
+
+sub create_dumpall($) {
+ my $backupdir = shift;
+ mkdir($backupdir, 0750) or error "mkdir $backupdir: $!";
+
+ my $pg16 = $version >= 16 ? "|| ' --icu-rules ' || daticurules" : "";
+ my $pg15 = $version >= 15 ? "CASE datlocprovider::text
+ WHEN 'c' THEN '--locale-provider libc'
+ WHEN 'i' THEN '--locale-provider icu --icu-locale ' || daticulocale $pg16
+ END," : "";
+ my $clusterquery = "SELECT
+ '--encoding', pg_catalog.pg_encoding_to_char(encoding),
+ '--lc-collate', datcollate,
+ '--lc-ctype', datctype,
+ $pg15
+ CASE WHEN current_setting('data_checksums')::boolean THEN '-- --data-checksums' END
+FROM pg_database WHERE datname = 'template0'";
+ system_or_error "psql",
+ "--cluster", "$version/$cluster",
+ "-XAtF", " ", "-c", $clusterquery,
+ "-o", "$backupdir/createcluster.opts";
+
+ system_or_error "pg_dumpall",
+ "--cluster", "$version/$cluster",
+ "--globals-only",
+ "--file", "$backupdir/globals.sql";
+
+ $pg16 = $version >= 16 ? "|| ' ICU_RULES ' || quote_literal(coalesce(daticurules, ''))" : "";
+ $pg15 = $version >= 15 ? "|| CASE datlocprovider::text
+ WHEN 'c' THEN 'LOCALE_PROVIDER libc'
+ WHEN 'i' THEN 'LOCALE_PROVIDER icu ICU_LOCALE ' || quote_literal(daticulocale) $pg16
+ END" : "";
+ my $dbquery = "/* database creation */
+SELECT
+ format('CREATE DATABASE %I WITH OWNER %I TEMPLATE template0 ENCODING %L LC_COLLATE %L LC_CTYPE %L',
+ datname, rolname, pg_encoding_to_char(encoding), datcollate, datctype) $pg15 || ';' AS command
+FROM pg_database
+LEFT JOIN pg_roles r ON r.oid = datdba
+WHERE datallowconn AND datname NOT IN ('postgres', 'template1')
+
+UNION ALL
+
+/* database options */
+SELECT
+ CASE
+ WHEN rolname IS NULL THEN format('ALTER DATABASE %I', datname)
+ ELSE format('ALTER ROLE %I IN DATABASE %I', rolname, datname)
+ END ||
+ format(' SET %I = ', match[1]) ||
+ CASE
+ WHEN match[1] IN ('local_preload_libraries', 'search_path', 'session_preload_libraries', 'shared_preload_libraries', 'temp_tablespaces', 'unix_socket_directories')
+ THEN match[2]
+ ELSE quote_literal(match[2])
+ END ||
+ ';' AS command
+FROM pg_db_role_setting s
+JOIN pg_database d ON d.oid = setdatabase
+LEFT JOIN pg_roles r ON r.oid = setrole,
+unnest(setconfig) u(setting),
+regexp_matches(setting, '(.+)=(.+)') m(match)";
+ system_or_error "psql",
+ "--cluster", "$version/$cluster",
+ "-XAtc", $dbquery,
+ "-o", "$backupdir/databases.sql";
+
+ my $dblist = 'SELECT datname FROM pg_database WHERE datallowconn ORDER BY datname';
+ my $databases = `psql --cluster '$version/$cluster' -XAtc '$dblist'`;
+ for my $datname ($databases =~ /(.+)/g) {
+ print "Dumping $datname to $backupdir/$datname.dump ...\n" if ($verbose);
+ system_or_error "pg_dump",
+ "--cluster", "$version/$cluster",
+ "--format=custom",
+ "--file", "$backupdir/$datname.dump",
+ $datname;
+ }
+}
+
+sub create_configbackup($) {
+ my $backupdir = shift;
+ $info{configdir} or error "cluster has no configdir";
+ system_or_error "tar",
+ "-C", $info{configdir},
+ "-cz", "-f", "$backupdir/config.tar.gz",
+ ".";
+}
+
+sub create_status($$$$) {
+ my ($type, $starttime, $backupdir, $status) = @_;
+ my $statusfile = "$backupdir/status";
+ my $endtime = time;
+ my $statusjson = {
+ cluster => $cluster,
+ duration => $endtime - $starttime,
+ end => strftime("%FT%H%M%SZ", gmtime($endtime)),
+ start => strftime("%FT%H%M%SZ", gmtime($starttime)),
+ status => $status,
+ type => $type,
+ version => $version,
+ };
+ if (my $hostname = `hostname`) {
+ chomp $hostname;
+ $statusjson->{hostname} = $hostname;
+ }
+ if (-e '/etc/machine-id') {
+ open my $fh, '/etc/machine-id';
+ my $machine_id = <$fh>;
+ close $fh;
+ if ($machine_id) {
+ chomp $machine_id;
+ $statusjson->{'machine-id'} = $machine_id;
+ }
+ }
+ if (-e '/etc/machine-info') {
+ open my $fh, '/etc/machine-info';
+ while (<$fh>) {
+ if (/^DEPLOYMENT=(.*)/) {
+ $statusjson->{'machine-deployment'} = $1;
+ }
+ }
+ close $fh;
+ }
+
+ my $json = JSON->new->canonical;
+ open F, '>', $statusfile or error "$statusfile: $!";
+ print F $json->encode($statusjson) . "\n" or error "$statusfile: $!";
+ close F or error "$$statusfile: $!";
+}
+
+sub sync($) {
+ my $backupdir = shift;
+ system_or_error "sync $backupdir/*";
+}
+
+sub expire_backups($$) {
+ my ($suffix, $number) = @_;
+ my @backups = glob("$clusterdir/*.$suffix");
+ my $found = 0;
+ for my $backup (reverse @backups) {
+ # iterate reversely over backups until we have found enough valid ones
+ if ($found >= $number) {
+ print "Removing $backup ...\n" if ($verbose);
+ $backup =~ /(.*)/; # untaint
+ system_or_error "rm", "-rf", $1;
+ } else {
+ if (-f "$backup/status") {
+ $found++;
+ }
+ }
+ }
+}
+
+sub delete_broken() {
+ my @backups = (glob("$clusterdir/*.backup"), glob("$clusterdir/*.dump"));
+ for my $backup (@backups) {
+ if (! -f "$backup/status") {
+ print "Removing $backup ...\n" if ($verbose);
+ $backup =~ /(.*)/; # untaint
+ system_or_error "rm", "-rf", $1;
+ }
+ }
+}
+
+# wal handling
+
+sub create_wal_directory() {
+ if (! -d $waldir) {
+ mkdir($waldir, 0750) or error "mkdir $waldir: $!";
+ }
+}
+
+sub receivewal() {
+ my $pg_receivewal = $version >= 10 ? 'pg_receivewal' : 'pg_receivexlog';
+
+ # create slot
+ system_or_error $pg_receivewal, "--cluster=$version/$cluster", "--slot", "pg_receivewal_service", "--create-slot", "--if-not-exists";
+
+ # launch pg_receivewal
+ $ENV{PGAPPNAME} = "pg_receivewal\@$version-$cluster.service";
+ my @cmd = ($pg_receivewal, "--cluster", "$version/$cluster",
+ "-D", $waldir, "--slot", "pg_receivewal_service");
+ push @cmd, "--compress=5" if ($version >= 10);
+ exec {$pg_receivewal} @cmd or error "exec $pg_receivewal: $!";
+}
+
+sub compresswal() {
+ chdir $waldir or return; # ok if not yet created
+ open my $lock, $waldir or error "open $waldir: $!"; # protect against concurrent runs
+ flock $lock, LOCK_EX or error "flock $waldir: $!";
+
+ for my $wal (glob "0???????????????????????") {
+ $wal =~ /^([0-9A-F]+)$/ or continue;
+ $wal = $1; # untaint
+ if (-f "$wal.gz") {
+ print STDERR "$waldir/$wal.gz already exists, skipping compression\n";
+ next;
+ }
+ system_or_error "if ! gzip < $wal > $wal.tmp.gz; then rm -f $wal.tmp.gz; exit 1; fi";
+ system_or_error "touch --reference=$wal $wal.tmp.gz";
+ system_or_error "mv $wal.tmp.gz $wal.gz";
+ system_or_error "sync", "$wal.gz";
+ unlink $wal;
+ }
+
+ close $lock;
+}
+
+sub archivecleanup() {
+ chdir $waldir or return; # ok if not yet created
+ my @backups = sort glob "$clusterdir/*.backup";
+ for my $backup (@backups) {
+ next unless (-f "$backup/status");
+
+ $backup =~ /(.*)/; # untaint
+ my $basetar = "$1/base.tar.gz";
+ my $backup_label = `tar --extract --occurrence=1 --to-stdout --file '$basetar' backup_label` or error "failed to extract backup_label from $basetar";
+
+ # START WAL LOCATION: 0/2B000028 (file 00000001000000000000002B)
+ $backup_label =~ /^START WAL LOCATION: .* \(file ([0-9A-F]+)\)/ or error "no start wal location in $basetar";
+ my $keep_file = $1;
+ system_or_error "pg_archivecleanup", "-x", ".gz", $waldir, $keep_file;
+
+ return 0; # process first backup only
+ }
+ error "no valid basebackups found in $clusterdir";
+}
+
+# info functions
+
+sub dirsize($) {
+ my $dir = shift;
+ my $size = 0;
+ my $files = 0;
+ for my $f (glob "$dir/*") {
+ $size += (stat $f)[7];
+ $files++;
+ }
+ return $size, $files;
+}
+
+sub list() {
+ print "Cluster $version $cluster backups in $clusterdir:\n";
+ my $totalsize = 0;
+ print "Dumps:\n";
+ for my $dir (sort glob "$clusterdir/*.dump") {
+ my ($size, $files) = dirsize($dir);
+ my $status = -f "$dir/status" ? '' : ' BROKEN';
+ print " $dir: $size Bytes$status\n";
+ $totalsize += $size;
+ }
+ print "Basebackups:\n";
+ for my $dir (sort glob "$clusterdir/*.backup") {
+ my ($size, $files) = dirsize($dir);
+ my $status = -f "$dir/status" ? '' : ' BROKEN';
+ print " $dir: $size Bytes$status\n";
+ $totalsize += $size;
+ }
+ if (-d "$clusterdir/wal") {
+ print "WAL:\n";
+ my ($size, $files) = dirsize("$clusterdir/wal");
+ print " $clusterdir/wal: $size Bytes, $files Files\n";
+ $totalsize += $size;
+ }
+ print "Total: $totalsize Bytes\n";
+}
+
+# main
+
+if ($action eq 'createdirectory') {
+ create_directory();
+
+} elsif ($action eq 'basebackup') {
+ error "basebackups of pre-9.1 servers are not supported" if ($version < 9.1);
+ my $starttime = time;
+ create_directory();
+ switch_to_cluster_owner();
+ my $backupdir = get_backupdir($starttime, 'backup');
+ $SIG{__DIE__} = sub { remove_backup_on_error($backupdir) };
+ create_basebackup($backupdir);
+ create_configbackup($backupdir);
+ create_status($action, $starttime, $backupdir, 'ok');
+ $SIG{__DIE__} = undef;
+ sync($backupdir);
+ compresswal();
+
+} elsif ($action eq 'dump') {
+ error "dumps of pre-9.3 servers are not supported" if ($version < 9.3);
+ my $starttime = time;
+ create_directory();
+ switch_to_cluster_owner();
+ my $backupdir = get_backupdir($starttime, 'dump');
+ $SIG{__DIE__} = sub { remove_backup_on_error($backupdir) };
+ create_dumpall($backupdir);
+ create_configbackup($backupdir);
+ create_status($action, $starttime, $backupdir, 'ok');
+ $SIG{__DIE__} = undef;
+ sync($backupdir);
+
+} elsif ($action eq 'expiredumps' and @ARGV == 2 and $ARGV[1] =~ /^(\d+)$/) {
+ switch_to_cluster_owner();
+ expire_backups('dump', $1);
+
+} elsif ($action eq 'expirebasebackups' and @ARGV == 2 and $ARGV[1] =~ /^(\d+)$/) {
+ switch_to_cluster_owner();
+ expire_backups('backup', $1);
+ archivecleanup();
+ compresswal();
+
+} elsif ($action eq 'deletebroken') {
+ switch_to_cluster_owner();
+ delete_broken();
+
+} elsif ($action eq 'receivewal') {
+ create_directory();
+ switch_to_cluster_owner();
+ create_wal_directory();
+ compresswal();
+ receivewal();
+
+} elsif ($action eq 'compresswal') {
+ switch_to_cluster_owner();
+ compresswal();
+
+} elsif ($action eq 'archivecleanup') {
+ switch_to_cluster_owner();
+ archivecleanup();
+ compresswal();
+
+} elsif ($action eq 'list') {
+ switch_to_cluster_owner();
+ list();
+
+} else {
+ help();
+ exit(1);
+}
+
+__END__
+
+=head1 NAME
+
+pg_backupcluster - simple pg_basebackup and pg_dump front-end
+
+=head1 SYNOPSIS
+
+B<pg_backupcluster> [I<options>] I<version> I<cluster> I<action>
+
+=head1 DESCRIPTION
+
+B<pg_backupcluster> provides a simple interface to create PostgreSQL cluster
+backups using L<pg_basebackup(1)> and L<pg_dump(1)>.
+
+To ease integration with B<systemd> operation, the alternative syntax
+"B<pg_basebackup> I<version>B<->I<cluster> I<action>" is also supported.
+
+=head1 ACTIONS
+
+=over 4
+
+=item B<createdirectory>
+
+Create /var/backups and /var/backups/I<version>-I<cluster>.
+This action can be run as root to create the directories required for backups.
+All other actions will also attempt to create the directories when missing, but
+can of course only do that when running as root. They will switch to the
+cluster owner after this step.
+
+=item B<basebackup>
+
+Backup using L<pg_basebackup(1)>. The resulting basebackup contains the WAL
+files required to run recovery on startup.
+
+=item B<dump>
+
+Backup using L<pg_dump(1)>. Global objects (users, tablespaces) are dumped
+using L<pg_dumpall(1)> B<--globals-only>. Individual databases are dumped into
+PostgreSQL's custom format.
+
+=item B<expirebasebackups> I<N>
+
+Remove all but last the I<N> basebackups.
+
+=item B<expiredumps> I<N>
+
+Remove all but last the I<N> dumps.
+
+=item B<receivewal>
+
+Launch pg_receivewal. WAL files are gzip-compressed in PG 10+.
+
+=item B<compresswal>
+
+Compress WAL files in archive.
+
+=item B<archivecleanup>
+
+Remove obsolete WAL files from archive using L<pg_archivecleanup(1)>.
+
+=item B<list>
+
+Show dumps, basebackups, and WAL, with size.
+
+=back
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<-c --checkpoint=spread|fast>
+
+Passed to B<pg_basebackup>. Default is B<spread>.
+
+=item B<-k --keep-on-error>
+
+Keep faulty backup directory on error. By default backups are delete on error.
+
+=item B<-v --verbose>
+
+Verbose output, even when not running on a terminal.
+
+=back
+
+=head1 FILES
+
+=over 4
+
+=item /var/backups
+
+Default root directory for cluster backup directories.
+
+=item /var/backups/I<version>-I<cluster>
+
+Default directory for cluster backups.
+
+=item /var/backups/I<version>-I<cluster>/I<timestamp>B<.basebackup>
+
+Backup from B<pg_backupcluster ... basebackup>.
+
+=over 4
+
+=item C<config.tar.gz>
+
+Tarball of cluster configuration directory (postgresql.conf, pg_hba.conf, ...)
+in /etc/postgresql.
+
+=item I<tablespace>C<.tar.gz>, C<pg_wal.tar.gz>, C<backup_manifest>
+
+Tablespace and WAL tarballs and backup info written by B<pg_basebackup>.
+
+=item C<status>
+
+Completion timestamp of backup run.
+
+=back
+
+=item /var/backups/I<version>-I<cluster>/I<timestamp>B<.dump>
+
+Backup from B<pg_backupcluster ... dump>.
+
+=over 4
+
+=item C<config.tar.gz>
+
+Tarball of cluster configuration directory (postgresql.conf, pg_hba.conf, ...)
+in /etc/postgresql.
+
+=item C<createcluster.opts>
+
+Options (encoding, locale, data checksums) to be passed to B<pg_createcluster>
+for restoring this cluster.
+
+=item C<globals.sql>
+
+Global objects (roles, tablespaces) from B<pg_dumpall --globals-only>.
+
+=item C<databases.sql>
+
+SQL commands to create databases and restore database-level options.
+
+=item I<database>C<.dump>
+
+Database dumps from B<pg_dump --format=custom>.
+
+=item C<status>
+
+Completion timestamp of backup run.
+
+=back
+
+=item /var/backups/I<version>-I<cluster>/B<wal>
+
+WAL files from B<pg_receivewal>.
+
+=back
+
+=head1 CAVEATS
+
+For dump-style backups, not all properties of the original cluster are preserved:
+
+=over 2
+
+=item * In PostgreSQL 10 and earlier, ALTER ROLE ... IN DATABASE is not supported.
+
+=item * Not all B<initdb> options are carried over. Currently supported are B<--encoding>,
+B<--lc-collate>, B<--lc-collate>, and B<-k --data-checksums>.
+
+=back
+
+The earliest PostgreSQL version supported for dumps is 9.3.
+For basebackups, the earliest supported version is 9.1.
+B<receivewal> (and hence archive recovery) are supported in 9.5 and later.
+
+=head1 SEE ALSO
+
+L<pg_restorecluster(1)>,
+L<pg_dump(1)>, L<pg_dumpall(1)>,
+L<pg_basebackup(1)>, L<pg_receivewal(1)>, L<pg_archivecleanup(1)>.
+
+=head1 AUTHOR
+
+Christoph Berg L<E<lt>myon@debian.orgE<gt>>
diff --git a/pg_buildext b/pg_buildext
new file mode 100755
index 0000000..304e8a1
--- /dev/null
+++ b/pg_buildext
@@ -0,0 +1,503 @@
+#!/bin/bash
+#
+# build a PostgreSQL module based on PGXS for given list of supported major
+# versions
+#
+# (C) 2010 Dimitri Fontaine <dfontaine@hi-media.com>
+# (C) 2011-2023 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.
+
+set -eu
+
+die() {
+ echo "$(basename $0): error: $*" >&2
+ exit 1
+}
+
+VENVARGS=""
+MAKEVARS=""
+while getopts "c:i:m:o:s" opt ; do
+ case $opt in
+ c|i|o) VENVARGS="$VENVARGS -$opt $OPTARG" ;;
+ m) MAKEVARS="$OPTARG" ;;
+ s) VENVARGS="$VENVARGS -$opt" ;;
+ *) exit 1 ;;
+ esac
+done
+# shift away args
+shift $(($OPTIND - 1))
+
+# positional arguments: action [srcdir] [target [opt]]
+[ "${1:-}" ] || die "no action given"
+action="$1"
+shift
+
+if [ -d "${1:-}" ] && [ "${2:-}" ]; then # optional: source directory
+ srcdir="${1:-}"
+ [ "$srcdir" = "." ] && srcdir="$PWD"
+ shift
+else
+ srcdir="$PWD"
+fi
+target="${1:-}"
+opt="${2:-}"
+
+prepare_env() {
+ local version=$1
+ vtarget=`echo $target | sed -e "s:%v:$version:g"`
+ pgc="/usr/lib/postgresql/$version/bin/pg_config"
+ [ -e "$pgc" ] || die "$pgc does not exist"
+ if [ "${CFLAGS:-}" ]; then
+ export COPT="$CFLAGS"
+ fi
+}
+
+configure() {
+ prepare_env $1
+ confopts=`echo $opt | sed -e "s:%v:$1:g"`
+
+ mkdir -p $vtarget
+ ( echo "calling configure in $vtarget" &&
+ cd $vtarget && $srcdir/configure $confopts PG_CONFIG="$pgc" VPATH="$srcdir" ) || return $?
+}
+
+build() {
+ prepare_env $1
+ if [ "$opt" ]; then
+ cflags="$(echo $opt | sed -e "s:%v:$1:g")"
+ export COPT="${COPT:+$COPT }$cflags"
+ fi
+
+ mkdir -p $vtarget
+ # if a Makefile was created by configure, use it, else the top level Makefile
+ [ -f $vtarget/Makefile ] || makefile="-f $srcdir/Makefile"
+ make -C $vtarget ${makefile:-} PG_CONFIG="$pgc" VPATH="$srcdir" USE_PGXS=1 $MAKEVARS || return $?
+}
+
+substvars() {
+ version="$1"
+ package="$2"
+
+ depends="postgresql-$version"
+ if [ -d "debian/$package/usr/lib/postgresql/$version/lib/bitcode" ]; then
+ pg_provides=$(dpkg-query --show --showformat='${Provides}' postgresql-$version | grep -o 'postgresql-[0-9]*-jit-llvm (= [0-9.]*')
+ llvm_version=${pg_provides#*= }
+ if [ "$llvm_version" ]; then # skip if server version doesn't have the Provides yet
+ depends="$depends, postgresql-$version-jit-llvm (>= $llvm_version)"
+ fi
+ fi
+ echo "postgresql:Depends=$depends" >> "debian/$package.substvars"
+
+ if ! grep -q postgresql:Depends debian/control; then # compat: add to misc:Depends
+ if grep -q ^misc:Depends "debian/$package.substvars"; then
+ sed -i -e "s/^misc:Depends=/misc:Depends=$depends, /" "debian/$package.substvars"
+ else
+ echo "misc:Depends=$depends" >> "debian/$package.substvars"
+ fi
+ fi
+}
+
+install() {
+ prepare_env $1
+ package=`echo $opt | sed -e "s:%v:$1:g"`
+
+ mkdir -p $vtarget
+ # if a Makefile was created by configure, use it, else the top level Makefile
+ [ -f $vtarget/Makefile ] || makefile="-f $srcdir/Makefile"
+ make -C $vtarget ${makefile:-} install DESTDIR="$PWD/debian/$package" PG_CONFIG="$pgc" VPATH="$srcdir" USE_PGXS=1 $MAKEVARS || return $?
+ substvars "$1" "$package"
+}
+
+clean() {
+ prepare_env $1
+ if [ "$vtarget" ]; then
+ rm -rf $vtarget
+ fi
+}
+
+loop() {
+ prepare_env $1
+ package=$(echo $target | sed -e "s:%v:$1:g")
+
+ echo "# $1: make"
+ make -C "$srcdir" PG_CONFIG="$pgc" USE_PGXS=1 $MAKEVARS || return $?
+ echo "# $1: make install"
+ make -C "$srcdir" install DESTDIR="$PWD/debian/$package" PG_CONFIG="$pgc" USE_PGXS=1 $MAKEVARS || return $?
+ substvars "$1" "$package"
+ echo "# $1: make clean"
+ make -C "$srcdir" clean PG_CONFIG="$pgc" USE_PGXS=1 $MAKEVARS # clean errors are fatal
+}
+
+installcheck() {
+ prepare_env $1
+
+ # ask pg_regress to output unified diffs
+ export PG_REGRESS_DIFF_OPTS="-U3"
+
+ # ask pg_virtualenv to create a non-system cluster
+ if [ "${NONROOT-unset}" = "unset" ]; then
+ export NONROOT=1
+ fi
+
+ # if a package pattern is given, tell pg_virtualenv where the installed files are
+ if [ "$opt" ]; then
+ pkg=$(echo "$opt" | sed -e "s:%v:$1:g")
+ PKGARGS="-p $pkg"
+ DESTDIR="DESTDIR=$PWD/debian/$pkg"
+ fi
+
+ if [ "$target" ] && [ "$target" != "." ]; then # if target is given, use it, else stay in the top source dir
+ # if a Makefile was created by configure, use it, else the top level Makefile
+ [ -f $vtarget/Makefile ] || makefile="-f $srcdir/Makefile"
+ if ! pg_virtualenv ${PKGARGS:-} $VENVARGS -v $1 \
+ make -C $vtarget ${makefile:-} installcheck ${DESTDIR:-} \
+ PG_CONFIG="$pgc" VPATH="$srcdir" USE_PGXS=1 $MAKEVARS; then
+ if [ -r $vtarget/regression.diffs ]; then
+ echo "**** $vtarget/regression.diffs ****"
+ cat $vtarget/regression.diffs
+ fi
+ return 1
+ fi
+ else
+ if ! pg_virtualenv ${PKGARGS:-} $VENVARGS -v $1 \
+ make installcheck ${DESTDIR:-} PG_CONFIG="$pgc" USE_PGXS=1 $MAKEVARS; then
+ if [ -r regression.diffs ]; then
+ echo "**** regression.diffs ****"
+ cat regression.diffs
+ fi
+ return 1
+ fi
+ # since we are in the top-level directory, clean up behind us
+ make clean PG_CONFIG="$pgc" USE_PGXS=1 $MAKEVARS
+ fi
+}
+
+run_run () {
+ local v=$1
+ shift
+
+ (
+ echo + "${@//%v/$v}"
+ "${@//%v/$v}" < $PSQLTMP
+ ) || return $?
+}
+run_run_installed () {
+ run_run "$@"
+}
+
+run_psql () {
+ prepare_env $1
+
+ # ask pg_virtualenv to create a non-system cluster
+ if [ "${NONROOT-unset}" = "unset" ]; then
+ export NONROOT=1
+ fi
+
+ # if a package pattern is given, tell pg_virtualenv where the installed files are
+ if [ "$opt" ]; then
+ pkg=$(echo "$opt" | sed -e "s:%v:$1:g")
+ PKGARGS="-p $pkg"
+ export DESTDIR="$PWD/debian/$pkg"
+ fi
+
+ (
+ if [ "$target" ] && [ "$target" != "." ]; then # if target is given, use it, else stay in the top source dir
+ cd $target
+ fi
+ pg_virtualenv ${PKGARGS:-} $VENVARGS -v $1 \
+ psql -Xe -v ON_ERROR_STOP=1 < $PSQLTMP
+ ) || return $?
+}
+
+run_virtualenv () {
+ prepare_env $1
+
+ # ask pg_virtualenv to create a non-system cluster
+ if [ "${NONROOT-unset}" = "unset" ]; then
+ export NONROOT=1
+ fi
+
+ # if a package pattern is given, tell pg_virtualenv where the installed files are
+ if [ "$opt" ]; then
+ pkg=$(echo "$opt" | sed -e "s:%v:$1:g")
+ PKGARGS="-p $pkg"
+ export DESTDIR="$PWD/debian/$pkg"
+ fi
+
+ (
+ if [ "$target" ] && [ "$target" != "." ]; then # if target is given, use it, else stay in the top source dir
+ cd $target
+ fi
+ pg_virtualenv ${PKGARGS:-} $VENVARGS -v $1 ${SHELL:-/bin/sh} -ex < $PSQLTMP
+ ) || return $?
+}
+
+versions() {
+ [ -e /usr/share/postgresql-common/supported-versions ] ||
+ die "/usr/share/postgresql-common/supported-versions not found"
+ [ -e debian/pgversions ] || die "debian/pgversions not found"
+ supported_versions=$(/usr/share/postgresql-common/supported-versions)
+ local version
+ while read version; do
+ case $version in
+ all) echo "$supported_versions" ;;
+ [1-9]*+)
+ for sup_version in $supported_versions; do
+ if dpkg --compare-versions "${version%+}" le "$sup_version"; then echo "$sup_version"; fi
+ done ;;
+ [1-9]*)
+ for sup_version in $supported_versions; do
+ if [ "$version" = "$sup_version" ]; then echo "$sup_version"; fi
+ done ;;
+ '#'*) ;;
+ '') ;;
+ *) echo "Syntax error in debian/pgversions: $version" >&2 ;;
+ esac
+ done < debian/pgversions
+}
+
+# list of PostgreSQL versions for which packages built from this source are installed
+# (not necessarily the same as listed in debian/control)
+installed_versions() {
+ [ -e debian/control.in ] || die "debian/control.in not found"
+ # extract package name template(s) from control.in, split into prefix and suffix
+ perl -lne 'print "$1 $2" if /^Package: (.*)PGVERSION(.*)/' debian/control.in | \
+ while read prefix suffix; do
+ # translate templates to actually installed packages, and extract version numbers
+ dpkg-query --showformat '${db:Status-Status} ${Package}\n' --show "$prefix*$suffix" | \
+ grep '^installed' | cut -d ' ' -f 2 | sed -e "s/^$prefix//; s/$suffix\$//" | grep -E '^[0-9.]+$'
+ # if suffix is empty, the grep will filter out noise like '13-dbgsym'
+ done | sort -uV
+}
+
+gencontrol() {
+ tmpcontrol=$(mktemp debian/control.XXXXXX)
+ if [ -f debian/tests/control.in ]; then
+ tmptestscontrol=$(mktemp debian/tests/control.XXXXXX)
+ fi
+ trap "rm -f $tmpcontrol ${tmptestscontrol:-}" EXIT
+
+ export PGVERSIONS="$(versions)"
+ [ "$PGVERSIONS" ] || die "No current PostgreSQL versions are supported by this package"
+
+ perl -e \
+ '$/ = ""; # read paragraphs
+ my @pgversions = split /\s+/, $ENV{PGVERSIONS};
+ my $newest_pgversion = $pgversions[-1];
+
+ sub replace_all ($) {
+ $_ = shift;
+ my @out;
+ for my $version (@pgversions) {
+ push @out, s/PGVERSIONS/$version/rg;
+ }
+ return join ", ", @out;
+ }
+
+ while (<>) {
+ chomp;
+ if (/^Package: .*PGVERSION/) {
+ foreach my $version (@pgversions) {
+ push @out, s/PGVERSION/$version/rg;
+ }
+ } else {
+ s/(
+ [^[:space:](),]*
+ PGVERSIONS
+ [^[:space:](),]*
+ (?:\s*\([^(),]*\))? # optional part in parentheses
+ [^[:space:](),]*
+ )/replace_all($1)/xeg;
+ s/PGVERSION/$newest_pgversion/g;
+ push @out, $_;
+ }
+ }
+ print join("\n\n", @out), "\n";
+ ' debian/control.in > $tmpcontrol
+
+ if [ -f debian/tests/control.in ]; then
+ cp debian/tests/control.in $tmptestscontrol
+ # find words (package names) containing PGVERSION
+ REGEXP='[[:alnum:]-]*PGVERSION[[:alnum:]-]*'
+ for pkgpattern in $(grep -Ewo "$REGEXP" debian/tests/control.in | sort -u); do
+ repl=""
+ # build an array of replacements separated by ,
+ for v in $(versions); do
+ repl="${repl:+$repl, }$(echo $pkgpattern | sed -e "s/PGVERSION/$v/g")"
+ done
+ # put array into control file
+ grep -q "$pkgpattern" $tmptestscontrol # assert the pattern didn't get already removed by an earlier replacement
+ sed -i -e "s/$pkgpattern/$repl/" $tmptestscontrol
+ done
+ fi
+}
+
+updatecontrol() {
+ cat $tmpcontrol > debian/control
+
+ if [ -f debian/tests/control.in ]; then
+ cat $tmptestscontrol > debian/tests/control
+ fi
+
+ # re-check build-dependencies (no error raised)
+ ( dpkg-checkbuilddeps 2>&1 || : ) | sed -e 's/error/notice/'
+}
+
+# when a version is included in the action, just act on this one (this is
+# useful if some extra work needs to be done per version, so the loop over
+# supported-versions needs to be in the script calling pg_buildext)
+
+case $action in
+ configure-*|build-*|install-*|clean-*|installcheck-*)
+ a=${action%%-*}
+ v=${action##$a-}
+ ret=0
+ echo "### PostgreSQL $v $a ###"
+ if $a $v; then
+ echo "### End $v $a ###"
+ else
+ ret=$?
+ echo "### End $v $a (FAILED with exit code $ret) ###"
+ fi
+ exit $ret
+ ;;
+
+ checkcontrol)
+ [ -f debian/control.in ] || exit 0 # silently exit if debian/control.in is not there
+ gencontrol
+ need_update=
+ if ! diff -u debian/control $tmpcontrol; then
+ if [ "${PG_UPDATECONTROL:-no}" != "no" ] || head -1 debian/changelog | grep -Eq -- '-backports|-pgdg|-pgapt'; then
+ echo "Notice: Updating debian/control from debian/control.in."
+ need_update=1
+ else
+ echo "Error: debian/control needs updating from debian/control.in. Run 'pg_buildext updatecontrol'."
+ echo "If you are seeing this message in a buildd log, a sourceful upload is required."
+ exit 1
+ fi
+ fi
+ if [ -f debian/tests/control.in ] && ! diff -u debian/tests/control $tmptestscontrol; then
+ echo "Notice: Updating debian/tests/control from debian/tests/control.in."
+ need_update=1
+ fi
+ [ "$need_update" ] && updatecontrol
+ exit 0
+ ;;
+
+ updatecontrol)
+ [ -f debian/control.in ] || die "debian/control.in is missing, cannot update debian/control"
+ gencontrol
+ updatecontrol
+ exit
+ ;;
+
+ clean)
+ if [ "$target" ]; then
+ pattern=$(echo "$target" | sed -e "s:%v:*:g")
+ echo rm -rf $pattern/
+ rm -rf $pattern/
+ fi
+ if [ "$opt" ]; then
+ pattern=$(echo "$opt" | sed -e "s:%v:*:g")
+ echo rm -rf debian/$pattern/ debian/$pattern.substvars
+ rm -rf debian/$pattern/ debian/$pattern.substvars
+ fi
+ if [ -f Makefile ]; then
+ make clean USE_PGXS=1
+ fi
+ exit
+ ;;
+
+ installed-versions)
+ installed_versions
+ exit
+ ;;
+
+ installcheck)
+ # prefer testing installed versions over supported versions as the set
+ # of versions might have changed (unless a package-pattern is provided)
+ if [ -f debian/control.in ] && [ -z "$opt" ]; then
+ versions=$(installed_versions)
+ else
+ versions=$(versions)
+ fi
+ [ "$versions" ] || exit 1
+ ret=0
+ for v in $versions; do
+ echo "### PostgreSQL $v $action ###"
+ if $action $v; then
+ echo "### End $v $action ###"
+ else
+ ret=$?
+ echo "### End $v $action (FAILED with exit code $ret) ###"
+ fi
+ done
+ exit $ret
+ ;;
+
+ run|run_installed|psql|virtualenv)
+ PSQLTMP=$(mktemp --tmpdir psql.XXXXXX)
+ trap "rm -f $PSQLTMP" EXIT
+ # if we are fed any input (and hence aren't reading from a terminal),
+ # store it in a tempfile
+ [ ! -t 0 ] && cat > $PSQLTMP
+
+ # prefer testing installed versions over supported versions as the set
+ # of versions might have changed (unless a package-pattern is provided)
+ if [ "$action" != "run" ] && [ -f debian/control.in ] && [ -z "$opt" ]; then
+ versions=$(installed_versions)
+ else
+ versions=$(versions)
+ fi
+ [ "$versions" ] || exit 1
+ ret=0
+ for v in $versions; do
+ echo "### PostgreSQL $v $action ###"
+ if run_$action $v "$@"; then
+ echo "### End $v $action ###"
+ else
+ ret=$?
+ echo "### End $v $action (FAILED with exit code $ret) ###"
+ fi
+ done
+ exit $ret
+ ;;
+esac
+
+# loop over versions
+
+ret=0
+for v in $(versions)
+do
+ case "$action" in
+ "supported-versions")
+ echo $v
+ ;;
+
+ configure|build|install|clean|loop)
+ [ "$target" ] || die "syntax: pg_buildext $action <target> [<srcdir>] [<opt>]"
+ echo "### PostgreSQL $v $action ###"
+ if $action $v; then
+ echo "### End $v $action ###"
+ else
+ ret=$?
+ echo "### End $v $action (FAILED with exit code $ret) ###"
+ fi
+ ;;
+
+ *)
+ die "unsupported action '$action'"
+ ;;
+ esac
+done
+
+exit $ret
diff --git a/pg_buildext.pod b/pg_buildext.pod
new file mode 100644
index 0000000..d76c701
--- /dev/null
+++ b/pg_buildext.pod
@@ -0,0 +1,358 @@
+=head1 NAME
+
+pg_buildext - Build and install a PostgreSQL extension
+
+=head1 SYNOPSIS
+
+B<pg_buildext> [I<options>] I<action> [I<src-dir>] [I<arguments>]
+
+=head1 DESCRIPTION
+
+B<pg_buildext> is a script that will build a PostgreSQL extension in a C<VPATH>
+way, for potentially several PostgreSQL server versions in parallel.
+It builds for the intersection of versions known in
+C<debian/pgversions> (versions supported by the package) and in
+C</usr/share/postgresql-common/supported-versions> (versions supported in this
+release).
+
+Many PostgreSQL extension packages require no special handling at build time
+and can use B<dh $@ --with pgxs> or B<dh $@ --with pgxs_loop> to
+automatically execute the steps outlined below.
+
+=head1 USAGE
+
+Packages using B<pg_buildext> should be prepared to build binaries for
+PostgreSQL versions that are not present in Debian unstable, e.g. for older
+releases when building backports for Debian (old)stable (possibly including
+backports of newer PostgreSQL releases), or for all PostgreSQL releases when
+the package is built for B<apt.postgresql.org>.
+
+As the set of binary packages depends on the target PostgreSQL versions,
+C<debian/control> is generated from a template in C<debian/control.in> when
+B<pg_buildext updatecontrol> is run.
+Package sections that contain B<PGVERSION> in the package name are replaced by
+a list of sections, filling in the supported PostgreSQL versions.
+Package sections that contain B<PGVERSION> outside the package name have the
+newest supported PostgreSQL version filled in (useful for meta packages);
+words containing B<PGVERSIONS> be replaced by a list of words with the
+supported versions filled in, this is most useful in B<Build-Depends>.
+Include
+C</usr/share/postgresql-common/pgxs_debian_control.mk> in C<debian/rules> to
+run a check at build time if updating debian/control is required.
+
+As B<pg_buildext> invokes B<make> for the B<build>, B<install>, and B<clean>
+actions, invocations from C<debian/rules> (which is a makefile) should be prefixed
+with B<+> so the sub-makes can talk with the make jobserver. Additional makefile
+variables can be passed to B<make> via the B<-m> option.
+
+Many extensions support B<make installcheck> testing using B<pg_regress>. As
+this needs the package to be installed, it cannot be run at build time.
+Instead, the tests should be run using B<autopkgtest> from C<debian/tests/*>.
+
+If C<debian/tests/control.in> exists, occurrences of package names containing
+B<PGVERSION> are replaced by lists of package names with the target PostgreSQL
+versions filled in. (If no replacing is needed in C<debian/tests/control>, it
+is fine to provide the tests control file directly.)
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<-cio> I<arg>
+
+=item B<-s>
+
+Passed to B<pg_virtualenv> when running B<installcheck>.
+
+=item B<-m> I<arg>
+
+Passed to B<make>.
+
+=back
+
+=head1 ACTIONS
+
+Most actions expect a directory name where to build the sources. It will get
+created for you if it does not exist. If the I<build-dir> contains a C<%v>
+sign, it will get replaced by the specific version of PostgreSQL being built
+against. (Usually this parameter is C<build-%v>.)
+
+=over 4
+
+=item B<supported-versions>
+
+Print effective list of supported versions, i.e. the intersection of the sets
+of versions supported by the system
+(from C</usr/share/postgresql-common/supported-versions>) and the package
+(from C<debian/pgversions>).
+
+Use this when building packages.
+
+=item B<installed-versions>
+
+In the list of installed packages, look for packages matching the B<PGVERSION>
+package name templates from C<debian/control.in>, and print the PostgreSQL
+major version number part.
+
+Use this when testing packages.
+
+=item B<checkcontrol>
+
+Check if C<debian/control> needs updating from C<debian/control.in>. This is
+invoked from C</usr/share/postgresql-common/pgxs_debian_control.mk>. When
+building for a B<backports> or B<pgdg> suite as determined by
+C<debian/changelog>, this action also updates the control file. Otherwise,
+B<updatecontrol> needs to be run manually.
+
+=item B<updatecontrol>
+
+Update C<debian/control> from C<debian/control.in>, and C<debian/tests/control>
+from C<debian/tests/control.in> if the latter exists.
+
+=item B<configure> [I<src-dir>] I<build-dir> [I<extra-configure-options>]
+
+For every supported version, call B<../configure> from the I<build-dir>
+directory. (Most PostgreSQL extensions do not have a configure script.)
+
+=item B<build> [I<src-dir>] I<build-dir> [I<extra-cflags>]
+
+Build the extension in the I<build-dir> directory.
+
+=item B<install> [I<src-dir>] I<build-dir> I<package-pattern>
+
+Invoke B<make install> from the I<build-dir> directory.
+The third parameter specifies the package name to use. Most packages
+use B<postgresql-%v-pkgname>. Make will be
+called with DESTDIR="$(CURDIR)/debian/I<package>".
+
+The B<dpkg> substitution variable B<postgresql:Depends> is set to depend
+on the required PostgreSQL server package. For compatibility with previous
+packaging standards, the dependency is also added to B<misc:Depends> if
+postgresql:Depends is not used.
+
+=item B<clean> [I<src-dir>] [I<build-dir>] [I<package-pattern>]
+
+Clean the build directories.
+
+=item B<loop> [I<src-dir>] I<package-pattern>
+
+As a variant to calling B<build> and B<install> separately for VPATH builds,
+loop over the supported PostgreSQL versions in the top source directory. This
+should be used if the package does not support VPATH builds. As it also invokes
+B<make install>, it should be placed were installation happens in debian/rules,
+rather than where build would normally be called.
+
+=item B<installcheck> [I<src-dir>] [I<build-dir>] [I<package-pattern>]
+
+Use B<pg_virtualenv make installcheck> to run the extension regression tests.
+This is meant to be run from C<debian/tests/control> using B<autopkgtest>. If
+I<build-dir> is omitted, the top source directory is used.
+
+If I<package-pattern> is given, options are passed to B<pg_virtualenv> to set
+up the temporary PostgreSQL instance to find extension files in
+C<debian/package-directory/>.
+
+Other than the other actions which run on the "supported" versions, if C<debian/control.in> exists, this one
+runs on the "installed" versions as reported by B<installed-versions> (unless
+I<package-pattern> is provided, which means we are called during a build).
+
+=item B<psql> [I<src-dir>] [I<build-dir>] [I<package-pattern>]
+
+=item B<virtualenv> [I<src-dir>] [I<build-dir>] [I<package-pattern>]
+
+Like B<installcheck>, but invokes B<psql>, or a shell, both wrapped in
+B<pg_virtualenv>. Input is read from stdin.
+
+=item B<run> [I<src-dir>] I<command>
+
+=item B<run_installed> [I<src-dir>] I<command>
+
+Runs I<command>, with B<%v> placeholder replaced on all supported versions, or
+all installed versions, respectively.
+
+=back
+
+Sometimes it is desirable to run extra code per version before invoking the
+action, in that case the loop over supported versions needs to be in the
+calling script. To facilitate this mode, actions can also be called as
+I<action>B<->I<version>. See the installcheck example below.
+
+=head1 SUPPORTED VERSIONS
+
+B<pg_buildext> reads C<debian/pgversions> to decide which PostgreSQL to build
+modules/extensions for. This file contains one PostgreSQL version number per
+line, in the following formats:
+
+=over 4
+
+=item B<all>
+
+Support all versions. This is recommended unless there are known incompatibilities.
+
+=item I<NN>
+
+Support this version.
+
+=item I<NN>B<+>
+
+Support this and all greater versions.
+
+=item B<#>I<...>
+
+Comment.
+
+=back
+
+For a version to be used, it must also be listed in the output of
+C</usr/share/postgresql-common/supported-versions>. See this file for how to
+configure the list of supported versions on your system.
+
+=head1 EXAMPLE
+
+=over 4
+
+=item B<debian/control.in:>
+
+ Source: postgresql-foobar
+ Rules-Requires-Root: no
+ Build-Depends:
+ debhelper,
+ postgresql-all <!nocheck>,
+ postgresql-server-dev-all (>= 217~),
+
+ Package: postgresql-PGVERSION-foobar
+ Architecture: any
+ Depends:
+ ${misc:Depends},
+ ${postgresql:Depends},
+ ${shlibs:Depends},
+
+=item B<debian/pgversions:>
+
+ all
+
+ # alternatives:
+ #9.6
+ #11+
+
+=item B<debian/rules> using B<dh $@ --with pgxs>:
+
+ #!/usr/bin/make -f
+
+ override_dh_installdocs:
+ dh_installdocs --all README.*
+
+ %:
+ dh $@ --with pgxs
+
+=item If the package does no support building from subdirectories, use B<dh $@ --with pgxs_loop>:
+
+ #!/usr/bin/make -f
+
+ %:
+ dh $@ --with pgxs_loop
+
+=item If the package does not use PGXS's "make installcheck" for testing:
+
+ override_dh_pgxs_test:
+
+=item B<debian/rules> using B<pg_buildext> directly:
+
+ #!/usr/bin/make -f
+
+ include /usr/share/postgresql-common/pgxs_debian_control.mk
+
+ # omit this if the package does not use autoconf
+ override_dh_auto_configure:
+ +pg_buildext configure build-%v "--libdir=/usr/lib/postgresql/%v/lib --datadir=/usr/share/postgresql-%v-foobar"
+
+ override_dh_auto_build:
+ +pg_buildext build build-%v
+
+ override_dh_auto_test:
+ # nothing to do here, see debian/tests/* instead
+
+ override_dh_auto_install:
+ +pg_buildext install build-%v postgresql-%v-foobar
+
+ override_dh_installdocs:
+ dh_installdocs --all README.*
+
+ override_dh_auto_clean:
+ +pg_buildext clean build-%v
+
+ %:
+ dh $@
+
+=item B<debian/tests/control:>
+
+ Depends: @, postgresql-server-dev-all
+ Tests: installcheck
+ Restrictions: allow-stderr
+
+=item B<debian/tests/control.in:> (optional)
+
+ Depends: @, postgresql-contrib-PGVERSION, postgresql-PGVERSION-bar
+ Tests: installcheck
+ Restrictions: allow-stderr
+
+=item B<debian/tests/installcheck:>
+
+ #!/bin/sh
+ pg_buildext installcheck
+ # alternatively: pg_buildext installcheck build-%v
+
+ # Running extra code before invoking the actual action:
+ set -e
+ for v in $(pg_buildext installed-versions); do
+ test -L build-$v/sql || ln -s ../sql build-$v/
+ test -L build-$v/expected || ln -s ../expected build-$v/
+ pg_buildext installcheck-$v build-$v
+ done
+
+=back
+
+=head1 SOURCE DIRECTORY
+
+If the package source code is not in the top level directory (i.e. the directory
+which has C<debian/> as subdirectory), use the I<src-dir> argument, where
+I<src-dir> must be an absolute path. Example:
+
+ override_dh_auto_build:
+ +pg_buildext build $(CURDIR)/postgresql-module build-%v
+
+=head1 COMPATIBILITY
+
+B<pg_buildext loop> was introduced in postgresql-server-dev-all (>= 141~).
+
+The usage of "all" or "NN+" in debian/pgversions was introduced in
+postgresql-server-dev-all (>= 148~).
+
+B<pg_buildext installcheck> was introduced in postgresql-server-dev-all (>=
+153~).
+
+B<PG_VIRTUALENV_UNSHARE=-n> was introduced in postgresql-common (>= 170~).
+
+Handling of C<debian/tests/control.in> with B<PGVERSION> replacement was
+introduced in postgresql-common (>= 171~).
+
+The action B<installed-versions> was introduced in postgresql-common (>= 208~).
+B<installcheck> was switched to use it in the same version.
+
+B<dh $@ --with pgxs> and B<pgxs_loop>, the corresponding B<--buildsystem>, and
+the B<psql> and B<virtualenv> actions were introduced in postgresql-server-dev-all (>= 217~).
+
+The replacement of B<PGVERSIONS> (plural) in debian/control.in and
+B<pg_buildext run> and B<run_installed> were introduced in
+postgresql-common (>= 256~).
+
+
+=head1 SEE ALSO
+
+C</usr/share/postgresql-common/supported-versions>, autopkgtest(1),
+pg_virtualenv(1).
+
+=head1 AUTHORS
+
+Dimitri Fontaine L<E<lt>dim@tapoueh.orgE<gt>>, with extensions by
+Christoph Berg L<E<lt>myon@debian.orgE<gt>>.
diff --git a/pg_checksystem b/pg_checksystem
new file mode 100755
index 0000000..bf61fef
--- /dev/null
+++ b/pg_checksystem
@@ -0,0 +1,59 @@
+#!/usr/bin/perl -wT
+
+# Check various system parameters for PostgreSQL. This needs to be run as root.
+#
+# (C) 2005-2009 Martin Pitt <mpitt@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 PgCommon;
+
+# Check write cache setting for given drive.
+# Arguments: <device>
+# Returns: 0 = disabled, 1 = enabled, -1: could not determine
+sub get_device_write_cache {
+ open DRV, $_[0] or return -1;
+ sub HDIO_GET_WCACHE () {0x30e;}
+ my $pval = pack 'l', 0;
+ ioctl DRV, &HDIO_GET_WCACHE, $pval or return -1;
+ my ($val) = unpack 'l', $pval;
+ close DRV;
+ return $val;
+}
+
+
+error 'This command needs to be executed as root' if $> != 0;
+
+# collect write cache-enabled drives with databases
+my %drive_wcache;
+my %seen;
+for my $v (get_versions) {
+ for my $c (get_version_clusters $v) {
+ my $datadir = PgCommon::cluster_data_directory $v, $c;
+ my $drv = PgCommon::get_file_device $datadir;
+ $drv =~ s/[0-9]+$//;
+ unless (exists $seen{$drv}) {
+ $drive_wcache{$drv} = 1 if (get_device_write_cache $drv) > 0;
+ $seen{$drv} = 1;
+ }
+ }
+}
+my @unsave_drives = keys %drive_wcache;
+
+if (@unsave_drives) {
+ print 'Warning: The following devices contain databases and have write
+caching enabled: ', (join ' ', @unsave_drives), '
+This could destroy the integrity of your databases in the event of power
+failure. Consider disabling the write cache with "hdparm -W 0 <device>".
+';
+}
diff --git a/pg_config b/pg_config
new file mode 100755
index 0000000..efdcf6d
--- /dev/null
+++ b/pg_config
@@ -0,0 +1,33 @@
+#!/bin/sh
+
+# If postgresql-server-dev-* is installed, call pg_config from the latest
+# available one. Otherwise fall back to libpq-dev's version.
+#
+# (C) 2011 Martin Pitt <mpitt@debian.org>
+# (C) 2014-2018 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.
+
+set -e
+PGBINROOT="/usr/lib/postgresql/"
+#redhat# PGBINROOT="/usr/pgsql-"
+LATEST_SERVER_DEV=`ls -v $PGBINROOT*/bin/pg_config 2>/dev/null|tail -n1`
+
+if [ -n "$LATEST_SERVER_DEV" ]; then
+ exec "$LATEST_SERVER_DEV" "$@"
+else
+ if [ -x /usr/bin/pg_config.libpq-dev ]; then
+ exec /usr/bin/pg_config.libpq-dev "$@"
+ else
+ echo "You need to install postgresql-server-dev-NN for building a server-side extension or libpq-dev for building a client-side application." >&2
+ exit 1
+ fi
+fi
diff --git a/pg_conftool b/pg_conftool
new file mode 100755
index 0000000..6772b66
--- /dev/null
+++ b/pg_conftool
@@ -0,0 +1,233 @@
+#!/usr/bin/perl
+# Read and edit PostgreSQL config files
+# vim:sw=4:et:
+#
+# (C) 2014-2017 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 Getopt::Long;
+use PgCommon qw(quote_conf_value user_cluster_map read_cluster_conf_file
+ read_conf_file set_conffile_value set_conf_value disable_conffile_value
+ disable_conf_value cluster_exists error config_bool);
+
+## option parsing
+
+sub help ($;$)
+{
+ my ($exit, $error) = @_;
+ print STDERR "Error: $error\n" if ($error);
+ print "Usage:
+ $0 [options] [<version> <cluster name>] [<configfile>] <command>
+
+Options:
+ -b --boolean Format output as boolean
+ -s --short Print only value
+ -v --verbose Verbose output
+ --help This help
+
+Commands:
+ show <parameter>|all
+ set <parameter> <value>
+ remove <parameter>
+ edit
+";
+ exit $exit;
+}
+
+my $boolean = 0;
+my $short = 0;
+my $verbose = 0;
+
+Getopt::Long::Configure ("bundling");
+help(1) unless GetOptions (
+ 'help' => sub { help(0); },
+ 'b|boolean' => \$boolean,
+ 's|short' => \$short,
+ 'v|verbose' => \$verbose,
+);
+
+# find command in argument array
+my $cmdidx;
+for (my $i = 0; $i < @ARGV; $i++) {
+ if ($ARGV[$i] =~ /^(show|get|set|remove|delete|disable|edit)$/) {
+ $cmdidx = $i;
+ last;
+ }
+}
+help(1, 'No command given') unless (defined $cmdidx);
+
+my ($version, $cluster, $conffile);
+if ($cmdidx == 0) { # operate on postgresql.conf in default cluster
+ ($version, $cluster) = user_cluster_map();
+ error "No default cluster found" unless ($cluster);
+ $conffile = 'postgresql.conf';
+} elsif ($cmdidx == 1) { # operate on given file; default cluster must exist if file is relative
+ $conffile = $ARGV[0];
+ unless ($conffile =~ m!^/!) {
+ ($version, $cluster) = user_cluster_map();
+ error "No default cluster found" unless ($cluster);
+ }
+} elsif ($cmdidx == 2) { # version cluster, postgresql.conf
+ ($version, $cluster, $conffile) = ($ARGV[0], $ARGV[1], 'postgresql.conf');
+} elsif ($cmdidx == 3) { # version cluster conffile
+ ($version, $cluster, $conffile) = ($ARGV[0], $ARGV[1], $ARGV[2]);
+} else {
+ help(1, 'Too many arguments before command');
+}
+
+if ($cluster) { # the cluster needs to exist unless an absolute conffile was given
+ error "Cluster $version $cluster does not exist"
+ unless (cluster_exists $version, $cluster);
+}
+
+splice @ARGV, 0, $cmdidx; # remove everything before the command
+$ARGV[0] = 'show' if ($ARGV[0] eq 'get'); # accept alternative variants of some commands
+$ARGV[0] = 'remove' if ($ARGV[0] =~ /delete|disable/);
+
+my ($cmd, $key, $value);
+if ($ARGV[0] =~ /^(edit)$/) {
+ help(1, "$ARGV[0] takes no argument") unless (@ARGV == 1);
+ ($cmd) = @ARGV;
+} elsif ($ARGV[0] =~ /^(show|remove)$/) {
+ help(1, "$ARGV[0] needs exactly one argument") unless (@ARGV == 2);
+ ($cmd, $key) = @ARGV;
+} else {
+ help(1, "$ARGV[0] needs exactly two arguments") unless (@ARGV == 3);
+ ($cmd, $key, $value) = @ARGV;
+}
+#print "$version $cluster $conffile $cmd $key $value\n";
+
+## main
+
+if ($cmd eq 'show') {
+ my %conf;
+ if ($conffile =~ m!^/!) {
+ %conf = read_conf_file ($conffile);
+ } else {
+ %conf = read_cluster_conf_file ($version, $cluster, $conffile);
+ }
+
+ if ($key eq 'all') {
+ foreach my $k (sort keys %conf) {
+ printf "%s = %s\n", $k, quote_conf_value($conf{$k});
+ }
+ } elsif (exists $conf{$key}) {
+ $conf{$key} = config_bool($conf{$key}) ? 'on' : 'off' if ($boolean); # normalize boolean output on request
+ if ($short) {
+ printf "%s\n", $conf{$key};
+ } else {
+ printf "%s = %s\n", $key, quote_conf_value($conf{$key});
+ }
+ } else {
+ print "# $key not found in $conffile\n" if ($verbose);
+ exit 1;
+ }
+
+} elsif ($cmd eq 'set') {
+ if ($conffile =~ m!^/!) {
+ set_conffile_value ($conffile, $key, $value);
+ } else {
+ set_conf_value ($version, $cluster, $conffile, $key, $value);
+ }
+
+} elsif ($cmd eq 'remove') {
+ if ($conffile =~ m!^/!) {
+ disable_conffile_value ($conffile, $key);
+ } else {
+ disable_conf_value ($version, $cluster, $conffile, $key);
+ }
+
+} elsif ($cmd eq 'edit') {
+ my $editor = 'vi';
+ if ($ENV{EDITOR}) {
+ ($editor) = $ENV{EDITOR} =~ /(.*)/; # untaint
+ }
+ if ($conffile =~ m!^/!) {
+ system $editor, $conffile;
+ } else {
+ system $editor, "$PgCommon::confroot/$version/$cluster/$conffile";
+ }
+}
+
+__END__
+
+=head1 NAME
+
+pg_conftool - read and edit PostgreSQL cluster configuration files
+
+=head1 SYNOPSIS
+
+B<pg_conftool> [I<options>] [I<version> I<cluster>] [I<configfile>] B<command>
+
+=head1 DESCRIPTION
+
+B<pg_conftool> allows showing and setting parameters in PostgreSQL configuration
+files.
+
+If I<version> I<cluster> is omitted, it defaults to the default cluster (see
+user_clusters(5) and postgresqlrc(5)). If I<configfile> is omitted, it defaults
+to B<postgresql.conf>. I<configfile> can also be a path, in which case
+I<version> I<cluster> is ignored.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<-b>, B<--boolean>
+
+Format boolean value as B<on> or B<off> (not for "show all").
+
+=item B<-s>, B<--short>
+
+Show only the value (instead of key = value pair).
+
+=item B<-v>, B<--verbose>
+
+Verbose output.
+
+=item B<--help>
+
+Print help.
+
+=back
+
+=head1 COMMANDS
+
+=over 4
+
+=item B<show> I<parameter>|B<all>
+
+Show a parameter, or all present in this config file.
+
+=item B<set> I<parameter> I<value>
+
+Set or update a parameter.
+
+=item B<remove> I<parameter>
+
+Remove (comment out) a parameter from a config file.
+
+=item B<edit>
+
+Open the config file in an editor. Unless B<$EDITOR> is set, B<vi> is used.
+
+=back
+
+=head1 SEE ALSO
+
+user_clusters(5), postgresqlrc(5)
+
+=head1 AUTHOR
+
+Christoph Berg L<E<lt>myon@debian.orgE<gt>>
diff --git a/pg_createcluster b/pg_createcluster
new file mode 100755
index 0000000..66f0b82
--- /dev/null
+++ b/pg_createcluster
@@ -0,0 +1,952 @@
+#!/usr/bin/perl -w
+
+# Create new PostgreSQL cluster or integrate an existing data directory into
+# the postgresql-common infrastructure.
+#
+# (C) 2005-2013 Martin Pitt <mpitt@debian.org>
+# (C) 2012-2021 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 Getopt::Long;
+use POSIX qw/lchown setlocale LC_ALL LC_CTYPE/;
+
+$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin'; # untaint
+
+my @postgres_user = getpwnam 'postgres';
+my %defaultconf;
+my $explicit_auth_config = 0;
+my $quiet;
+
+chdir '/';
+
+# call initdb
+sub init_db {
+ my ($version, $cluster, $datadir, $owneruid, $ownergid, $local_method, $host_method, $initdb_opts_from_cli) = @_;
+ $datadir = readlink $datadir if (-l $datadir);
+
+ if (system 'install', '-d', '-o', $owneruid, '-g', $ownergid, $datadir) {
+ error 'could not create data directory; you might need to run this program with root privileges';
+ }
+
+ # disable copy-on-write semantics for PostgreSQL data on btrfs and similar;
+ # this fails on file systems which don't support it, so ignore errors
+ system "chattr +C '$datadir' 2>/dev/null";
+
+ my @initdb = (get_program_path 'initdb', $version);
+ die 'Internal error: could not determine initdb path' unless $initdb[0];
+ push @initdb, ('-D', $datadir);
+ if (my $waldir = $defaultconf{'waldir'} || $defaultconf{'xlogdir'}) {
+ my $wal = $version >= 10 ? 'wal' : 'xlog'; # renamed in PG 10
+ push @initdb, ("--${wal}dir", replace_v_c ($waldir, $version, $cluster));
+ }
+ unless ($explicit_auth_config) {
+ if ($version >= '9.2') {
+ push @initdb, ('--auth-local', $local_method);
+ push @initdb, ('--auth-host', $host_method);
+ } else {
+ # trust is the default, but set it explicitly to mute a warning from initdb
+ # the actual method will be filled in by setup_pg_hba()
+ push @initdb, ('-A', 'trust');
+ }
+ }
+
+ # cluster startup message
+ if ($version >= 14) {
+ push @initdb, '--no-instructions';
+ } else {
+ # ask initdb to print a different cluster start command (Debian patch)
+ $ENV{CLUSTER_START_COMMAND} = "pg_ctlcluster $version $cluster start";
+ }
+
+ # options from config and command line
+ if (my $options = $defaultconf{'initdb_options'}) {
+ push @initdb, split (/\s+/, replace_v_c ($options, $version, $cluster));
+ }
+ push @initdb, @$initdb_opts_from_cli;
+
+ if (fork) {
+ wait;
+ error 'initdb failed' if $?;
+ } else{
+ change_ugid $owneruid, $ownergid;
+ print "@initdb\n" unless ($quiet);
+ close STDOUT if ($quiet); # suppress initdb output
+ exec @initdb;
+ }
+}
+
+# move a file to a directory with defined permissions; set <postgresql.conf option> in
+# postgresql.conf.
+# Arguments: <source file> <target dir> <uid> <gid> <perms> <postgresql.conf option>
+sub move_conffile {
+ my ($file, $target, $version, $cluster, $uid, $gid, $perms, $confopt) = @_;
+ my $realfile = $file;
+ while (-l $realfile) {
+ $realfile = readlink $realfile;
+ }
+ if (-e $file) {
+ install_file $realfile, $target, $uid, $gid, $perms;
+ unlink $file, $realfile;
+
+ my @pathcomps = split ('/', $file);
+ $target .= '/' . $pathcomps[-1];
+ PgCommon::set_conf_value $version, $cluster, 'postgresql.conf', $confopt, $target if $confopt;
+ } else {
+ error "move_conffile: required configuration file $realfile does not exist";
+ }
+}
+
+# Set up the default pg_hba.conf file:
+# - Add a "local all" entry to pg_hba.conf for the db superuser before all
+# other entries.
+# - Change default authentication for host entries to md5.
+sub setup_pg_hba {
+ my ($version, $owneruid, $confdir, $local_method, $host_method) = @_;
+ my $user = (getpwuid $owneruid)[0];
+ my $fname = "$confdir/pg_hba.conf";
+ my $su_comment = "
+# DO NOT DISABLE!
+# If you change this first entry you will need to make sure that the
+# database superuser can access the database using some other method.
+# Noninteractive access to all databases is required during automatic
+# maintenance (custom daily cronjobs, replication, and similar tasks).
+#
+# Database administrative login by Unix domain socket
+";
+
+ open my $hba, '<', $fname or error "could not open $fname for reading";
+ my $search = 1;
+ my @lines;
+ while (my $line = <$hba>) {
+ # add superuser entry before column description line
+ if ($search && $line =~ /#.*TYPE\s+DATABASE/) {
+ push @lines, $su_comment;
+ push @lines, sprintf "%-7s %-15s %-39s %s\n", 'local', 'all', $user, $local_method;
+ push @lines, "\n";
+ $search = 0;
+ }
+
+ if ($version < '9.2' and not $explicit_auth_config) {
+ # default authentication for Unix socket connections
+ if ($line =~ /^#?local/) {
+ $line =~ s/trust/$local_method/;
+ }
+
+ # default authentication for TCP connections
+ if ($line =~ /^#?host/) {
+ $line =~ s/trust/$host_method/;
+ }
+ }
+
+ push @lines, $line;
+ }
+ close $hba;
+
+ error "setup_pg_hba: did not find insert position" if $search;
+
+ open my $new_hba, '>', $fname or error "could not open $fname for writing";
+ foreach (@lines) {
+ print $new_hba $_;
+ }
+ close $new_hba;
+}
+
+#
+# Execution starts here
+#
+
+# this flag gets set when we started creating the cluster, and thus we need to
+# clean up on errors
+my $cleanup_cruft = 0;
+
+# command line arguments
+
+my $startconf = '';
+my @pgoptions;
+my $createclusterconf = "$PgCommon::common_confdir/createcluster.conf";
+my $environmentfile = "$PgCommon::common_confdir/environment";
+my ($owneruid, $ownergid, $socketdir, $datadir, $custom_logfile, $start, $port);
+my ($encoding, $locale, $lc_collate, $lc_ctype, $lc_messages, $lc_monetary, $lc_numeric, $lc_time);
+my ($no_status);
+
+exit 1 unless GetOptions ('u|user=s' => \$owneruid, 'g|group=s' => \$ownergid,
+ 's|socketdir=s' => \$socketdir, 'd|datadir=s' => \$datadir,
+ 'start' => \$start, 'e|encoding=s' => \$encoding,
+ 'l|logfile=s' => \$custom_logfile, 'start-conf=s' => \$startconf,
+ 'o|pgoption=s' => sub { $_[1] =~ /(.*?)=(.*)/ or error ("No '=' in pgoption '$_[1]'");
+ push @pgoptions, [$1, $2];
+ },
+ 'createclusterconf=s' => \$createclusterconf,
+ 'environment=s' => \$environmentfile,
+ 'no-status' => \$no_status,
+ 'p|port=i' => \$port,
+ 'q|quiet' => \$quiet,
+ '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);
+
+# read defaults from /etc/postgresql-common/createcluster.conf
+%defaultconf = PgCommon::read_conf_file ($createclusterconf);
+# process --pgoption parameters
+foreach my $guc (@pgoptions) {
+ if ($guc->[1] eq '') {
+ delete $defaultconf{$guc->[0]};
+ } else {
+ $defaultconf{$guc->[0]} = $guc->[1];
+ }
+}
+
+$explicit_auth_config = 1 if ($defaultconf{initdb_options} and $defaultconf{initdb_options} =~ /(^| )(-\w*A|--auth\b)/);
+
+# check validity of locale
+unless (setlocale (LC_ALL, "")) {
+ my $env = join "\n",
+ map { " $_: $ENV{$_}" }
+ grep { /^(LC_|LANG)/ } sort keys %ENV;
+ error ("The locale requested by the environment is invalid:\n$env")
+}
+
+if (@ARGV < 2) {
+ print "Usage: $0 [options] <version> <cluster name> [-- <initdb options>]
+
+Options:
+ -u <uid> cluster owner and superuser (default: 'postgres')
+ -g <gid> group for data files (default: primary group of owner)
+ -d <dir> data directory (default:
+ /var/lib/postgresql/<version>/<cluster name>)
+ -s <dir> socket directory (default: /var/run/postgresql for clusters
+ owned by 'postgres', /tmp for other clusters)
+ -l <dir> path to desired log file (default:
+ /var/log/postgresql/postgresql-<version>-<cluster>.log)
+ --locale <encoding>
+ set cluster locale (default: inherit from environment)
+ --lc-collate/ctype/messages/monetary/numeric/time <locale>
+ like --locale, but only set for a particular category
+ -e <encoding> Default encoding (default: derived from locale)
+ -p <port> port number (default: next free port starting from 5432)
+ --start start the cluster after creating it
+ --start-conf auto|manual|disabled
+ Set automatic startup behaviour in start.conf (default: 'auto')
+ --createclusterconf=file alternative createcluster.conf to use
+ --environment=file alternative environment file to use
+ <initdb options> other options to pass to initdb
+";
+ exit 1;
+}
+
+$startconf ||= $defaultconf{'start_conf'} || 'auto';
+error "Invalid --start-conf value: $startconf" if $startconf ne 'auto' &&
+ $startconf ne 'manual' && $startconf ne 'disabled';
+
+if ($owneruid) {
+ $owneruid = (getpwnam $owneruid)[2] unless $owneruid =~ /^\d+$/;
+} elsif ($> == 0) {
+ $owneruid = getpwnam 'postgres';
+ error 'User postgres does not exist' unless $owneruid;
+} else
+{
+ $owneruid = $>;
+}
+
+if ($ownergid) {
+ $ownergid = (getgrnam $ownergid)[2] unless $ownergid =~ /^\d+$/;
+} else {
+ $ownergid = (getpwuid $owneruid)[3];
+}
+
+error 'clusters must not be owned by root' unless $owneruid;
+
+my ($version) = $ARGV[0] =~ /^(\d+\.?\d+)$/;
+error "invalid version '$ARGV[0]'" unless defined $version;
+my ($cluster) = $ARGV[1] =~ /^([-.\w]+)$/;
+error "invalid cluster name '$ARGV[1]'" unless defined $cluster;
+if ($cluster =~ /-/ and -t 1) {
+ print "Warning: cluster names containing dashes (-) will cause problems when running from systemd. Continuing anyway\n";
+}
+splice @ARGV, 0, 2;
+
+my @initdb_opts_from_cli;
+# options passed through to initdb
+push @initdb_opts_from_cli, ('--encoding', $encoding) if $encoding;
+push @initdb_opts_from_cli, ('--locale', $locale) if $locale;
+push @initdb_opts_from_cli, ('--lc-collate', $lc_collate) if $lc_collate;
+push @initdb_opts_from_cli, ('--lc-ctype', $lc_ctype) if $lc_ctype;
+push @initdb_opts_from_cli, ('--lc-messages', $lc_messages) if $lc_messages;
+push @initdb_opts_from_cli, ('--lc-monetary', $lc_monetary) if $lc_monetary;
+push @initdb_opts_from_cli, ('--lc-numeric', $lc_numeric) if $lc_numeric;
+push @initdb_opts_from_cli, ('--lc-time', $lc_time) if $lc_time;
+
+# initdb options passed after --
+foreach my $argv (@ARGV) {
+ # the user passed an authentication method, don't mess with initdb and pg_hba.conf
+ if ($argv =~ /^(-\w*A|--auth\b)/) { # -A --auth --auth-host --auth-local
+ $explicit_auth_config = 1;
+ }
+ push @initdb_opts_from_cli, $argv =~ /(.*)/; # untaint
+}
+
+# pg_hba.conf authentication settings
+my $local_method = $version >= 9.1 ? 'peer' :
+ ($version >= 8.4 ? 'ident' :
+ 'ident sameuser');
+my $host_method = $version >= 14 ? 'scram-sha-256' : 'md5';
+
+# create parent of data directory if missing
+my $datadirp_created;
+if (!defined $datadir) {
+ $datadir = replace_v_c ($defaultconf{'data_directory'} || "/var/lib/postgresql/%v/%c", $version, $cluster);
+ $datadir =~ s!/+$!!;
+ my $pd = $datadir;
+ $pd =~ s!/[^/]*$!!;
+
+ # ensure that the version data dir is owned by postgres as well, so that
+ # it can be administrated without root permissions
+ if (!stat $pd) {
+ my @install = qw(install -d);
+ push @install, '-o', $postgres_user[2], '-g', $postgres_user[3] if ($> == 0);
+ system @install, $pd;
+ $datadirp_created = $pd; # clean up in case of error
+ }
+}
+my $confdirp = "$PgCommon::confroot/$version";
+my $confdir = "$confdirp/$cluster";
+
+# some sanity checks
+error "no initdb program for version $version found" unless get_program_path 'initdb', $version;
+error 'cluster configuration already exists'
+ if -e "$confdir/postgresql.conf" || -e "$confdir/pg_hba.conf";
+
+if (defined $port) {
+ error 'port must be a positive integer between 1024 and 65535'
+ unless $port =~ /^\d+/ && $port >= 1024 && $port <= 65535;
+} else {
+ $port = next_free_port;
+}
+
+# create configuration directory
+if (!stat $confdirp) {
+ my @install = qw(install -d);
+ push @install, '-o', $postgres_user[2], '-g', $postgres_user[3] if ($> == 0);
+ system @install, $confdirp;
+}
+# now we created the first new directory for this cluster and start to rollback
+# on error
+$cleanup_cruft = 1;
+
+error 'could not create configuration directory; you might ' .
+ 'need to run this program with root privileges' if system ('install', '-d', $confdir);
+
+# check whether we have an already existing cluster; check version and
+# determine owner in this case
+my $newcluster = 0;
+
+if (-f "$datadir/PG_VERSION") {
+ open my $fh, '<', "$datadir/PG_VERSION" or error "could not open $datadir/PG_VERSION";
+ chomp(my $existingver = <$fh>);
+ close $fh;
+
+ ($owneruid, $ownergid) = (stat "$datadir/PG_VERSION")[4,5];
+ if ($existingver == $version) {
+ print "Configuring already existing cluster (configuration: $confdir, data: $datadir, owner: $owneruid:$ownergid)\n";
+ } else {
+ error "$datadir contains a version $existingver cluster, but $version was requested";
+ }
+} else {
+ print "Creating new PostgreSQL cluster $version/$cluster ...\n";
+ init_db $version, $cluster, $datadir, $owneruid, $ownergid,
+ $local_method, $host_method, \@initdb_opts_from_cli;
+ $newcluster = 1;
+}
+
+# create default "start" file
+set_cluster_start_conf $version, $cluster, $startconf;
+
+# create default pg_ctl.conf file
+set_cluster_pg_ctl_conf $version, $cluster, '';
+
+# move conffiles, setup permissions
+move_conffile "$datadir/postgresql.conf", $confdir, $version, $cluster, $owneruid, $ownergid, '644';
+move_conffile "$datadir/pg_hba.conf", $confdir, $version, $cluster, $owneruid, $ownergid, '640', 'hba_file';
+move_conffile "$datadir/pg_ident.conf", $confdir, $version, $cluster, $owneruid, $ownergid, '640', 'ident_file';
+foreach my $f ($datadir, $confdir, "$confdir/start.conf", "$confdir/pg_ctl.conf") {
+ lchown $owneruid, $ownergid, $f or error "lchown $f: $!";
+}
+
+PgCommon::set_conf_value $version, $cluster, 'postgresql.conf', 'data_directory', $datadir;
+
+# add access for database superuser
+setup_pg_hba $version, $owneruid, $confdir, $local_method, $host_method
+ if $newcluster and not $explicit_auth_config;
+
+# configure socket directory
+if (! $socketdir && ! -e '/var/run/postgresql') {
+ system 'install', '-d', '-o', $postgres_user[2], '-g', $postgres_user[3], '/var/run/postgresql';
+}
+if ($socketdir && ! -e $socketdir) {
+ if (system 'install', '-d', '-m', '0755', '-o', $owneruid, '-g', $ownergid, $socketdir) {
+ error "could not create socket directory $socketdir";
+ }
+}
+my $orig_euid = $>;
+my $orig_egid = $);
+$) = $ownergid;
+$> = $owneruid;
+unless ($socketdir) {
+ if ($version < 9.4 and $PgCommon::rpm) {
+ $socketdir = '/tmp'; # PGDG 9.3 and earlier defaults to /tmp in libpq et al.
+ } elsif (-w '/var/run/postgresql') {
+ $socketdir = '/var/run/postgresql';
+ } else {
+ $socketdir='/tmp';
+ }
+}
+set_cluster_socketdir $version, $cluster, $socketdir;
+$> = $orig_euid;
+$) = $orig_egid;
+
+set_cluster_port $version, $cluster, $port;
+
+# create log file
+if (! -d '/var/log/postgresql') {
+ mkdir '/var/log/postgresql' or
+ error "could not create log directory; you might need to run this program with root privileges";
+ chmod 01775, '/var/log/postgresql';
+ lchown 0, $postgres_user[3], '/var/log/postgresql';
+}
+my $real_logfile = $custom_logfile || "/var/log/postgresql/postgresql-$version-$cluster.log";
+error "logfile $real_logfile is a directory, not a file" if (-d $real_logfile);
+if (! -e $real_logfile) {
+ open my $fh, '>>', $real_logfile or error "could not create log file $real_logfile";
+}
+chmod 0640, $real_logfile;
+my $g;
+if ($owneruid < 500) {
+ $g = (getgrnam 'adm')[2];
+} else {
+ $g = $ownergid;
+}
+lchown $owneruid, $g, $real_logfile;
+# if we are using a non-default log file, create a log symlink
+if ($custom_logfile) {
+ symlink $real_logfile, "$confdir/log";
+ lchown $owneruid, $ownergid, "$confdir/log";
+}
+
+# SSL configuration
+my $want_ssl = PgCommon::config_bool($defaultconf{ssl} || 'on');
+# older versions (<= 9.1 as of 2019-03) do not support ssl anymore
+my $postgres = get_program_path('postgres', $version);
+my $ldd = `ldd $postgres 2>/dev/null`;
+$want_ssl = 0 if ($ldd and $ldd !~ /libssl/);
+
+# Check whether we can access the SSL private key as the cluster owner
+my $ssl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key';
+my $ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem';
+my $ssl_ca_file = "$PgCommon::common_confdir/root.crt";
+my $ssl_key_access;
+my ($uid, $euid, $gid, $egid) = ($<, $>, $(, $));
+change_ugid $owneruid, $ownergid;
+$ssl_key_access = -r $ssl_key_file;
+
+# check for stats_temp_directory access
+delete $defaultconf{stats_temp_directory} if ($version < 8.4 or $version >= 15);
+if ($defaultconf{stats_temp_directory}) {
+ my $stats_temp_directory = replace_v_c ($defaultconf{stats_temp_directory}, $version, $cluster);
+ $stats_temp_directory =~ s!/$!!; # strip trailing slash
+ my $stats_temp_parent = $stats_temp_directory;
+ $stats_temp_parent =~ s!/[^/]+$!!;
+ if (-d $stats_temp_directory) {
+ if (! -w $stats_temp_directory) {
+ print "Warning: The selected stats_temp_directory $stats_temp_directory
+is not writable for the cluster owner. Not adding this setting in
+postgresql.conf.\n";
+ delete $defaultconf{stats_temp_directory};
+ }
+ } elsif (! -d $stats_temp_parent) {
+ print "Warning: The parent $stats_temp_parent of the selected
+stats_temp_directory does not exist. Not adding this setting in
+postgresql.conf.\n";
+ delete $defaultconf{stats_temp_directory};
+ } elsif (! -w $stats_temp_parent) {
+ print "Warning: The parent $stats_temp_parent of the selected
+stats_temp_directory is not writable for the cluster owner. Not adding this
+setting in postgresql.conf.\n";
+ delete $defaultconf{stats_temp_directory};
+ }
+ # create the stats directory now. pg_ctlcluster would create it anyway, but
+ # when using pg_upgradecluster -m upgrade, it is not run before the cluster
+ # is started for the first time
+ if ($defaultconf{stats_temp_directory}) {
+ system 'install', '-d', '-m', '750', '-o', $owneruid, '-g', $ownergid, $stats_temp_directory;
+ }
+}
+
+$> = $euid;
+$< = $uid;
+$( = $gid;
+$) = $egid;
+die "changing euid back: $!" if $> != $euid;
+die "changing egid back: $!" if $) != $egid;
+
+# enable SSL if we have the snakeoil default certificate
+if ($want_ssl && $newcluster && -e $ssl_cert_file && $ssl_key_access) {
+ if ($version >= '9.2') {
+ PgCommon::set_conf_value $version, $cluster, 'postgresql.conf',
+ 'ssl_cert_file', $ssl_cert_file;
+ PgCommon::set_conf_value $version, $cluster, 'postgresql.conf',
+ 'ssl_key_file', $ssl_key_file;
+ } else {
+ symlink $ssl_cert_file, "$datadir/server.crt";
+ symlink $ssl_key_file, "$datadir/server.key";
+ }
+
+ PgCommon::set_conf_value $version, $cluster, 'postgresql.conf', 'ssl', 'on';
+}
+
+# SSL client certificate CA
+if ($want_ssl && $newcluster && -e $ssl_ca_file) {
+ # check if we have a cert in there or just the boilerplate installed by our postinst
+ open my $fh, '<', $ssl_ca_file or error "could not open $ssl_ca_file for reading";
+ my $val;
+ read $fh, $val, 4096;
+ if ($val =~ /^-----BEGIN CERTIFICATE-----/m) {
+ if ($version >= '9.2') {
+ PgCommon::set_conf_value $version, $cluster, 'postgresql.conf',
+ 'ssl_ca_file', $ssl_ca_file;
+ } else {
+ symlink $ssl_ca_file, $datadir.'/root.crt';
+ }
+ }
+}
+
+# SSL client certificate revocation list
+if ($want_ssl && $newcluster && -e "$PgCommon::common_confdir/root.crl") {
+ if ($version >= '9.2') {
+ PgCommon::set_conf_value $version, $cluster, 'postgresql.conf',
+ 'ssl_crl_file', "$PgCommon::common_confdir/root.crl";
+ } else {
+ symlink "$PgCommon::common_confdir/root.crl", $datadir.'/root.crl';
+ }
+}
+
+# create default (empty) environment file
+my $defaultenv = "# environment variables for postgres processes
+# This file has the same syntax as postgresql.conf:
+# VARIABLE = simple_value
+# VARIABLE2 = 'any value!'
+# I. e. you need to enclose any value which does not only consist of letters,
+# numbers, and '-', '_', '.' in single quotes. Shell commands are not
+# evaluated.
+";
+if (-e $environmentfile) {
+ open my $env, '<', $environmentfile or error "could not read environment file $environmentfile";
+ local $/; # slurp mode
+ $defaultenv = <$env>;
+}
+$defaultenv = replace_v_c ($defaultenv, $version, $cluster);
+open my $env, '>', "$confdir/environment" or error "could not create environment file $confdir/environment";
+print $env $defaultenv;
+close $env;
+chmod 0644, "$confdir/environment";
+lchown $owneruid, $ownergid, "$confdir/environment";
+
+$cleanup_cruft = 0;
+
+# configure to create external PID file
+if ($socketdir eq '/var/run/postgresql') {
+ PgCommon::set_conf_value $version, $cluster, 'postgresql.conf', 'external_pid_file', "/var/run/postgresql/$version-$cluster.pid";
+}
+
+# handle other createcluster.conf parameters, including --pgoption parameters
+foreach my $guc (sort keys %defaultconf) {
+ next if $guc =~ /^(create_main_cluster|start_conf|data_directory|waldir|xlogdir|initdb_options|ssl)$/;
+ next if $guc eq 'logging_collector' and $version < 8.3;
+ next if $guc eq 'cluster_name' and $version < 9.5;
+ my $val = replace_v_c ($defaultconf{$guc}, $version, $cluster);
+ $guc =~ s/^add_include/include/; # remove harness from include directives in createcluster.conf
+ if ($guc eq 'include_dir') {
+ next if ($version < 9.3);
+ if ($val =~ /^[\w.]+$/ and not -e "$confdir/$val") { # create directory relative to new config directory
+ mkdir "$confdir/$val", 0755;
+ lchown $owneruid, $ownergid, "$confdir/$val";
+ }
+ }
+ PgCommon::set_conf_value $version, $cluster, 'postgresql.conf', $guc, $val;
+}
+
+# notify systemd about the new cluster
+if (not exists $ENV{'PG_CLUSTER_CONF_ROOT'} and $startconf eq 'auto' and -d '/run/systemd/system') {
+ if ($> == 0) {
+ system 'systemctl daemon-reload';
+ } elsif (-t 1) {
+ print "Warning: systemd does not know about the new cluster yet. Operations like \"service postgresql start\" will not handle it. To fix, run:\n";
+ print " sudo systemctl daemon-reload\n";
+ }
+}
+
+# notify apt about the new cluster
+if (not exists $ENV{'PG_CLUSTER_CONF_ROOT'} and $> == 0) {
+ system "/usr/share/postgresql-common/pg_updateaptconfig";
+}
+
+# start it if requested
+if ($start) {
+ system 'pg_ctlcluster', $version, $cluster, 'start';
+ die "Could not start cluster\n" if ($?);
+}
+
+# finally, show the cluster we created
+system 'pg_lsclusters', $version, $cluster unless ($quiet or $no_status);
+
+END {
+ # clean up cruft if something went wrong
+ if ($cleanup_cruft && defined $version && defined $cluster) {
+ system "pg_dropcluster $version $cluster 2>/dev/null";
+ rmdir $datadirp_created if ($datadirp_created); # clean up after early errors which pg_dropcluster doesn't handle
+ exit 1;
+ }
+}
+
+__END__
+
+=head1 NAME
+
+pg_createcluster - create a new PostgreSQL cluster
+
+=head1 SYNOPSIS
+
+B<pg_createcluster> [I<options>] I<version> I<name> [B<--> I<initdb options>]
+
+=head1 DESCRIPTION
+
+B<pg_createcluster> creates a new PostgreSQL server cluster (i. e. a
+collection of databases served by a L<postgres(1)> instance) and
+integrates it into the multi-version/multi-cluster architecture of the
+B<postgresql-common> package.
+
+Every cluster is uniquely identified by its version and name. The name can be
+arbitrary. The default cluster that is created on installation of a server
+package is C<main>. However, you might wish to create other clusters for
+testing, with other superusers, a cluster for each user on a shared server,
+etc. C<pg_createcluster> will abort with an error if you try to create a
+cluster with a name that already exists for that version.
+
+For compatibility with B<systemd> service units, the cluster name should not
+contain any dashes (B<->). B<pg_ctlcluster> will warn about the problem, but
+succeed with the operation.
+
+Given a major PostgreSQL I<version> (like "8.2" or "8.3") and a cluster
+I<name>, it creates the necessary configuration files in
+C</etc/postgresql/>I<version>C</>I<name>C</>; in particular these are
+C<postgresql.conf>, C<pg_ident.conf>, C<pg_hba.conf>, a postgresql-common
+specific configuration file C<start.conf> (see B<STARTUP CONTROL> below),
+C<pg_ctl.conf>, and a symbolic link C<log> which points to the log file (by
+default, C</var/log/postgresql/postgresql->I<version>C<->I<name>C<.log>).
+
+C<postgresql.conf> is automatically adapted to use the next available port, i.
+e. the first port (starting from 5432) which is not yet used by an already
+existing cluster.
+
+If the data directory does not yet exist, PostgreSQL's L<initdb(1)> command is
+used to generate a new cluster structure. If the data directory already exists,
+it is integrated into the B<postgresql-common> structure by moving the
+configuration file and setting the data_directory option. Please note that this
+I<only> works for data directories which were created directly with B<initdb>, i.
+e. all the configuration files (C<postgresql.conf> etc.) must be present in the
+data directory.
+
+If a custom socket directory is given and it does not exist, it is created.
+
+If the log file does not exist, it is created. In any case the permissions are
+adjusted to allow write access to the cluster owner. Please note that
+C<postgresql.conf> can be customized to specify C<log_directory> and/or
+C<log_filename>; if at least one of these options is present, then the symbolic
+link C<log> in the cluster configuration directory is ignored.
+
+If the default snakeoil SSL certificate exists
+(C</etc/ssl/certs/ssl-cert-snakeoil.pem> and
+C</etc/ssl/private/ssl-cert-snakeoil.key>), and the C<postgres> user is in the
+C<ssl-cert> Unix group, B<pg_createcluster> configures the cluster to use this
+certificate, and enables SSL. Therefore all clusters will use the same SSL
+certificate by default. For versions up to 9.1, symlinks in the data directory
+will be created (C<server.crt> and C<server.key>); for 9.2 and later, the
+appropriate C<postgresql.conf> options will be set (C<ssl_cert_file> and
+C<ssl_key_file>). Of course you can replace this with a cluster specific
+certificate. Similarly for C</etc/postgresql-common/root.crt> and
+C</etc/postgresql-common/root.crl>, these files will be configured as client
+certificate CA and revocation list, when present. (C<root.crt> is initially a
+placeholder that will only be used if real certificates are added to the file.)
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<-u> I<user>, B<--user=>I<user>
+
+Set the user who owns the cluster and becomes the database superuser to the
+given name or uid. By default, this is the user B<postgres>. A cluster must
+not be owned by root.
+
+=item B<-g> I<group>, B<--group=>I<group>
+
+Change the group of the cluster related data files. By default this will be the
+primary group of the database owner.
+
+=item B<-d> I<dir>, B<--datadir=>I<dir>
+
+Explicitly set the data directory path, which is used to store all the actual
+databases and tables. This will become quite big (easily in the order of five
+times the amount of actual data stored in the cluster). Defaults to
+C</var/lib/postgresql/>I<version>C</>I<cluster>.
+
+=item B<-s> I<dir>, B<--socketdir=>I<dir>
+
+Explicitly set the directory where the L<postgres(1)> server stores the Unix
+socket for local connections. Defaults to C</var/run/postgresql/> for clusters
+owned by the user B<postgres>, and C</tmp> for clusters owned by other users.
+Please be aware that C</tmp> is an unsafe directory since everybody can create
+a socket there and impersonate the database server. If the given directory does
+not exist, it is created with appropriate permissions.
+
+=item B<-l> I<path>, B<--logfile=>I<path>
+
+Explicitly set the path for the L<postgres(1)> server log file. Defaults to
+C</var/log/postgresql/postgresql->I<version>C<->I<cluster>C<.log>.
+
+=item B<--locale=>I<locale>
+
+Set the default locale for the database cluster. If this option is not
+specified, the locale is inherited from the environment that
+B<pg_createcluster> runs in.
+
+=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<-e> I<encoding>, B<--encoding=>I<encoding>
+
+Select the encoding of the template database. This will also be the default
+encoding of any database you create later, unless you override it there. The
+default is derived from the locale, or SQL_ASCII if that does not work. The
+character sets supported by the PostgreSQL server are described in the
+documentation.
+
+B<Note>: It is not recommended to set this option directly! Set the locale
+instead.
+
+=item B<-p> I<port>, B<--port=>I<port>
+
+Select the port the new cluster listens on (for the Unix socket and the TCP
+port); this must be a number between 1024 and 65535, since PostgreSQL does not
+run as root and thus needs an unprivileged port number. By default the next
+free port starting from 5432 is assigned.
+
+=item B<-q> B<--quiet> B<--no-status>
+
+Suppress output from B<initdb> and (or only) the cluster status message at the
+end of the output.
+
+=item B<--start>
+
+Immediately start a server for the cluster after creating it (i. e. call
+C<pg_ctlcluster> I<version cluster> C<start> on it). By default, the cluster is
+not started.
+
+=item B<--start-conf=>B<auto>|B<manual>|B<disabled>
+
+Set the initial value in the C<start.conf> configuration file. See B<STARTUP
+CONTROL> below. By default, B<auto> is used, which means that the cluster is
+handled by C</etc/init.d/postgresql>, i. e. starts and stops
+automatically on system boot.
+
+=item B<-o> I<guc>B<=>I<value>, B<--pgoption> I<guc>B<=>I<value>
+
+Configuration option to set in the new C<postgresql.conf> file.
+
+=item B<--createclusterconf=>I<file>
+
+Alternative B<createcluster.conf> file to use. Default is
+C</etc/postgresql-common/createcluster.conf> (or
+C<$PGSYSCONFDIR/createcluster.conf>).
+
+=item B<--environment=>I<file>
+
+Alternative default B<environment> file to use. Default is
+C</etc/postgresql-common/environment> (or C<$PGSYSCONFDIR/environment>).
+If the file is missing, a placeholder string is used.
+%v and %c are replaced; see DEFAULT VALUES below.
+
+=item B<--> I<initdb options>
+
+Options passed directly to L<initdb(1)>.
+
+Per default, B<pg_createcluster> will update the C<pg_hba.conf> file generated
+by initdb to use peer authentication on local (unix) connections, and md5 on
+TCP (host) connections. If explicit authentication config is included here
+(B<-A>, B<--auth>, B<--auth-host>, B<--auth-local>), the C<pg_hba.conf> file
+will be left untouched.
+
+I<Note:> If only one of B<--auth-host> and B<--auth-local> is provided, the
+other setting will default to B<trust> as per B<initdb>'s defaults, opening a
+potential security risk.
+
+=back
+
+=head1 STARTUP CONTROL
+
+The C<start.conf> file in the cluster configuration directory controls the
+start/stop behavior of that cluster's postgres process. The file can contain
+comment lines (started with '#'), empty lines, and must have exactly one
+line with one of the following keywords:
+
+=over 4
+
+=item B<auto>
+
+The postgres process is started/stopped automatically in the init script.
+
+When running from B<systemd>, the cluster is started/stopped when
+B<postgresql.service> is started/stopped.
+This is also the default if the file is missing.
+
+=item B<manual>
+
+The postgres process is not handled by the init script, but manually
+controlling the cluster with L<pg_ctlcluster(1)> is permitted.
+
+When running from B<systemd>, the cluster is not started automatically when
+B<postgresql.service> is started. However, stopping/restarting
+B<postgresql.service> will stop/restart the cluster. The cluster can be started
+using B<systemctl start postgresql@>I<version>B<->I<cluster>.
+
+=item B<disabled>
+
+Neither the init script, L<pg_ctlcluster(1)>, nor B<postgresql@.service> are permitted to start/stop the
+cluster. Please be aware that this will not stop the cluster owner from calling
+lower level tools to control the postgres process; this option is only meant
+to prevent accidents during maintenance, not more.
+
+=back
+
+When running from B<systemd>, invoke B<systemctl daemon-reload> after editing
+C<start.conf>.
+
+The C<pg_ctl.conf> file in the cluster configuration directory can contain
+additional options passed to B<pg_ctl> of that cluster.
+
+=head1 DEFAULT VALUES
+
+Some default values used by B<pg_createcluster> can be modified in
+C</etc/postgresql-common/createcluster.conf>. Occurrences of B<%v> are replaced
+by the major version number, and B<%c> by the cluster name. Use B<%%> for a
+literal B<%>.
+
+=over 4
+
+=item B<create_main_cluster> (Default: B<true>)
+
+Create a B<main> cluster when a new postgresql-NN server package is installed.
+
+=item B<start_conf> (Default: B<auto>)
+
+Default C<start.conf> value to use.
+
+=back
+
+=over 4
+
+=item B<data_directory> (Default: B</var/lib/postgresql/%v/%c>)
+
+Default data directory.
+
+=item B<waldir|xlogdir> (Default: unset)
+
+Default directory for transaction logs. When used, B<initdb> will create a
+symlink from C<pg_wal> (PostgreSQL 9.6 and earlier: C<pg_xlog>) in the data
+directory to this location. Unset by default, i.e. transaction logs remain in
+the data directory. Both spellings of this option are accepted, and translated
+to the correct initdb invocation depending on the cluster version.
+
+=item B<initdb_options> (Default: unset)
+
+Other options to pass to B<initdb>.
+
+=item Other options
+
+All other options listed are copied into the new cluster's postgresql.conf, e.g.:
+
+ listen_addresses = '*'
+ log_line_prefix = '%%t '
+
+Some postgresql.conf options are treated specially:
+
+=over 4
+
+=item B<ssl>
+
+Only added to postgresql.conf if the default snakeoil certificates exist and
+are readable for the cluster owner as detailed above.
+
+=item B<stats_temp_directory>
+
+Only added to postgresql.conf if existing, and writable for the cluster owner,
+or else if the parent directory is writable. Not used on PostgreSQL 15 or later.
+
+=back
+
+=item Include files
+
+=over 4
+
+=item B<include>
+
+=item B<include_if_exists>
+
+=item B<include_dir>
+
+B<createcluster.conf> supports the same include directives as
+B<postgresql.conf>.
+
+=item B<add_include>
+
+=item B<add_include_if_exists>
+
+=item B<add_include_dir>
+
+To add include directives to the new postgresql.conf file, use the B<add_*>
+directives. The B<add_> prefix is removed.
+
+=back
+
+=back
+
+=head1 SEE ALSO
+
+L<initdb(1)>, L<pg_ctlcluster(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>>
diff --git a/pg_ctlcluster b/pg_ctlcluster
new file mode 100755
index 0000000..1684700
--- /dev/null
+++ b/pg_ctlcluster
@@ -0,0 +1,649 @@
+#!/usr/bin/perl -wT
+
+# multiversion/cluster aware pg_ctl wrapper; this also supplies the correct
+# configuration parameters to 'start', and makes sure that postgres really
+# stops on 'stop'.
+#
+# (C) 2005-2009 Martin Pitt <mpitt@debian.org>
+# (C) 2009 Cyril Bouthors <cyril@bouthors.org>
+# (C) 2013-2021 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 Getopt::Long;
+use POSIX qw/setsid dup2 :sys_wait_h/;
+use PgCommon;
+use Fcntl qw(SEEK_SET O_RDWR O_CREAT O_EXCL);
+use POSIX qw(lchown);
+
+my ($version, $cluster, $pg_ctl, $force);
+my (@postgres_auxoptions, @pg_ctl_opts_from_cli);
+my (%postgresql_conf, %info);
+my $mode = 'fast'; # default shutdown mode
+my $foreground = 0; # don't daemonize, use postgres instead of pg_ctl
+my $stdlog = 0; # when run in foreground, still log to the default logfile
+
+# untaint environment
+$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
+delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
+chdir '/';
+
+# If a pid file is already present, delete it if it is stale/invalid, or exit
+# with a notice if it belongs to an already running postgres.
+sub start_check_pid_file {
+ my $pidfile = $info{'pgdata'}.'/postmaster.pid';
+ if (PgCommon::check_pidfile_running $pidfile) {
+ print "Cluster is already running.\n";
+ exit 2;
+ }
+
+ # Remove invalid or stale PID file
+ if (-e $pidfile) {
+ unlink $pidfile;
+ print "Removed stale pid file.\n";
+ }
+}
+
+# Check if a pid file is not present or it is invalid. If so, clean up/abort.
+sub stop_check_pid_file {
+ my $pidfile = $info{'pgdata'}.'/postmaster.pid';
+ my $pid = read_pidfile $pidfile;
+ return if (defined $pid && PgCommon::check_pidfile_running $pidfile);
+ if ($info{'running'}) {
+ error 'pid file is invalid, please manually kill the stale server process.';
+ }
+
+ # Remove invalid or stale PID file
+ if (-e $pidfile) {
+ unlink $pidfile;
+ print "Removed stale pid file.\n";
+ }
+ print "Cluster is not running.\n";
+ exit 2;
+}
+
+# check if a cluster reliably connects or fails
+# Arguments: <version> <cluster> <port> <socket dir>
+sub cluster_port_ready {
+ my ($v, $c, $p, $sd) = @_;
+ my $psql = get_program_path('psql', $v);
+ error 'cluster_port_ready: could not find psql binary' unless $psql;
+ my $n = 0;
+ my $result = 0;
+
+ # probe until we get three successful or failed connections in a row
+ my $nopwoption = $v >= 8.4 ? '-w' : '';
+ if ($v < 8.4) {
+ $ENV{'PGPASSWORD'} = 'foo'; # prevent hangs if superuser cannot connect without password
+ }
+ my $out;
+ while ($n < ($result ? 10 : 3)) {
+ select undef, undef, undef, 0.5;
+ $out = `LC_MESSAGES=C $psql -h '$sd' --port $p $nopwoption -Xc '' template1 2>&1 >/dev/null`;
+
+ if ($? == $result) {
+ $n++;
+ } else {
+ $n = 0;
+ }
+ $result = $?;
+ }
+
+ if ($out =~ /FATAL:|no password supplied/) {
+ print STDERR "Warning: connection to the database failed, disabling startup checks:\n$out\n";
+ return cluster_port_running $v, $c, $p;
+ }
+ return !$result;
+}
+
+sub start {
+ start_check_pid_file;
+
+ # check conflicting port
+ if (cluster_port_running $version, $cluster, $info{'port'}) {
+ my $sockdir = get_cluster_socketdir $version, $cluster;
+ error("Port conflict: another instance is already running on $sockdir with port $info{'port'}");
+ }
+
+ # prepare environment (empty except for content of 'environment', and LANG)
+ %ENV = read_cluster_conf_file $version, $cluster, 'environment';
+ # set LANG so non-ascii chars in the server log are not replaced by '?' (affected are non-session contexts)
+ unless (exists $ENV{LANG}) {
+ my $lc_messages = PgCommon::get_conf_value $version, $cluster, 'postgresql.conf', 'lc_messages';
+ $ENV{LANG} = $lc_messages if $lc_messages;
+ }
+ # 9.5 configures OOM killer using env vars
+ if ($version >= 9.5) {
+ $ENV{PG_OOM_ADJUST_FILE} = "/proc/self/oom_score_adj" unless (exists $ENV{PG_OOM_ADJUST_FILE});
+ # PG_OOM_ADJUST_VALUE defaults to 0, but can be overridden here
+ }
+
+ my $postgres_opts = '';
+ my $usd = $version >= 9.3 ? 'unix_socket_directories' : 'unix_socket_directory';
+ if (!(PgCommon::get_conf_value $version, $cluster, 'postgresql.conf', $usd)) {
+ $postgres_opts .= "-c $usd=\"$info{'socketdir'}\"";
+ }
+
+ my $cdir = $info{'configdir'};
+ $postgres_opts .= " -c config_file=\"$cdir/postgresql.conf\"";
+ if (!(PgCommon::get_conf_value $version, $cluster, 'postgresql.conf', 'hba_file')) {
+ $postgres_opts .= " -c hba_file=\"$cdir/pg_hba.conf\"";
+ }
+ if (!(PgCommon::get_conf_value $version, $cluster, 'postgresql.conf', 'ident_file')) {
+ $postgres_opts .= " -c ident_file=\"$cdir/pg_ident.conf\"";
+ }
+
+ if ((-d '/var/run/postgresql') && !defined (PgCommon::get_conf_value $version, $cluster, 'postgresql.conf', 'external_pid_file')) {
+ # check whether /var/run/postgresql/ is writable as the cluster owner
+ my $vrp_writable;
+ if ($> == 0) {
+ change_ugid $info{'owneruid'}, $info{'ownergid'};
+ $vrp_writable = -w '/var/run/postgresql';
+ $< = $> = 0;
+ $( = $) = 0;
+ } else {
+ $vrp_writable = -w '/var/run/postgresql';
+ }
+ if ($vrp_writable) {
+ $postgres_opts .= " -c external_pid_file=\"/var/run/postgresql/$version-$cluster.pid\"";
+ }
+ }
+
+ $postgres_opts .= ' ' . (join ' ', @postgres_auxoptions);
+ ($postgres_opts) = $postgres_opts =~ /(.*)/; # untaint
+
+ if ($foreground) {
+ if ($stdlog and $info{'logfile'}) {
+ my $fd = POSIX::open($info{logfile}, POSIX::O_WRONLY|POSIX::O_APPEND|POSIX::O_CREAT) or error "Could not open logfile $info{logfile}";
+ dup2($fd, 1);
+ dup2($fd, 2);
+ POSIX::close($fd) or error "Could not close log fd";
+ }
+ my $postgres = get_program_path 'postgres', $version;
+ error "Could not find postgres executable for version $version" unless ($postgres);
+ exec '/bin/sh', '-c', "exec $postgres $postgres_opts" or
+ error "Executing $postgres failed: $!"
+ }
+
+ # only supply or default logfile path when none is given explicitly in
+ # postgresql.conf
+ my @options = ($pg_ctl, 'start', '-D', $info{'pgdata'});
+ my $logsize = 0;
+ if ($info{'logfile'}) {
+ push @options, ('-l', $info{'logfile'});
+ # remember current size of the log
+ $logsize = (stat $info{'logfile'})[7] || 0; # ignore stat errors
+ }
+
+ push @options, @pg_ctl_opts_from_cli if @pg_ctl_opts_from_cli;
+
+ my %pg_ctl_opts_from_file = read_cluster_conf_file $version, $cluster, 'pg_ctl.conf';
+ push @options, split(' ', $pg_ctl_opts_from_file{'pg_ctl_options'})
+ if defined $pg_ctl_opts_from_file{'pg_ctl_options'} and $pg_ctl_opts_from_file{'pg_ctl_options'} ne '';
+
+ push @options, ('-s', '-o', $postgres_opts);
+
+ if (fork) {
+ wait;
+ if ($?) {
+ my $exit = $? >> 8;
+ print STDERR "Error: $pg_ctl @options exited with status $exit: $!\n";
+ my $currlogsize = (stat $info{'logfile'})[7] if $info{'logfile'} && -r $info{'logfile'};
+ if ($currlogsize) {
+ open LOG, $info{'logfile'} or
+ error "Could not open log file " . $info{'logfile'};
+ seek LOG, $logsize, SEEK_SET;
+ print STDERR $_ while <LOG>;
+ }
+ exit $exit;
+ }
+ } else {
+ setsid or error "could not start session: $!";
+ if ($info{'logfile'}) {
+ my $fd = POSIX::open($info{'logfile'}, POSIX::O_WRONLY|POSIX::O_APPEND|POSIX::O_CREAT) or error "Could not open logfile $info{'logfile'}";
+ dup2($fd, 1);
+ dup2($fd, 2);
+ POSIX::close($fd) or error "Could not close log fd";
+ }
+ exec $pg_ctl @options or error "could not exec $pg_ctl @options: $!";
+ }
+
+ # wait a bit until the socket exists
+ my $success = 0;
+ my $currlogsize = 0;
+ my $pidfile = $info{'pgdata'}.'/postmaster.pid';
+ for (my $attempt = 0; $attempt < 60; $attempt++) {
+ select (undef, undef, undef, 0.5);
+ $currlogsize = (stat $info{'logfile'})[7] if $info{'logfile'} && -r $info{'logfile'};
+ if (cluster_port_running $version, $cluster, $info{'port'}) {
+ $success = 1;
+ last;
+ }
+
+ # if postgres wrote something, but the process does not exist any
+ # more, there must be a problem and we can stop immediately
+ last if ($currlogsize > $logsize && !PgCommon::check_pidfile_running $pidfile);
+ }
+
+ # OK, the server runs, now wait until it stabilized
+ if ($success) {
+ $success = cluster_port_ready $version, $cluster, $info{'port'}, $info{'socketdir'};
+ }
+
+ if (!$success) {
+ if ($currlogsize) {
+ print STDERR "The PostgreSQL server failed to start. Please check the log output:\n";
+ open LOG, $info{'logfile'} or
+ error "Could not open log file " . $info{'logfile'};
+ seek LOG, $logsize, SEEK_SET;
+ print STDERR $_ while <LOG>;
+ } else {
+ print STDERR "The PostgreSQL server failed to start. Please check the log output.\n";
+ }
+ exit 1;
+ }
+
+ return 0;
+}
+
+sub stop {
+ stop_check_pid_file;
+ my $result = 1;
+
+ my @options = ('stop', '-s', '-w', '-D', $info{'pgdata'});
+
+ push @options, @pg_ctl_opts_from_cli if @pg_ctl_opts_from_cli;
+
+ my %pg_ctl_opts_from_file = read_cluster_conf_file $version, $cluster, 'pg_ctl.conf';
+ push @options, split(' ', $pg_ctl_opts_from_file{'pg_ctl_options'})
+ if defined $pg_ctl_opts_from_file{'pg_ctl_options'} and $pg_ctl_opts_from_file{'pg_ctl_options'} ne '';
+
+ if (!fork()) {
+ close STDOUT;
+ exec $pg_ctl, @options, '-m', ($force ? 'fast' : $mode);
+ } else {
+ wait;
+ $result = $? >> 8;
+ }
+
+ # try harder if forced and server hasn't stopped yet
+ if ($force and -f $info{'pgdata'}.'/postmaster.pid') {
+ print "(does not shutdown gracefully, now stopping immediately)\n";
+ $result = system $pg_ctl, @options, '-m', 'immediate';
+ }
+
+ # external_pid_file files are currently not removed by postgres itself
+ if ($result == 0) {
+ unlink "/var/run/postgresql/$version-$cluster.pid";
+ }
+
+ return $result;
+}
+
+sub restart {
+ my $result;
+
+ if ($info{'running'}) {
+ $result = stop;
+ return $result if $result;
+ }
+ return start;
+}
+
+sub reload {
+ exec $pg_ctl, '-D', $info{'pgdata'}, '-s', @pg_ctl_opts_from_cli, 'reload';
+}
+
+sub status {
+ exec $pg_ctl, '-D', $info{'pgdata'}, 'status';
+}
+
+sub promote {
+ exec $pg_ctl, '-D', $info{'pgdata'}, '-s', @pg_ctl_opts_from_cli, 'promote';
+}
+
+#
+# main
+#
+
+my ($skip_systemctl_redirect, $bindir);
+
+exit 1 unless GetOptions ('o|options=s' => \@postgres_auxoptions,
+ 'f|force' => \$force,
+ 'm|mode=s' => \$mode,
+ 'foreground' => \$foreground,
+ 'skip-systemctl-redirect' => \$skip_systemctl_redirect,
+ 'stdlog' => \$stdlog,
+ 'bindir=s' => \$bindir,
+);
+
+if ($mode =~ /^(s(mart)?|f(ast)?|i(mmediate)?)$/) {
+ $mode = $1; # untaint
+} else {
+ print "Invalid -m mode, valid are: smart fast immediate\n";
+ exit 1;
+}
+
+($bindir) = $bindir =~ /^(\/.*)$/ if $bindir; # untaint
+
+# parse command
+my $version_re = qr/(\d+\.?\d)/;
+my $cluster_re = qr/([^'"\s]+)/;
+my $action_re = qr/(start|stop|restart|reload|status|promote)/;
+my $action;
+
+if (@ARGV >= 3 and "$ARGV[0] $ARGV[1] $ARGV[2]" =~ /^$version_re $cluster_re $action_re$/) {
+ ($version, $cluster, $action) = ($1, $2, $3);
+ splice @ARGV, 0, 3;
+} elsif (@ARGV >= 3 and "$ARGV[0] $ARGV[1] $ARGV[2]" =~ /^$action_re $version_re $cluster_re$/) {
+ ($action, $version, $cluster) = ($1, $2, $3);
+ splice @ARGV, 0, 3;
+} elsif (@ARGV >= 2 and "$ARGV[0] $ARGV[1]" =~ m!^$version_re(?:[-/])$cluster_re $action_re$!) {
+ ($version, $cluster, $action) = ($1, $2, $3);
+ splice @ARGV, 0, 2;
+} elsif (@ARGV >= 2 and "$ARGV[0] $ARGV[1]" =~ m!^$action_re $version_re(?:[-/])$cluster_re$!) {
+ ($action, $version, $cluster) = ($1, $2, $3);
+ splice @ARGV, 0, 2;
+} else {
+ error "Usage: $0 <version> <cluster> <action> [-- <pg_ctl options>]";
+}
+
+@pg_ctl_opts_from_cli=();
+foreach my $argv (@ARGV) {
+ push @pg_ctl_opts_from_cli, $argv =~ /(.*)/; # untaint
+}
+
+error "specified cluster '$version $cluster' does not exist" unless cluster_exists $version, $cluster;
+%info = cluster_info ($version, $cluster);
+validate_cluster_owner \%info;
+
+unless ($action eq 'stop') {
+ # check if cluster is disabled in start.conf
+ error 'Cluster is disabled' if $info{'start'} eq 'disabled';
+}
+
+# redirect the request through systemd
+if (not $skip_systemctl_redirect and getppid() != 1 and # not run from init
+ -d '/run/systemd/system' and not exists $ENV{_SYSTEMCTL_SKIP_REDIRECT} and # systemd is running
+ not $foreground and # foreground operation not requested
+ not exists $ENV{'PG_CLUSTER_CONF_ROOT'} and # not handling user clusters
+ $action =~ /^(start|stop|restart)$/ # redirect only these actions
+) {
+ $action = $1; # untaint
+ system 'systemctl', 'is-active', '-q', "postgresql\@$version-$cluster";
+ my $unit_active = $? == 0;
+
+ # if extra options are given, proceed with pg_ctlcluster with a warning
+ if (@postgres_auxoptions != 0 or @pg_ctl_opts_from_cli != 0 or $bindir) { # extra options given
+ print "Notice: extra pg_ctl/postgres options given, bypassing systemctl for $action operation\n" if (-t 1);
+ # if we are root, redirect to systemctl unless stopping a cluster running outside systemd
+ } elsif ($> == 0) {
+ if ($action eq 'stop' and not $unit_active) {
+ # proceed with pg_ctlcluster
+ } else {
+ #print "Redirecting $action request to systemctl\n" if (-t 1);
+ system 'systemctl', $action, "postgresql\@$version-$cluster";
+ exit $? >> 8;
+ # program end
+ }
+ # as non-root, raise an error on restarting a cluster running from systemd
+ } elsif ($action eq 'restart' and $unit_active) {
+ error "cluster is running from systemd, can only restart it as root. Try instead:\n sudo systemctl $action postgresql\@$version-$cluster";
+ # program end
+ # otherwise just raise a warning on start and restart as non-root
+ } elsif (-t 1) {
+ if ($action =~ /start/) {
+ print "Warning: the cluster will not be running as a systemd service. Consider using systemctl:\n";
+ print " sudo systemctl $action postgresql\@$version-$cluster\n";
+ } elsif ($unit_active) {
+ print "Warning: stopping the cluster using pg_ctlcluster will mark the systemd unit as failed. Consider using systemctl:\n";
+ print " sudo systemctl $action postgresql\@$version-$cluster\n";
+ }
+ }
+}
+
+# recreate missing standard log dir
+if ($> == 0 && ! -e '/var/log/postgresql' &&
+ $info{'logfile'} =~ m!^/var/log/postgresql!) {
+ system 'install', '-d', '-m', '1775', '-o', 'root', '-g', 'postgres', '/var/log/postgresql';
+}
+
+# recreate missing log file
+if ($action ne 'stop' && $info{'logfile'} && ! -e $info{'logfile'}) {
+ if ($> == 0) { # drop privileges; this is important if logfile
+ # was determined via an /etc/postgresql/.../log symlink
+ change_ugid $info{'owneruid'}, $info{'ownergid'};
+ }
+ sysopen (L, $info{'logfile'}, O_RDWR|O_CREAT|O_EXCL) or
+ error 'Could not create log file ' . $info{'logfile'};
+ close L;
+ chmod 0640, $info{'logfile'};
+ $< = $> = 0; # will silently fail if we were not root before, that's intended
+ $( = $) = 0;
+ if ($info{'owneruid'} < 500) {
+ my $g = (getgrnam 'adm')[2];
+ lchown $info{'owneruid'}, $g, $info{'logfile'} if (defined $g);
+ }
+}
+
+if ($action ne 'stop') {
+ # recreate /var/run/postgresql while possibly still running as root
+ if (! -d '/var/run/postgresql') {
+ system 'install', '-d', '-m', 2775, '-o', 'postgres', '-g', 'postgres', '/var/run/postgresql';
+ }
+
+ # allow creating socket directories below /var/run/postgresql for any user
+ if ($info{socketdir} =~ m!^(/var)/run/postgresql/[\w_.-]+$! and ! -d $info{socketdir}) {
+ if (mkdir $info{socketdir}, 02775) { # don't use "install" here as it would allow stealing existing directories
+ chown $info{owneruid}, $info{ownergid}, $info{socketdir};
+ } else {
+ error "Could not create $info{socketdir}: $!";
+ }
+ }
+
+ # allow creating stats_temp_directory below /var/run/postgresql for any user
+ if ($info{config}->{stats_temp_directory} and $info{config}->{stats_temp_directory}=~ m!^(/var)/run/postgresql/[\w_.-]+$! and ! -d $info{config}->{stats_temp_directory}) {
+ if (mkdir $info{config}->{stats_temp_directory}, 0750) { # don't use "install" here as it would allow stealing existing directories
+ chown $info{owneruid}, $info{ownergid}, $info{config}->{stats_temp_directory};
+ } else {
+ error "Could not create $info{config}->{stats_temp_directory}: $!";
+ }
+ }
+}
+
+if ($> == 0) {
+ # have postgres start with increased OOM killer protection; 9.0 and
+ # later has builtin support for resetting the adjustment of child processes
+ if ($action eq 'start' and $version >= '9.0' and not $PgCommon::rpm) {
+ if (-w '/proc/self/oom_score_adj') {
+ open F, '>/proc/self/oom_score_adj';
+ print F "-900\n";
+ close F;
+ }
+ }
+
+ change_ugid $info{'owneruid'}, $info{'ownergid'};
+}
+
+# we are running as the cluster owner now
+
+if( $> != $info{'owneruid'} ) {
+ error 'You must run this program as the cluster owner ('.
+ (getpwuid $info{'owneruid'})[0].') or root';
+}
+
+# create socket directory (if it wasn't already created in /var/run/postgresql by the code above)
+if ($action ne 'stop' && ! -d $info{socketdir}) {
+ system 'install', '-d', '-m', 2775, $info{socketdir};
+}
+
+# create stats_temp_directory (if it wasn't already created in /var/run/postgresql by the code above)
+if ($action ne 'stop' && $info{config}->{stats_temp_directory} && ! -d $info{config}->{stats_temp_directory}) {
+ system 'install', '-d', '-m', 750, $info{config}->{stats_temp_directory};
+}
+
+$pg_ctl = $bindir ? "$bindir/pg_ctl" : get_program_path ('pg_ctl', $version);
+error "Could not find pg_ctl executable for version $version" unless ($pg_ctl);
+
+# do the action
+no strict 'refs';
+exit &$action;
+
+__END__
+
+=head1 NAME
+
+pg_ctlcluster - start/stop/restart/reload a PostgreSQL cluster
+
+=head1 SYNOPSIS
+
+B<pg_ctlcluster> [I<options>] I<cluster-version> I<cluster-name> I<action> [B<--> I<pg_ctl options>]
+
+where I<action> = B<start>|B<stop>|B<restart>|B<reload>|B<status>|B<promote>
+
+=head1 DESCRIPTION
+
+This program controls the B<postgres> server for a particular cluster. It
+essentially wraps the L<pg_ctl(1)> command. It determines the cluster version
+and data path and calls the right version of B<pg_ctl> with appropriate
+configuration parameters and paths.
+
+You have to start this program as the user who owns the database cluster or as
+root.
+
+To ease integration with B<systemd> operation, the alternative syntax
+"B<pg_ctlcluster> I<version>B<->I<cluster> I<action>" is also supported,
+as well as putting the action first (matching the ordering used by B<systemctl>).
+
+=head1 ACTIONS
+
+=over 4
+
+=item B<start>
+
+A log file for this specific cluster is created if it does not exist yet (by
+default,
+C</var/log/postgresql/postgresql->I<cluster-version>C<->I<cluster-name>C<.log>),
+and a PostgreSQL server process (L<postgres(1)>) is started on it. Exits with
+0 on success, with 2 if the server is already running, and with 1 on other
+failure conditions.
+
+=item B<stop>
+
+Stops the L<postgres(1)> server of the given cluster. By default, "fast"
+shutdown mode is used.
+
+=item B<restart>
+
+Stops the server if it is running and starts it (again).
+
+=item B<reload>
+
+Causes the configuration files to be re-read without a full shutdown of the
+server.
+
+=item B<status>
+
+Checks whether a server is running. If it is, the PID and the command line
+options that were used to invoke it are displayed.
+
+=item B<promote>
+
+Commands a running standby server to exit recovery and begin read-write
+operations.
+
+=back
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<-f>|B<--force>
+
+For B<stop> and B<restart>, the "fast" mode is used which rolls back all active
+transactions, disconnects clients immediately and thus shuts down cleanly. If
+that does not work, shutdown is attempted again in "immediate" mode, which can
+leave the cluster in an inconsistent state and thus will lead to a recovery run
+at the next start. If this still does not help, the B<postgres> process is
+killed. Exits with 0 on success, with 2 if the server is not running, and with
+1 on other failure conditions. This mode should only be used when the machine
+is about to be shut down.
+
+=item B<-m>|B<--mode> [B<smart>|B<fast>|B<immediate>]
+
+Shutdown mode to use for B<stop> and B<restart> actions, default is B<fast>.
+See pg_ctl(1) for documentation.
+
+=item B<--foreground>
+
+Start B<postgres> in foreground, without daemonizing via B<pg_ctl>.
+
+=item B<--stdlog>
+
+When B<--foreground> is in use, redirect stderr to the standard logfile in
+C</var/log/postgresql/>. (Default when not run in foreground.)
+
+=item B<--skip-systemctl-redirect>
+
+When running as root, B<pg_ctlcluster> redirects actions to B<systemctl> so
+running clusters are properly supervised by B<systemd>. This option skips the
+redirect; it is used in the B<postgresql@.service> unit file. The redirect is
+also skipped if additional B<postgres> or B<pg_ctl> options are provided.
+
+=item B<--bindir> I<directory>
+
+Path to B<pg_ctl>. (Default is C</usr/lib/postgresql/>I<version>C</bin>.)
+
+=item B<-o>|B<--options> I<option>
+
+Pass given I<option> as command line option to the C<postgres> process. It is
+possible to specify B<-o> multiple times. See L<postgres(1)> for a
+description of valid options.
+
+=item I<pg_ctl options>
+
+Pass given I<pg_ctl options> as command line options to B<pg_ctl>. See L<pg_ctl(1)>
+for a description of valid options.
+
+=back
+
+=head1 FILES
+
+=over 4
+
+=item C</etc/postgresql/>I<cluster-version>C</>I<cluster-name>C</pg_ctl.conf>
+
+This configuration file contains cluster specific options to be passed to
+L<pg_ctl(1)>.
+
+=item C</etc/postgresql/>I<cluster-version>C</>I<cluster-name>C</start.conf>
+
+This configuration file controls the start/stop behavior of the cluster. See
+section "STARTUP CONTROL" in L<pg_createcluster(8)> for details.
+
+=back
+
+=head1 BUGS
+
+Changing the port number on startup using B<-o -p> will not work as it breaks
+the checks for running clusters.
+
+=head1 SEE ALSO
+
+L<pg_createcluster(8)>, L<pg_ctl(1)>, L<pg_wrapper(1)>, L<pg_lsclusters(1)>,
+L<postgres(1)>
+
+=head1 AUTHOR
+
+Martin Pitt L<E<lt>mpitt@debian.orgE<gt>>
+
diff --git a/pg_dropcluster b/pg_dropcluster
new file mode 100755
index 0000000..def4554
--- /dev/null
+++ b/pg_dropcluster
@@ -0,0 +1,226 @@
+#!/usr/bin/perl -wT
+
+# Completely delete a PostgreSQL cluster. Fails if there is still a server
+# process attached.
+#
+# (C) 2005-2009 Martin Pitt <mpitt@debian.org>
+# (C) 2015-2021 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 Getopt::Long;
+use PgCommon;
+
+# untaint environment
+$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
+delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
+
+my $stopserver = 0;
+exit 1 unless GetOptions ('stop-server' => \$stopserver, 'stop' => \$stopserver);
+
+# command line options
+if ($#ARGV != 1) {
+ print "Usage: $0 [--stop] <version> <cluster>\n";
+ exit 1;
+}
+
+my ($version) = $ARGV[0] =~ /^(\d+\.?\d+)$/;
+my ($cluster) = $ARGV[1] =~ /^([-.\w]+)$/;
+error 'invalid version' unless defined $version;
+error 'invalid cluster name' unless defined $cluster;
+
+my %info;
+my $c; # configuration directory
+my $startconf;
+
+if (cluster_exists $version, $cluster) {
+ %info = cluster_info ($version, $cluster);
+ validate_cluster_owner \%info if ($info{pgdata} && -d $info{pgdata}); # ignore missing data directory
+
+ if ($info{'running'}) {
+ if ($stopserver) {
+ if ($info{'pgdata'} && -d $info{'pgdata'}) {
+ if (system ('pg_ctlcluster', $version, $cluster, 'stop')) {
+ error 'could not stop server, aborting';
+ }
+ } else {
+ print STDERR "Warning: corrupted cluster: data directory does not exist any more, but server is still running; you have to manually kill the postgres process\n";
+ }
+ } else {
+ error 'This cluster is still running. Stop it or supply the --stop option';
+ }
+ }
+ $c = $info{'configdir'};
+ $startconf = $info{'start'};
+} else {
+ $c = "/etc/postgresql/$version/$cluster";
+
+ # check if we have a broken cluster, clean up if necessary
+ -d $c or error 'specified cluster does not exist';
+}
+
+# disable systemd-enabled clusters
+if (not exists $ENV{'PG_CLUSTER_CONF_ROOT'} and -d '/run/systemd/system') {
+ if ($> == 0) {
+ system "systemctl", "disable", "postgresql\@$version-$cluster";
+ system "systemctl", "disable", "--now", "pg_receivewal\@$version-$cluster";
+ system "systemctl", "disable", "--now", "pg_basebackup\@$version-$cluster.timer";
+ system "systemctl", "disable", "--now", "pg_dump\@$version-$cluster.timer";
+ }
+}
+
+if ($info{'pgdata'} && -d $info{'pgdata'}) {
+ # remove custom wal directory
+ if ($info{waldir} and -d $info{waldir}) {
+ my $walowner = (stat($info{waldir}))[4];
+ if (defined $walowner and $walowner == $info{owneruid}) {
+ my $result = system 'rm', '-r', '--', $info{waldir};
+ if ($result) {
+ print STDERR "Warning: could not remove wal directory $info{waldir}";
+ }
+ } else {
+ print STDERR "Warning: wal directory $info{waldir} is not owned by uid $info{owneruid}, not removing\n";
+ }
+ }
+
+ # remove tablespace subdirectories that belong to our version
+ for my $link (glob "$info{pgdata}/pg_tblspc/*") {
+ next unless (-l $link);
+ my $tblspc = readlink $link;
+ my $tblspcowner = (stat($tblspc))[4];
+ if (defined $tblspcowner and $tblspcowner == $info{owneruid}) {
+ if ($version >= 9.0) {
+ for my $dir (glob "$tblspc/PG_${version}_*") {
+ my $dirowner = (stat($dir))[4];
+ if (defined $dirowner and $dirowner == $info{owneruid}) {
+ my $result = system 'rm', '-r', '--', ($dir =~ /(.*)/); # untaint
+ if ($result) {
+ print STDERR "Warning: could not remove tablespace directory $dir";
+ }
+ } else {
+ print STDERR "Warning: tablespace subdirectory $dir (in tablespace linked from $link) is not owned by uid $info{owneruid}, not removing\n";
+ }
+ }
+ } else { # before 9.0
+ if (open my $fh, '<', "$tblspc/PG_VERSION") {
+ my $v = <$fh>;
+ chomp $v;
+ close $fh;
+ if ($v eq $version) {
+ $tblspc =~ /(.*)/; # untaint
+ my $result = system "rm -r -- $1/*";
+ } else {
+ print STDERR "Warning: tablespace directory $tblspc (linked from $link) is from PostgreSQL version $v, not removing\n";
+ }
+ } else {
+ print STDERR "Warning: tablespace directory $tblspc (linked from $link) is not a PostgreSQL directory, not removing\n";
+ }
+ }
+ } else {
+ print STDERR "Warning: tablespace directory $tblspc (linked from $link) is not owned by uid $info{owneruid}, not considering\n";
+ }
+ }
+
+ # remove pgdata
+ my $result = system 'rm', '-r', $info{'pgdata'};
+ if ($result) {
+ if (! -w ($info{'pgdata'} . '/..')) {
+ error 'you might need to run this program with root privileges';
+ }
+ exit $result;
+ }
+} else {
+ print STDERR "Warning: corrupted cluster: data directory does not exist\n";
+}
+
+# remove config
+unlink $c.'/pg_hba.conf', $c.'/pg_ident.conf', $c.'/postgresql.conf',
+ $c.'/start.conf', $c.'/log', $c.'/autovacuum_log', $c.'/pgdata',
+ $c.'/environment', $c.'/pg_ctl.conf';
+rmdir $_ foreach (map { /(.*)/ && $1 } glob "$c/*"); # remove empty conf.d directories
+
+unlink $info{'logfile'} if defined ($info{'logfile'});
+if ($info{'socketdir'} and $info{'socketdir'} !~ /^(\/tmp|\/var\/run\/postgresql)\/?$/) {
+ rmdir $info{'socketdir'};
+}
+rmdir $c;
+rmdir "/etc/postgresql/$version";
+rmdir "/var/lib/postgresql/$version/$cluster";
+rmdir "/var/lib/postgresql/$version";
+unlink "/var/log/postgresql/postgresql-$version-$cluster.log";
+# remove logrotated files
+foreach my $f (</var/log/postgresql/postgresql-$version-$cluster.log.[1-9]*>) {
+ unlink ($f =~ /(.*)/); # untaint
+}
+
+# remove stats_temp_directory
+my $statstempdir = $info{config}->{stats_temp_directory};
+if ($statstempdir) {
+ my $statsowner = (stat($statstempdir))[4];
+ if (defined $statsowner and defined $info{owneruid} and $statsowner == $info{owneruid}) {
+ foreach my $f (<$statstempdir/*.stat>) {
+ unlink ($f =~ /(.*)/); # untaint
+ }
+ rmdir $statstempdir;
+ }
+}
+
+# notify systemd when an autostarted cluster went away
+if (not exists $ENV{'PG_CLUSTER_CONF_ROOT'} and $startconf and $startconf eq 'auto' and -d '/run/systemd/system') {
+ if ($> == 0) {
+ system 'systemctl daemon-reload';
+ } elsif (-t 1) {
+ print "Warning: systemd was not informed about the removed cluster yet. Operations like \"service postgresql start\" might fail. To fix, run:\n";
+ print " sudo systemctl daemon-reload\n";
+ }
+}
+
+# notify apt about the new cluster
+if (not exists $ENV{'PG_CLUSTER_CONF_ROOT'} and $> == 0) {
+ system "/usr/share/postgresql-common/pg_updateaptconfig";
+}
+
+exit 0;
+
+__END__
+
+=head1 NAME
+
+pg_dropcluster - completely delete a PostgreSQL cluster
+
+=head1 SYNOPSIS
+
+B<pg_dropcluster> [B<--stop>] I<cluster-version> I<cluster-name>
+
+=head1 DESCRIPTION
+
+This program removes all files that belong to a given PostgreSQL cluster; that
+includes the data, wal, and tablespace directories, the log file, and all configuration files that
+were created by L<pg_createcluster(1)>. If the configuration directory
+(C</etc/postgresql/>I<version>C</>I<cluster>) is empty after this, it is
+removed as well.
+An empty socket directory other than B</var/run/postgresql> or B</tmp> is
+also removed.
+
+Usually a cluster which still has a running server attached will not be
+deleted. To override this, the B<--stop> option forces a server shutdown
+before the files are removed.
+
+=head1 SEE ALSO
+
+L<pg_createcluster(1)>, L<pg_ctlcluster(1)>
+
+=head1 AUTHOR
+
+Martin Pitt L<E<lt>mpitt@debian.orgE<gt>>
+
diff --git a/pg_getwal b/pg_getwal
new file mode 100755
index 0000000..8e64e87
--- /dev/null
+++ b/pg_getwal
@@ -0,0 +1,96 @@
+#!/bin/sh
+
+# retrieve a WAL file from a pg_receivewal archive
+#
+# Copyright (C) 2021-2022 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.
+
+set -eu
+
+binroot="/usr/lib/postgresql/"
+#redhat# binroot="/usr/pgsql-"
+
+if [ -z "${2:-}" ]; then
+ echo "Syntax: $0 /path/to/wal/%f %p" >&2
+ exit 1
+fi
+
+file="$1"
+path="$2"
+
+# sanity-check the first argument
+waldir="$(dirname $file)"
+if ! [ -d "$waldir" ]; then
+ echo "$0: $waldir is not a directory" >&2
+ exit 129
+fi
+
+if [ -f "$file.gz" ]; then
+ gunzip < "$file.gz" > "$path" || exit 129
+
+elif [ -f "$file.lz4" ]; then
+ unlz4 < "$file.lz4" > "$path" || exit 129
+
+elif [ -f "$file" ]; then
+ cp "$file" "$path" || exit 129
+
+elif [ -f "$file.gz.partial" ] || [ -f "$file.lz4.partial" ]; then
+ if [ -s "$file.gz.partial" ]; then
+ gunzip < "$file.gz.partial" > "$path" || exit 129
+ elif [ -s "$file.lz4.partial" ]; then
+ unlz4 < "$file.lz4.partial" > "$path" || exit 129
+ else
+ # .gz.partial starts completely empty, gunzip doesn't like that
+ touch "$path" || exit 129
+ fi
+
+ # expand file to original size
+ version=$(cat PG_VERSION) || exit 129
+ wal_file_size=$(LC_ALL=C "$binroot$version/bin/pg_controldata" . | awk '/^Bytes per WAL segment:/ { print $5 }') || exit 129
+ [ "$wal_file_size" -gt 0 ] || exit 129
+ truncate --size="$wal_file_size" "$path" || exit 129
+
+elif [ -f "$file.partial" ]; then
+ cp "$file.partial" "$path" || exit 129
+
+else
+ # file not found, exit silently in order not to spam the server log with errors
+ exit 1
+fi
+
+exit 0
+
+: <<=cut
+
+=head1 NAME
+
+pg_getwal - retrieve a WAL file from a pg_receivewal archive
+
+=head1 SYNOPSIS
+
+B<pg_getwal> I</path/to/wal/%f> I<%p>
+
+=head1 DESCRIPTION
+
+B<pg_getwal> retrieves and decompresses files from a WAL archive maintained by
+B<pg_receivewal> and B<pg_backupcluster>. It is put into PostgreSQL's
+B<restore_command> by B<pg_restorecluster>.
+
+=head1 SEE ALSO
+
+L<pg_restorecluster(1)>, L<pg_backupcluster(1)>.
+
+=head1 AUTHOR
+
+Christoph Berg L<E<lt>myon@debian.orgE<gt>>
+
+=cut
diff --git a/pg_hba b/pg_hba
new file mode 100755
index 0000000..350d251
--- /dev/null
+++ b/pg_hba
@@ -0,0 +1,144 @@
+#!/usr/bin/perl -w
+
+# Add, remove, or test a pg_hba.conf entry.
+#
+# (C) 2005-2009 Martin Pitt <mpitt@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 PgCommon;
+use Getopt::Long;
+use Net::CIDR;
+
+# global variables
+
+my $ip = ''; # default to local unix socket
+my $force_ssl = 0;
+my ($method, $ver_cluster, $db, $user);
+my $mode;
+my @hba;
+
+# Print an error message to stderr and exit with status 2
+sub error2 {
+ print STDERR 'Error: ', $_[0], "\n";
+ exit 2;
+}
+
+# Check if s1 is equal to s2 or s2 is 'all'.
+# Arguments: <s1> <s2>
+sub match_all {
+ return ($_[1] eq 'all' || $_[0] eq $_[1]);
+}
+
+# Check if given IP matches the specification in the HBA record.
+# Arguments: <ip> <ref to hba hash>
+sub match_ip {
+ my ($ip, $hba) = @_;
+
+ # Don't try to mix IPv4 and IPv6 addresses since that will make cidrlookup
+ # croak
+ return 0 if ((index $ip, ':') < 0) ^ ((index $$hba{'ip'}, ':') < 0);
+
+ return Net::CIDR::cidrlookup ($ip, $$hba{'ip'});
+}
+
+# Check if arguments match any line
+# Return: 1 if match was found, 0 otherwise.
+sub mode_test {
+ foreach my $hbarec (@hba) {
+ if (!defined($$hbarec{'type'})) {
+ next;
+ }
+ next if $$hbarec{'type'} eq 'comment';
+ next unless match_all ($user, $$hbarec{'user'}) &&
+ match_all ($db, $$hbarec{'db'}) &&
+ $$hbarec{'method'} eq $method;
+
+ if ($ip) {
+ return 1 if
+ (($force_ssl && $$hbarec{'type'} eq 'hostssl') ||
+ $$hbarec{'type'} =~ /^host/) &&
+ match_ip ($ip, $hbarec);
+ } else {
+ return 1 if $$hbarec{'type'} eq 'local';
+ }
+ }
+
+ return 0;
+}
+
+# Print hba conf.
+sub mode_print {
+ foreach my $hbarec (@hba) {
+ print "$$hbarec{'line'}\n";
+ }
+}
+
+# Generate a pg_hba.conf line that matches the command line args.
+sub create_hba_line {
+ if ($ip) {
+ return sprintf "%-7s %-11s %-11s %-35s %s\n",
+ $force_ssl ? 'hostssl' : 'host', $db, $user, $ip, $method;
+ } else {
+ return sprintf "%-7s %-11s %-47s %s\n", 'local', $db, $user, $method;
+ }
+}
+
+# parse arguments
+
+my $ip_arg;
+exit 3 unless GetOptions (
+ 'cluster=s' => \$ver_cluster,
+ 'ip=s' => \$ip_arg,
+ 'method=s' => \$method,
+ 'force-ssl' => \$force_ssl
+);
+
+if ($#ARGV != 2) {
+ print STDERR "Usage: $0 mode [options] <database> <user>\n";
+ exit 2;
+}
+($mode, $db, $user) = @ARGV;
+
+error2 '--cluster must be specified' unless $ver_cluster;
+my ($version, $cluster) = split ('/', $ver_cluster);
+error2 'No version specified with --cluster' unless $version;
+error2 'No cluster specified with --cluster' unless $cluster;
+error2 'Cluster does not exist' unless cluster_exists $version, $cluster;
+if (defined $ip_arg) {
+ $ip = Net::CIDR::cidrvalidate $ip_arg;
+ error2 'Invalid --ip argument' unless defined $ip;
+}
+
+unless (defined $method) {
+ $method = ($ip ? 'md5' : 'ident sameuser');
+}
+error2 'Invalid --method argument' unless PgCommon::valid_hba_method($method);
+
+# parse file
+
+my $hbafile = "/etc/postgresql/$version/$cluster/pg_hba.conf";
+@hba = read_pg_hba $hbafile;
+error2 "Could not read $hbafile" unless $#hba;
+
+if ($mode eq 'pg_test_hba') {
+ if (mode_test) {
+ exit 0;
+ } else {
+ print create_hba_line();
+ exit 1;
+ }
+} elsif ($mode eq 'pg_print_hba') {
+ mode_print();
+} else {
+ error2 "Unknown mode: $mode";
+}
diff --git a/pg_lsclusters b/pg_lsclusters
new file mode 100755
index 0000000..fec09d7
--- /dev/null
+++ b/pg_lsclusters
@@ -0,0 +1,184 @@
+#!/usr/bin/perl -wT
+# Show all PostgreSQL clusters in a list
+#
+# (C) 2005-2009 Martin Pitt <mpitt@debian.org>
+# (C) 2013-2018 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 PgCommon;
+use Getopt::Long;
+
+sub help ($)
+{
+ my $exit = shift;
+ print "Usage: $0 [-hjs]
+
+Options:
+ -h --no-header Omit column headers in output
+ -j --json JSON output
+ -s --start-conf Include start.conf information in status column
+ --help Print help\n";
+
+ exit $exit;
+}
+
+# option handling
+my $no_header;
+my $json;
+my $start_conf;
+help(1) unless GetOptions (
+ 'help' => sub { help(0); },
+ 'h|no-header' => \$no_header,
+ 'j|json' => \$json,
+ 's|start-conf' => \$start_conf,
+);
+
+my (@versions, $ls_cluster);
+if (@ARGV == 1 and $ARGV[0] =~ m{^(\d+(?:\.\d+)?)[-/]([-.\w]+)$}) {
+ push @versions, $1;
+ $ls_cluster = $2;
+} elsif (@ARGV >= 1) {
+ $ARGV[0] =~ /^(\d+(?:\.\d+)?)$/ or error "Invalid version number \"$ARGV[0]\"";
+ push @versions, $1;
+} else {
+ @versions = get_versions();
+}
+if (@ARGV >= 2 and $ARGV[1] =~ /^([-.\w]+)$/) {
+ $ls_cluster = $1;
+}
+
+error "Cluster @versions $ls_cluster does not exist"
+ if ($ls_cluster and not cluster_exists(@versions, $ls_cluster));
+
+# data collection
+my @lines;
+push @lines, ['Ver', 'Cluster', 'Port', 'Status', 'Owner', 'Data directory', 'Log file']
+ unless ($no_header);
+my $jsoninfo = [];
+
+foreach my $v (@versions) {
+ my $pg_log = $v >= 10 ? 'log' : 'pg_log'; # log directory in PGDATA changed in PG 10
+ my @clusters = $ls_cluster ? $ls_cluster : get_version_clusters $v;
+ foreach my $c (@clusters) {
+ my %info = cluster_info $v, $c;
+
+ my $status = $info{'running'} ? "online" : "down";
+ $status .= ",recovery" if ($info{'recovery'});
+ $status .= ",$info{supervisor}" if ($info{'supervisor'});
+ $status .= ",$info{start}" if ($start_conf);
+ unless (-e "${PgCommon::binroot}$v/bin/postgres") {
+ $status .= ",binaries_missing";
+ $info{binaries_missing} = 1;
+ }
+
+ my $logfile = $info{logfile} // '<unknown>'; # default logfile in /var/log/postgresql
+ if (config_bool ($info{config}->{logging_collector})) {
+ my $path = $info{config}->{log_directory} || $pg_log;
+ my $file = $info{config}->{log_filename} || 'postgresql-%Y-%m-%d_%H%M%S.log';
+ $logfile = "$path/$file";
+ }
+ my $destination = $info{config}->{log_destination} || 'stderr';
+ $destination =~ s/stderr/$logfile/;
+ my $csvlog = $logfile;
+ $csvlog =~ s/(?:\.log)?$/.csv/;
+ $destination =~ s/csvlog/$csvlog/;
+
+ push @lines, [$v, $c, $info{'port'} // '<unknown>', $status,
+ defined $info{'owneruid'} ? (getpwuid $info{'owneruid'})[0] : '<unknown>',
+ $info{'pgdata'} || '<unknown>', $destination];
+ $info{version} = $v;
+ $info{cluster} = $c;
+ push @$jsoninfo, \%info;
+ }
+}
+
+# output
+if ($json) {
+ eval { require JSON; };
+ error 'Please install JSON.pm for JSON output (Debian: libjson-perl)'
+ if ($@);
+ print JSON::encode_json($jsoninfo) . "\n";
+ exit 0;
+}
+
+my @colwidth = qw(1 1 1 1 1 1 1);
+foreach my $line (@lines) {
+ for (my $i = 0; $i < @$line - 1; $i++) { # skip adjustment for last column
+ my $len = length @$line[$i];
+ $colwidth[$i] = $len if ($len > $colwidth[$i]);
+ }
+}
+
+my $color = -t 1; # color output if stdout is a terminal
+my $fmtstring = join ' ', map { "%-${_}s" } @colwidth;
+
+foreach my $line (@lines) {
+ if ($color and $line->[0] ne 'Ver') { # don't color header
+ printf "\033[%dm$fmtstring\033[0m\n",
+ ($line->[3] =~ /^online/ ? 32 : 31), # green, red
+ @$line;
+ } else {
+ printf "$fmtstring\n", @$line;
+ }
+}
+
+__END__
+
+=head1 NAME
+
+pg_lsclusters - show information about all PostgreSQL clusters
+
+=head1 SYNOPSIS
+
+B<pg_lsclusters> [I<options>] [I<version> [I<cluster>]]
+
+=head1 DESCRIPTION
+
+This command list the status and some configuration details of all clusters.
+If a version and optionally a cluster name are given, only these are shown.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<-h>, B<--no-header>
+
+Do not print the column header line.
+
+=item B<-j>, B<--json>
+
+Output information in JSON format. Needs JSON.pm installed.
+(Debian package: libjson-perl)
+
+=item B<-s>, B<--start-conf>
+
+Include F<start.conf> information in status column.
+
+=item B<--help>
+
+Print usage help.
+
+=back
+
+=head1 NOTES
+
+The cluster status is shown as B<online> or B<down>. If a F<recovery.conf> file
+is found in the data directory, B<,recovery> is appended. The latter needs read
+access to the data directory, which only root and the cluster owner have.
+
+The output lines are colored green and red to indicate the cluster status
+visually.
+
+=head1 AUTHOR
+
+Martin Pitt L<E<lt>mpitt@debian.orgE<gt>>
diff --git a/pg_renamecluster b/pg_renamecluster
new file mode 100755
index 0000000..e5f96bb
--- /dev/null
+++ b/pg_renamecluster
@@ -0,0 +1,176 @@
+#!/usr/bin/perl -wT
+
+# Rename a PostgreSQL cluster
+#
+# (C) 2014-2021 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 Getopt::Long;
+use POSIX;
+
+# untaint environment
+$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
+delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
+
+if (@ARGV != 3) {
+ print "Usage: $0 [OPTIONS] <version> <old cluster name> <new cluster name>\n";
+ exit 1;
+}
+
+my ($version) = $ARGV[0] =~ /^(\d+\.?\d+)$/;
+my ($oldcluster) = $ARGV[1] =~ /^([-.\w]+)$/;
+my ($newcluster) = $ARGV[2] =~ /^([-.\w]+)$/;
+if ($newcluster =~ /-/ and -t 1) {
+ print "Warning: cluster names containing dashes (-) will cause problems when running from systemd. Continuing anyway\n";
+}
+
+error "Old and new name must be different"
+ if ($oldcluster eq $newcluster);
+error "specified cluster $version $oldcluster does not exist"
+ unless (cluster_exists $version, $oldcluster);
+error "target cluster $version $newcluster already exists"
+ if (cluster_exists $version, $newcluster);
+my %info = cluster_info ($version, $oldcluster);
+validate_cluster_owner \%info;
+
+# stopping old cluster, so that we notice early when there are still
+# connections
+if ($info{'running'}) {
+ print "Stopping cluster $version $oldcluster ...\n";
+ my @argv = ('pg_ctlcluster', $version, $oldcluster, 'stop');
+ error "Could not stop cluster" if system @argv;
+}
+
+# Arguments: <string>, <from>, <to>
+sub strrepl {
+ my ($s, $f, $t) = @_;
+ $s =~ s/\b\Q$f\E\b/$t/g;
+ return $s;
+}
+
+# rename config directory
+my $olddir = "$PgCommon::confroot/$version/$oldcluster";
+my $newdir = "$PgCommon::confroot/$version/$newcluster";
+rename $olddir, $newdir or error "Could not rename config directory $olddir: $!";
+
+# adapt paths to configuration files
+my %c = read_cluster_conf_file $version, $newcluster, 'postgresql.conf';
+if ($c{hba_file}) {
+ PgCommon::set_conf_value $version, $newcluster, 'postgresql.conf', 'hba_file',
+ strrepl($c{hba_file}, $oldcluster, $newcluster);
+}
+if ($c{ident_file}) {
+ PgCommon::set_conf_value $version, $newcluster, 'postgresql.conf', 'ident_file',
+ strrepl($c{ident_file}, $oldcluster, $newcluster);
+}
+if ($c{external_pid_file}) {
+ PgCommon::set_conf_value $version, $newcluster, 'postgresql.conf', 'external_pid_file',
+ strrepl($c{external_pid_file}, $oldcluster, $newcluster);
+}
+
+# update cluster_name
+if ($c{cluster_name}) {
+ PgCommon::set_conf_value $version, $newcluster, 'postgresql.conf', 'cluster_name',
+ strrepl ($c{cluster_name}, $oldcluster, $newcluster);
+}
+
+
+# rename data directory
+if ($info{pgdata}) {
+ my $newpgdata = strrepl ($info{pgdata}, $oldcluster, $newcluster);
+ if ($info{pgdata} ne $newpgdata) {
+ rename $info{pgdata}, $newpgdata or
+ error "Could not rename data directory $info{pgdata}: $!";
+ PgCommon::set_conf_value $version, $newcluster, 'postgresql.conf',
+ 'data_directory', $newpgdata;
+ }
+}
+
+# rename stats_temp_directory
+my $statstempdir = $info{config}->{stats_temp_directory};
+if ($statstempdir) {
+ my $newstatstempdir = strrepl ($statstempdir, $oldcluster, $newcluster);
+ if ($statstempdir ne $newstatstempdir) {
+ PgCommon::set_conf_value $version, $newcluster, 'postgresql.conf',
+ 'stats_temp_directory', $newstatstempdir;
+ if (-d $statstempdir) {
+ rename $statstempdir, $newstatstempdir or
+ error "Could not rename stats temp directory $statstempdir}: $!";
+ }
+ }
+}
+
+# rename old log files
+my $logdir = "/var/log/postgresql";
+if (opendir LOG, $logdir) {
+ while (my $logfile = readdir LOG) {
+ next unless $logfile =~ /^(\Qpostgresql-$version-$oldcluster.log\E.*)/;
+ $logfile = $1; # untaint
+ my $f = strrepl ($logfile, $oldcluster, $newcluster);
+ rename "$logdir/$logfile", "$logdir/$f" or error "rename $logdir/$logfile: $!";
+ }
+ closedir LOG;
+}
+
+# notify systemd about the new cluster
+if (not exists $ENV{'PG_CLUSTER_CONF_ROOT'} and -d '/run/systemd/system') {
+ if ($> == 0) {
+ system 'systemctl daemon-reload';
+ } elsif (-t 1) {
+ print "Warning: systemd does not know about the new cluster yet. Operations like \"service postgresql start\" will not handle it. To fix, run:\n";
+ print " sudo systemctl daemon-reload\n";
+ }
+}
+
+# start cluster if it was running before
+if ($info{'running'}) {
+ print "Starting cluster $version $newcluster ...\n";
+ my @argv = ('pg_ctlcluster', $version, $newcluster, 'start');
+ error "Could not start cluster" if system @argv;
+}
+
+__END__
+
+=head1 NAME
+
+pg_renamecluster - rename a PostgreSQL cluster
+
+=head1 SYNOPSIS
+
+B<pg_renamecluster> I<version> I<oldname> I<newname>
+
+=head1 DESCRIPTION
+
+B<pg_renamecluster> changes the name of a PostgreSQL cluster, i. e. the name of
+the config directory in /etc/postgresql/I<version>/ along with the data
+directory in /var/lib/postgresql/I<version>/. Existing log files in
+/var/log/postgresql/ are also renamed. The cluster is stopped and started for
+the operation.
+
+The following B<postgresql.conf> config options are updated to refer to the
+changed path names: B<data_directory>, B<hba_file>, B<ident_file>,
+B<external_pid_file>, B<stats_temp_directory>, B<cluster_name>.
+
+=head1 OPTIONS
+
+None.
+
+=head1 SEE ALSO
+
+L<pg_createcluster(8)>, L<pg_dropcluster(8)>, L<pg_lsclusters(1)>, L<pg_wrapper(1)>
+
+=head1 AUTHOR
+
+Christoph Berg L<E<lt>myon@debian.orgE<gt>>
diff --git a/pg_restorecluster b/pg_restorecluster
new file mode 100755
index 0000000..0a4af55
--- /dev/null
+++ b/pg_restorecluster
@@ -0,0 +1,436 @@
+#!/usr/bin/perl -wT
+
+# pg_restorecluster: restore from a pg_backupcluster backup
+#
+# Copyright (C) 2021 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 Cwd 'abs_path';
+use Getopt::Long;
+use PgCommon;
+
+my ($version, $cluster);
+
+# untaint environment
+$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
+delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
+umask 022;
+
+sub help () {
+ print "Syntax: $0 [options] <version> <cluster> <backup>
+Options:
+ -d --datadir DIR Data directory for restored cluster (default per createcluster.conf)
+ -p --port PORT Use port PORT for restored cluster (default is next free port)
+ -s --start Start cluster after restoring (default for restore from dump)
+ --archive Configure recovery from WAL archive
+ --pitr TIMEST Configure point-in-time recovery to TIMESTAMP from WAL archive
+ --wal-archive DIR Read WAL from archive DIR (default <backup>/../wal)
+";
+}
+
+my $createclusterconf = "$PgCommon::common_confdir/createcluster.conf";
+my ($datadir, $port, $start, $recovery_target_time, $archive_recovery, $wal_archive);
+
+exit 1 unless GetOptions (
+ 'd|datadir=s' => \$datadir,
+ 'p|port=s' => \$port,
+ 's|start' => \$start,
+ 'archive' => \$archive_recovery,
+ 'pitr=s' => \$recovery_target_time,
+ 'recovery-target-time=s' => \$recovery_target_time,
+ 'wal-archive=s' => \$wal_archive,
+ 'createclusterconf=s' => \$createclusterconf,
+);
+if ($recovery_target_time) {
+ ($recovery_target_time) = $recovery_target_time =~ /(.*)/; # untaint
+}
+
+# accept both "version cluster" and "version[-/]cluster"
+if (@ARGV >= 2 and $ARGV[0] =~ m!^(\d+\.?\d)[-/]([^/]+)$!) {
+ ($version, $cluster) = ($1, $2);
+ shift @ARGV;
+} elsif (@ARGV >= 3 and $ARGV[0] =~ /^(\d+\.?\d)$/) {
+ $version = $1;
+ ($cluster) = ($ARGV[1]) =~ m!^([^/]+)$!;
+ shift @ARGV;
+ shift @ARGV;
+} else {
+ help();
+ exit 1;
+}
+
+error "cluster $version $cluster already exists" if cluster_exists $version, $cluster;
+
+my %defaultconf = PgCommon::read_conf_file ($createclusterconf);
+
+# functions to be run as root
+
+sub create_cluster_directories($$$$) {
+ my ($owneruid, $ownergid, $configdir, $datadir) = @_;
+ my @postgres_user = getpwnam 'postgres';
+ my ($pg_uid, $pg_gid) = (getpwnam 'postgres')[2, 3];
+
+ for my $pgdir ("/etc/postgresql", "/etc/postgresql/$version", "/var/lib/postgresql", "/var/lib/postgresql/$version") {
+ if (! -e $pgdir) {
+ mkdir $pgdir or error "mkdir $pgdir: $!";
+ chown $pg_uid, $pg_gid, $pgdir or error "chown $pgdir: $!";
+ }
+ }
+ mkdir $configdir or error "mkdir $configdir: $!";
+ chown $owneruid, $ownergid, $configdir or error "chown $configdir: $!";
+ mkdir $datadir, 0700 or error "mkdir $datadir: $!";
+ chown $owneruid, $ownergid, $datadir or error "chown $datadir: $!";
+}
+
+sub create_cluster($$$$$) {
+ my ($backup, $owneruid, $ownergid, $configdir, $datadir) = @_;
+
+ my @createclusteropts = ();
+ if (-f "$backup/createcluster.opts") {
+ open my $fh, "$backup/createcluster.opts" or error "$backup/createcluster.opts: $!";
+ local $/; # slurp mode
+ my ($opts) = <$fh> =~ /(.*)/; # untaint
+ @createclusteropts = split /\s+/, $opts;
+ close $fh;
+ }
+
+ system_or_error "pg_createcluster",
+ "--datadir", $datadir,
+ "--user", $owneruid, "--group", $ownergid,
+ $version, $cluster,
+ "--",
+ @createclusteropts;
+}
+
+sub start_cluster() {
+ print "Starting cluster $version $cluster ...\n";
+ system_or_error "pg_ctlcluster", $version, $cluster, "start";
+}
+
+sub switch_to_cluster_owner($$) {
+ my ($owneruid, $ownergid) = @_;
+ change_ugid $owneruid, $ownergid;
+}
+
+# restore functions
+
+sub unpack_tar($$$) {
+ my ($backup, $tar, $dir) = @_;
+
+ if (-f "$backup/$tar.gz") {
+ $tar = "$tar.gz";
+ } elsif (-f "$backup/$tar.bz2") {
+ $tar = "$tar.bz2";
+ } elsif (-f "$backup/$tar.xz") {
+ $tar = "$tar.xz";
+ } elsif (-f "$backup/$tar") {
+ # do nothing
+ } else {
+ error "$backup/config.tar* is missing";
+ }
+
+ print "Restoring $backup/$tar to $dir ...\n";
+ system_or_error "tar", "-C", $dir, "-xf", "$backup/$tar";
+}
+
+sub restore_config($$) {
+ my ($backup, $configdir) = @_;
+ unpack_tar($backup, "config.tar", $configdir);
+}
+
+sub update_config($$$) {
+ my ($configdir, $datadir, $port) = @_;
+ my %settings = (
+ data_directory => $datadir,
+ hba_file => "$configdir/pg_hba.conf",
+ ident_file => "$configdir/pg_ident.conf",
+ external_pid_file => "/var/run/postgresql/$version-$cluster.pid",
+ port => $port,
+ );
+ $settings{cluster_name} = "$version/$cluster" if ($version >= 9.5);
+ $settings{stats_temp_directory} = "/var/run/postgresql/$version-$cluster.pg_stat_tmp" if ($version < 15);
+ my %config = PgCommon::read_conf_file("$configdir/postgresql.conf");
+ for my $guc (sort keys %settings) {
+ if (not exists $config{$guc} or $config{$guc} ne $settings{$guc}) {
+ print "Setting $guc = $settings{$guc}\n";
+ PgCommon::set_conffile_value("$configdir/postgresql.conf", $guc, $settings{$guc});
+ }
+ }
+}
+
+sub restore_basebackup($$) {
+ my ($backup, $datadir) = @_;
+ unpack_tar($backup, "base.tar", $datadir);
+}
+
+sub restore_wal($$) {
+ my ($backup, $datadir) = @_;
+ return if ($version < 10); # WAL contained in base.tar.gz in PG 9.x
+ unpack_tar($backup, "pg_wal.tar", "$datadir/pg_wal");
+}
+
+sub archive_recovery_options($$$) {
+ my ($backup, $datadir, $wal_archive) = @_;
+
+ $wal_archive = abs_path($wal_archive) or error "$wal_archive: $!";
+ -d $wal_archive or error "$wal_archive is not a directory";
+
+ print "Setting archive recovery options";
+ my $recovery_options = "restore_command = '/usr/share/postgresql-common/pg_getwal $wal_archive/%f %p'\n";
+ if ($recovery_target_time) {
+ $recovery_options .= "recovery_target_time = '$recovery_target_time'\n";
+ $recovery_options .= "recovery_target_action = 'promote'\n";
+ }
+
+ if ($version >= 12) {
+ my $autoconf = "$datadir/postgresql.auto.conf";
+ open my $fh, ">>", $autoconf or error "$autoconf: $!";
+ print $fh $recovery_options or error "$autoconf: $!";
+ close $fh or error "$autoconf: $!";
+
+ my $recoverysignal = "$datadir/recovery.signal";
+ open my $fh2, ">", $recoverysignal or error "$recoverysignal: $!";
+ close $fh2 or error "$recoverysignal: $!";
+
+ } else {
+ my $recoveryconf = "$datadir/recovery.conf";
+ open my $fh, ">>", $recoveryconf or error "$recoveryconf: $!";
+ print $fh $recovery_options or error "$recoveryconf: $!";
+ close $fh or error "$recoveryconf: $!";
+ }
+}
+
+sub reset_archive_recovery_options() {
+ if ($version >= 12) {
+ system_or_error "psql",
+ "--cluster", "$version/$cluster",
+ "-XAtqc", "ALTER SYSTEM RESET restore_command";
+ system_or_error "psql",
+ "--cluster", "$version/$cluster",
+ "-XAtqc", "ALTER SYSTEM RESET recovery_target_time"
+ if ($recovery_target_time);
+ system_or_error "psql",
+ "--cluster", "$version/$cluster",
+ "-XAtqc", "ALTER SYSTEM RESET recovery_target_action"
+ if ($recovery_target_time);
+ }
+}
+
+sub restore_globals($$) {
+ my ($backup, $owneruid) = @_;
+ my $owner = (getpwuid $owneruid)[0] or error "UID $owneruid has no name";
+
+ print "Restoring $backup/globals.sql ...\n";
+ open my $globals, "$backup/globals.sql" or error "$backup/globals.sql: $!";
+ open my $psql, "|-", "psql", "--cluster", "$version/$cluster", "-vON_ERROR_STOP=1", "-Xq" or error "psql: $!";
+ while (my $line = <$globals>) {
+ next if ($line eq "CREATE ROLE $owner;\n");
+ print $psql $line or error "psql: $!";
+ }
+ close $globals;
+ close $psql;
+ error "psql failed" if ($?);
+}
+
+sub create_databases($) {
+ my ($backup) = @_;
+
+ print "Creating databases from $backup/databases.sql ...\n";
+ system_or_error "psql", "--cluster", "$version/$cluster", "-vON_ERROR_STOP=1", "-Xqf", "$backup/databases.sql";
+}
+
+sub restore_dumps($) {
+ my ($backup) = @_;
+
+ for my $dump (sort glob "$backup/*.dump") {
+ $dump =~ m!(.*/([^/]*).dump)$!;
+ $dump = $1; # untaint
+ my $db = $2;
+ print "Restoring $dump to database $db ...\n";
+ system_or_error "pg_restore", "--cluster", "$version/$cluster", "-d", $db, $dump;
+ }
+}
+
+sub wait_for_recovery() {
+ my $sleep = 1;
+
+ print "Waiting for end of recovery ...\n";
+ while (1) {
+ open my $psql, "-|", "psql", "--cluster", "$version/$cluster", "-XAtc", "SELECT pg_is_in_recovery()" or error "psql: $!";
+ my $status = <$psql>;
+ error "psql: $!" unless (defined $status);
+ close $psql or error "psql: $!";
+ last if ($status eq "f\n");
+ sleep($sleep++);
+ }
+}
+
+sub analyze() {
+ system_or_error "vacuumdb",
+ "--cluster", "$version/$cluster",
+ "--analyze-only",
+ ($version >= 9.4 ? "--analyze-in-stages" : ()),
+ "--all";
+}
+
+sub lscluster() {
+ system_or_error "pg_lsclusters", $version, $cluster;
+}
+
+# main
+
+my ($backup) = $ARGV[0] =~ /(.*)/; # untaint
+error "$backup is not a directory" unless (-d $backup);
+$backup =~ s/\/$//; # strip trailing slash
+my ($owneruid, $ownergid) = (stat $backup)[4, 5];
+my $configdir = "/etc/postgresql/$version/$cluster";
+$datadir //= replace_v_c($defaultconf{data_directory} // "/var/lib/postgresql/%v/%c", $version, $cluster);
+($datadir) = $datadir =~ /(.*)/; # untaint
+$wal_archive //= "$backup/../wal";
+$port //= next_free_port();
+
+if ($backup =~ /\.backup$/) {
+ create_cluster_directories($owneruid, $ownergid, $configdir, $datadir);
+ if (fork == 0) {
+ switch_to_cluster_owner($owneruid, $ownergid);
+ restore_config($backup, $configdir);
+ update_config($configdir, $datadir, $port);
+ restore_basebackup($backup, $datadir);
+ restore_wal($backup, $datadir);
+ archive_recovery_options($backup, $datadir, $wal_archive)
+ if ($archive_recovery or $recovery_target_time);
+ exit(0);
+ }
+ wait;
+ exit(1) if ($?);
+ if ($start) {
+ print "\n";
+ start_cluster();
+ switch_to_cluster_owner($owneruid, $ownergid);
+ wait_for_recovery();
+ reset_archive_recovery_options() if ($archive_recovery or $recovery_target_time);
+ analyze();
+ }
+ print "\n";
+ lscluster();
+
+} elsif ($backup =~ /\.dump$/) {
+ create_cluster($backup, $owneruid, $ownergid, $configdir, $datadir);
+ print "\n";
+ if (fork == 0) {
+ switch_to_cluster_owner($owneruid, $ownergid);
+ restore_config($backup, $configdir);
+ update_config($configdir, $datadir, $port);
+ exit(0);
+ }
+ wait;
+ exit(1) if ($?);
+ start_cluster();
+ switch_to_cluster_owner($owneruid, $ownergid);
+ restore_globals($backup, $owneruid);
+ create_databases($backup);
+ restore_dumps($backup);
+ analyze();
+ print "\n";
+ lscluster();
+
+} else {
+ error "$backup must end in either .backup or .dump";
+}
+
+__END__
+
+=head1 NAME
+
+pg_restorecluster - Restore from a pg_backupcluster backup
+
+=head1 SYNOPSIS
+
+B<pg_restorecluster> [I<options>] I<version> I<cluster> I<backup>
+
+=head1 DESCRIPTION
+
+B<pg_restorecluster> restores a PostgreSQL cluster from a backup created by
+B<pg_backupcluster>. The cluster will be newly created in the system using the
+name provided on the command line; this allows renaming a cluster on restore.
+The restored cluster configuration will be updated to reflect the new name and
+location.
+
+The I<backup> name passed must end in either B<.basebackup> or B<.dump>;
+usually this will be the full path to a backup directory in
+C</var/backups/postgresql/version-cluster/> as reported by
+B<pg_backupcluster ... list>.
+
+Basebackups are restored as-is. For dumps, B<pg_createcluster> is used to
+create a new cluster, and schema and data are restored via B<pg_restore>.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<-d --datadir> I<DIR>
+
+Use I<DIR> as data directory for the restored cluster (default per
+createcluster.conf, by default /var/lib/postgresql/I<version>/I<cluster>).
+
+=item B<-p --port> I<N>
+
+Use port I<N> for restored cluster (default is next free port).
+
+=item B<-s --start>
+
+Start cluster after restoring (default for restore from dump; off for
+basebackup restores).
+
+After the cluster has been started, B<ANALYZE> is run on all databases.
+
+=item B<--archive>
+
+Configure cluster for recovery from WAL archive. This sets B<restore_command>
+to retrieve WAL files from I<backup>B</../wal>.
+
+=item B<--pitr> I<TIMESTAMP>
+
+=item B<--recovery-target-time> I<TIMESTAMP>
+
+Additionally to setting B<restore_command>, set B<recovery_target_time> to
+I<TIMESTAMP> for point-in-time recovery. Also sets
+B<recovery_target_action='promote'>.
+
+=item B<--wal-archive> I<DIR>
+
+For archive recovery, read WAL from archive I<DIR> (default is
+I<backup>B</../wal>).
+
+=back
+
+=head1 FILES
+
+=over 4
+
+=item /var/backups
+
+Default root directory for cluster backup directories.
+
+=back
+
+See L<pg_backupcluster(1)> for a description of files.
+
+=head1 SEE ALSO
+
+L<pg_backupcluster(1)>, L<pg_restore(1)>, L<vacuumdb(1)>.
+
+=head1 AUTHOR
+
+Christoph Berg L<E<lt>myon@debian.orgE<gt>>
diff --git a/pg_updateaptconfig b/pg_updateaptconfig
new file mode 100755
index 0000000..7e041c5
--- /dev/null
+++ b/pg_updateaptconfig
@@ -0,0 +1,41 @@
+#!/bin/sh
+
+# Tell apt which PostgreSQL versions have clusters present
+
+set -eu
+
+APTCONFDIR="/etc/apt/apt.conf.d"
+[ -d "$APTCONFDIR" ] || exit 0 # skip generation on RPM systems
+APTCONF="$APTCONFDIR/02autoremove-postgresql"
+TMPCONF="$(mktemp --tmpdir pg_updateaptconfig.XXXXXX)"
+trap "rm -f $TMPCONF" EXIT
+
+cat > $TMPCONF <<EOF
+// DO NOT EDIT!
+// File maintained by /usr/share/postgresql-common/pg_updateaptconfig.
+//
+// Mark all PostgreSQL packages as NeverAutoRemove for which PostgreSQL
+// clusters exist. This is especially important when the "postgresql" meta
+// package changes its dependencies to a new version, which might otherwise
+// trigger the old postgresql-NN package to be automatically removed, rendering
+// the old database cluster inaccessible.
+
+APT
+{
+ NeverAutoRemove
+ {
+EOF
+
+pg_lsclusters -h | cut -d ' ' -f 1 | uniq | while read version; do
+ echo " \"^postgresql.*-$version\";" >> $TMPCONF
+done
+
+cat >> $TMPCONF <<EOF
+ };
+};
+EOF
+
+if ! cmp --silent $TMPCONF $APTCONF; then
+ cp $TMPCONF $APTCONF
+ chmod 444 $APTCONF
+fi
diff --git a/pg_updatedicts b/pg_updatedicts
new file mode 100755
index 0000000..8d8b50b
--- /dev/null
+++ b/pg_updatedicts
@@ -0,0 +1,139 @@
+#!/usr/bin/perl -w
+
+# Create dictionaries and affix rules palatable for PostgreSQL, using installed
+# myspell and hunspell dictionaries.
+#
+# (C) 2008-2009 Martin Pitt <mpitt@debian.org>
+# (C) 2012-2017 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;
+my @srcdirs = ('/usr/share/hunspell', '/usr/share/myspell/dicts');
+my $cachedir = '/var/cache/postgresql/dicts';
+my $pgsharedir = '/usr/share/postgresql/';
+
+use PgCommon;
+
+# determine encoding of an .aff file
+sub get_encoding {
+ open my $fh, '<', $_[0] or die "cannot open $_[0]: $!";
+ while (<$fh>) {
+ if (/^SET ([\w-]+)\s*$/) { return $1; }
+ }
+ return undef;
+}
+
+umask 022;
+if ((system 'mkdir', '-p', $cachedir) != 0) {
+ exit 1;
+}
+
+# keep track of all up to date files, so that we can clean up cruft
+my %current;
+
+print "Building PostgreSQL dictionaries from installed myspell/hunspell packages...\n";
+for my $d (@srcdirs) {
+ for my $aff (glob "$d/*.aff") {
+ next if -l $aff; # ignore symlinks
+ my $dic = substr($aff, 0, -3) . 'dic';
+ if (! -f $dic) {
+ print STDERR "ERROR: $aff does not have corresponding $dic, ignoring\n";
+ next;
+ }
+
+ my $enc = get_encoding $aff;
+ if (!$enc) {
+ print STDERR "ERROR: no encoding defined in $aff, ignoring\n";
+ next;
+ }
+
+ my $locale = substr ((split '/', $aff)[-1], 0, -4);
+ $locale =~ tr /A-Z/a-z/;
+
+ $current{"$cachedir/$locale.affix"} = undef;
+ $current{"$cachedir/$locale.dict"} = undef;
+
+ # convert to UTF-8 and write to cache dir
+ print " $locale\n";
+ if ((system 'iconv', '-f', $enc, '-t', 'UTF-8', '-o',
+ "$cachedir/$locale.affix", $aff) != 0) {
+ unlink "$cachedir/$locale.affix";
+ print STDERR "ERROR: Conversion of $aff failed\n";
+ next;
+ }
+ if ((system 'iconv', '-f', $enc, '-t', 'UTF-8', '-o',
+ "$cachedir/$locale.dict", $dic) != 0) {
+ unlink "$cachedir/$locale.affix";
+ unlink "$cachedir/$locale.dict";
+ print STDERR "ERROR: Conversion of $dic failed\n";
+ next;
+ }
+
+ # install symlinks to all versions >= 8.3
+ foreach my $v (get_versions) {
+ next if $v < '8.3';
+ my $dest = "$pgsharedir/$v/tsearch_data";
+ next if ! -d $dest;
+ $current{"$dest/$locale.affix"} = undef;
+ $current{"$dest/$locale.dict"} = undef;
+ next if -e "$dest/$locale.affix" && ! -l "$dest/$locale.affix";
+ next if -e "$dest/$locale.dict" && ! -l "$dest/$locale.dict";
+ unlink "$dest/$locale.affix";
+ unlink "$dest/$locale.dict";
+ symlink "$cachedir/$locale.affix", "$dest/$locale.affix";
+ symlink "$cachedir/$locale.dict", "$dest/$locale.dict";
+ }
+ }
+}
+
+# clean up files for locales which do not exist any more
+print "Removing obsolete dictionary files:\n";
+foreach my $f (glob "$cachedir/*") {
+ next if exists $current{$f};
+ print " $f\n";
+ unlink $f;
+}
+foreach my $f ((glob "$pgsharedir/*/tsearch_data/*.affix"),
+ (glob "$pgsharedir/*/tsearch_data/*.dict")) {
+ next unless -l $f;
+ next if exists $current{$f};
+ print " $f\n";
+ unlink $f;
+}
+
+__END__
+
+=head1 NAME
+
+pg_updatedicts - build PostgreSQL dictionaries from myspell/hunspell ones
+
+=head1 SYNOPSIS
+
+B<pg_updatedicts>
+
+=head1 DESCRIPTION
+
+B<pg_updatedicts> makes dictionaries and affix files from installed myspell
+and hunspell dictionary packages available to PostgreSQL for usage with tsearch
+and word stem support. In particular, it takes all I<*.dic> and I<*.aff> files
+from /usr/share/myspell/dicts/, converts them to UTF-8, puts them into
+/var/cache/postgresql/dicts/ with I<*.dict> and I<*.affix> suffixes, and
+symlinks them into /usr/share/postgresql/I<version>/tsearch_data/, where
+PostgreSQL looks for them.
+
+Through postgresql-common's dpkg trigger, this program is automatically run
+whenever a myspell or hunspell dictionary package is installed or upgraded.
+
+=head1 AUTHOR
+
+Martin Pitt L<E<lt>mpitt@debian.orgE<gt>>
diff --git a/pg_upgradecluster b/pg_upgradecluster
new file mode 100755
index 0000000..06f2bf0
--- /dev/null
+++ b/pg_upgradecluster
@@ -0,0 +1,966 @@
+#!/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-2023 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 ($old_locale_provider, $old_icu_locale, $old_icu_rules);
+my $maintenance_db = 'template1';
+my $keep_on_error = 0;
+
+# 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');
+ }
+ $deprecate->(\%c, 'vacuum_cleanup_index_scale_factor', 'does not exist anymore');
+ }
+
+ if ($newversion >= '15') {
+ $deprecate->(\%c, 'stats_temp_directory', 'does not exist anymore');
+ }
+
+ if ($newversion >= '16') {
+ $deprecate->(\%c, 'promote_trigger_file', 'does not exist anymore, use pg_promote() instead');
+ $deprecate->(\%c, 'vacuum_defer_cleanup_age', 'does not exist anymore');
+ $deprecate->(\%c, 'force_parallel_mode', 'does not exist anymore');
+ }
+
+ if ($newversion >= '17') {
+ $deprecate->(\%c, 'db_user_namespace', 'does not exist anymore');
+ $deprecate->(\%c, 'old_snapshot_threshold', 'does not exist anymore');
+ }
+}
+
+sub migrate_config_files() {
+ # 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;
+ }
+ }
+ }
+}
+
+# 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, $old_locale_provider, $old_icu_locale, $old_icu_rules) = 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 ($?);
+ }
+}
+
+sub run_upgrade_scripts($) {
+ my $phase = shift;
+
+ print "Running $phase 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, $phase);
+ exit;
+ }
+
+ my @argv = ('run-parts', '--lsbsysinit', '-a', $version, '-a', $newcluster,
+ '-a', $newversion, '-a', $phase,
+ "$PgCommon::common_confdir/pg_upgradecluster.d");
+ error "$PgCommon::common_confdir/pg_upgradecluster.d script failed" if system @argv;
+ exit;
+ }
+ wait;
+ if ($? > 0) {
+ unless ($keep_on_error) {
+ print STDERR "Error during running upgrade hooks, removing new cluster\n";
+ system 'pg_dropcluster', '--stop', $newversion, $newcluster;
+ }
+ exit 1;
+ }
+}
+
+#
+# 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,
+ 'keep-on-error' => \$keep_on_error,
+ ) 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);
+} 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;
+if ($jobs) {
+ ($jobs) = $jobs =~ /^(\d+)$/;
+ $ENV{PGJOBS} = $jobs; # make setting available to upgrade hooks
+}
+
+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);
+validate_cluster_owner \%info;
+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
+
+my $upgrade_port = next_free_port;
+
+# create new cluster, preserving encoding and locales
+my @argv = ('pg_createcluster', '-u', $info{'owneruid'}, '-g', $info{'ownergid'},
+ '--socketdir', $info{'socketdir'}, '--port', $upgrade_port, '--no-status',
+ $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;
+push @argv, ('--');
+push @argv, ('--locale-provider', $old_locale_provider) if $old_locale_provider;
+push @argv, ('--icu-locale', $old_icu_locale) if $old_icu_locale;
+push @argv, ('--icu-rules', $old_icu_rules) if $old_icu_rules;
+push @argv, ('--data-checksums') if $oldcontrol->{'Data page checksum version'}; # 0 = off
+push @argv, ('--encryption-key-command', $info{config}->{encryption_key_command}) if $info{config}->{encryption_key_command}; # PostgreSQL TDE
+
+# call pg_createcluster
+delete $ENV{'LC_ALL'};
+error "Could not create target cluster" if system @argv;
+print "\n";
+
+# migrate config files to new cluster before running upgrade
+%newinfo = cluster_info($newversion, $newcluster);
+migrate_config_files();
+set_cluster_port $newversion, $newcluster, $upgrade_port; # use free port during upgrade
+%newinfo = cluster_info($newversion, $newcluster); # re-read info after migrate_config_files
+
+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
+run_upgrade_scripts('init') if ($upgrade_scripts);
+print "\n";
+
+# 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
+ 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.
+ 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";
+
+ # Move output files to our log directory
+ if (-d (my $outdir = "$newinfo{pgdata}/pg_upgrade_output.d")) {
+ system mv => $outdir, $logdir
+ and error "Could not move $outdir to $logdir";
+ }
+ rmdir $logdir; # remove it if it's empty
+ print "pg_upgradecluster: pg_upgrade output scripts are in $logdir\n" if (-d $logdir);
+
+ exit 1 if ($status != 0);
+ }
+
+ exit 0;
+}
+
+wait;
+
+if ($?) {
+ print "\n";
+
+ unless ($keep_on_error) {
+ 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)
+ system 'pg_ctlcluster', $version, $cluster, 'stop'; # ignore errors, it might be down anyway
+ if ($info{running}) {
+ print "Starting old cluster again ...\n";
+ if (system 'pg_ctlcluster', $version, $cluster, 'start') {
+ error 'could not start old cluster, please do that manually';
+ }
+ }
+ exit 1;
+}
+
+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;
+unless ($keep_port) {
+ set_cluster_port $version, $cluster, $upgrade_port;
+ set_cluster_port $newversion, $newcluster, $info{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 upgraded cluster on port $newinfo{port}...\n";
+ @argv = ('pg_ctlcluster', $newversion, $newcluster, 'start');
+ error "Could not start upgraded cluster; please check configuration and log files" if system @argv;
+}
+
+# Run upgrade scripts in finish phase
+if ($upgrade_scripts) {
+ if ($start) {
+ run_upgrade_scripts('finish');
+ } else {
+ print "Warning: Skipping upgrade scripts because --no-start was given\n";
+ }
+}
+
+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
+switching 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 passed on to pg_upgrade. See L<pg_upgrade(1)> for details.
+It is also used by the B<analyze> upgrade hook (via the B<PGJOBS> environment
+variable).
+
+=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.
+
+=item B<--keep-on-error>
+
+If upgrading fails, the newly created cluster is removed. This option disables
+that.
+
+=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>>
diff --git a/pg_upgradecluster.d/analyze b/pg_upgradecluster.d/analyze
new file mode 100755
index 0000000..f410365
--- /dev/null
+++ b/pg_upgradecluster.d/analyze
@@ -0,0 +1,33 @@
+#!/bin/sh
+#
+# Run ANALYZE on all databases in the upgraded cluster
+
+set -eu
+
+oldversion="$1"
+cluster="$2"
+newversion="$3"
+phase="$4"
+
+case $newversion in
+ 9.2|9.3)
+ analyze="--analyze-only"
+ ;;
+ *)
+ analyze="--analyze-in-stages"
+ ;;
+esac
+
+case $newversion in
+ 9.5|9.6|[1-7]*)
+ [ "${PGJOBS:-}" ] && jobs="--jobs=$PGJOBS"
+ ;;
+esac
+
+case $phase in
+ finish)
+ vacuumdb --cluster "$newversion/$cluster" --all $analyze ${jobs:-}
+ ;;
+esac
+
+exit 0
diff --git a/pg_virtualenv b/pg_virtualenv
new file mode 100755
index 0000000..f4cfe03
--- /dev/null
+++ b/pg_virtualenv
@@ -0,0 +1,280 @@
+#!/bin/bash
+
+# Create a throw-away PostgreSQL environment for running regression tests.
+# This does not interfere with existing clusters.
+#
+# (C) 2005-2012 Martin Pitt <mpitt@debian.org>
+# (C) 2012-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.
+
+set -e # no -u here as that breaks PGCONF_OPTS[@]
+
+# wrap ourselves in newpid if requested
+if [ "$PG_VIRTUALENV_NEWPID" ]; then
+ unset PG_VIRTUALENV_NEWPID
+ exec newpid $0 "$@"
+fi
+
+# wrap ourselves in unshare if requested
+if [ "$PG_VIRTUALENV_UNSHARE" ]; then
+ export _PG_VIRTUALENV_UNSHARE="$PG_VIRTUALENV_UNSHARE"
+ unset PG_VIRTUALENV_UNSHARE
+ exec unshare $_PG_VIRTUALENV_UNSHARE -- $0 "$@"
+fi
+if [ "$_PG_VIRTUALENV_UNSHARE" ]; then
+ unset _PG_VIRTUALENV_UNSHARE
+ # start localhost interface
+ if [ -x /bin/ip ]; then
+ ip link set dev lo up || true
+ else
+ ifconfig lo up || true
+ fi
+fi
+
+disable_fakeroot ()
+{
+ case ${LD_PRELOAD:-} in
+ *fakeroot*) LD_PRELOAD=$(echo "$LD_PRELOAD" | sed -e 's/[^ ]*fakeroot[^ ]*//g') ;;
+ esac
+}
+
+help ()
+{
+ echo "pg_virtualenv: Create throw-away PostgreSQL environment for regression tests"
+ echo "Syntax: $0 [options] [command]"
+ echo " -a use all installed server versions"
+ echo " -v 'version ...' list of PostgreSQL versions to run [default: latest]"
+ echo " -c 'options' extra options to pass to pg_createcluster"
+ echo " -i 'initdb opts' extra initdb options to pass to pg_createcluster"
+ echo " -o 'guc=value' postgresql.conf options to pass to pg_createcluster"
+ echo " -p 'package' set options to find extension files in debian/package/"
+ echo " -s open a shell when command fails"
+ echo " -t use a temporary cluster directory even as root"
+ exit ${1:-0}
+}
+
+# option parsing
+PGBINROOT="/usr/lib/postgresql/"
+#redhat# PGBINROOT="/usr/pgsql-"
+PG_VERSIONS=""
+PGCONF_OPTS=()
+while getopts "ac:i:ho:p:stv:" opt ; do
+ case $opt in
+ a) for d in $PGBINROOT*/bin/pg_ctl; do
+ # prepend version so latest ends up first (i.e. on port 5432)
+ dir=${d%%/bin/pg_ctl}
+ PG_VERSIONS="${dir#$PGBINROOT} ${PG_VERSIONS:-}"
+ done ;;
+ c) CREATE_OPTS="$OPTARG" ;;
+ i) INITDB_OPTS="$OPTARG" ;;
+ h) help ;;
+ o) PGCONF_OPTS+=("--pgoption" "$OPTARG") ;;
+ p) PACKAGE="$OPTARG" ;;
+ s) run_shell=1 ;;
+ t) NONROOT=1 ;;
+ v) PG_VERSIONS="$OPTARG" ;;
+ *) help 1 ;;
+ esac
+done
+if [ -z "$PG_VERSIONS" ]; then
+ # use latest version
+ d=$(ls -v $PGBINROOT*/bin/pg_ctl 2> /dev/null | tail -1)
+ if [ -z "$d" ]; then
+ echo "Could not determine PostgreSQL version, are any PostgreSQL server packages installed?" >&2
+ exit 2
+ fi
+ dir=${d%%/bin/pg_ctl}
+ PG_VERSIONS="${dir#$PGBINROOT}"
+fi
+# shift away args
+shift $(($OPTIND - 1))
+# if no command is given, open a shell
+[ "${1:-}" ] || set -- ${SHELL:-/bin/sh}
+
+# generate a password
+if [ -x /usr/bin/pwgen ]; then
+ export PGPASSWORD=$(pwgen 20 1)
+else
+ export PGPASSWORD=$(dd if=/dev/urandom bs=1k count=1 2>/dev/null | md5sum - | awk '{ print $1 }')
+fi
+
+# we are not root
+if [ "$(id -u)" != 0 ]; then
+ NONROOT=1
+fi
+# we aren't really root in fakeroot (but leave it enabled for commands spawned)
+case ${LD_PRELOAD:-} in
+ *fakeroot*) NONROOT=1 ;;
+esac
+
+# non-root operation: create a temp dir where we store everything
+if [ "${NONROOT:-}" ]; then
+ WORKDIR=$(mktemp -d -t pg_virtualenv.XXXXXX)
+ if [ $(id -u) = 0 ]; then
+ chown postgres:postgres $WORKDIR
+ umask 022
+ fi
+ export PG_CLUSTER_CONF_ROOT="$WORKDIR/postgresql"
+ export PGUSER="${USER:-${LOGNAME:-$(id -un)}}"
+ [ "$PGUSER" = "root" ] && PGUSER="postgres"
+ PGSYSCONFDIR="$WORKDIR/postgresql-common" # no export yet so pg_createcluster uses the original createcluster.conf
+ mkdir "$PGSYSCONFDIR" "$WORKDIR/log"
+ PWFILE="$PGSYSCONFDIR/pwfile"
+ LOGDIR="$WORKDIR/log"
+ echo "$PGPASSWORD" > "$PWFILE"
+
+ cleanup () {
+ set +e
+ disable_fakeroot
+ for v in $PG_VERSIONS; do
+ # don't drop existing clusters named "regress"
+ [ -f $PG_CLUSTER_CONF_ROOT/$v/regress/.by_pg_virtualenv ] || continue
+ echo "Dropping cluster $v/regress ..."
+ pg_ctlcluster --mode immediate $v regress stop
+ pg_dropcluster $v regress
+ done
+ rm -rf $WORKDIR
+ }
+ trap cleanup 0 HUP INT QUIT ILL ABRT PIPE TERM
+
+# root: keep everything in the standard locations
+else
+ for v in $PG_VERSIONS; do
+ if [ -d /etc/postgresql/$v/regress ]; then
+ echo "Cluster $v/regress exists, refusing to overwrite" >&2
+ exit 2
+ fi
+ done
+
+ : ${PGSYSCONFDIR:=/etc/postgresql-common}
+ pg_service="$PGSYSCONFDIR/pg_service.conf"
+
+ export PGUSER="postgres"
+ PWFILE=$(mktemp -t pgpassword.XXXXXX)
+ echo "$PGPASSWORD" > "$PWFILE" # write password before chowning the file
+ chown postgres:postgres "$PWFILE"
+
+ cleanup () {
+ set +e
+ rm -f $PWFILE $pg_service
+ if [ -f $pg_service.pg_virtualenv-save.$$ ]; then
+ mv -f $pg_service.pg_virtualenv-save.$$ $pg_service
+ fi
+ for v in $PG_VERSIONS; do
+ # don't drop existing clusters named "regress"
+ [ -f /etc/postgresql/$v/regress/.by_pg_virtualenv ] || continue
+ echo "Dropping cluster $v/regress ..."
+ rm -f /etc/postgresql/$v/regress/.by_pg_virtualenv
+ pg_ctlcluster --mode immediate $v regress stop
+ pg_dropcluster $v regress
+ done
+ }
+ trap cleanup 0 HUP INT QUIT ILL ABRT PIPE TERM
+
+ if [ -f $pg_service ]; then
+ mv --no-clobber $pg_service $pg_service.pg_virtualenv-save.$$
+ fi
+fi
+
+# create postgres environments
+for v in $PG_VERSIONS; do
+ # create temporary cluster
+ if [ "${PACKAGE:-}" ]; then
+ if ! grep -q 'extension_destdir' /usr/share/postgresql/$v/postgresql.conf.sample; then
+ echo "$0: This PostgreSQL $v installation does not support 'extension_destdir', skipping this version"
+ [ "$PG_VERSIONS" = "$v" ] && exit 0 # only one version requested
+ continue
+ fi
+ PKGARGS="--pgoption extension_destdir=$PWD/debian/$PACKAGE --pgoption dynamic_library_path=$PWD/debian/$PACKAGE/usr/lib/postgresql/$v/lib:/usr/lib/postgresql/$v/lib"
+ fi
+ # we chdir to / so programs don't throw "could not change directory to ..."
+ (
+ cd /
+ case $v in
+ 8*|9.0|9.1|9.2) : ;;
+ *) NOSYNC="--nosync" ;;
+ esac
+ if [ "${NONROOT:-}" ]; then
+ # stats_temp_directory in /var/run/postgresql needs root (or postgres), disable it
+ case $v in
+ 8.4|9.*|1[01234]) PGCONF_OPTS+=("--pgoption" "stats_temp_directory=") ;;
+ esac
+ fi
+ # disable fakeroot here because we really can't run as root
+ # (and still switch to postgres when using fakeroot as root user)
+ disable_fakeroot
+ pg_createcluster --quiet \
+ ${PGPORT:+-p "$PGPORT"} \
+ ${NONROOT:+-d "$WORKDIR/data/$v/regress"} \
+ ${NONROOT:+-l "$WORKDIR/log/postgresql-$v-regress.log"} \
+ ${CREATE_OPTS:-} --pgoption fsync=off ${PKGARGS:-} "${PGCONF_OPTS[@]}" \
+ $v regress -- \
+ --username="$PGUSER" --pwfile="$PWFILE" $NOSYNC ${INITDB_OPTS:-}
+ # in fakeroot, the username will likely default to "postgres" otherwise
+ echo "This is a temporary throw-away cluster" > ${PG_CLUSTER_CONF_ROOT:-/etc/postgresql}/$v/regress/.by_pg_virtualenv
+ # start cluster with coredumps enabled
+ [ $v != 8.2 ] && ENABLE_COREDUMPS="-c"
+ pg_ctlcluster $v regress start -- ${ENABLE_COREDUMPS:-}
+ )
+ port=$(pg_conftool -s $v regress show port)
+
+ # record cluster information in service file
+ cat >> $PGSYSCONFDIR/pg_service.conf <<EOF
+[$v]
+host=localhost
+port=$port
+dbname=postgres
+user=$PGUSER
+password=$PGPASSWORD
+
+EOF
+done
+
+export PGSYSCONFDIR
+export PGHOST="localhost"
+export PGDATABASE="postgres"
+unset PGHOSTADDR PGPORT PGSERVICE # unset variables that might interfere
+case $PG_VERSIONS in
+ *\ *) ;; # multiple versions: do not set PGPORT because that breaks --cluster
+ *)
+ export PGPORT="$port"
+ export PG_CONFIG="$PGBINROOT$PG_VERSIONS/bin/pg_config"
+ export PGVERSION="$PG_VERSIONS"
+ ;;
+esac
+
+# run program
+"$@" || EXIT="$?"
+if [ ${EXIT:-0} -gt 0 ]; then
+ for log in ${LOGDIR:-/var/log/postgresql}/*.log; do
+ echo "*** $log (last 100 lines) ***"
+ tail -100 $log
+ done
+
+ if command -v gdb > /dev/null; then
+ for v in $PG_VERSIONS; do
+ PGDATA=$(pg_conftool -s $v regress show data_directory)
+ for core in $PGDATA/core*; do
+ [ -f "$core" ] || continue
+ echo "*** $core ***"
+ gdb -batch -ex "bt full" /usr/lib/postgresql/$v/bin/postgres "$core"
+ done
+ done
+ fi
+
+ if [ "${run_shell:-}" ]; then
+ echo "pg_virtualenv: command exited with status $EXIT, dropping you into a shell"
+ ${SHELL:-/bin/sh}
+ fi
+fi
+
+exit ${EXIT:-0}
diff --git a/pg_virtualenv.pod b/pg_virtualenv.pod
new file mode 100644
index 0000000..e1e05f0
--- /dev/null
+++ b/pg_virtualenv.pod
@@ -0,0 +1,122 @@
+=head1 NAME
+
+pg_virtualenv - Create a throw-away PostgreSQL environment for running regression tests
+
+=head1 SYNOPSIS
+
+B<pg_virtualenv> [I<OPTIONS>] [B<-v> 'I<version ...>'] [I<command>]
+
+=head1 DESCRIPTION
+
+B<pg_virtualenv> creates a virtual PostgreSQL server environment, and sets
+environment variables such that I<command> can access the PostgreSQL database
+server(s). The servers are destroyed when I<command> exits.
+
+The environment variables B<PGHOST>, B<PGDATABASE>, B<PGUSER>, and
+B<PGPASSWORD> will be set. Per default, a single new cluster is created,
+using the newest PostgreSQL server version installed. The cluster will use the
+first available port number starting from B<5432>, and B<PGPORT> will be set.
+B<PGVERSION> is set the the PostgreSQL major version number.
+
+When clusters for more than one versions are created, they will differ in the
+port number used, and B<PGPORT> and B<PGVERSION> are not set. The clusters are
+named I<version>/regress. To access a cluster, set
+B<PGCLUSTER=>I<version>B</regress>. For ease of access, the clusters are also
+registered in F</etc/postgresql-common/pg_service.conf>, with the version
+number as cluster name. Clusters can be accessed by passing the connection
+string "B<service=>I<version>", e.g. B<psql service=9.2>.
+
+When invoked as root, the clusters are created in F</etc/postgresql/> as usual;
+for other users, B<PG_CLUSTER_CONF_ROOT> and B<PGSYSCONFDIR> are
+set to a temporary directory where all files belonging to the clusters are
+created.
+
+If I<command> fails, the tail of the PostgreSQL server log is shown.
+Additionally, if B<gdb> is available, the backtrace from any PostgreSQL
+coredump is show.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<-a>
+
+Use all PostgreSQL server versions installed.
+
+=item B<-v> I<version ...>
+
+Use these versions (space-separated list).
+
+=item B<-c> I<pg_createcluster options>
+
+Extra options to pass to B<pg_createcluster>.
+
+=item B<-i> I<initdb options>
+
+Extra initdb options to pass to B<pg_createcluster>.
+
+=item B<-o> I<guc>B<=>I<value>
+
+Configuration option to set in the C<postgresql.conf> file, passed to
+B<pg_createcluster>.
+
+=item B<-p> I<package>
+
+Set B<extension_destdir> and B<dynamic_library_path> in cluster to enable
+loading and testing extensions at build-time from B<debian/>I<package>B</>.
+
+This is a Debian-specific PostgreSQL patch.
+
+=item B<-s>
+
+Launch a shell inside the virtual environment when I<command> fails.
+
+=item B<-t>
+
+Install clusters in a temporary directory, even when running as root.
+
+=item B<-h>
+
+Show program help.
+
+=back
+
+=head1 EXAMPLE
+
+ # pg_virtualenv make check
+
+=head1 NOTES
+
+When run with fakeroot(1), B<pg_virtualenv> will fall back to the non-root mode
+of operation. Running "fakeroot pg_virtualenv" as root will fail, though.
+
+=head1 ENVIRONMENT
+
+=over 4
+
+=item B<PG_VIRTUALENV_NEWPID>=yes
+
+When non-empty, B<pg_virtualenv> will re-exec itself using newpid(1).
+
+=item B<PG_VIRTUALENV_UNSHARE>=I<flags>
+
+When non-empty, B<pg_virtualenv> will re-exec itself using unshare(1) using
+these flags.
+
+=item B<PGPORT>=I<n>
+
+When set, the value is used for the (single) cluster created.
+
+=back
+
+=head1 COMPATIBILITY
+
+B<PGVERSION> is set in postgresql-common (>= 219~).
+
+=head1 SEE ALSO
+
+initdb(1), pg_createcluster(1).
+
+=head1 AUTHOR
+
+Christoph Berg L<E<lt>myon@debian.orgE<gt>>
diff --git a/pg_wrapper b/pg_wrapper
new file mode 100755
index 0000000..5b741c5
--- /dev/null
+++ b/pg_wrapper
@@ -0,0 +1,329 @@
+#!/usr/bin/perl
+# Call a PostgreSQL client program with the version, cluster and default
+# database specified in ~/.postgresqlrc or
+# /etc/postgresql-common/user_clusters.
+#
+# (C) 2005-2009 Martin Pitt <mpitt@debian.org>
+# (C) 2013-2022 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 warnings;
+use strict;
+use POSIX;
+use PgCommon;
+
+my ($version, $cluster);
+my $explicit_host = exists $ENV{PGHOST};
+my $explicit_port = $ENV{PGPORT};
+my $explicit_service = exists $ENV{PGSERVICE};
+
+# Evaluate PGCLUSTER (unless PGHOST is set as well)
+if (exists $ENV{'PGCLUSTER'} and not $explicit_host) {
+ ($version, $cluster) = split ('/', $ENV{'PGCLUSTER'}, 2);
+ error "Invalid version $version specified in PGCLUSTER" unless version_exists $version;
+ error 'No cluster specified with $PGCLUSTER' unless $cluster;
+}
+
+# Check for --cluster argument and filter it out
+for (my $i = 0; $i <= $#ARGV; ++$i) {
+ last if $ARGV[$i] eq '--';
+
+ if ($ARGV[$i] eq '--cluster') {
+ error '--cluster option needs an argument (<version>/<cluster>)' if ($i >= $#ARGV);
+
+ ($version, $cluster) = split ('/', $ARGV[$i+1], 2);
+ error "Invalid version $version specified with --cluster" unless version_exists $version;
+ error 'No cluster specified with --cluster' unless $cluster;
+
+ splice @ARGV, $i, 2;
+ last;
+ } elsif ($ARGV[$i] =~ /^--cluster=(\d+\.?\d)\/(.+)/) {
+ ($version, $cluster) = ($1, $2);
+ error "Invalid version $version specified with --cluster" unless version_exists $version;
+ error 'No cluster specified with --cluster' unless $cluster;
+
+ splice @ARGV, $i, 1;
+ last;
+ }
+ # --host or -h on command line, drop info from PGCLUSTER
+ if ($ARGV[$i] =~ /^--host\b/ || $ARGV[$i] =~ /^-\w*h\w*$/) {
+ ($version, $cluster) = (undef, undef);
+ $explicit_host = 1;
+ delete $ENV{PGCLUSTER};
+ }
+ # --port or -p on command line
+ if ($ARGV[$i] =~ /^--port\b(?:=(\d+))?/ || $ARGV[$i] =~ /^-\w*p(\d+)?$/) {
+ if (defined $1) {
+ $explicit_port = $1;
+ } elsif ($i < $#ARGV) {
+ $explicit_port = $ARGV[$i+1];
+ }
+ }
+ # "service=" in connection string
+ if ($ARGV[$i] =~ /\bservice=/) {
+ $explicit_service = 1;
+ }
+}
+
+# if only a port is specified, look for local cluster on specified port
+if ($explicit_port and not $version and not $cluster and not $explicit_host and not $explicit_service) {
+ LOOP: foreach my $v (reverse get_versions()) {
+ foreach my $c (get_version_clusters $v) {
+ my $p = get_cluster_port $v, $c;
+ if ($p eq $explicit_port) {
+ $version = $v;
+ # set PGCLUSTER variable for information
+ $ENV{PGCLUSTER} = "$version/$c";
+ last LOOP;
+ }
+ }
+ }
+}
+
+# if we don't have a cluster, and no specific host or port was given, consult postgresqlrc
+# or fall back to default port cluster (on 5432), or undef otherwise
+my ($db);
+($version, $cluster, $db) = user_cluster_map() unless ($cluster or $explicit_host or $explicit_port);
+
+my ($host, $port);
+
+if ($cluster) {
+ # check if we have a network cluster (N.N/the.host.name:port)
+ if ($cluster =~ /^(\S+):(\d*)$/) {
+ $host = $1;
+ $port = $2 || $PgCommon::defaultport;
+ } elsif (not cluster_exists($version, $cluster)) {
+ # a specific cluster was requested, error out because it doesn't exist
+ error "Cluster $version $cluster does not exist";
+ } else {
+ $host = get_cluster_socketdir ($version, $cluster);
+ $port = get_cluster_port($version, $cluster);
+ }
+ # set PGCLUSTER variable for information
+ $ENV{PGCLUSTER} = "$version/$cluster";
+}
+
+# setup environment
+$ENV{'PGSYSCONFDIR'} //= '/etc/postgresql-common';
+$ENV{'PGHOST'} = $host if ($host);
+$ENV{'PGPORT'} = $port if $port && !$ENV{'PGPORT'};
+$ENV{'PGDATABASE'} = $db if $db && !$ENV{'PGDATABASE'};
+
+# check under which name we were called
+my $cmdname = (split '/', $0)[-1];
+
+unless ($version or $explicit_host or $explicit_port or $explicit_service) {
+ print STDERR "Warning: No existing cluster is suitable as a default target. Please see man pg_wrapper(1) how to specify one.\n";
+}
+
+# if we have no version yet, use the latest version. If we were called as psql,
+# pg_archivecleanup, or pg_isready, always use latest version
+if (not $version or $cmdname =~ /^(psql|pg_archivecleanup|pg_isready)$/) {
+ my $max_version;
+ if ($version and $version < 9.2) { # psql 15 only supports PG 9.2+
+ $max_version = 14;
+ }
+ $version = get_newest_version($cmdname, $max_version);
+}
+unless ($version) {
+ error 'You must install at least one postgresql-client-<version> package';
+}
+error "PostgreSQL version $version is not installed" unless -d "$PgCommon::binroot$version";
+
+my $cmd;
+if ($cmdname eq 'pg_wrapper') {
+ error "pg_wrapper called directly but no program given as argument"
+ if (@ARGV == 0);
+ $cmd = shift; # will be unshifted back below
+} else {
+ $cmd = get_program_path ($cmdname, $version);
+}
+
+# libreadline is a lot better than libedit, so prefer that on versions that still use it
+if ($cmdname eq 'psql' and $version < 13 and not $PgCommon::rpm) {
+ my @readlines;
+ # non-multiarch path
+ @readlines = sort(</lib/libreadline.so.?>);
+
+ unless (@readlines) {
+ # get multiarch dir for our architecture
+ if (open PS, '-|', '/usr/bin/ldd', $cmd) {
+ my $out;
+ read PS, $out, 10000;
+ close PS;
+ if ($out =~ m!/libreadline.so!) {
+ # already linked against libreadline
+ @readlines = ();
+ }
+ else
+ {
+ my ($lib_path) = $out =~ m!(/lib/.*)/libedit.so!;
+
+ @readlines = sort(<$lib_path/libreadline.so.?>);
+ }
+ }
+ }
+
+ if (@readlines) {
+ $ENV{'LD_PRELOAD'} = ($ENV{'LD_PRELOAD'} or '') . ':' . $readlines[-1];
+ }
+}
+
+error "pg_wrapper: $cmdname was not found in $PgCommon::binroot$version/bin" unless $cmd;
+unshift @ARGV, $cmd;
+exec @ARGV;
+
+__END__
+
+=head1 NAME
+
+pg_wrapper - wrapper for PostgreSQL client commands
+
+=head1 SYNOPSIS
+
+I<client-program> [B<--cluster> I<version>/I<cluster>] [...]
+
+(I<client-program>: B<psql>, B<createdb>, B<dropuser>, and all other client
+programs installed in C</usr/lib/postgresql/>I<version>C</bin>).
+
+=head1 DESCRIPTION
+
+This program is run only as a link to names which correspond to PostgreSQL
+programs in C</usr/lib/postgresql/>I<version>C</bin>. It determines the
+configured cluster and database for the user and calls the appropriate version
+of the desired program to connect to that cluster and database, supplying any
+specified options to that command.
+
+The target cluster is selected by the following means, in descending order of
+precedence:
+
+=over
+
+=item
+
+explicit specification with the B<--host> option
+
+=item
+
+explicit specification with the B<--cluster> option
+
+=item
+
+if the B<PGHOST> environment variable is set, no further cluster selection is
+performed. The default PostgreSQL version and port number (from the command
+line, the environment variable B<PGPORT>, or default 5432) will be used.
+
+=item
+
+explicit specification with the B<PGCLUSTER> environment variable
+
+=item
+
+if a port is given (either via B<-p>, B<--port>, or B<PGPORT>), and no host is
+given, the local cluster matching that port number is used
+
+=item
+
+matching entry in C<~/.postgresqlrc> (see L<postgresqlrc(5)>), if that
+file exists
+
+=item
+
+matching entry in C</etc/postgresql-common/user_clusters> (see
+L<user_clusters(5)>), if that file exists
+
+=item
+
+If only one cluster exists on the local system, that one will be selected.
+
+=item
+
+If several clusters exist on the local system, the one listening on the default port 5432
+will be selected.
+
+=back
+
+If none of these rules match, B<pg_wrapper> does not set any environment
+variables and the program called will likely error out with a message like
+"could not connect to server: Connection refused".
+
+For B<psql>, B<pg_archivecleanup>, and B<pg_isready>, B<pg_wrapper> will always use the binary from
+the newest PostgreSQL version installed, as these are downwards compatible.
+If the cluster version is older than 9.2, the newest considered binary version is 14.
+
+Note that B<pg_wrapper> needs to be able to read the server config to get the
+port number to connect to. If a non-standard port is configured in a place that
+pg_wrapper cannot read, connecting will fail. This particularly holds if the
+port was configured via B<ALTER SYSTEM> in C<postgresql.auto.conf> and
+pg_wrapper is invoked as any user other than B<postgres> and B<root>.
+
+=head1 OPTIONS
+
+=over
+
+=item B<--cluster> I<version>B</>I<cluster>
+
+=item B<--cluster> I<version>B</>I<host>B<:>[I<port>]
+
+I<cluster> is either the name of a cluster on the local system, or takes the form
+I<host>:I<port> for a remote cluster. If I<port> is left empty (i. e. you just
+specify I<host:>), it defaults to 5432.
+
+=back
+
+=head1 ENVIRONMENT
+
+=over
+
+=item B<PGCLUSTER>
+
+If C<$PGCLUSTER> is set, its value (of the form I<version>/I<cluster>)
+specifies the desired cluster, similar to the B<--cluster> option. However, if
+B<--cluster> is specified, it overrides the value of C<$PGCLUSTER>.
+
+=item B<PG_CLUSTER_CONF_ROOT>
+
+This specifies an alternative base directory for cluster configurations. This
+is usually C</etc/postgresql/>, but for testing/development purposes you can
+change this to point to e. g. your home directory, so that you can use the
+postgresql-common tools without root privileges.
+
+=item B<PGSYSCONFDIR>
+
+This is the location of PostgreSQL's and postgresql-common's global
+configuration (e. g. C<pg_service.conf>, L<user_clusters(5)>). The default is
+C</etc/postgresql-common/>.
+
+=back
+
+=head1 FILES
+
+=over
+
+=item C</etc/postgresql-common/user_clusters>
+
+stores the default cluster and database for users and groups as set by
+the administrators.
+
+=item C<$HOME/.postgresqlrc>
+
+stores defaults set by the user himself.
+
+=back
+
+=head1 SEE ALSO
+
+L<user_clusters(5)>, L<postgresqlrc(5)>
+
+=head1 AUTHOR
+
+Martin Pitt L<E<lt>mpitt@debian.orgE<gt>>
diff --git a/pgdg/Makefile b/pgdg/Makefile
new file mode 100644
index 0000000..1dc7152
--- /dev/null
+++ b/pgdg/Makefile
@@ -0,0 +1,2 @@
+all:
+ if [ -f $(HOME)/apt.postgresql.org/pgapt.conf ]; then ./update; fi
diff --git a/pgdg/apt.postgresql.org.asc b/pgdg/apt.postgresql.org.asc
new file mode 100644
index 0000000..8480576
--- /dev/null
+++ b/pgdg/apt.postgresql.org.asc
@@ -0,0 +1,77 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQINBE6XR8IBEACVdDKT2HEH1IyHzXkb4nIWAY7echjRxo7MTcj4vbXAyBKOfjja
+UrBEJWHN6fjKJXOYWXHLIYg0hOGeW9qcSiaa1/rYIbOzjfGfhE4x0Y+NJHS1db0V
+G6GUj3qXaeyqIJGS2z7m0Thy4Lgr/LpZlZ78Nf1fliSzBlMo1sV7PpP/7zUO+aA4
+bKa8Rio3weMXQOZgclzgeSdqtwKnyKTQdXY5MkH1QXyFIk1nTfWwyqpJjHlgtwMi
+c2cxjqG5nnV9rIYlTTjYG6RBglq0SmzF/raBnF4Lwjxq4qRqvRllBXdFu5+2pMfC
+IZ10HPRdqDCTN60DUix+BTzBUT30NzaLhZbOMT5RvQtvTVgWpeIn20i2NrPWNCUh
+hj490dKDLpK/v+A5/i8zPvN4c6MkDHi1FZfaoz3863dylUBR3Ip26oM0hHXf4/2U
+A/oA4pCl2W0hc4aNtozjKHkVjRx5Q8/hVYu+39csFWxo6YSB/KgIEw+0W8DiTII3
+RQj/OlD68ZDmGLyQPiJvaEtY9fDrcSpI0Esm0i4sjkNbuuh0Cvwwwqo5EF1zfkVj
+Tqz2REYQGMJGc5LUbIpk5sMHo1HWV038TWxlDRwtOdzw08zQA6BeWe9FOokRPeR2
+AqhyaJJwOZJodKZ76S+LDwFkTLzEKnYPCzkoRwLrEdNt1M7wQBThnC5z6wARAQAB
+tBxQb3N0Z3JlU1FMIERlYmlhbiBSZXBvc2l0b3J5iQJOBBMBCAA4AhsDBQsJCAcD
+BRUKCQgLBRYCAwEAAh4BAheAFiEEuXsK/KoaR/BE8kSgf8x9RqzMTPgFAlhtCD8A
+CgkQf8x9RqzMTPgECxAAk8uL+dwveTv6eH21tIHcltt8U3Ofajdo+D/ayO53LiYO
+xi27kdHD0zvFMUWXLGxQtWyeqqDRvDagfWglHucIcaLxoxNwL8+e+9hVFIEskQAY
+kVToBCKMXTQDLarz8/J030Pmcv3ihbwB+jhnykMuyyNmht4kq0CNgnlcMCdVz0d3
+z/09puryIHJrD+A8y3TD4RM74snQuwc9u5bsckvRtRJKbP3GX5JaFZAqUyZNRJRJ
+Tn2OQRBhCpxhlZ2afkAPFIq2aVnEt/Ie6tmeRCzsW3lOxEH2K7MQSfSu/kRz7ELf
+Cz3NJHj7rMzC+76Rhsas60t9CjmvMuGONEpctijDWONLCuch3Pdj6XpC+MVxpgBy
+2VUdkunb48YhXNW0jgFGM/BFRj+dMQOUbY8PjJjsmVV0joDruWATQG/M4C7O8iU0
+B7o6yVv4m8LDEN9CiR6r7H17m4xZseT3f+0QpMe7iQjz6XxTUFRQxXqzmNnloA1T
+7VjwPqIIzkj/u0V8nICG/ktLzp1OsCFatWXh7LbU+hwYl6gsFH/mFDqVxJ3+DKQi
+vyf1NatzEwl62foVjGUSpvh3ymtmtUQ4JUkNDsXiRBWczaiGSuzD9Qi0ONdkAX3b
+ewqmN4TfE+XIpCPxxHXwGq9Rv1IFjOdCX0iG436GHyTLC1tTUIKF5xV4Y0+cXIOI
+RgQQEQgABgUCTpdI7gAKCRDFr3dKWFELWqaPAKD1TtT5c3sZz92Fj97KYmqbNQZP
++ACfSC6+hfvlj4GxmUjp1aepoVTo3weJAhwEEAEIAAYFAk6XSQsACgkQTFprqxLS
+p64F8Q//cCcutwrH50UoRFejg0EIZav6LUKejC6kpLeubbEtuaIH3r2zMblPGc4i
++eMQKo/PqyQrceRXeNNlqO6/exHozYi2meudxa6IudhwJIOn1MQykJbNMSC2sGUp
+1W5M1N5EYgt4hy+qhlfnD66LR4G+9t5FscTJSy84SdiOuqgCOpQmPkVRm1HX5X1+
+dmnzMOCk5LHHQuiacV0qeGO7JcBCVEIDr+uhU1H2u5GPFNHm5u15n25tOxVivb94
+xg6NDjouECBH7cCVuW79YcExH/0X3/9G45rjdHlKPH1OIUJiiX47OTxdG3dAbB4Q
+fnViRJhjehFscFvYWSqXo3pgWqUsEvv9qJac2ZEMSz9x2mj0ekWxuM6/hGWxJdB+
++985rIelPmc7VRAXOjIxWknrXnPCZAMlPlDLu6+vZ5BhFX0Be3y38f7GNCxFkJzl
+hWZ4Cj3WojMj+0DaC1eKTj3rJ7OJlt9S9xnO7OOPEUTGyzgNIDAyCiu8F4huLPaT
+ape6RupxOMHZeoCVlqx3ouWctelB2oNXcxxiQ/8y+21aHfD4n/CiIFwDvIQjl7dg
+mT3u5Lr6yxuosR3QJx1P6rP5ZrDTP9khT30t+HZCbvs5Pq+v/9m6XDmi+NlU7Zuh
+Ehy97tL3uBDgoL4b/5BpFL5U9nruPlQzGq1P9jj40dxAaDAX/WKJAj0EEwEIACcC
+GwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AFAlB5KywFCQPDFt8ACgkQf8x9RqzM
+TPhuCQ//QAjRSAOCQ02qmUAikT+mTB6baOAakkYq6uHbEO7qPZkv4E/M+HPIJ4wd
+nBNeSQjfvdNcZBA/x0hr5EMcBneKKPDj4hJ0panOIRQmNSTThQw9OU351gm3YQct
+AMPRUu1fTJAL/AuZUQf9ESmhyVtWNlH/56HBfYjE4iVeaRkkNLJyX3vkWdJSMwC/
+LO3Lw/0M3R8itDsm74F8w4xOdSQ52nSRFRh7PunFtREl+QzQ3EA/WB4AIj3VohIG
+kWDfPFCzV3cyZQiEnjAe9gG5pHsXHUWQsDFZ12t784JgkGyO5wT26pzTiuApWM3k
+/9V+o3HJSgH5hn7wuTi3TelEFwP1fNzI5iUUtZdtxbFOfWMnZAypEhaLmXNkg4zD
+kH44r0ss9fR0DAgUav1a25UnbOn4PgIEQy2fgHKHwRpCy20d6oCSlmgyWsR40EPP
+YvtGq49A2aK6ibXmdvvFT+Ts8Z+q2SkFpoYFX20mR2nsF0fbt1lfH65P64dukxeR
+GteWIeNakDD40bAAOH8+OaoTGVBJ2ACJfLVNM53PEoftavAwUYMrR910qvwYfd/4
+6rh46g1Frr9SFMKYE9uvIJIgDsQB3QBp71houU4H55M5GD8XURYs+bfiQpJG1p7e
+B8e5jZx1SagNWc4XwL2FzQ9svrkbg1Y+359buUiP7T6QXX2zY++JAj0EEwEIACcC
+GwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AFAlEqbZUFCQg2wEEACgkQf8x9RqzM
+TPhFMQ//WxAfKMdpSIA9oIC/yPD/dJpY/+DyouOljpE6MucMy/ArBECjFTBwi/j9
+NYM4ynAk34IkhuNexc1i9/05f5RM6+riLCLgAOsADDbHD4miZzoSxiVr6GQ3YXMb
+OGld9kV9Sy6mGNjcUov7iFcf5Hy5w3AjPfKuR9zXswyfzIU1YXObiiZT38l55pp/
+BSgvGVQsvbNjsff5CbEKXS7q3xW+WzN0QWF6YsfNVhFjRGj8hKtHvwKcA02wwjLe
+LXVTm6915ZUKhZXUFc0vM4Pj4EgNswH8Ojw9AJaKWJIZmLyW+aP+wpu6YwVCicxB
+Y59CzBO2pPJDfKFQzUtrErk9irXeuCCLesDyirxJhv8o0JAvmnMAKOLhNFUrSQ2m
++3EnF7zhfz70gHW+EG8X8mL/EN3/dUM09j6TVrjtw43RLxBzwMDeariFF9yC+5bL
+tnGgxjsB9Ik6GV5v34/NEEGf1qBiAzFmDVFRZlrNDkq6gmpvGnA5hUWNr+y0i01L
+jGyaLSWHYjgw2UEQOqcUtTFK9MNzbZze4mVaHMEz9/aMfX25R6qbiNqCChveIm8m
+Yr5Ds2zdZx+G5bAKdzX7nx2IUAxFQJEE94VLSp3npAaTWv3sHr7dR8tSyUJ9poDw
+gw4W9BIcnAM7zvFYbLF5FNggg/26njHCCN70sHt8zGxKQINMc6SJAj0EEwEIACcC
+GwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AFAlLpFRkFCQ6EJy0ACgkQf8x9RqzM
+TPjOZA//Zp0e25pcvle7cLc0YuFr9pBv2JIkLzPm83nkcwKmxaWayUIG4Sv6pH6h
+m8+S/CHQij/yFCX+o3ngMw2J9HBUvafZ4bnbI0RGJ70GsAwraQ0VlkIfg7GUw3Tz
+voGYO42rZTru9S0K/6nFP6D1HUu+U+AsJONLeb6oypQgInfXQExPZyliUnHdipei
+4WR1YFW6sjSkZT/5C3J1wkAvPl5lvOVthI9Zs6bZlJLZwusKxU0UM4Btgu1Sf3nn
+JcHmzisixwS9PMHE+AgPWIGSec/N27a0KmTTvImV6K6nEjXJey0K2+EYJuIBsYUN
+orOGBwDFIhfRk9qGlpgt0KRyguV+AP5qvgry95IrYtrOuE7307SidEbSnvO5ezNe
+mE7gT9Z1tM7IMPfmoKph4BfpNoH7aXiQh1Wo+ChdP92hZUtQrY2Nm13cmkxYjQ4Z
+gMWfYMC+DA/GooSgZM5i6hYqyyfAuUD9kwRN6BqTbuAUAp+hCWYeN4D88sLYpFh3
+paDYNKJ+Gf7Yyi6gThcV956RUFDH3ys5Dk0vDL9NiWwdebWfRFbzoRM3dyGP889a
+OyLzS3mh6nHzZrNGhW73kslSQek8tjKrB+56hXOnb4HaElTZGDvD5wmrrhN94kby
+Gtz3cydIohvNO9d90+29h0eGEDYti7j7maHkBKUAwlcPvMg5m3Y=
+=DA1T
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/pgdg/apt.postgresql.org.gpg b/pgdg/apt.postgresql.org.gpg
new file mode 100644
index 0000000..afa15cb
--- /dev/null
+++ b/pgdg/apt.postgresql.org.gpg
Binary files differ
diff --git a/pgdg/apt.postgresql.org.sh b/pgdg/apt.postgresql.org.sh
new file mode 100755
index 0000000..6dd5bbe
--- /dev/null
+++ b/pgdg/apt.postgresql.org.sh
@@ -0,0 +1,291 @@
+#!/bin/sh
+
+# script to add apt.postgresql.org to sources.list.d
+
+# Copyright (C) 2013-2023 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.
+#
+# The full text of the GPL is distributed as in
+# /usr/share/common-licenses/GPL-2 on Debian systems.
+
+SOURCESLIST="/etc/apt/sources.list.d/pgdg.sources"
+TYPES="deb"
+COMPONENTS="main"
+PGDG="pgdg"
+
+# variables imported from https://git.postgresql.org/gitweb/?p=pgapt.git;a=blob;f=pgapt.conf
+# checked out in $HOME/apt.postgresql.org/; run "make" to update
+PG_BETA_VERSION="16"
+PG_DEVEL_VERSION="17"
+PG_REPOSITORY_DISTS="sid trixie bookworm bullseye buster mantic lunar kinetic jammy focal bionic"
+PG_ARCHIVE_DISTS="sid trixie bookworm bullseye buster stretch jessie wheezy squeeze lenny etch mantic lunar kinetic jammy impish hirsute groovy focal eoan disco cosmic bionic zesty xenial wily utopic saucy precise lucid"
+
+while getopts "c:f:h:ipstv:y" opt ; do
+ case $opt in
+ c) COMPONENTS="main $OPTARG" ;; # make these extra components available
+ f) SOURCESLIST=$OPTARG ;; # sources.list filename to write to
+ h) HOST="$OPTARG" ;; # hostname to use in sources.list
+ i) INSTALL="yes" ;; # install packages for version given with -v
+ p) PURGE="yes" ;; # purge existing postgresql packages
+ s) TYPES="deb deb-src" ;; # include source repository as well
+ t) PGDG="pgdg-testing" ;; # use *-pgdg or *-pgdg-testing
+ v) PGVERSION="$OPTARG" ;; # set up sources.list to use this version (useful for beta/devel packages)
+ y) ;; # don't ask for confirmation
+ *) exit 5 ;;
+ esac
+ YES="yes" # don't ask for confirmation if any option is given
+done
+# shift away args
+shift $((OPTIND - 1))
+# check options
+if [ "$INSTALL" ] && [ -z "$PGVERSION" ]; then
+ echo "With -i, a version to install must be provided (-v)"
+ exit 1
+fi
+
+# codename from command line
+CODENAME="$1"
+# parse os-release
+if [ -z "$CODENAME" ] && [ -f /etc/os-release ]; then
+ . /etc/os-release
+ if [ "$VERSION_CODENAME" ]; then # added in buster/xenial
+ CODENAME="$VERSION_CODENAME"
+ else
+ # Debian: VERSION="7.0 (wheezy)"
+ # Ubuntu: VERSION="13.04, Raring Ringtail"
+ # VERSION="18.04.1 LTS (Bionic Beaver)"
+ CODENAME=$(echo $VERSION | sed -ne 's/.*(\(.*\)).*/\1/') # works on Debian only
+ fi
+fi
+# try lsb_release
+if [ -z "$CODENAME" ] && command -v lsb_release >/dev/null; then
+ CODENAME=$(lsb_release -cs 2>/dev/null)
+fi
+# guess from sources.list
+if [ -z "$CODENAME" ] && [ -f /etc/apt/sources.list ]; then
+ CODENAME=$(grep '^deb ' /etc/apt/sources.list | head -n1 | awk '{ print $3 }')
+fi
+# complain if no result yet
+if [ -z "$CODENAME" ]; then
+ cat <<EOF
+Could not determine the distribution codename. Please report this as a bug to
+pgsql-pkg-debian@postgresql.org. As a workaround, you can call this script with
+the proper codename as parameter, e.g. "$0 squeeze".
+EOF
+ exit 1
+fi
+
+# errors are non-fatal above
+set -eu
+
+if [ "${HOST:-}" ]; then
+ :
+elif echo "$PG_REPOSITORY_DISTS" | grep -qw "$CODENAME"; then
+ # known distribution on apt.postgresql.org
+ HOST="apt.postgresql.org"
+elif echo "$PG_ARCHIVE_DISTS" | grep -qw "$CODENAME"; then
+ # known distribution on apt.postgresql.org
+ HOST="apt-archive.postgresql.org"
+else # unknown distribution, verify on the web
+ HOST="apt.postgresql.org"
+ DISTURL="https://$HOST/pub/repos/apt/dists/"
+ if [ -x /usr/bin/curl ]; then
+ DISTHTML=$(curl -s $DISTURL || :)
+ elif [ -x /usr/bin/wget ]; then
+ DISTHTML=$(wget --quiet -O - $DISTURL || :)
+ fi
+ if [ "${DISTHTML:-}" ]; then
+ if ! echo "$DISTHTML" | grep -q "$CODENAME-$PGDG"; then
+ cat <<EOF
+Your system is using the distribution codename $CODENAME, but $CODENAME-$PGDG
+does not seem to be a valid distribution on
+$DISTURL
+
+We abort the installation here. If you want to use a distribution different
+from your system, you can call this script with an explicit codename, e.g.
+"$0 precise".
+
+For more information, refer to https://wiki.postgresql.org/wiki/Apt
+or ask on the mailing list for assistance: pgsql-pkg-debian@postgresql.org
+EOF
+ exit 1
+ fi
+ fi
+fi
+
+cat <<EOF
+This script will enable the PostgreSQL APT repository on $HOST on
+your system. The distribution codename used will be $CODENAME-$PGDG.
+
+EOF
+
+if [ -z "${YES:-}" ]; then
+ echo -n "Press Enter to continue, or Ctrl-C to abort."
+ read enter
+ echo
+fi
+
+# keyring needs to be readable for apt
+umask 022
+# prefer .gpg keyring from postgresql-common
+KEYRING="/usr/share/postgresql-common/pgdg/apt.postgresql.org.gpg"
+# otherwise, use the .asc key
+test -e $KEYRING || KEYRING="/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc"
+echo "Using keyring $KEYRING"
+# write .asc key to disk if not yet present
+if ! test -e $KEYRING; then
+ mkdir -p /usr/share/postgresql-common/pgdg
+ cat > $KEYRING <<EOF
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQINBE6XR8IBEACVdDKT2HEH1IyHzXkb4nIWAY7echjRxo7MTcj4vbXAyBKOfjja
+UrBEJWHN6fjKJXOYWXHLIYg0hOGeW9qcSiaa1/rYIbOzjfGfhE4x0Y+NJHS1db0V
+G6GUj3qXaeyqIJGS2z7m0Thy4Lgr/LpZlZ78Nf1fliSzBlMo1sV7PpP/7zUO+aA4
+bKa8Rio3weMXQOZgclzgeSdqtwKnyKTQdXY5MkH1QXyFIk1nTfWwyqpJjHlgtwMi
+c2cxjqG5nnV9rIYlTTjYG6RBglq0SmzF/raBnF4Lwjxq4qRqvRllBXdFu5+2pMfC
+IZ10HPRdqDCTN60DUix+BTzBUT30NzaLhZbOMT5RvQtvTVgWpeIn20i2NrPWNCUh
+hj490dKDLpK/v+A5/i8zPvN4c6MkDHi1FZfaoz3863dylUBR3Ip26oM0hHXf4/2U
+A/oA4pCl2W0hc4aNtozjKHkVjRx5Q8/hVYu+39csFWxo6YSB/KgIEw+0W8DiTII3
+RQj/OlD68ZDmGLyQPiJvaEtY9fDrcSpI0Esm0i4sjkNbuuh0Cvwwwqo5EF1zfkVj
+Tqz2REYQGMJGc5LUbIpk5sMHo1HWV038TWxlDRwtOdzw08zQA6BeWe9FOokRPeR2
+AqhyaJJwOZJodKZ76S+LDwFkTLzEKnYPCzkoRwLrEdNt1M7wQBThnC5z6wARAQAB
+tBxQb3N0Z3JlU1FMIERlYmlhbiBSZXBvc2l0b3J5iQJOBBMBCAA4AhsDBQsJCAcD
+BRUKCQgLBRYCAwEAAh4BAheAFiEEuXsK/KoaR/BE8kSgf8x9RqzMTPgFAlhtCD8A
+CgkQf8x9RqzMTPgECxAAk8uL+dwveTv6eH21tIHcltt8U3Ofajdo+D/ayO53LiYO
+xi27kdHD0zvFMUWXLGxQtWyeqqDRvDagfWglHucIcaLxoxNwL8+e+9hVFIEskQAY
+kVToBCKMXTQDLarz8/J030Pmcv3ihbwB+jhnykMuyyNmht4kq0CNgnlcMCdVz0d3
+z/09puryIHJrD+A8y3TD4RM74snQuwc9u5bsckvRtRJKbP3GX5JaFZAqUyZNRJRJ
+Tn2OQRBhCpxhlZ2afkAPFIq2aVnEt/Ie6tmeRCzsW3lOxEH2K7MQSfSu/kRz7ELf
+Cz3NJHj7rMzC+76Rhsas60t9CjmvMuGONEpctijDWONLCuch3Pdj6XpC+MVxpgBy
+2VUdkunb48YhXNW0jgFGM/BFRj+dMQOUbY8PjJjsmVV0joDruWATQG/M4C7O8iU0
+B7o6yVv4m8LDEN9CiR6r7H17m4xZseT3f+0QpMe7iQjz6XxTUFRQxXqzmNnloA1T
+7VjwPqIIzkj/u0V8nICG/ktLzp1OsCFatWXh7LbU+hwYl6gsFH/mFDqVxJ3+DKQi
+vyf1NatzEwl62foVjGUSpvh3ymtmtUQ4JUkNDsXiRBWczaiGSuzD9Qi0ONdkAX3b
+ewqmN4TfE+XIpCPxxHXwGq9Rv1IFjOdCX0iG436GHyTLC1tTUIKF5xV4Y0+cXIOI
+RgQQEQgABgUCTpdI7gAKCRDFr3dKWFELWqaPAKD1TtT5c3sZz92Fj97KYmqbNQZP
++ACfSC6+hfvlj4GxmUjp1aepoVTo3weJAhwEEAEIAAYFAk6XSQsACgkQTFprqxLS
+p64F8Q//cCcutwrH50UoRFejg0EIZav6LUKejC6kpLeubbEtuaIH3r2zMblPGc4i
++eMQKo/PqyQrceRXeNNlqO6/exHozYi2meudxa6IudhwJIOn1MQykJbNMSC2sGUp
+1W5M1N5EYgt4hy+qhlfnD66LR4G+9t5FscTJSy84SdiOuqgCOpQmPkVRm1HX5X1+
+dmnzMOCk5LHHQuiacV0qeGO7JcBCVEIDr+uhU1H2u5GPFNHm5u15n25tOxVivb94
+xg6NDjouECBH7cCVuW79YcExH/0X3/9G45rjdHlKPH1OIUJiiX47OTxdG3dAbB4Q
+fnViRJhjehFscFvYWSqXo3pgWqUsEvv9qJac2ZEMSz9x2mj0ekWxuM6/hGWxJdB+
++985rIelPmc7VRAXOjIxWknrXnPCZAMlPlDLu6+vZ5BhFX0Be3y38f7GNCxFkJzl
+hWZ4Cj3WojMj+0DaC1eKTj3rJ7OJlt9S9xnO7OOPEUTGyzgNIDAyCiu8F4huLPaT
+ape6RupxOMHZeoCVlqx3ouWctelB2oNXcxxiQ/8y+21aHfD4n/CiIFwDvIQjl7dg
+mT3u5Lr6yxuosR3QJx1P6rP5ZrDTP9khT30t+HZCbvs5Pq+v/9m6XDmi+NlU7Zuh
+Ehy97tL3uBDgoL4b/5BpFL5U9nruPlQzGq1P9jj40dxAaDAX/WKJAj0EEwEIACcC
+GwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AFAlB5KywFCQPDFt8ACgkQf8x9RqzM
+TPhuCQ//QAjRSAOCQ02qmUAikT+mTB6baOAakkYq6uHbEO7qPZkv4E/M+HPIJ4wd
+nBNeSQjfvdNcZBA/x0hr5EMcBneKKPDj4hJ0panOIRQmNSTThQw9OU351gm3YQct
+AMPRUu1fTJAL/AuZUQf9ESmhyVtWNlH/56HBfYjE4iVeaRkkNLJyX3vkWdJSMwC/
+LO3Lw/0M3R8itDsm74F8w4xOdSQ52nSRFRh7PunFtREl+QzQ3EA/WB4AIj3VohIG
+kWDfPFCzV3cyZQiEnjAe9gG5pHsXHUWQsDFZ12t784JgkGyO5wT26pzTiuApWM3k
+/9V+o3HJSgH5hn7wuTi3TelEFwP1fNzI5iUUtZdtxbFOfWMnZAypEhaLmXNkg4zD
+kH44r0ss9fR0DAgUav1a25UnbOn4PgIEQy2fgHKHwRpCy20d6oCSlmgyWsR40EPP
+YvtGq49A2aK6ibXmdvvFT+Ts8Z+q2SkFpoYFX20mR2nsF0fbt1lfH65P64dukxeR
+GteWIeNakDD40bAAOH8+OaoTGVBJ2ACJfLVNM53PEoftavAwUYMrR910qvwYfd/4
+6rh46g1Frr9SFMKYE9uvIJIgDsQB3QBp71houU4H55M5GD8XURYs+bfiQpJG1p7e
+B8e5jZx1SagNWc4XwL2FzQ9svrkbg1Y+359buUiP7T6QXX2zY++JAj0EEwEIACcC
+GwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AFAlEqbZUFCQg2wEEACgkQf8x9RqzM
+TPhFMQ//WxAfKMdpSIA9oIC/yPD/dJpY/+DyouOljpE6MucMy/ArBECjFTBwi/j9
+NYM4ynAk34IkhuNexc1i9/05f5RM6+riLCLgAOsADDbHD4miZzoSxiVr6GQ3YXMb
+OGld9kV9Sy6mGNjcUov7iFcf5Hy5w3AjPfKuR9zXswyfzIU1YXObiiZT38l55pp/
+BSgvGVQsvbNjsff5CbEKXS7q3xW+WzN0QWF6YsfNVhFjRGj8hKtHvwKcA02wwjLe
+LXVTm6915ZUKhZXUFc0vM4Pj4EgNswH8Ojw9AJaKWJIZmLyW+aP+wpu6YwVCicxB
+Y59CzBO2pPJDfKFQzUtrErk9irXeuCCLesDyirxJhv8o0JAvmnMAKOLhNFUrSQ2m
++3EnF7zhfz70gHW+EG8X8mL/EN3/dUM09j6TVrjtw43RLxBzwMDeariFF9yC+5bL
+tnGgxjsB9Ik6GV5v34/NEEGf1qBiAzFmDVFRZlrNDkq6gmpvGnA5hUWNr+y0i01L
+jGyaLSWHYjgw2UEQOqcUtTFK9MNzbZze4mVaHMEz9/aMfX25R6qbiNqCChveIm8m
+Yr5Ds2zdZx+G5bAKdzX7nx2IUAxFQJEE94VLSp3npAaTWv3sHr7dR8tSyUJ9poDw
+gw4W9BIcnAM7zvFYbLF5FNggg/26njHCCN70sHt8zGxKQINMc6SJAj0EEwEIACcC
+GwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AFAlLpFRkFCQ6EJy0ACgkQf8x9RqzM
+TPjOZA//Zp0e25pcvle7cLc0YuFr9pBv2JIkLzPm83nkcwKmxaWayUIG4Sv6pH6h
+m8+S/CHQij/yFCX+o3ngMw2J9HBUvafZ4bnbI0RGJ70GsAwraQ0VlkIfg7GUw3Tz
+voGYO42rZTru9S0K/6nFP6D1HUu+U+AsJONLeb6oypQgInfXQExPZyliUnHdipei
+4WR1YFW6sjSkZT/5C3J1wkAvPl5lvOVthI9Zs6bZlJLZwusKxU0UM4Btgu1Sf3nn
+JcHmzisixwS9PMHE+AgPWIGSec/N27a0KmTTvImV6K6nEjXJey0K2+EYJuIBsYUN
+orOGBwDFIhfRk9qGlpgt0KRyguV+AP5qvgry95IrYtrOuE7307SidEbSnvO5ezNe
+mE7gT9Z1tM7IMPfmoKph4BfpNoH7aXiQh1Wo+ChdP92hZUtQrY2Nm13cmkxYjQ4Z
+gMWfYMC+DA/GooSgZM5i6hYqyyfAuUD9kwRN6BqTbuAUAp+hCWYeN4D88sLYpFh3
+paDYNKJ+Gf7Yyi6gThcV956RUFDH3ys5Dk0vDL9NiWwdebWfRFbzoRM3dyGP889a
+OyLzS3mh6nHzZrNGhW73kslSQek8tjKrB+56hXOnb4HaElTZGDvD5wmrrhN94kby
+Gtz3cydIohvNO9d90+29h0eGEDYti7j7maHkBKUAwlcPvMg5m3Y=
+=DA1T
+-----END PGP PUBLIC KEY BLOCK-----
+EOF
+fi
+
+for version in ${PGVERSION:-}; do
+ # devel version comes from *-pgdg-snapshot (with lower default apt pinning priority)
+ if dpkg --compare-versions $version ge "${PG_DEVEL_VERSION:-999}"; then
+ COMPONENTS="$COMPONENTS $version" # devel component is likely empty, but add it to be sure
+ DEVEL_COMPONENT="${DEVEL_COMPONENT:-} $version"
+ PIN="-t $CODENAME-pgdg-snapshot"
+ # beta version needs a different component
+ elif dpkg --compare-versions $version ge "${PG_BETA_VERSION:-999}"; then
+ COMPONENTS="$COMPONENTS $version"
+ fi
+
+ # select packages to install
+ PACKAGES="${PACKAGES:-} postgresql-$version postgresql-server-dev-$version"
+ case $version in
+ 8*|9*) PACKAGES="$PACKAGES postgresql-contrib-$version" ;;
+ esac
+done
+
+echo "Writing $SOURCESLIST ..."
+cat > $SOURCESLIST <<EOF
+Types: $TYPES
+URIs: https://$HOST/pub/repos/apt
+Suites: $CODENAME-$PGDG
+Components: $COMPONENTS
+Signed-By: $KEYRING
+EOF
+
+# write a separate section for devel without main so we don't include all of snapshot
+if [ "${DEVEL_COMPONENT:-}" ]; then
+cat >> $SOURCESLIST <<EOF
+
+Types: $TYPES
+URIs: https://$HOST/pub/repos/apt
+Suites: $CODENAME-pgdg-snapshot
+Components: ${DEVEL_COMPONENT# }
+Signed-By: $KEYRING
+EOF
+fi
+
+if [ "$SOURCESLIST" = "/etc/apt/sources.list.d/pgdg.sources" ]; then
+ # remove pgdg.list when upgrading to pgdg.sources
+ rm -vf /etc/apt/sources.list.d/pgdg.list /etc/apt/trusted.gpg.d/apt.postgresql.org.gpg
+fi
+
+echo
+echo "Running apt-get update ..."
+apt-get update
+
+cat <<EOF
+
+You can now start installing packages from $HOST.
+
+Have a look at https://wiki.postgresql.org/wiki/Apt for more information;
+most notably the FAQ at https://wiki.postgresql.org/wiki/Apt/FAQ
+EOF
+
+# remove/install packages
+export DEBIAN_FRONTEND=noninteractive
+if [ "${PURGE:-}" ]; then
+ echo
+ echo "Purging existing PostgreSQL packages ..."
+ apt-get -y purge postgresql-client-common
+fi
+if [ "${INSTALL:-}" ]; then
+ echo
+ echo "Installing packages for PostgreSQL $PGVERSION ..."
+ apt-get -y -o DPkg::Options::=--force-confnew \
+ install ${PIN:-} $PACKAGES
+fi
diff --git a/pgdg/update b/pgdg/update
new file mode 100755
index 0000000..4a937df
--- /dev/null
+++ b/pgdg/update
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+. $HOME/apt.postgresql.org/pgapt.conf
+
+set -eux
+
+sed -i \
+ -e "s/^PG_BETA_VERSION=.*/PG_BETA_VERSION=\"${PG_BETA_VERSION:-}\"/" \
+ -e "s/^PG_DEVEL_VERSION=.*/PG_DEVEL_VERSION=\"$PG_DEVEL_VERSION\"/" \
+ -e "s/^PG_REPOSITORY_DISTS=.*/PG_REPOSITORY_DISTS=\"$PG_REPOSITORY_DISTS\"/" \
+ -e "s/^PG_ARCHIVE_DISTS=.*/PG_ARCHIVE_DISTS=\"$PG_ARCHIVE_DISTS\"/" \
+ apt.postgresql.org.sh
diff --git a/pgxs_debian_control.mk b/pgxs_debian_control.mk
new file mode 100644
index 0000000..2761e1c
--- /dev/null
+++ b/pgxs_debian_control.mk
@@ -0,0 +1,13 @@
+#
+# produce a debian/control file from a debian/control.in
+#
+# In debian/rules, include /usr/share/postgresql-common/pgxs_debian_control.mk
+#
+# Author: Dimitri Fontaine <dfontaine@hi-media.com>
+#
+debian/control: debian/control.in debian/pgversions
+ pg_buildext checkcontrol
+
+# run check when clean is invoked
+clean: debian/control
+.PHONY: debian/control
diff --git a/postgresqlrc.5 b/postgresqlrc.5
new file mode 100644
index 0000000..b0e52a5
--- /dev/null
+++ b/postgresqlrc.5
@@ -0,0 +1,42 @@
+.TH POSTGRESQLRC 5 "Feburary 2005" "Debian" "Debian PostgreSQL infrastructure"
+
+.SH NAME
+~/.postgresqlrc \- Per\-user PostgreSQL cluster configuration
+
+.SH DESCRIPTION
+The file
+.B ~/.postgresqlrc
+configures the default PostgreSQL version/cluster and the default
+database for an user. If it is not present, the system\-wide file
+.B /etc/postgresql\-common/user_clusters
+is used instead.
+
+.SH FORMAT
+.P
+Comments are introduced by the character
+.BR # .
+Comments may follow data on a line; the first comment character terminates
+the data.
+Leading whitespace and blank lines are ignored.
+
+The first uncommented, non\-blank line is used, all following lines are ignored.
+
+Fields must be given in the following order, separated by white space:
+
+.TP
+.B VERSION
+The major PostgreSQL version of the cluster to connect to.
+.TP
+.B CLUSTER
+The name of a cluster to connect to. A remote cluster is specified
+with \fIhost\fR:\fIport\fR. If port is empty, it defaults to 5432.
+.TP
+.B DATABASE
+Within the cluster, the database to which the user will connect by default
+if he does not specify a database on the command line. If this is
+.BR * ,
+the default database will be the one named by the user's login id.
+
+.SH SEE ALSO
+.BR pg_wrapper (1),
+.BR user_clusters (5)
diff --git a/rpm/README b/rpm/README
new file mode 100644
index 0000000..a6a9067
--- /dev/null
+++ b/rpm/README
@@ -0,0 +1,52 @@
+postgresql-common for RedHat
+============================
+
+The postgresql-common framework was written for Debian/Ubuntu, but most parts
+of it work as well on other operating systems. The RPM port changes little in
+the original code, and even uses many files from the debian/ directory for
+building the packages.
+
+No separate PostgreSQL client/server packages are provided; the port is backed
+by the PGDG RPM packages from yum.postgresql.org.
+
+The filesystem layout is unchanged, /etc/postgresql, /etc/postgresql-common,
+and /var/lib/postgresql are used as before.
+
+Differences between the Debian and RedHat operating modes are:
+
+* /var/run/postgresql/ is still used for external pid files, but the default
+ unix socket directory is /tmp, to match the RPM packages' default.
+
+* The postgres system user home is /var/lib/pgsql.
+
+* The binroot is changed from /usr/lib/postgresql/ to /usr/pgsql-. (Note the
+ missing trailing slash, the idea is that the version number can just be
+ appended to the path, e.g. /usr/lib/postgresql/9.4/bin becomes
+ /usr/pgsql-9.4/bin.)
+
+* The various symlinks for frontend programs in /usr/bin like psql are not
+ direct symlinks to pg_wrapper, but are added as high-priority alternatives to
+ the alternatives symlinks set up by the PostgreSQL RPM packages.
+
+* SSL is disabled by default because there is no easily available snakeoil
+ certificate. Proper certificates can be configured in createcluster.conf.
+
+* No attempt is made to setup OOM killer protection for the postmaster process.
+
+* On Debian, the /etc/init.d/postgresql init script skips versions that have
+ their own /etc/init.d/postgresql-x.y init script, mostly for compatibility
+ with legacy packages before the advent of the postgresql-common framework.
+ The RPM packages provide /etc/init.d/postgresql-x.y scripts, which are
+ ignored by /etc/init.d/postgresql. The postgresql-x.y scripts will not do
+ anything as long as the user does not use them to create clusters in
+ /var/lib/pgsql. (In which case they continue to work as if postgresql-common
+ was not present.)
+
+* Debian's pre/postinst/rm maintainer scripts are not used. Mostly this means
+ there is no automatic integration of tsearch with system-provided
+ dictionaries.
+
+The postgresql-common testsuite is supported if perl-Test-Simple and
+perl-Time-HiRes are installed.
+
+ -- Christoph Berg <christoph.berg@credativ.de> Thu, 26 Jun 2014 16:59:47 +0200
diff --git a/rpm/init-functions-compat b/rpm/init-functions-compat
new file mode 100644
index 0000000..da13596
--- /dev/null
+++ b/rpm/init-functions-compat
@@ -0,0 +1,12 @@
+# Functions missing in older /lib/lsb/init-functions scripts
+
+function_exists () {
+ type $1 >/dev/null 2>&1
+}
+
+function_exists log_daemon_msg || log_daemon_msg () { echo -n "$1:${2:+ $2}"; }
+function_exists log_progress_msg || log_progress_msg () { echo -n " $1"; }
+function_exists log_end_msg || log_end_msg () { if [ $1 -eq 0 ]; then echo "."; else echo "failed!"; fi; }
+# this one exists, but we provide it anyway so we don't need to depend on redhat-lsb-core:
+function_exists log_warning_msg || log_warning_msg () { echo "$1"; }
+function_exists log_failure_msg || log_failure_msg () { echo "$1"; }
diff --git a/rpm/postgresql-common.spec b/rpm/postgresql-common.spec
new file mode 100644
index 0000000..cb2e45b
--- /dev/null
+++ b/rpm/postgresql-common.spec
@@ -0,0 +1,139 @@
+Name: postgresql-common
+Version: %{version}
+Release: 1%{?dist}
+BuildArch: noarch
+Summary: PostgreSQL database-cluster manager
+Packager: Debian PostgreSQL Maintainers <pkg-postgresql-public@lists.alioth.debian.org>
+
+License: GPLv2+
+URL: https://packages.debian.org/sid/%{name}
+Source0: http://ftp.debian.org/debian/pool/main/p/%{name}/%{name}_%{version}.tar.xz
+Requires: postgresql-client-common
+Requires: perl-JSON
+
+%description
+The postgresql-common package provides a structure under which
+multiple versions of PostgreSQL may be installed and/or multiple
+clusters maintained at one time.
+
+%package -n postgresql-client-common
+Summary: manager for multiple PostgreSQL client versions
+%description -n postgresql-client-common
+The postgresql-client-common package provides a structure under which
+multiple versions of PostgreSQL client programs may be installed at
+the same time. It provides a wrapper which selects the right version
+for the particular cluster you want to access (with a command line
+option, an environment variable, /etc/postgresql-common/user_clusters,
+or ~/.postgresqlrc).
+
+%prep
+# unpack tarball, ignoring the name of the top level directory inside
+%setup -c
+mv */* .
+
+%build
+make
+
+%install
+rm -rf %{buildroot}
+# install in subpackages using the Debian files
+for inst in debian/*.install; do
+ pkg=$(basename $inst .install)
+ [ "$pkg" = "postgresql-server-dev-all" ] && continue
+ echo "### Reading $pkg files list from $inst ###"
+ while read file dir; do
+ [ "$file" = "supported_versions" ] && continue # only relevant on Debian
+ mkdir -p %{buildroot}/$dir
+ cp -r $file %{buildroot}/$dir
+ echo "/$dir/${file##*/}" >> files-$pkg
+ done < $inst
+done
+# install manpages
+for manpages in debian/*.manpages; do
+ pkg=$(basename $manpages .manpages)
+ [ "$pkg" = "postgresql-server-dev-all" ] && continue
+ echo "### Reading $pkg manpages list from $manpages ###"
+ while read file; do
+ section="${file##*.}"
+ mandir="%{buildroot}%{_mandir}/man$section"
+ mkdir -p $mandir
+ for f in $file; do # expand wildcards
+ cp $f $mandir
+ echo "%doc %{_mandir}/man$section/$(basename $f).gz" >> files-$pkg
+ done
+ done < $manpages
+done
+# install pg_wrapper symlinks by augmenting the existing pgdg.rpm alternatives
+cat debian/postgresql-*common.links | \
+while read dest link; do
+ name="pgsql-$(basename $link)"
+ echo "update-alternatives --install /$link $name /$dest 9999" >> postgresql-client-common.post
+ echo "update-alternatives --remove $name /$dest" >> postgresql-client-common.preun
+done
+# activate rpm-specific tweaks
+sed -i -e 's/#redhat# //' \
+ %{buildroot}/lib/systemd/system-generators/postgresql-generator \
+ %{buildroot}/usr/bin/pg_config \
+ %{buildroot}/usr/bin/pg_virtualenv \
+ %{buildroot}/usr/share/perl5/PgCommon.pm \
+ %{buildroot}/usr/share/postgresql-common/init.d-functions \
+ %{buildroot}/usr/share/postgresql-common/pg_getwal
+# install init script
+mkdir -p %{buildroot}/etc/init.d %{buildroot}/etc/logrotate.d
+cp debian/postgresql-common.postgresql.init %{buildroot}/etc/init.d/postgresql
+#cp debian/postgresql-common.postinst %{buildroot}/usr/share/postgresql-common
+cp rpm/init-functions-compat %{buildroot}/usr/share/postgresql-common
+# ssl defaults to 'off' here because we don't have pregenerated snakeoil certs
+sed -e 's/__SSL__/off/' createcluster.conf > %{buildroot}/etc/postgresql-common/createcluster.conf
+cp debian/postgresql-common.logrotate %{buildroot}/etc/logrotate.d/postgresql-common
+
+%files -n postgresql-common -f files-postgresql-common
+%attr(0755, root, root) %config /etc/init.d/postgresql
+#%attr(0755, root, root) /usr/share/postgresql-common/postgresql-common.postinst
+/usr/share/postgresql-common/init-functions-compat
+%config /etc/postgresql-common/createcluster.conf
+%config /etc/logrotate.d/postgresql-common
+
+%if 0%{?rhel} >= 7
+%config /lib/systemd/system/*.service
+%config /lib/systemd/system/*.timer
+%config /lib/systemd/system-generators/postgresql-generator
+%endif
+
+%files -n postgresql-client-common -f files-postgresql-client-common
+
+%post
+# create postgres user
+groupadd -g 26 -o -r postgres >/dev/null 2>&1 || :
+useradd -M -n -g postgres -o -r -d /var/lib/pgsql -s /bin/bash \
+ -c "PostgreSQL Server" -u 26 postgres >/dev/null 2>&1 || :
+# create directories so postgres can create clusters without root
+install -d -o postgres -g postgres /etc/postgresql /var/lib/postgresql /var/lib/pgsql /var/log/postgresql /var/run/postgresql
+# install logrotate config
+version_lt () {
+ newest=$( ( echo "$1"; echo "$2" ) | sort -V | tail -n1)
+ [ "$1" != "$newest" ]
+}
+lrversion=$(rpm --queryformat '%{VERSION}' -q logrotate)
+if version_lt $lrversion 3.8; then
+ echo "Adjusting /etc/logrotate.d/postgresql-common for logrotate version $lrversion"
+ sed -i -e '/ su /d' /etc/logrotate.d/postgresql-common || :
+fi
+
+%post -n postgresql-client-common -f postgresql-client-common.post
+update-alternatives --install /usr/bin/ecpg pgsql-ecpg /usr/share/postgresql-common/pg_wrapper 9999
+
+%preun -n postgresql-client-common -f postgresql-client-common.preun
+update-alternatives --remove pgsql-ecpg /usr/share/postgresql-common/pg_wrapper
+
+%changelog
+* Tue Sep 29 2020 Christoph Berg <myon@debian.org> 217-1
+- Drop postgresql-server-dev-all package, it's debian-specific only.
+* Fri Dec 09 2016 Bernd Helmle <bernd.helmle@credativ.de> 177-1
+- New upstream release 177
+* Fri Jun 03 2016 Bernd Helmle <bernd.helmle@credativ.de> 174-2
+- Fix package dependencies and systemd integration
+* Thu Aug 7 2014 Christoph Berg <christoph.berg@credativ.de> 160-1
+- Omit the LD_PRELOAD logic in pg_wrapper
+* Thu Jun 5 2014 Christoph Berg <christoph.berg@credativ.de> 158-1
+- Initial specfile version
diff --git a/server/README b/server/README
new file mode 100644
index 0000000..867757a
--- /dev/null
+++ b/server/README
@@ -0,0 +1,2 @@
+The files in this directory are only used when compiling Debian packages of the
+PostgreSQL database server. They are not required at run time.
diff --git a/server/catversion b/server/catversion
new file mode 100755
index 0000000..fc4af08
--- /dev/null
+++ b/server/catversion
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+# Extract server catalog and control file version numbers.
+# This information is stored in the packages and used at install time to
+# determine if an in-major-version pg_upgradecluster is required.
+
+set -eu
+
+CATVERSION=$(awk '/^#define CATALOG_VERSION_NO/ { print $3 }' src/include/catalog/catversion.h)
+CONTROLVERSION=$(awk '/^#define PG_CONTROL_VERSION/ { print $3 }' src/include/catalog/pg_control.h)
+
+case $CONTROLVERSION in
+ # control file versions used in PG 9.6 .. 15
+ # don't append to catversion to avoid spurious warnings for users of existing packages
+ 960|1002|1100|1201|1300) echo "$CATVERSION" ;;
+ *) echo "$CATVERSION-$CONTROLVERSION" ;;
+esac
diff --git a/server/pg_config.pl b/server/pg_config.pl
new file mode 100755
index 0000000..2c90236
--- /dev/null
+++ b/server/pg_config.pl
@@ -0,0 +1,76 @@
+#!/usr/bin/perl
+
+# Perl reimplementation of PostgreSQL's pg_config binary.
+# We provide this as /usr/bin/pg_config to support cross-compilation using
+# libpq-dev. Also, this makes the two installed pg_config copies not conflict
+# via their debugging symbols.
+#
+# This code is released under the terms of the PostgreSQL License.
+# Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group
+# Author: Christoph Berg
+
+use strict;
+use warnings;
+
+# no arguments, print all items
+if (@ARGV == 0) {
+ while (<DATA>) {
+ last if /^$/; # begin of help section
+ print;
+ }
+ exit 0;
+}
+
+# --help or -?
+if (grep {$_ =~ /^(--help|-\?)$/} @ARGV) {
+ while (<DATA>) {
+ last if /^$/; # begin of help section
+ }
+ print; # include empty line in output
+ while (<DATA>) {
+ next if /^Report bugs/; # Skip bug address in the perl version
+ print;
+ }
+ exit 0;
+}
+
+# specific value(s) requested
+my %options;
+my $help;
+while (<DATA>) {
+ last if /^$/; # begin of help section
+ /^(\S+) = (.*)/ or die "malformatted data item";
+ $options{'--' . lc $1} = $2;
+}
+
+foreach my $arg (@ARGV) {
+ unless ($options{$arg}) {
+ print "pg_config: invalid argument: $arg\n";
+ print "Try \"pg_config --help\" for more information.\n";
+ exit 1;
+ }
+ print "$options{$arg}\n";
+}
+
+exit 0;
+
+# The DATA section consists of the `pg_config` output (one KEY = value item per
+# line), and the `pg_config --help` text. The first --help line is empty, which
+# we use to detect the beginning of the help section.
+
+__DATA__
+INCLUDEDIR = /usr/include/postgresql
+
+pg_config provides information about the installed version of PostgreSQL.
+
+Usage:
+ pg_config [OPTION]...
+
+Options:
+ --includedir show location of C header files of the client
+ interfaces
+ -?, --help show this help, then exit
+
+With no arguments, all known items are shown.
+
+Report bugs to <pgsql-bugs@postgresql.org>.
diff --git a/server/postgresql.mk b/server/postgresql.mk
new file mode 100644
index 0000000..8e73f66
--- /dev/null
+++ b/server/postgresql.mk
@@ -0,0 +1,294 @@
+#!/usr/bin/make -f
+
+# The PostgreSQL server packages include this in their debian/rules file.
+
+# MAJOR_VER is used in path names (15 for /usr/lib/postgresql/15)
+ifndef MAJOR_VER
+$(error MAJOR_VER must be defined before including this file)
+endif
+# MAJOR_PKG is used in package names (15 for postgresql-15)
+MAJOR_PKG = $(MAJOR_VER)
+
+# path to auxiliary build files
+AUX_MK_DIR = /usr/share/postgresql-common/server
+
+# version comparison
+version_ge = $(shell dpkg --compare-versions $(MAJOR_VER) ge $(1) && echo y)
+
+# include dpkg makefiles
+include /usr/share/dpkg/architecture.mk
+include /usr/share/dpkg/pkg-info.mk
+include /usr/share/dpkg/vendor.mk
+export DEB_BUILD_MAINT_OPTIONS = hardening=+all
+DPKG_EXPORT_BUILDFLAGS = 1
+include /usr/share/dpkg/buildflags.mk
+
+# strict symbol checking
+export DPKG_GENSYMBOLS_CHECK_LEVEL = 4
+
+# server catalog version
+CATVERSION = $(shell $(AUX_MK_DIR)/catversion)
+
+# configure flags
+
+CONFIGURE_FLAGS = \
+ --with-tcl \
+ --with-perl \
+ --with-python \
+ --with-pam \
+ --with-openssl \
+ --with-libxml \
+ --with-libxslt \
+ --mandir=/usr/share/postgresql/$(MAJOR_VER)/man \
+ --docdir=/usr/share/doc/postgresql-doc-$(MAJOR_VER) \
+ --sysconfdir=/etc/postgresql-common \
+ --datarootdir=/usr/share/ \
+ --datadir=/usr/share/postgresql/$(MAJOR_VER) \
+ --bindir=/usr/lib/postgresql/$(MAJOR_VER)/bin \
+ --libdir=/usr/lib/$(DEB_HOST_MULTIARCH)/ \
+ --libexecdir=/usr/lib/postgresql/ \
+ --includedir=/usr/include/postgresql/ \
+ --with-extra-version=" ($(DEB_VENDOR) $(DEB_VERSION))" \
+ --enable-nls \
+ --enable-thread-safety \
+ --enable-debug \
+ --enable-dtrace \
+ --disable-rpath \
+ --with-uuid=e2fs \
+ --with-gnu-ld \
+ --with-gssapi \
+ --with-ldap \
+ --with-pgport=5432 \
+ --with-system-tzdata=/usr/share/zoneinfo \
+ AWK=mawk \
+ MKDIR_P='/bin/mkdir -p' \
+ PROVE='/usr/bin/prove' \
+ PYTHON=/usr/bin/python3 \
+ TAR='/bin/tar' \
+ XSLTPROC='xsltproc --nonet' \
+ CFLAGS='$(CFLAGS)' \
+ LDFLAGS='$(LDFLAGS)'
+
+ifeq ($(call version_ge,9.4),y)
+ CONFIGURE_FLAGS += --enable-tap-tests
+endif
+
+ifeq ($(call version_ge,9.5),y)
+ ifneq ($(findstring $(DEB_HOST_ARCH), alpha),)
+ CONFIGURE_FLAGS += --disable-spinlocks
+ endif
+endif
+
+ifeq ($(call version_ge,10),y)
+ CONFIGURE_FLAGS += --with-icu
+endif
+
+ifeq ($(call version_ge,11)$(filter pkg.postgresql.nollvm,$(DEB_BUILD_PROFILES)),y)
+ # if package depends on LLVM, use it
+ LLVM_VERSIONED_DEP=$(shell grep 'llvm-[0-9]*-dev' debian/control | grep -v "!$(DEB_HOST_ARCH)" | grep -o '[0-9]*' | head -n1)
+ LLVM_DEP=$(shell grep 'llvm-dev' debian/control | grep -v "!$(DEB_HOST_ARCH)")
+ ifneq ($(LLVM_VERSIONED_DEP)$(LLVM_DEP),)
+ ifneq ($(LLVM_VERSIONED_DEP),)
+ LLVM_VERSION = $(LLVM_VERSIONED_DEP)
+ LLVM_CONFIG = /usr/bin/llvm-config-$(LLVM_VERSION)
+ else
+ LLVM_CONFIG = $(lastword $(shell ls -v /usr/bin/llvm-config-*))
+ LLVM_VERSION = $(subst /usr/bin/llvm-config-,,$(LLVM_CONFIG))
+ endif
+ CONFIGURE_FLAGS += --with-llvm LLVM_CONFIG=$(LLVM_CONFIG) CLANG=/usr/bin/clang-$(LLVM_VERSION)
+ else
+ LLVM_VERSION = 0.invalid # mute dpkg error on empty version fields in debian/control
+ endif
+ TEMP_CONFIG = TEMP_CONFIG=$(AUX_MK_DIR)/test-with-jit.conf
+else
+ LLVM_VERSION = 0.invalid # mute dpkg error on empty version fields in debian/control
+endif
+
+ifeq ($(call version_ge,14),y)
+ CONFIGURE_FLAGS += --with-lz4
+endif
+
+ifeq ($(call version_ge,15),y)
+ ifeq ($(filter pkg.postgresql.nozstd,$(DEB_BUILD_PROFILES)),)
+ CONFIGURE_FLAGS += --with-zstd
+ endif
+endif
+
+ifeq ($(call version_ge,17),y)
+ WITH_PG_BSD_INDENT = y
+endif
+
+# Facilitate hierarchical profile generation on amd64 (#730134)
+ifeq ($(DEB_HOST_ARCH),amd64)
+ CFLAGS += -fno-omit-frame-pointer
+endif
+
+# Work around an ICE bug in GCC 11.2.0, see
+# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=103395
+ifneq ($(findstring $(DEB_HOST_ARCH), armel armhf),)
+ CFLAGS+= -DSTAP_SDT_ARG_CONSTRAINT=g
+endif
+
+ifeq ($(DEB_HOST_ARCH_OS),linux)
+ CONFIGURE_FLAGS += --with-systemd
+ CONFIGURE_FLAGS += --with-selinux
+endif
+
+ifneq ($(filter pkg.postgresql.cassert,$(DEB_BUILD_PROFILES)),)
+ CONFIGURE_FLAGS += --enable-cassert
+ GENCONTROL_FLAGS += -Vcassert='$${Newline}$${Newline}This package has been built with cassert enabled.'
+endif
+
+# alpha/hppa fail stats tests with postgresql-15
+# hurd implemented semaphores only recently and tests still fail a lot
+# ia64 fails the infinite_recurse() test with postgresql-16
+# plperl fails on kfreebsd-* (#704802)
+# sh4 lets qemu segfaults when building postgresql-16
+# sparc64 fails bin/summarization-and-inprogress-insertion test with postgresql-15
+ifneq ($(filter alpha hppa hurd% ia64 kfreebsd% sh4 sparc64,$(DEB_HOST_ARCH)),)
+ TEST_FAIL_COMMAND = echo "Ignoring test failures on this architecture"
+else
+ TEST_FAIL_COMMAND = exit 1
+endif
+
+# recipes
+
+%:
+ dh $@
+
+override_dh_auto_configure:
+ dh_auto_configure --builddirectory=build -- $(CONFIGURE_FLAGS)
+ # remove pre-built documentation
+ rm -fv doc/src/sgml/*-stamp
+
+ifeq ($(filter nodoc,$(DEB_BUILD_PROFILES)),)
+override_dh_auto_build-indep:
+ $(MAKE) -C build/doc all # build man + html
+endif
+
+override_dh_auto_build-arch:
+ # set MAKELEVEL to 0 to force building submake-generated-headers in src/Makefile.global(.in)
+ MAKELEVEL=0 $(MAKE) -C build/src all
+ $(MAKE) -C build/doc man # build man only
+ $(MAKE) -C build/config all
+ $(MAKE) -C build/contrib all
+ # build tutorial stuff
+ $(MAKE) -C build/src/tutorial NO_PGXS=1
+ifeq ($(WITH_PG_BSD_INDENT),y)
+ $(MAKE) -C build/src/tools/pg_bsd_indent
+endif
+
+override_dh_auto_install-arch:
+ $(MAKE) -C build/doc/src/sgml install-man DESTDIR=$(CURDIR)/debian/tmp
+ $(MAKE) -C build/src install DESTDIR=$(CURDIR)/debian/tmp
+ $(MAKE) -C build/config install DESTDIR=$(CURDIR)/debian/tmp
+ $(MAKE) -C build/contrib install DESTDIR=$(CURDIR)/debian/tmp
+ # move SPI examples into server package (they wouldn't be in the doc package in an -A build)
+ mkdir -p debian/postgresql-$(MAJOR_PKG)/usr/share/doc/postgresql-$(MAJOR_VER)
+ mv debian/tmp/usr/share/doc/postgresql-doc-$(MAJOR_VER)/extension debian/postgresql-$(MAJOR_PKG)/usr/share/doc/postgresql-$(MAJOR_VER)/examples
+ifeq ($(WITH_PG_BSD_INDENT),y)
+ $(MAKE) -C build/src/tools/pg_bsd_indent install DESTDIR=$(CURDIR)/debian/tmp
+ install -m755 src/tools/pgindent/pgindent $(CURDIR)/debian/tmp/usr/lib/postgresql/$(MAJOR_VER)/bin
+ install -m644 src/tools/pgindent/typedefs.list $(CURDIR)/debian/tmp/usr/share/postgresql/$(MAJOR_VER)
+endif
+
+ifeq ($(filter nodoc,$(DEB_BUILD_PROFILES)),)
+override_dh_auto_install-indep:
+ $(MAKE) -C build/doc install DESTDIR=$(CURDIR)/debian/tmp
+endif
+
+override_dh_makeshlibs:
+ dh_makeshlibs -Xusr/lib/postgresql/$(MAJOR_VER)
+
+override_dh_auto_clean:
+ rm -rf build
+
+override_dh_installchangelogs:
+ dh_installchangelogs HISTORY
+
+override_dh_compress:
+ dh_compress -X.source -X.c
+ # compress manpages (excluding debian/tmp/)
+ gzip -9n $(CURDIR)/debian/*-*/usr/share/postgresql/*/man/man*/*.[137]
+
+override_dh_install-arch:
+ dh_install -a
+
+ # link README.Debian.gz to postgresql-common
+ mkdir -p debian/postgresql-$(MAJOR_PKG)/usr/share/doc/postgresql-$(MAJOR_VER)
+ ln -s ../postgresql-common/README.Debian.gz debian/postgresql-$(MAJOR_PKG)/usr/share/doc/postgresql-$(MAJOR_VER)/README.Debian.gz
+
+ # assemble perl version of pg_config in libpq-dev
+ mkdir -p debian/libpq-dev/usr/bin
+ sed -ne '1,/__DATA__/p' $(AUX_MK_DIR)/pg_config.pl > debian/libpq-dev/usr/bin/pg_config
+ LC_ALL=C debian/postgresql-client-$(MAJOR_PKG)/usr/lib/postgresql/$(MAJOR_VER)/bin/pg_config | sed -e 's![^ ]*/debian/postgresql-client-$(MAJOR_PKG)!!' >> debian/libpq-dev/usr/bin/pg_config
+ LC_ALL=C debian/postgresql-client-$(MAJOR_PKG)/usr/lib/postgresql/$(MAJOR_VER)/bin/pg_config --help >> debian/libpq-dev/usr/bin/pg_config
+ chmod 755 debian/libpq-dev/usr/bin/pg_config
+ [ "$$(debian/libpq-dev/usr/bin/pg_config --bindir)" = "/usr/lib/postgresql/$(MAJOR_VER)/bin" ]
+
+ # remove actual build path from Makefile.global for reproducibility
+ sed -i -e "s!^abs_top_builddir.*!abs_top_builddir = /build/postgresql-$(MAJOR_PKG)/build!" \
+ -e "s!^abs_top_srcdir.*!abs_top_srcdir = /build/postgresql-$(MAJOR_PKG)/build/..!" \
+ -e 's!-f\(debug\|file\)-prefix-map=[^ ]* !!g' \
+ debian/postgresql-client-$(MAJOR_PKG)/usr/lib/postgresql/$(MAJOR_VER)/lib/pgxs/src/Makefile.global
+
+ # these are shipped in the pl packages
+ bash -c "rm -v debian/postgresql-$(MAJOR_PKG)/usr/share/postgresql/$(MAJOR_VER)/extension/{plperl,plpython,pltcl,*_pl}*"
+ bash -c "rm -v debian/postgresql-$(MAJOR_PKG)/usr/lib/postgresql/$(MAJOR_VER)/lib/{plperl,plpython,pltcl,*_pl}*"
+ bash -c "rm -rfv debian/postgresql-$(MAJOR_PKG)/usr/lib/postgresql/$(MAJOR_VER)/lib/bitcode/*{plperl,plpython,pltcl}*"
+
+ # record catversion in a file
+ echo $(CATVERSION) > debian/postgresql-$(MAJOR_PKG)/usr/share/postgresql/$(MAJOR_VER)/catalog_version
+
+override_dh_install-indep:
+ dh_install -i
+
+ if [ -d debian/postgresql-doc-$(MAJOR_PKG) ]; then set -e; \
+ install -d debian/postgresql-doc-$(MAJOR_PKG)/usr/share/doc/postgresql-doc-$(MAJOR_VER)/tutorial; \
+ install src/tutorial/*.c src/tutorial/*.source src/tutorial/Makefile src/tutorial/README debian/postgresql-doc-$(MAJOR_PKG)/usr/share/doc/postgresql-doc-$(MAJOR_VER)/tutorial; \
+ fi
+
+override_dh_auto_test-indep:
+ # nothing to do
+
+override_dh_auto_test-arch:
+ifeq (, $(findstring nocheck, $(DEB_BUILD_OPTIONS)))
+ # when tests fail, print newest log files
+ # initdb doesn't like LANG and LC_ALL to contradict, unset LANG and LC_CTYPE here
+ # temp-install wants to be invoked from a top-level make, unset MAKELEVEL here
+ # tell pg_upgrade to create its sockets in /tmp to avoid too long paths
+ unset LANG LC_CTYPE MAKELEVEL; ulimit -c unlimited; \
+ if ! make -C build check-world \
+ $(TEMP_CONFIG) \
+ PGSOCKETDIR="/tmp" \
+ PG_TEST_EXTRA='ssl' \
+ PROVE_FLAGS="--verbose"; \
+ then \
+ for l in `find build -name 'regression.*' -o -name '*.log' -o -name '*_log_*' | perl -we 'print map { "$$_\n"; } sort { (stat $$a)[9] <=> (stat $$b)[9] } map { chomp; $$_; } <>' | tail -n 10`; do \
+ echo "******** $$l ********"; \
+ cat $$l; \
+ done; \
+ for c in `find build -name 'core*'`; do \
+ echo "******** $$c ********"; \
+ gdb -batch -ex 'bt full' build/tmp_install/usr/lib/postgresql/$(MAJOR_VER)/bin/postgres $$c || :; \
+ done; \
+ $(TEST_FAIL_COMMAND); \
+ fi
+ifeq ($(WITH_PG_BSD_INDENT),y)
+ $(MAKE) -C build/src/tools/pg_bsd_indent test DESTDIR=$(CURDIR)/debian/tmp
+endif
+endif
+
+override_dh_installdeb-arch:
+ dh_installdeb
+ # record catversion in preinst
+ sed -i -e 's/@CATVERSION@/$(CATVERSION)/' debian/postgresql-$(MAJOR_PKG)/DEBIAN/preinst
+
+override_dh_gencontrol:
+ # record catversion in .deb control file
+ dh_gencontrol $(EXCLUDE_PACKAGES) -- -Vpostgresql:Catversion=$(CATVERSION) -Vllvm:Version=$(LLVM_VERSION) $(GENCONTROL_FLAGS)
+
+ifneq ($(EXCLUDE_PACKAGES),)
+override_dh_builddeb:
+ dh_builddeb $(EXCLUDE_PACKAGES)
+endif
diff --git a/server/test-with-jit.conf b/server/test-with-jit.conf
new file mode 100644
index 0000000..c68a521
--- /dev/null
+++ b/server/test-with-jit.conf
@@ -0,0 +1,8 @@
+# config used by pg_regress --temp-config
+
+fsync = off
+
+# force JITing of all queries in tests
+jit = on
+jit_above_cost = 0
+jit_optimize_above_cost = 1000
diff --git a/systemd/README.systemd b/systemd/README.systemd
new file mode 100644
index 0000000..c818ad3
--- /dev/null
+++ b/systemd/README.systemd
@@ -0,0 +1,55 @@
+systemd unit files for PostgreSQL on Debian/Ubuntu
+--------------------------------------------------
+
+Each cluster is run as a separate service, called postgresql@version-cluster.
+pg_ctlcluster is invoked with --skip-systemctl-redirect. Logging still goes to
+/var/log/postgresql.
+
+There is a parent service called postgresql.service, that starts/stops/restarts/
+reloads all individual services that are configured as "auto" in
+/etc/postgresql/*/*/start.conf.
+
+The link between start.conf and postgresql.service is established by
+postgresql-generator, which creates symlinks in
+/run/systemd/generator/postgresql.service.wants/.
+
+Backup
+------
+
+Two backup mechanisms are being offered as systemd services: basebackups
+capable of point in time recovery (PITR, the preferred method), and SQL-level
+dumps.
+
+pg_basebackup@version-cluster.service
+pg_basebackup@version-cluster.timer
+
+ Weekly basebackup in /var/backups/postgresql/version/cluster.
+ By default, 3 copies are being kept.
+
+ To enable, run
+ systemctl enable --now pg_basebackup@version-cluster.timer
+ systemctl start pg_basebackup@version-cluster.service
+
+pg_receivewal@version-cluster.service
+
+ WAL archival to be used with pg_basebackup@.service for PITR.
+
+ To enable, run
+ systemctl enable --now pg_basebackup@version-cluster.timer pg_receivewal@version-cluster.service
+ systemctl start pg_basebackup@version-cluster.service
+
+pg_dump@version-cluster.service
+pg_dump@version-cluster.timer
+
+ Weekly SQL dump in /var/backups/postgresql/version/cluster.
+ By default, 3 copies are being kept.
+
+ To enable, run
+ systemctl enable --now pg_dump@version-cluster.timer
+ systemctl start pg_dump@version-cluster.service
+
+The mechanisms provided are meant to be used with low to medium size databases.
+For larger databases, or databases with high write volume, we advise to use a
+full-size backup solution such as pgbackrest or barman.
+
+ -- Christoph Berg <myon@debian.org> Mon, 08 Mar 2021 13:45:26 +0100
diff --git a/systemd/system-generators/postgresql-generator b/systemd/system-generators/postgresql-generator
new file mode 100755
index 0000000..12e8102
--- /dev/null
+++ b/systemd/system-generators/postgresql-generator
@@ -0,0 +1,38 @@
+#!/bin/sh
+
+# This systemd generator creates dependency symlinks that make all PostgreSQL
+# clusters with "auto" in their start.conf file be started/stopped/reloaded
+# when postgresql.service is started/stopped/reloaded.
+
+set -eu
+
+gendir="$1"
+wantdir="$1/postgresql.service.wants"
+bindir="/usr/lib/postgresql/"
+#redhat# bindir="/usr/pgsql-"
+pgservice="/lib/systemd/system/postgresql@.service"
+
+mkdir -p "$wantdir"
+
+for conf in /etc/postgresql/*/*/postgresql.conf; do
+ # abort loop if glob was not expanded (but accept dead symlinks)
+ if ! test -e "$conf" && ! test -L "$conf"; then continue; fi
+
+ dir="${conf%/*}"
+
+ # evaluate start.conf
+ if [ -e "$dir/start.conf" ]; then
+ start=$(sed 's/#.*$//; /^[[:space:]]*$/d; s/^\s*//; s/\s*$//' "$dir/start.conf")
+ else
+ start=auto
+ fi
+ [ "$start" = "auto" ] || continue
+
+ verdir="${dir%/*}"
+ version="${verdir##*/}"
+ test -x "$bindir$version/bin/postgres" || continue # package got removed
+ cluster="${dir##*/}"
+ ln -s "$pgservice" "$wantdir/postgresql@$version-$cluster.service"
+done
+
+exit 0
diff --git a/systemd/system/pg_basebackup@.service b/systemd/system/pg_basebackup@.service
new file mode 100644
index 0000000..011c11e
--- /dev/null
+++ b/systemd/system/pg_basebackup@.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=Basebackup of PostgreSQL Cluster %i
+AssertPathExists=/etc/postgresql/%I/postgresql.conf
+Wants=postgresql@%i.service
+After=postgresql@%i.service
+RequiresMountsFor=/var/backups/postgresql
+
+[Service]
+Type=oneshot
+User=postgres
+Environment="KEEP=3"
+ExecStartPre=+/usr/bin/pg_backupcluster %i createdirectory
+ExecStart=/usr/bin/pg_backupcluster %i basebackup
+ExecStart=/usr/bin/pg_backupcluster %i expirebasebackups $KEEP
diff --git a/systemd/system/pg_basebackup@.timer b/systemd/system/pg_basebackup@.timer
new file mode 100644
index 0000000..da0bb3f
--- /dev/null
+++ b/systemd/system/pg_basebackup@.timer
@@ -0,0 +1,12 @@
+[Unit]
+Description=Weekly Basebackup of PostgreSQL Cluster %i
+AssertPathExists=/etc/postgresql/%I/postgresql.conf
+
+[Timer]
+OnCalendar=weekly
+RandomizedDelaySec=1h
+FixedRandomDelay=true
+
+[Install]
+# when enabled, start along with postgresql@%i
+WantedBy=postgresql@%i.service
diff --git a/systemd/system/pg_compresswal@.service b/systemd/system/pg_compresswal@.service
new file mode 100644
index 0000000..e5eae6b
--- /dev/null
+++ b/systemd/system/pg_compresswal@.service
@@ -0,0 +1,9 @@
+[Unit]
+Description=Compress WAL of PostgreSQL Cluster %i
+AssertPathExists=/etc/postgresql/%I/postgresql.conf
+RequiresMountsFor=/var/backups/postgresql
+
+[Service]
+Type=oneshot
+User=postgres
+ExecStart=/usr/bin/pg_backupcluster %i compresswal
diff --git a/systemd/system/pg_compresswal@.timer b/systemd/system/pg_compresswal@.timer
new file mode 100644
index 0000000..6dddbb9
--- /dev/null
+++ b/systemd/system/pg_compresswal@.timer
@@ -0,0 +1,12 @@
+[Unit]
+Description=Daily Compress WAL of PostgreSQL Cluster %i
+AssertPathExists=/etc/postgresql/%I/postgresql.conf
+
+[Timer]
+OnCalendar=daily
+RandomizedDelaySec=1h
+FixedRandomDelay=true
+
+[Install]
+# when enabled, start along with pg_receivewal@%i
+WantedBy=pg_receivewal@%i.service
diff --git a/systemd/system/pg_dump@.service b/systemd/system/pg_dump@.service
new file mode 100644
index 0000000..a7f7f3d
--- /dev/null
+++ b/systemd/system/pg_dump@.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=Dump of PostgreSQL Cluster %i
+AssertPathExists=/etc/postgresql/%I/postgresql.conf
+Wants=postgresql@%i.service
+After=postgresql@%i.service
+RequiresMountsFor=/var/backups/postgresql
+
+[Service]
+Type=oneshot
+User=postgres
+Environment="KEEP=3"
+ExecStartPre=+/usr/bin/pg_backupcluster %i createdirectory
+ExecStart=/usr/bin/pg_backupcluster %i dump
+ExecStart=/usr/bin/pg_backupcluster %i expiredumps $KEEP
diff --git a/systemd/system/pg_dump@.timer b/systemd/system/pg_dump@.timer
new file mode 100644
index 0000000..a1d2799
--- /dev/null
+++ b/systemd/system/pg_dump@.timer
@@ -0,0 +1,12 @@
+[Unit]
+Description=Weekly Dump of PostgreSQL Cluster %i
+AssertPathExists=/etc/postgresql/%I/postgresql.conf
+
+[Timer]
+OnCalendar=weekly
+RandomizedDelaySec=1h
+FixedRandomDelay=true
+
+[Install]
+# when enabled, start along with postgresql@%i
+WantedBy=postgresql@%i.service
diff --git a/systemd/system/pg_receivewal@.service b/systemd/system/pg_receivewal@.service
new file mode 100644
index 0000000..a15b432
--- /dev/null
+++ b/systemd/system/pg_receivewal@.service
@@ -0,0 +1,21 @@
+[Unit]
+Description=WAL archival of PostgreSQL Cluster %i
+AssertPathExists=/etc/postgresql/%I/postgresql.conf
+Wants=postgresql@%i.service
+After=postgresql@%i.service
+RequiresMountsFor=/var/backups/postgresql
+
+[Service]
+Type=simple
+User=postgres
+ExecStartPre=+/usr/bin/pg_backupcluster %i createdirectory
+ExecStart=/usr/bin/pg_backupcluster %i receivewal
+Restart=on-failure
+RestartSec=1min
+# pg_receivewal only flushes compressed output on SIGINT
+# (https://www.postgresql.org/message-id/flat/Yvo/5No5S0c4EFMj%40msg.df7cb.de)
+KillSignal=SIGINT
+
+[Install]
+# when enabled, start along with postgresql@%i
+WantedBy=postgresql@%i.service
diff --git a/systemd/system/postgresql.service b/systemd/system/postgresql.service
new file mode 100644
index 0000000..f53834e
--- /dev/null
+++ b/systemd/system/postgresql.service
@@ -0,0 +1,18 @@
+# postgresql.service is the meta unit for managing all PostgreSQL clusters on
+# the system at once. Conceptually, this unit is more like a systemd target,
+# but we are using a service since targets cannot be reloaded.
+#
+# The unit actually managing PostgreSQL clusters is postgresql@.service,
+# instantiated as postgresql@15-main.service for individual clusters.
+
+[Unit]
+Description=PostgreSQL RDBMS
+
+[Service]
+Type=oneshot
+ExecStart=/bin/true
+ExecReload=/bin/true
+RemainAfterExit=on
+
+[Install]
+WantedBy=multi-user.target
diff --git a/systemd/system/postgresql@.service b/systemd/system/postgresql@.service
new file mode 100644
index 0000000..8eed65c
--- /dev/null
+++ b/systemd/system/postgresql@.service
@@ -0,0 +1,40 @@
+# systemd service template for PostgreSQL clusters. The actual instances will
+# be called "postgresql@version-cluster", e.g. "postgresql@9.3-main". The
+# variable %i expands to "version-cluster", %I expands to "version/cluster".
+# (%I breaks for cluster names containing dashes.)
+
+[Unit]
+Description=PostgreSQL Cluster %i
+AssertPathExists=/etc/postgresql/%I/postgresql.conf
+RequiresMountsFor=/etc/postgresql/%I /var/lib/postgresql/%I
+PartOf=postgresql.service
+ReloadPropagatedFrom=postgresql.service
+Before=postgresql.service
+# stop server before networking goes down on shutdown
+After=network.target
+
+[Service]
+Type=forking
+# -: ignore startup failure (recovery might take arbitrarily long)
+# the actual pg_ctl timeout is configured in pg_ctl.conf
+ExecStart=-/usr/bin/pg_ctlcluster --skip-systemctl-redirect %i start
+# 0 is the same as infinity, but "infinity" needs systemd 229
+TimeoutStartSec=0
+ExecStop=/usr/bin/pg_ctlcluster --skip-systemctl-redirect -m fast %i stop
+TimeoutStopSec=1h
+ExecReload=/usr/bin/pg_ctlcluster --skip-systemctl-redirect %i reload
+PIDFile=/run/postgresql/%i.pid
+SyslogIdentifier=postgresql@%i
+# prevent OOM killer from choosing the postmaster (individual backends will
+# reset the score to 0)
+OOMScoreAdjust=-900
+# restarting automatically will prevent "pg_ctlcluster ... stop" from working,
+# so we disable it here. Also, the postmaster will restart by itself on most
+# problems anyway, so it is questionable if one wants to enable external
+# automatic restarts.
+#Restart=on-failure
+# (This should make pg_ctlcluster stop work, but doesn't:)
+#RestartPreventExitStatus=SIGINT SIGTERM
+
+[Install]
+WantedBy=multi-user.target
diff --git a/t/001_packages.t b/t/001_packages.t
new file mode 100644
index 0000000..697e871
--- /dev/null
+++ b/t/001_packages.t
@@ -0,0 +1,87 @@
+# Check that the necessary packages are installed
+
+use warnings;
+use strict;
+
+use lib 't';
+use TestLib;
+use POSIX qw/setlocale LC_ALL LC_MESSAGES/;
+
+use Test::More tests => $PgCommon::rpm ? (3 + 9*@MAJORS) : (15 + 7*@MAJORS);
+
+ok (-f "/etc/os-release", "/etc/os-release exists");
+my ($os, $osversion) = os_release();
+ok (defined $os, "OS is $os");
+ok (defined $osversion, "OS version is $osversion");
+
+note "PostgreSQL versions installed: @MAJORS\n";
+my $f = $ENV{'PG_FLAVOR'} // '';
+
+if ($PgCommon::rpm) {
+ foreach my $v (@MAJORS) {
+ my $vv = $v;
+ $vv =~ s/\.//;
+
+ ok ((rpm_installed "postgresql$vv$f"), "postgresql$vv$f installed");
+ ok ((rpm_installed "postgresql$vv$f-libs"), "postgresql$vv$f-libs installed");
+ ok ((rpm_installed "postgresql$vv$f-server"), "postgresql$vv$f-server installed");
+ ok ((rpm_installed "postgresql$vv$f-contrib"), "postgresql$vv$f-contrib installed");
+ ok ((rpm_installed "postgresql$vv$f-plperl"), "postgresql$vv$f-plperl installed");
+ SKIP: {
+ skip "No python2 support", 1 unless ($v <= 12);
+ ok ((rpm_installed "postgresql$vv$f-plpython"), "postgresql$vv$f-plpython installed");
+ }
+ ok ((rpm_installed "postgresql$vv$f-plpython3"), "postgresql$vv$f-plpython3 installed");
+ ok ((rpm_installed "postgresql$vv$f-pltcl"), "postgresql$vv$f-pltcl installed");
+ ok ((rpm_installed "postgresql$vv$f-devel"), "postgresql$vv$f-devel installed");
+ }
+ exit;
+}
+
+my $docpkgs = 0;
+foreach my $v (@MAJORS) {
+ ok ((deb_installed "postgresql-$v$f"), "postgresql-$v$f installed");
+ SKIP: {
+ skip "No python2 support", 1 unless ($v <= 11 and $PgCommon::have_python2);
+ ok ((deb_installed "postgresql-plpython-$v$f"), "postgresql-plpython-$v$f installed");
+ }
+ if ($v >= '9.1') {
+ ok ((deb_installed "postgresql-plpython3-$v$f"), "postgresql-plpython3-$v$f installed");
+ } else {
+ pass "no Python 3 package for version $v";
+ }
+ ok ((deb_installed "postgresql-plperl-$v$f"), "postgresql-plperl-$v$f installed");
+ ok ((deb_installed "postgresql-pltcl-$v$f"), "postgresql-pltcl-$v$f installed");
+ ok ((deb_installed "postgresql-server-dev-$v$f"), "postgresql-server-dev-$v$f installed");
+ SKIP: {
+ skip "No postgresql-contrib-$v$f package for version $v", 1 if ($v >= 10);
+ ok ((deb_installed "postgresql-contrib-$v$f"), "postgresql-contrib-$v$f installed");
+ }
+ my $docpkg = "postgresql-doc-$v$f";
+ if (deb_installed $docpkg) {
+ note "$docpkg installed";
+ $docpkgs++;
+ }
+}
+ok $docpkgs, "At least one doc package installed";
+
+ok ((deb_installed 'libecpg-dev'), 'libecpg-dev installed');
+ok ((deb_installed 'procps'), 'procps installed');
+ok ((deb_installed 'netcat-openbsd'), 'netcat-openbsd installed');
+
+ok ((deb_installed 'hunspell-en-us'), 'hunspell-en-us installed');
+
+# check installed locales to fail tests early if they are missing
+ok ((setlocale(LC_MESSAGES, '') =~ /utf8|UTF-8/), 'system has a default UTF-8 locale');
+ok (setlocale (LC_ALL, "ru_RU"), 'locale ru_RU exists');
+ok (setlocale (LC_ALL, "ru_RU.UTF-8"), 'locale ru_RU.UTF-8 exists');
+
+my $key_file = '/etc/ssl/private/ssl-cert-snakeoil.key';
+my $pem_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem';
+ok ((getgrnam('ssl-cert'))[3] =~ /postgres/,
+ 'user postgres in the UNIX group ssl-cert');
+ok (-e $key_file, "$key_file exists");
+is (exec_as ('postgres', "cat $key_file > /dev/null"), 0, "$key_file is readable for postgres");
+ok (-e $pem_file, "$pem_file exists");
+
+# vim: filetype=perl
diff --git a/t/002_existing_clusters.t b/t/002_existing_clusters.t
new file mode 100644
index 0000000..93cc782
--- /dev/null
+++ b/t/002_existing_clusters.t
@@ -0,0 +1,11 @@
+# Check that no clusters and postgres processes are present for this test.
+
+use strict;
+use Test::More tests => 8;
+
+use lib 't';
+use TestLib;
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/003_alternatives.t b/t/003_alternatives.t
new file mode 100644
index 0000000..dd49d6b
--- /dev/null
+++ b/t/003_alternatives.t
@@ -0,0 +1,37 @@
+# Check that alternatives symlinks point to the correct locations
+
+use warnings;
+use strict;
+
+use lib 't';
+use TestLib;
+
+use Test::More tests => $PgCommon::rpm ? 1 : 9;
+
+if ($PgCommon::rpm) {
+ ok "No alternatives test on RedHat";
+ exit;
+}
+
+# server/client link group
+my $newest_version = $ALL_MAJORS[-1];
+note "Newest PG version installed is $newest_version";
+program_ok 0, 'update-alternatives --list psql.1.gz', 0, 'psql.1.gz link group';
+program_ok 0, 'update-alternatives --list postmaster.1.gz', 2, 'postmaster.1.gz link group does not exist'; # removed in pg-common 248
+for my $name (qw(psql pg_dump postgres pg_ctl)) {
+ is readlink "/etc/alternatives/$name.1.gz", "/usr/share/postgresql/$newest_version/man/man1/$name.1.gz", "$name.1.gz alternative";
+}
+
+# doc link group
+my $newest_doc_version = `dpkg -l 'postgresql-doc-[1-9]*' | sed -ne 's/^ii postgresql-doc-\\([0-9.]*\\).*/\\1/p' | sort -g | tail -n 1`;
+note "Newest PG doc version installed is $newest_doc_version";
+SKIP: {
+ skip "No SPI_connect.3.gz link group on 8.x", 3 if ($newest_doc_version < 9.0);
+ chomp $newest_doc_version;
+ program_ok 0, 'update-alternatives --list SPI_connect.3.gz', 0, 'SPI_connect.3.gz link group';
+ for my $name (qw(SPI_connect SPI_exec)) {
+ is readlink "/etc/alternatives/$name.3.gz", "/usr/share/postgresql/$newest_doc_version/man/man3/$name.3.gz", "$name.3.gz alternative";
+ }
+}
+
+# vim: filetype=perl
diff --git a/t/005_PgCommon.t b/t/005_PgCommon.t
new file mode 100644
index 0000000..e1bc337
--- /dev/null
+++ b/t/005_PgCommon.t
@@ -0,0 +1,311 @@
+# Check PgCommon library functions.
+
+use strict;
+
+use File::Temp qw/tempdir/;
+
+use lib '.';
+use PgCommon;
+
+use lib 't';
+use TestLib;
+
+use Test::More tests => 24;
+
+my $tdir = tempdir (CLEANUP => 1);
+$PgCommon::confroot = $tdir;
+
+# test read_pg_hba with valid file
+open P, ">$tdir/pg_hba.conf" or die "Could not create $tdir/pg_hba.conf: $!";
+print P <<EOF;
+# comment
+local all postgres ident sameuser
+
+# TYPE DATABASE USER CIDR-ADDRESS METHOD
+local foo nobody trust
+local foo nobody crypt
+local foo nobody,joe krb5
+local foo,bar nobody ident
+local all +foogrp password
+host \@inc all 127.0.0.1/32 md5
+hostssl all \@inc 192.168.0.0 255.255.0.0 pam
+hostnossl all all 192.168.0.0 255.255.0.0 reject
+EOF
+close P;
+
+my @expected_records = (
+ { 'type' => 'local', 'db' => 'all', 'user' => 'postgres', 'method' => 'ident sameuser' },
+ { 'type' => 'local', 'db' => 'foo', 'user' => 'nobody', 'method' => 'trust' },
+ { 'type' => 'local', 'db' => 'foo', 'user' => 'nobody', 'method' => 'crypt' },
+ { 'type' => 'local', 'db' => 'foo', 'user' => 'nobody,joe', 'method' => 'krb5' },
+ { 'type' => 'local', 'db' => 'foo,bar', 'user' => 'nobody', 'method' => 'ident' },
+ { 'type' => 'local', 'db' => 'all', 'user' => '+foogrp', 'method' => 'password' },
+ { 'type' => 'host', 'db' => '@inc', 'user' => 'all', 'method' => 'md5', 'ip' => '127.0.0.1', 'mask' => '32'},
+ { 'type' => 'hostssl', 'db' => 'all', 'user' => '@inc', 'method' => 'pam', 'ip' => '192.168.0.0', 'mask' => '255.255.0.0'},
+ { 'type' => 'hostnossl', 'db' => 'all', 'user' => 'all', 'method' => 'reject', 'ip' => '192.168.0.0', 'mask' => '255.255.0.0'},
+);
+
+my @hba = read_pg_hba "$tdir/pg_hba.conf";
+foreach my $entry (@hba) {
+ next if $$entry{'type'} eq 'comment';
+ if ($#expected_records < 0) {
+ fail '@expected_records is already empty';
+ next;
+ }
+ my $expected = shift @expected_records;
+ my $parsedstr = '';
+ my $expectedstr = '';
+ foreach my $k (keys %$expected) {
+ $parsedstr .= $k . ':\'' . $$entry{$k} . '\' ';
+ $expectedstr .= $k . ':\'' . $$expected{$k} . '\' ';
+ if ($$expected{$k} ne $$entry{$k}) {
+ fail "mismatch: $expectedstr ne $parsedstr";
+ last;
+ }
+ }
+ pass 'correctly parsed line \'' . $$entry{'line'} . "'";
+}
+
+ok (($#expected_records == -1), '@expected_records has correct number of entries');
+
+# test read_pg_hba with invalid file
+my $invalid_hba = <<EOF;
+foo all all md5
+local all all foo
+host all all foo
+host all all 127.0.0.1/32 foo
+host all all md5
+host all all 127.0.0.1/32 0.0.0.0 md5
+host all all 127.0.0.1 md5
+EOF
+open P, ">$tdir/pg_hba.conf" or die "Could not create $tdir/pg_hba_invalid.conf: $!";
+print P $invalid_hba;
+close P;
+
+@hba = read_pg_hba "$tdir/pg_hba.conf";
+is (scalar (split "\n", $invalid_hba), $#hba+1, 'returned read_pg_hba array has correct number of records');
+foreach my $entry (@hba) {
+ is $$entry{'type'}, undef, 'line \'' . $$entry{'line'} . '\' parsed as invalid';
+}
+
+# test read_conf_file()
+my %conf = PgCommon::read_conf_file '/nonexisting';
+is_deeply \%conf, {}, 'read_conf_file returns empty dict for nonexisting file';
+
+mkdir "$tdir/8.4";
+mkdir "$tdir/8.4/test" or die "mkdir: $!";
+mkdir "$tdir/conf.d" or die "mkdir: $!";
+my $c = "$tdir/8.4/test/foo.conf";
+open F, ">$c" or die "Could not create $c: $!";
+print F <<EOF;
+# test configuration file
+
+# Commented_Int = 12
+# commented_str='foobar'
+# commented_bool off
+# commented_bool2 off # comment
+# commented_bool3 just a comment
+
+#intval = 1
+Intval = 42
+cintval=1 # blabla
+floatval = 1.5e+3
+strval 'hello'
+strval2 'world'
+cstrval = 'bye' # comment
+emptystr = ''
+cemptystr = '' # moo!
+#testpath = '/bin/bad'
+testpath = '/bin/test'
+QuoteStr = 'test ! -f \\'/tmp/%f\\' && echo \\'yes\\''
+EOF
+close F;
+%conf = PgCommon::read_conf_file "$c";
+is_deeply (\%conf, {
+ 'intval' => 42,
+ 'cintval' => 1,
+ 'floatval' => '1.5e+3',
+ 'strval' => 'hello',
+ 'strval2' => 'world',
+ 'cstrval' => 'bye',
+ 'testpath' => '/bin/test',
+ 'emptystr' => '',
+ 'cemptystr' => '',
+ 'quotestr' => "test ! -f '/tmp/%f' && echo 'yes'"
+ }, 'read_conf_file() parsing');
+
+# test read_conf_file() with include directives
+open F, ">$tdir/8.4/test/condinc.conf" or die "Could not create $tdir/condinc.conf: $!";
+print F "condint = 42\n";
+close F;
+
+open F, ">$tdir/bar.conf" or die "Could not create $tdir/bar.conf: $!";
+print F <<EOF;
+# test configuration file
+
+# Commented_Int = 24
+# commented_str = 'notme'
+
+intval = -1
+include '8.4/test/foo.conf' # foo
+include_dir 'conf.d' # bar
+strval = 'howdy'
+include_if_exists '/nonexisting.conf'
+include_if_exists = '8.4/test/condinc.conf'
+EOF
+close F;
+
+open F, ">$tdir/conf.d/sub.conf" or die "Could not create $tdir/conf.d/sub.conf: $!";
+print F <<EOF;
+subvalue = 1
+include = '../relative.conf'
+EOF
+close F;
+
+open F, ">$tdir/relative.conf" or die "Could not create $tdir/relative.conf: $!";
+print F <<EOF;
+relativevalue = 1
+include '$tdir/absolute.conf'
+EOF
+close F;
+
+open F, ">$tdir/absolute.conf" or die "Could not create $tdir/absolute.conf: $!";
+print F <<EOF;
+absolutevalue = 1
+EOF
+close F;
+
+%conf = PgCommon::read_conf_file "$tdir/bar.conf";
+is_deeply (\%conf, {
+ 'intval' => 42,
+ 'cintval' => 1,
+ 'floatval' => '1.5e+3',
+ 'strval' => 'howdy',
+ 'strval2' => 'world',
+ 'cstrval' => 'bye',
+ 'testpath' => '/bin/test',
+ 'emptystr' => '',
+ 'cemptystr' => '',
+ 'quotestr' => "test ! -f '/tmp/%f' && echo 'yes'",
+ 'condint' => 42,
+ 'subvalue' => 1,
+ 'relativevalue' => 1,
+ 'absolutevalue' => 1,
+ }, 'read_conf_file() parsing with include directives');
+
+
+# test set_conf_value()
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'commented_int', '24';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'commented_str', 'new foo';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'commented_bool', 'on';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'commented_bool2', 'on';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'commented_bool3', 'on';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'intval', '39';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'cintval', '5';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'strval', 'Howdy';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'newval', 'NEW!';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'testpath', '/bin/new';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'include_dir', 'conf.d';
+
+open F, "$c";
+my $conf;
+read F, $conf, 1024;
+close F;
+is ($conf, <<EOF, 'set_conf_value');
+# test configuration file
+
+Commented_Int = 24
+commented_str='new foo'
+commented_bool on
+commented_bool2 on # comment
+# commented_bool3 just a comment
+
+#intval = 1
+Intval = 39
+cintval=5 # blabla
+floatval = 1.5e+3
+strval Howdy
+strval2 'world'
+cstrval = 'bye' # comment
+emptystr = ''
+cemptystr = '' # moo!
+#testpath = '/bin/bad'
+testpath = '/bin/new'
+QuoteStr = 'test ! -f \\'/tmp/%f\\' && echo \\'yes\\''
+commented_bool3 = on
+newval = 'NEW!'
+include_dir = 'conf.d'
+EOF
+
+# test disable_conf_value()
+PgCommon::disable_conf_value '8.4', 'test', 'foo.conf', 'intval', 'ints are out of fashion';
+PgCommon::disable_conf_value '8.4', 'test', 'foo.conf', 'cstrval', 'not used any more';
+PgCommon::disable_conf_value '8.4', 'test', 'foo.conf', 'nonexisting', 'NotMe';
+PgCommon::disable_conf_value '8.4', 'test', 'foo.conf', 'testpath', 'now 2 comments';
+
+open F, "$c";
+read F, $conf, 1024;
+close F;
+is ($conf, <<EOF, 'disable_conf_value');
+# test configuration file
+
+Commented_Int = 24
+commented_str='new foo'
+commented_bool on
+commented_bool2 on # comment
+# commented_bool3 just a comment
+
+#intval = 1
+#Intval = 39 #ints are out of fashion
+cintval=5 # blabla
+floatval = 1.5e+3
+strval Howdy
+strval2 'world'
+#cstrval = 'bye' # comment #not used any more
+emptystr = ''
+cemptystr = '' # moo!
+#testpath = '/bin/bad'
+#testpath = '/bin/new' #now 2 comments
+QuoteStr = 'test ! -f \\'/tmp/%f\\' && echo \\'yes\\''
+commented_bool3 = on
+newval = 'NEW!'
+include_dir = 'conf.d'
+EOF
+
+# test replace_conf_value()
+PgCommon::replace_conf_value '8.4', 'test', 'foo.conf', 'strval',
+ 'renamedstrval', 'newstrval', 'goodbye';
+PgCommon::replace_conf_value '8.4', 'test', 'foo.conf', 'nonexisting',
+ 'renamednonexisting', 'newnonexisting', 'XXX';
+
+open F, "$c";
+read F, $conf, 1024;
+close F;
+is ($conf, <<EOF, 'replace_conf_value');
+# test configuration file
+
+Commented_Int = 24
+commented_str='new foo'
+commented_bool on
+commented_bool2 on # comment
+# commented_bool3 just a comment
+
+#intval = 1
+#Intval = 39 #ints are out of fashion
+cintval=5 # blabla
+floatval = 1.5e+3
+#strval Howdy #renamedstrval
+newstrval = goodbye
+strval2 'world'
+#cstrval = 'bye' # comment #not used any more
+emptystr = ''
+cemptystr = '' # moo!
+#testpath = '/bin/bad'
+#testpath = '/bin/new' #now 2 comments
+QuoteStr = 'test ! -f \\'/tmp/%f\\' && echo \\'yes\\''
+commented_bool3 = on
+newval = 'NEW!'
+include_dir = 'conf.d'
+EOF
+
+# vim: filetype=perl
diff --git a/t/006_next_free_port.t b/t/006_next_free_port.t
new file mode 100644
index 0000000..4a82bf3
--- /dev/null
+++ b/t/006_next_free_port.t
@@ -0,0 +1,49 @@
+# Check PgCommon's next_free_port()
+
+use strict;
+
+use lib '.';
+use PgCommon;
+
+use lib 't';
+use TestLib;
+
+use Test::More tests => 5;
+
+# test next_free_port(). We are intentionally using nc as an external tool,
+# using perl would replicate what next_free_port is doing, and that would
+# be a pointless test.
+use IPC::Open2;
+use Time::HiRes qw(usleep);
+my @pids;
+# no ports open
+is (next_free_port, 5432, 'next_free_port is 5432');
+
+# open a localhost ipv4 socket
+push @pids, open2(\*CHLD_OUT, \*CHLD_IN, qw(nc -4 -l 127.0.0.1 5432));
+usleep 2*$delay;
+is (next_free_port, 5433, 'next_free_port detects localhost ipv4 socket');
+# open a wildcard ipv4 socket
+push @pids, open2(\*CHLD_OUT, \*CHLD_IN, qw(nc -4 -l 5433));
+usleep 2*$delay;
+is (next_free_port, 5434, 'next_free_port detects wildcard ipv4 socket');
+
+SKIP: {
+ $^V =~ /^v(\d+\.\d+)/; # parse perl version
+ skip "perl <= 5.10 does not have proper IPv6 support", 2 if ($1 <= 5.10);
+ skip "skipping IPv6 tests", 2 if ($ENV{SKIP_IPV6});
+
+ # open a localhost ipv6 socket
+ push @pids, open2(\*CHLD_OUT, \*CHLD_IN, qw(nc -6 -l ::1 5434));
+ usleep 2*$delay;
+ is (next_free_port, 5435, 'next_free_port detects localhost ipv6 socket');
+ # open a wildcard ipv6 socket
+ push @pids, open2(\*CHLD_OUT, \*CHLD_IN, qw(nc -6 -l 5435));
+ usleep 2*$delay;
+ is (next_free_port, 5436, 'next_free_port detects wildcard ipv6 socket');
+}
+
+# clean up
+kill 15, @pids;
+
+# vim: filetype=perl
diff --git a/t/007_pg_conftool.t b/t/007_pg_conftool.t
new file mode 100644
index 0000000..e8465ef
--- /dev/null
+++ b/t/007_pg_conftool.t
@@ -0,0 +1,85 @@
+# Test pg_conftool
+
+use strict;
+use warnings;
+
+use Test::More tests => 41;
+use File::Temp qw/tempdir/;
+use lib '.';
+use PgCommon;
+use lib 't';
+use TestLib;
+
+my $tdir = tempdir (CLEANUP => 1);
+$ENV{'PG_CLUSTER_CONF_ROOT'} = $tdir;
+
+open F, "> $tdir/different.conf";
+print F "a = '5'\n";
+print F "#b = '6'\n";
+close F;
+
+note 'test without cluster';
+is_program_out 0, "pg_conftool show all", 1, "Error: No default cluster found\n";
+is_program_out 0, "pg_conftool foo.conf show all", 1, "Error: No default cluster found\n";
+is_program_out 0, "pg_conftool $tdir/different.conf show all", 0, "a = 5\n";
+is_program_out 0, "pg_conftool 9.7 main show all", 1, "Error: Cluster 9.7 main does not exist\n";
+
+my $version = $MAJORS[-1];
+die "Tests past this point need PostgreSQL installed" unless ($version);
+mkdir "$tdir/$version";
+mkdir "$tdir/$version/main";
+
+open F, "> $tdir/$version/main/postgresql.conf";
+print F "a = '1'\n";
+print F "#b = '2'\n";
+close F;
+
+open F, "> $tdir/$version/main/other.conf";
+print F "a = '3'\n";
+print F "#b = '4'\n";
+close F;
+
+sub pgconf {
+ undef $/;
+ open F, "$tdir/$version/main/postgresql.conf";
+ my $f = <F>;
+ close F;
+ return $f;
+}
+
+sub differentconf {
+ undef $/;
+ open F, "$tdir/different.conf";
+ my $f = <F>;
+ close F;
+ return $f;
+}
+
+note 'test show';
+is_program_out 0, "pg_conftool show all", 0, "a = 1\n";
+is_program_out 0, "pg_conftool other.conf show all", 0, "a = 3\n";
+is_program_out 0, "pg_conftool $tdir/different.conf show all", 0, "a = 5\n";
+is_program_out 0, "pg_conftool $version main show all", 0, "a = 1\n";
+is_program_out 0, "pg_conftool $version main other.conf show all", 0, "a = 3\n";
+is_program_out 0, "pg_conftool show a", 0, "a = 1\n";
+is_program_out 0, "pg_conftool -s show a", 0, "1\n";
+
+note 'test set';
+is_program_out 0, "pg_conftool set c 7", 0, "";
+undef $/; # slurp mode
+is pgconf, "a = '1'\n#b = '2'\nc = 7\n", "file contains new setting";
+is_program_out 0, "pg_conftool set a 8", 0, "";
+is pgconf, "a = 8\n#b = '2'\nc = 7\n", "file contains updated setting";
+is_program_out 0, "pg_conftool $tdir/different.conf set a 9", 0, "";
+is differentconf, "a = 9\n#b = '6'\n", "file with path contains updated setting";
+
+note 'test remove';
+is_program_out 0, "pg_conftool remove a", 0, "";
+is pgconf, "#a = 8\n#b = '2'\nc = 7\n", "setting removed from file";
+is_program_out 0, "pg_conftool $tdir/different.conf remove a", 0, "";
+is differentconf, "#a = 9\n#b = '6'\n", "setting removed from file with path";
+
+note 'test edit';
+$ENV{EDITOR} = 'cat';
+is_program_out 0, "pg_conftool edit", 0, "#a = 8\n#b = '2'\nc = 7\n";
+is_program_out 0, "pg_conftool $tdir/different.conf edit", 0, "#a = 9\n#b = '6'\n";
diff --git a/t/010_defaultport_cluster.t b/t/010_defaultport_cluster.t
new file mode 100644
index 0000000..1ba0daa
--- /dev/null
+++ b/t/010_defaultport_cluster.t
@@ -0,0 +1,33 @@
+# We try to call psql with --version and then on localhost. Since there are no
+# clusters, we expect an error message that the connection to port 5432 is
+# refused. This checks that pg_wrapper correctly picks the default port and
+# uses the highest available version.
+
+use strict;
+use Test::More tests => 14;
+
+use lib 't';
+use TestLib;
+
+like_program_out 0, 'psql --version', 0, qr/^psql \(PostgreSQL\) $ALL_MAJORS[-1]/,
+ 'pg_wrapper selects highest available version number';
+
+like_program_out 0, 'env LC_MESSAGES=C psql -h 127.0.0.1 -l', 2, qr/could not connect|connection to server .* failed/,
+ 'connecting to localhost fails with no clusters';
+
+# We check if PGCLUSTER, --cluster, and native psql options are evaluated with
+# correct priority. (This is related to the checks in t/090_multicluster.t, but
+# easier to do here because no clusters are running.)
+
+like_program_out 0, "env LC_MESSAGES=C PGCLUSTER=$MAJORS[-1]/127.0.0.2:5431 psql -l",
+ 2, qr/(could not connect|connection to server).*127.0.0.2.* port 5431/s, 'pg_wrapper uses host and port from PGCLUSTER';
+like_program_out 0, "env LC_MESSAGES=C PGCLUSTER=$MAJORS[-1]/127.0.0.2:5431 psql --cluster $MAJORS[-1]/127.0.0.3:5430 -l",
+ 2, qr/(could not connect|connection to server).*127.0.0.3.* port 5430/s, 'pg_wrapper uses --cluster from the command line';
+like_program_out 0, "env LC_MESSAGES=C PGCLUSTER=$MAJORS[-1]/127.0.0.2:5431 psql -h 127.0.0.3 -l",
+ 2, qr/(could not connect|connection to server).*127.0.0.3.* port 5432/s, 'pg_wrapper ignores PGCLUSTER with -h on the command line';
+like_program_out 0, "env LC_MESSAGES=C PGCLUSTER=$MAJORS[-1]/127.0.0.2:5431 psql --host 127.0.0.3 -l",
+ 2, qr/(could not connect|connection to server).*127.0.0.3.* port 5432/s, 'pg_wrapper ignores PGCLUSTER with --host on the command line';
+like_program_out 0, "env LC_MESSAGES=C PGCLUSTER=$MAJORS[-1]/127.0.0.2:5431 PGHOST=127.0.0.3 psql -l",
+ 2, qr/(could not connect|connection to server).*127.0.0.3.* port 5432/s, 'pg_wrapper ignores PGCLUSTER if PGHOST is set';
+
+# vim: filetype=perl
diff --git a/t/015_start_stop.t b/t/015_start_stop.t
new file mode 100644
index 0000000..a6c4ebf
--- /dev/null
+++ b/t/015_start_stop.t
@@ -0,0 +1,174 @@
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => 71 * @MAJORS;
+
+my $systemd = (-d "/run/systemd/system" and not $ENV{_SYSTEMCTL_SKIP_REDIRECT});
+note $systemd ? "We are running systemd" : "We are not running systemd";
+
+# check cluster status
+# arguments: <version> <pg_ctlcluster exit status> <systemctl exit status> <text to print>
+sub check_status {
+ my ($v, $ctlstatus, $scstatus, $text) = @_;
+ program_ok (0, "pg_ctlcluster $v main status", $ctlstatus, "cluster $v main $text");
+ if ($systemd) {
+ program_ok (0, "systemctl status postgresql\@$v-main", $scstatus, "service postgresql\@$v-main $text");
+ } else {
+ pass '';
+ }
+}
+
+sub check_major {
+ my $v = $_[0];
+ my $ctlstopped = $v >= 9.2 ? 3 : 1; # pg_ctl status "not running" changed in 9.2
+ note "Running tests for $v";
+
+ note "Cluster does not exist yet"; ###############################
+ check_status $v, 1, 3, "does not exist";
+
+ # try to start postgresql
+ if ($systemd) {
+ program_ok (0, "systemctl start postgresql");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql start");
+ }
+ check_status $v, 1, 3, "does not exist";
+
+ # try to start specific cluster
+ if ($systemd) {
+ program_ok (0, "systemctl start postgresql\@$v-main", 1);
+ } else {
+ program_ok (0, "/etc/init.d/postgresql start $v");
+ }
+ check_status $v, 1, 3, "does not exist";
+
+ note "Start/stop postgresql using system tools"; ###############################
+
+ # create cluster
+ program_ok (0, "pg_createcluster $v main");
+ check_status $v, $ctlstopped, 3, "is stopped";
+
+ # start postgresql
+ if ($systemd) {
+ program_ok (0, "systemctl start postgresql");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql start");
+ }
+ check_status $v, 0, 0, "is running";
+
+ # start postgresql again
+ if ($systemd) {
+ program_ok (0, "systemctl start postgresql");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql start");
+ }
+ check_status $v, 0, 0, "is already running";
+
+ # stop postgresql
+ if ($systemd) {
+ program_ok (0, "systemctl stop postgresql");
+ sleep 6; # FIXME: systemctl stop postgresql is not yet synchronous (#759725)
+ } else {
+ program_ok (0, "/etc/init.d/postgresql stop");
+ }
+ check_status $v, $ctlstopped, 3, "is stopped";
+
+ # stop postgresql again
+ if ($systemd) {
+ program_ok (0, "systemctl stop postgresql");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql stop");
+ }
+ check_status $v, $ctlstopped, 3, "is already stopped";
+
+ note "Start/stop specific cluster using system tools"; ###############################
+
+ # start cluster using system tools
+ if ($systemd) {
+ program_ok (0, "systemctl start postgresql\@$v-main");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql start $v");
+ }
+ check_status $v, 0, 0, "is running";
+
+ # try start cluster again
+ if ($systemd) {
+ program_ok (0, "systemctl start postgresql\@$v-main");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql start $v");
+ }
+ check_status $v, 0, 0, "is running";
+
+ # restart cluster
+ if ($systemd) {
+ program_ok (0, "systemctl restart postgresql\@$v-main");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql restart $v");
+ }
+ check_status $v, 0, 0, "is running";
+
+ # stop cluster
+ if ($systemd) {
+ program_ok (0, "systemctl stop postgresql\@$v-main");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql stop $v");
+ }
+ check_status $v, $ctlstopped, 3, "is stopped";
+
+ # try to stop cluster again
+ if ($systemd) {
+ program_ok (0, "systemctl stop postgresql\@$v-main");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql stop $v");
+ }
+ check_status $v, $ctlstopped, 3, "is already stopped";
+
+ # drop cluster
+ program_ok (0, "pg_dropcluster $v main");
+ check_status $v, 1, 3, "does not exist";
+
+ note "Start/stop specific cluster using pg_*cluster"; ###############################
+
+ # try to start cluster
+ program_ok (0, "pg_ctlcluster start $v main", 1); # syntax variation: action version cluster
+ check_status $v, 1, 3, "does not exist";
+
+ # create cluster and start it
+ program_ok (0, "pg_createcluster $v main --start");
+ check_status $v, 0, 0, "is running";
+
+ # try to start cluster again
+ my $exitagain = $systemd ? 0 : 2;
+ program_ok (0, "pg_ctlcluster $v main start", $exitagain);
+ check_status $v, 0, 0, "is already running";
+
+ # restart cluster
+ program_ok (0, "pg_ctlcluster $v-main restart"); # syntax variation: version-cluster action
+ check_status $v, 0, 0, "is running";
+
+ # stop cluster
+ program_ok (0, "pg_ctlcluster $v main stop");
+ check_status $v, $ctlstopped, 3, "is stopped";
+
+ # try to stop cluster again
+ program_ok (0, "pg_ctlcluster $v main stop", 2);
+ check_status $v, $ctlstopped, 3, "is already stopped";
+
+ # start cluster
+ program_ok (0, "pg_ctlcluster start $v-main"); # syntax variation: action version-cluster
+ check_status $v, 0, 0, "is running";
+
+ # stop server, clean up, check for leftovers
+ program_ok (0, "pg_dropcluster $v main --stop");
+
+ check_clean;
+}
+
+foreach (@MAJORS) {
+ check_major $_;
+}
+
+# vim: filetype=perl
diff --git a/t/020_create_sql_remove.t b/t/020_create_sql_remove.t
new file mode 100644
index 0000000..d208b99
--- /dev/null
+++ b/t/020_create_sql_remove.t
@@ -0,0 +1,452 @@
+# We create a cluster, execute some basic SQL commands, drop it again, and
+# check that we did not leave anything behind.
+
+use strict;
+
+use File::Temp qw(tempdir);
+use POSIX qw/dup2/;
+use Time::HiRes qw/usleep/;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => 147 * @MAJORS;
+
+$ENV{_SYSTEMCTL_SKIP_REDIRECT} = 1; # FIXME: testsuite is hanging otherwise
+
+sub check_major {
+ my $v = $_[0];
+ note "Running tests for $v";
+
+ # create cluster
+ my $xlogdir = tempdir("/tmp/$v.xlog.XXXXXX", CLEANUP => 1);
+ rmdir $xlogdir; # recreated by initdb
+ if ($v > 8.2) {
+ my $start_command = $v >= 14 ? "$v *main *5432 *online" : # initdb --no-instructions in 14+
+ ($v >= 11 and not $PgCommon::rpm) ? "pg_ctlcluster" : "pg_ctl"; # CLUSTER_START_COMMAND supported in initdb 11+
+ like_program_out 'root', "pg_createcluster $v main --start -- -X $xlogdir", 0, qr/$start_command/,
+ "pg_createcluster $v main";
+ } else { # 8.2 does not have -X yet
+ like_program_out 'root', "pg_createcluster $v main --start", 0, qr/pg_ctl/,
+ "pg_createcluster $v main";
+ system "mv /var/lib/postgresql/$v/main/pg_xlog $xlogdir";
+ system "ln -s $xlogdir /var/lib/postgresql/$v/main/pg_xlog";
+ }
+
+ # check that a /var/run/postgresql/ pid file is created
+ my @contents = ('.s.PGSQL.5432', '.s.PGSQL.5432.lock', "$v-main.pid", "$v-main.pg_stat_tmp");
+ pop @contents if ($v < 8.4 or $v >= 15); # remove pg_stat_tmp
+ unless ($PgCommon::rpm and $v < 9.4) {
+ ok_dir '/var/run/postgresql/', [@contents],
+ 'Socket and pid file are in /var/run/postgresql/';
+ } else {
+ ok_dir '/var/run/postgresql/', [grep {/main/} @contents], 'Pid File is in /var/run/postgresql/';
+ }
+
+ # check that the xlog/wal symlink was created
+ my $first_xlog = $v >= 9.0 ? "000000010000000000000001" : "000000010000000000000000";
+ my @expectdir = ($first_xlog, "archive_status");
+ push @expectdir, "summaries" if ($v >= 17);
+ ok_dir $xlogdir, [@expectdir],
+ "xlog/wal directory $xlogdir was created";
+
+ # check pg_hba.conf auth methods
+ my $local_method = $v >= 9.1 ? 'peer' :
+ ($v >= 8.4 ? 'ident' :
+ 'sameuser'); # actually "ident sameuser", but the test is lazy
+ my $host_method = $v >= 14 ? 'scram-sha-256' : 'md5';
+ my (%local_methods, %host_methods);
+ open my $fh, "/etc/postgresql/$v/main/pg_hba.conf";
+ while (<$fh>) {
+ $local_methods{$1} = 1 if (/^local.*\s(\S+)/);
+ $host_methods{$1} = 1 if (/^host.*\s(\S+)/);
+ }
+ close $fh;
+ is_deeply [keys %local_methods], [$local_method], "local method in pg_hba.conf is $local_method";
+ is_deeply [keys %host_methods], [$host_method], "host method in pg_hba.conf is $host_method";
+
+ # verify that exactly one postgres master is running
+ my @pm_pids = pidof ('postgres');
+ is $#pm_pids, 0, 'Exactly one postgres master process running';
+
+ # check environment
+ my %safe_env = qw/LC_ALL 1 LC_CTYPE 1 LANG 1 PWD 1 PGLOCALEDIR 1 PGSYSCONFDIR 1 PG_GRANDPARENT_PID 1 PG_OOM_ADJUST_FILE 1 PG_OOM_ADJUST_VALUE 1 SHLVL 1 PGDATA 1 _ 1/;
+ my %env = pid_env 'postgres', $pm_pids[0];
+ foreach (keys %env) {
+ fail "postgres has unsafe environment variable $_" unless exists $safe_env{$_};
+ }
+
+ # activate external_pid_file
+ PgCommon::set_conf_value $v, 'main', 'postgresql.conf', 'external_pid_file', '';
+
+ # add variable to environment file, restart, check if it's there
+ open E, ">>/etc/postgresql/$v/main/environment" or
+ die 'could not open environment file for appending';
+ print E "PGEXTRAVAR1 = 1 # short one\nPGEXTRAVAR2='foo bar '\n\n# comment";
+ close E;
+ is_program_out 0, "pg_ctlcluster $v main restart", 0, '',
+ 'cluster restarts with new environment file';
+
+ @pm_pids = pidof ('postgres');
+ is $#pm_pids, 0, 'Exactly one postgres master process running';
+ %env = pid_env 'postgres', $pm_pids[0];
+ is $env{'PGEXTRAVAR1'}, '1', 'correct value of PGEXTRAVAR1 in environment';
+ is $env{'PGEXTRAVAR2'}, 'foo bar ', 'correct value of PGEXTRAVAR2 in environment';
+
+ # Now there should not be an external PID file any more, since we set it
+ # explicitly
+ unless ($PgCommon::rpm and ($v < 9.4 or $v >= 15)) {
+ ok_dir '/var/run/postgresql', [grep {! /pid/} @contents],
+ 'Socket and stats dir, but not PID file in /var/run/postgresql/';
+ } else {
+ ok_dir '/var/run/postgresql', ["$v-main.pg_stat_tmp"], 'Only stats dir in /var/run/postgresql/';
+ }
+
+ # verify that the correct client version is selected
+ like_program_out 'postgres', 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $v/,
+ 'pg_wrapper+createdb selects version number of cluster';
+
+ # we always want to use the latest version of "psql", though.
+ my $max_version = $ALL_MAJORS[-1];
+ if ($v < 9.2) {
+ # if version is older than 9.2 pick v14 at most
+ $max_version = (grep { $_ <= 14 } @ALL_MAJORS)[-1];
+ }
+ like_program_out 'postgres', 'psql --version', 0, qr/^psql \(PostgreSQL\) $max_version/,
+ "pg_wrapper+psql selects version $max_version";
+
+ my $default_log = "/var/log/postgresql/postgresql-$v-main.log";
+
+ # verify that the cluster is displayed
+ my $ls = `pg_lsclusters -h`;
+ $ls =~ s/\s+/ /g;
+ $ls =~ s/\s*$//;
+ is $ls, "$v main 5432 online postgres /var/lib/postgresql/$v/main $default_log",
+ 'pg_lscluster reports online cluster on port 5432';
+
+ # verify that the log file is actually used
+ ok !-z $default_log, 'log file is actually used';
+
+ # verify configuration file permissions
+ my $postgres_uid = (getpwnam 'postgres')[2];
+ my @st = stat "/etc/postgresql";
+ is $st[4], $postgres_uid, '/etc/postgresql is owned by user "postgres"';
+ my @st = stat "/etc/postgresql/$v";
+ is $st[4], $postgres_uid, 'version configuration directory file is owned by user "postgres"';
+ my @st = stat "/etc/postgresql/$v/main";
+ is $st[4], $postgres_uid, 'configuration directory file is owned by user "postgres"';
+
+ # verify data file permissions
+ my @st = stat "/var/lib/postgresql/$v";
+ is $st[4], $postgres_uid, 'version data directory file is owned by user "postgres"';
+ my @st = stat "/var/lib/postgresql/$v/main";
+ is $st[4], $postgres_uid, 'data directory file is owned by user "postgres"';
+
+ # verify log file permissions
+ my @logstat = stat $default_log;
+ is $logstat[2], 0100640, 'log file has 0640 permissions';
+ is $logstat[4], $postgres_uid, 'log file is owned by user "postgres"';
+ is $logstat[5], (getgrnam 'adm')[2], 'log file is owned by group "adm"';
+
+ # check default log file configuration; when not specifying -l with
+ # pg_createcluster, we should not have a 'log' symlink
+ ok !-e "/etc/postgresql/$v/main/log", 'no log symlink by default';
+ ok !-z $default_log, "$default_log is the default log if log symlink is missing";
+ like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/^$v\s+main.*$default_log\n$/;
+
+ # verify that log symlink works
+ is ((exec_as 'root', "pg_ctlcluster $v main stop"), 0, 'stopping cluster');
+ usleep $delay;
+ truncate "$default_log", 0; # empty log file
+ my $p = (PgCommon::cluster_data_directory $v, 'main') . '/mylog';
+ symlink $p, "/etc/postgresql/$v/main/log";
+ is ((exec_as 'root', "pg_ctlcluster $v main start"), 0,
+ 'restarting cluster with nondefault log symlink');
+ ok !-z $p, "log target is used as log file";
+ ok -z $default_log, "default log is not used";
+ like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/^$v\s+main.*$p\n$/;
+ is ((exec_as 'root', "pg_ctlcluster $v main stop"), 0, 'stopping cluster');
+ usleep $delay;
+ truncate "$default_log", 0; # empty log file
+
+ # verify that explicitly configured log file trumps log symlink
+ PgCommon::set_conf_value ($v, 'main', 'postgresql.conf',
+ ($v >= '8.3' ? 'logging_collector' : 'redirect_stderr'), 'on');
+ PgCommon::set_conf_value $v, 'main', 'postgresql.conf', 'log_filename', "$v#main.log";
+ is ((exec_as 'root', "pg_ctlcluster $v main start"), 0,
+ 'restarting cluster with explicitly configured log file');
+ ok -z $default_log, "default log is not used";
+ ok !-z $p, "log symlink target is used for startup message";
+ my $pg_log = $v >= 10 ? 'log' : 'pg_log'; # log directory in PGDATA changed in PG 10
+ my @l = glob ((PgCommon::cluster_data_directory $v, 'main') . "/$pg_log/$v#main.log*");
+ is $#l, 0, 'exactly one log file';
+ ok (-e $l[0] && ! -z $l[0], 'custom log is actually used');
+ SKIP: { skip "no logging_collector in $v", 2 if ($v < 8.3);
+ like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/^$v\s+main.*$v#main.log\n$/;
+ }
+
+ # clean up
+ PgCommon::disable_conf_value ($v, 'main', 'postgresql.conf',
+ ($v >= '8.3' ? 'logging_collector' : 'redirect_stderr'), '');
+ PgCommon::disable_conf_value $v, 'main', 'postgresql.conf', 'log_filename', '';
+ unlink "/etc/postgresql/$v/main/log";
+
+ # check that log creation does not escalate privileges
+ program_ok 'root', "pg_ctlcluster $v main stop", 0, 'stopping cluster';
+ unlink $default_log;
+ symlink "/etc/postgres-hack", $default_log;
+ program_ok 'root', "pg_ctlcluster $v main start", 1, 'starting cluster with rouge /var/log/postgresql symlink fails';
+ ok !-f "/etc/postgres-hack", "/etc/postgres-hack was not created";
+ unlink $default_log;
+ program_ok 'root', "pg_ctlcluster $v main start", 0, 'restarting cluster';
+
+ # verify that processes do not have an associated terminal
+ unlike_program_out 0, 'ps -o tty -U postgres h', 0, qr/tty|pts/,
+ 'postgres processes do not have an associated terminal';
+
+ # verify that SSL is enabled (which should work for user postgres in a
+ # default installation)
+ my $ssl = config_bool (PgCommon::get_conf_value $v, 'main', 'postgresql.conf', 'ssl');
+ my $ssl_linked = `ldd $PgCommon::binroot$v/bin/postgres | grep libssl`;
+ my ($os, $osversion) = os_release();
+ if ($PgCommon::rpm) {
+ isnt $ssl_linked, '', 'Server is linked with SSL support';
+ is $ssl, undef, 'SSL is disabled in postgresql.conf';
+ } elsif ($v <= 9.1 and (($os eq 'debian' and ($osversion eq 'unstable' or $osversion > 9)) or # stretch had 1.0 and 1.1
+ ($os eq 'ubuntu' and $osversion > 18.04))) { # bionic had 1.0 and 1.1
+ is $ssl_linked, '', 'Server is linked without SSL support (old version with only OpenSSL 1.0 support)';
+ is $ssl, undef, 'SSL is disabled in postgresql.conf';
+ } else {
+ isnt $ssl_linked, '', 'Server is linked with SSL support';
+ is $ssl, 1, 'SSL is enabled in postgresql.conf';
+ }
+
+ # Create user nobody, a database 'nobodydb' for him, check the database list
+ my $outref;
+ is ((exec_as 'nobody', 'psql -l 2>/dev/null', $outref), 2, 'psql -l fails for nobody');
+ is ((exec_as 'postgres', 'createuser nobody -D -R -S'), 0, 'createuser nobody');
+ is ((exec_as 'postgres', 'createdb -O nobody nobodydb'), 0, 'createdb nobodydb');
+ is ((exec_as 'nobody', 'psql -ltA|grep "|" | cut -f1-3 -d"|"', $outref), 0, 'psql -ltA succeeds for nobody');
+ is ($$outref, 'nobodydb|nobody|UTF8
+postgres|postgres|UTF8
+template0|postgres|UTF8
+template1|postgres|UTF8
+', 'psql -ltA output');
+
+ # Then fill nobodydb with some data.
+ is ((exec_as 'nobody', 'psql nobodydb -c "create table phone (name varchar(255) PRIMARY KEY, tel int NOT NULL)" 2>/dev/null'),
+ 0, 'SQL command: create table');
+ is ((exec_as 'nobody', 'psql nobodydb -c "insert into phone values (\'Bob\', 1)"'), 0, 'SQL command: insert into table values');
+ is ((exec_as 'nobody', 'psql nobodydb -c "insert into phone values (\'Alice\', 2)"'), 0, 'SQL command: insert into table values');
+ is ((exec_as 'nobody', 'psql nobodydb -c "insert into phone values (\'Bob\', 3)"'), 1, 'primary key violation');
+
+ # Check table contents
+ is_program_out 'nobody', 'psql -tAc "select * from phone order by name" nobodydb', 0,
+ 'Alice|2
+Bob|1
+', 'SQL command output: select -tA';
+ is_program_out 'nobody', 'psql -txc "select * from phone where name = \'Alice\'" nobodydb', 0,
+ 'name | Alice
+tel | 2
+
+', 'SQL command output: select -tx';
+ is_program_out 'nobody', 'psql -tAxc "select * from phone where name = \'Alice\'" nobodydb', 0,
+ 'name|Alice
+tel|2
+', 'SQL command output: select -tAx';
+
+ sub create_extension ($$) {
+ my ($v, $extension) = @_;
+ return "psql -qc 'CREATE EXTENSION $extension' nobodydb" if ($v >= 9.1);
+ return "createlang --cluster $v/main $extension nobodydb";
+ }
+
+ # Check PL/Perl untrusted
+ my $fn_cmd = 'CREATE FUNCTION read_file() RETURNS text AS \'open F, \\"/etc/passwd\\"; \\$buf = <F>; close F; return \\$buf;\' LANGUAGE plperl';
+ is ((exec_as 'nobody', create_extension($v, 'plperlu')), 1, 'CREATE EXTENSION plperlu fails for user nobody');
+ is_program_out 'postgres', create_extension($v, 'plperlu'), 0, '', 'CREATE EXTENSION plperlu succeeds for user postgres';
+ is ((exec_as 'nobody', "psql nobodydb -qc \"${fn_cmd}u;\""), 1, 'creating PL/PerlU function as user nobody fails');
+ is ((exec_as 'postgres', "psql nobodydb -qc \"${fn_cmd};\""), 1, 'creating unsafe PL/Perl function as user postgres fails');
+ is_program_out 'postgres', "psql nobodydb -qc \"${fn_cmd}u;\"", 0, '', 'creating PL/PerlU function as user postgres succeeds';
+ like_program_out 'nobody', 'psql nobodydb -Atc "select read_file()"',
+ 0, qr/^root:/, 'calling PL/PerlU function';
+
+ # Check PL/Perl trusted
+ my $pluser = ($v >= '8.3') ? 'nobody' : 'postgres'; # pg_pltemplate allows non-superusers to install trusted languages in 8.3+
+ is_program_out $pluser, create_extension($v, 'plperl'), 0, '', "CREATE EXTENSION plperl succeeds for user $pluser";
+ is ((exec_as 'nobody', "psql nobodydb -qc \"${fn_cmd};\""), 1, 'creating unsafe PL/Perl function as user nobody fails');
+ is_program_out 'nobody', 'psql nobodydb -qc "CREATE FUNCTION remove_vowels(text) RETURNS text AS \'\\$_[0] =~ s/[aeiou]/_/ig; return \\$_[0];\' LANGUAGE plperl;"',
+ 0, '', 'creating PL/Perl function as user nobody succeeds';
+ is_program_out 'nobody', 'psql nobodydb -Atc "select remove_vowels(\'foobArish\')"',
+ 0, "f__b_r_sh\n", 'calling PL/Perl function';
+
+ # Check PL/Python (untrusted)
+ SKIP: {
+ skip "No python2 support", 6 unless ($v <= 11 and $PgCommon::have_python2);
+ is_program_out 'postgres', create_extension($v, 'plpythonu'), 0, '', 'CREATE EXTENSION plpythonu succeeds for user postgres';
+ is_program_out 'postgres', 'psql nobodydb -qc "CREATE FUNCTION capitalize(text) RETURNS text AS \'import sys; return args[0].capitalize() + sys.version[0]\' LANGUAGE plpythonu;"',
+ 0, '', 'creating PL/Python function as user postgres succeeds';
+ is_program_out 'nobody', 'psql nobodydb -Atc "select capitalize(\'foo\')"',
+ 0, "Foo2\n", 'calling PL/Python function';
+ }
+
+ # Check PL/Python3 (untrusted)
+ if ($v >= '9.1') {
+ is_program_out 'postgres', create_extension($v, 'plpython3u'), 0, '', 'CREATE EXTENSION plpython3u succeeds for user postgres';
+ is_program_out 'postgres', 'psql nobodydb -qc "CREATE FUNCTION capitalize3(text) RETURNS text AS \'import sys; return args[0].capitalize() + sys.version[0]\' LANGUAGE plpython3u;"',
+ 0, '', 'creating PL/Python3 function as user postgres succeeds';
+ is_program_out 'nobody', 'psql nobodydb -Atc "select capitalize3(\'foo\')"',
+ 0, "Foo3\n", 'calling PL/Python function';
+ } else {
+ pass "Skipping PL/Python3 test for version $v...";
+ pass '...';
+ pass '...';
+ pass '...';
+ pass '...';
+ pass '...';
+ }
+
+ # Check PL/Tcl (trusted/untrusted)
+ is_program_out 'postgres', create_extension($v, 'pltcl'), 0, '', 'CREATE EXTENSION pltcl succeeds for user postgres';
+ is_program_out 'postgres', create_extension($v, 'pltclu'), 0, '', 'CREATE EXTENSION pltclu succeeds for user postgres';
+ is_program_out 'nobody', 'psql nobodydb -qc "CREATE FUNCTION tcl_max(integer, integer) RETURNS integer AS \'if {\\$1 > \\$2} {return \\$1}; return \\$2\' LANGUAGE pltcl STRICT;"',
+ 0, '', 'creating PL/Tcl function as user nobody succeeds';
+ is_program_out 'postgres', 'psql nobodydb -qc "CREATE FUNCTION tcl_max_u(integer, integer) RETURNS integer AS \'if {\\$1 > \\$2} {return \\$1}; return \\$2\' LANGUAGE pltclu STRICT;"',
+ 0, '', 'creating PL/TclU function as user postgres succeeds';
+ is_program_out 'nobody', 'psql nobodydb -Atc "select tcl_max(3,4)"', 0,
+ "4\n", 'calling PL/Tcl function';
+ is_program_out 'nobody', 'psql nobodydb -Atc "select tcl_max_u(5,4)"', 0,
+ "5\n", 'calling PL/TclU function';
+
+ # fake rotated logs to check that they are cleaned up properly
+ open L, ">$default_log.1" or die "could not open fake rotated log file";
+ print L "old log .1\n";
+ close L;
+ open L, ">$default_log.2" or die "could not open fake rotated log file";
+ print L "old log .2\n";
+ close L;
+ if (system "gzip -9 $default_log.2") {
+ die "could not gzip fake rotated log";
+ }
+
+ # Check that old-style pgdata symbolic link still works (p-common 0.90+
+ # does not create them any more, but they still need to work for existing
+ # installations)
+ is ((exec_as 'root', "pg_ctlcluster $v main stop"), 0, 'stopping cluster');
+ my $datadir = PgCommon::get_conf_value $v, 'main', 'postgresql.conf', 'data_directory';
+ symlink $datadir, "/etc/postgresql/$v/main/pgdata";
+
+ # data_directory should trump the pgdata symlink
+ PgCommon::set_conf_value $v, 'main', 'postgresql.conf', 'data_directory', '/nonexisting';
+ like_program_out 0, "pg_ctlcluster $v main start", 1,
+ qr/\/nonexisting is not accessible/,
+ 'cluster fails to start with invalid data_directory and valid pgdata symlink';
+
+ # if only pgdata symlink is present, it is authoritative
+ PgCommon::disable_conf_value $v, 'main', 'postgresql.conf', 'data_directory', 'disabled for test';
+ is_program_out 0, "pg_ctlcluster $v main start", 0, '',
+ 'cluster restarts with pgdata symlink';
+
+ # check properties of backend processes
+ pipe RH, WH;
+ my $psql = fork;
+ if (!$psql) {
+ close WH;
+ my @pw = getpwnam 'nobody';
+ change_ugid $pw[2], $pw[3];
+ open(STDIN, "<& RH");
+ dup2(POSIX::open('/dev/null', POSIX::O_WRONLY), 1);
+ exec 'psql', '-Xq', '-vPROMPT1=', 'nobodydb' or die "could not exec psql process: $!";
+ }
+ close RH;
+ select WH; $| = 1; # make unbuffered
+
+ open my $pidfile, "/var/lib/postgresql/$v/main/postmaster.pid";
+ my $master_pid = <$pidfile>;
+ chomp $master_pid;
+ close $pidfile;
+
+ my $client_pid;
+ while (!$client_pid) {
+ usleep $delay;
+ $client_pid = `ps --user postgres hu | grep 'postgres.*: nobody nobodydb' | grep -v grep | awk '{print \$2}'`;
+ ($client_pid) = ($client_pid =~ /(\d+)/); # untaint
+ }
+
+ # OOM score adjustment under Linux: postmaster gets bigger shields for >=
+ # 9.0, but client backends stay at default; this might not work in
+ # containers with restricted privileges, so skip the check there
+ my $adj;
+ my $detect_virt = system 'systemd-detect-virt --container --quiet'; # from systemd
+ open F, "/proc/$master_pid/oom_score_adj";
+ $adj = <F>;
+ chomp $adj;
+ close F;
+ if ($v >= '9.0' and not $PgCommon::rpm) {
+ SKIP: {
+ skip 'skipping postmaster OOM killer adjustment in container', 1 if $detect_virt == 0;
+ cmp_ok $adj, '<=', -500, 'postgres master has OOM killer protection';
+ }
+ } else {
+ is $adj, 0, 'postgres master has no OOM adjustment';
+ }
+
+ open F, "/proc/$client_pid/oom_score_adj";
+ $adj = <F>;
+ chomp $adj;
+ close F;
+ is $adj, 0, 'postgres client backend has no OOM adjustment';
+
+ # test process title update
+ like_program_out 0, "ps h $client_pid", 0, qr/ idle\s*$/, 'process title is idle';
+ print WH "BEGIN;\n";
+ usleep $delay;
+ like_program_out 0, "ps h $client_pid", 0, qr/idle in transaction/, 'process title is idle in transaction';
+
+ close WH;
+ kill 15, $psql;
+ waitpid $psql, 0;
+
+ # log file gets re-created by pg_ctlcluster
+ is ((exec_as 0, "pg_ctlcluster $v main stop"), 0, 'stopping cluster');
+ unlink $default_log;
+ is ((exec_as 0, "pg_ctlcluster $v main start"), 0, 'starting cluster as postgres works without a log file');
+ ok (-e $default_log && ! -z $default_log, 'log file got recreated and used');
+
+ # create tablespaces
+ my $spc1 = tempdir("/tmp/$v.spc1.XXXXXX", CLEANUP => 1);
+ my $spc2 = tempdir("/tmp/$v.spc2.XXXXXX", CLEANUP => 1);
+ is (mkdir ("$spc2/PG_99_fakedirectory"), 1, 'creating a directory in spc2');
+ chown $postgres_uid, 0, $spc1, $spc2, "$spc2/PG_99_fakedirectory";
+ is_program_out 'postgres', "psql -qc \"CREATE TABLESPACE spc1 LOCATION '$spc1'\"", 0, '', 'creating tablespace spc1';
+ is_program_out 'postgres', "psql -qc 'CREATE TABLE tbl1 (x int) TABLESPACE spc1'", 0, '', 'creating a table in spc1';
+ SKIP: {
+ skip "Non-empty tablespaces not supported before 9.0", 4 if ($v < 9.0);
+ is_program_out 'postgres', "psql -qc \"CREATE TABLESPACE spc2 LOCATION '$spc2'\"", 0, '', 'creating tablespace spc2';
+ is_program_out 'postgres', "psql -qc 'CREATE TABLE tbl2 (x int) TABLESPACE spc2'", 0, '', 'creating a table in spc2';
+ }
+
+ # check apt config
+ is_program_out 0, "grep -Eo 'postgresql.[0-9.*-]+' /etc/apt/apt.conf.d/02autoremove-postgresql", 0,
+ "postgresql.*-$v\n", "Correct apt NeverAutoRemove config";
+
+ # stop server, clean up, check for leftovers
+ ok ((system "pg_dropcluster $v main --stop") == 0,
+ 'pg_dropcluster removes cluster');
+
+ is (-e $xlogdir, undef, "xlog/wal directory $xlogdir was deleted");
+ ok_dir $spc1, [], "tablespace spc1 was emptied";
+ ok_dir $spc2, [qw(PG_99_fakedirectory)], "tablespace spc2 was emptied";
+
+ is_program_out 0, "grep -Eo 'postgresql.[0-9.*-]+' /etc/apt/apt.conf.d/02autoremove-postgresql", 1,
+ "", "Correct apt NeverAutoRemove config";
+
+ check_clean;
+}
+
+foreach (@MAJORS) {
+ check_major $_;
+}
+
+# vim: filetype=perl
diff --git a/t/021_pg_renamecluster.t b/t/021_pg_renamecluster.t
new file mode 100644
index 0000000..b3e1cb6
--- /dev/null
+++ b/t/021_pg_renamecluster.t
@@ -0,0 +1,43 @@
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => 20;
+
+my $v = $MAJORS[-1];
+
+# create cluster
+ok ((system "pg_createcluster $v main --start >/dev/null") == 0,
+ "pg_createcluster $v main");
+
+# test pg_renamecluster with a running cluster
+program_ok (0, "pg_renamecluster $v main donau");
+is_program_out 'postgres', 'psql -tAc "show data_directory"', 0,
+ "/var/lib/postgresql/$v/donau\n", 'cluster is running and data_directory was moved';
+is ((PgCommon::get_conf_value $v, 'donau', 'postgresql.conf', 'hba_file'),
+ "/etc/postgresql/$v/donau/pg_hba.conf", 'pg_hba.conf location updated');
+is ((PgCommon::get_conf_value $v, 'donau', 'postgresql.conf', 'ident_file'),
+ "/etc/postgresql/$v/donau/pg_ident.conf", 'pg_ident.conf location updated');
+is ((PgCommon::get_conf_value $v, 'donau', 'postgresql.conf', 'external_pid_file'),
+ "/var/run/postgresql/$v-donau.pid", 'external_pid_file location updated');
+ok (-f "/var/run/postgresql/$v-donau.pid", 'external_pid_file exists');
+SKIP: {
+ skip "no stats_temp_directory in $v", 2 if ($v < 8.4 or $v >= 15);
+ is ((PgCommon::get_conf_value $v, 'donau', 'postgresql.conf', 'stats_temp_directory'),
+ "/var/run/postgresql/$v-donau.pg_stat_tmp", 'stats_temp_directory location updated');
+ ok (-d "/var/run/postgresql/$v-donau.pg_stat_tmp", 'stats_temp_directory exists');
+}
+SKIP: {
+ skip "cluster name not supported in $v", 1 if ($v < 9.5);
+ is (PgCommon::get_conf_value ($v, 'donau', 'postgresql.conf', 'cluster_name'), "$v/donau", "cluster_name is updated");
+}
+
+# stop server, clean up, check for leftovers
+ok ((system "pg_dropcluster $v donau --stop") == 0,
+ 'pg_dropcluster removes cluster');
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/022_recovery.t b/t/022_recovery.t
new file mode 100644
index 0000000..b70a8b5
--- /dev/null
+++ b/t/022_recovery.t
@@ -0,0 +1,54 @@
+# We create a cluster, stop it ungracefully, and check if recovery works.
+
+use strict;
+
+use POSIX qw/dup2/;
+use Time::HiRes qw/usleep/;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => 17 * @MAJORS;
+
+sub check_major {
+ my $v = $_[0];
+ note "Running tests for $v";
+
+ # create cluster
+ program_ok (0, "pg_createcluster $v main --start >/dev/null");
+
+ # try an immediate shutdown and restart
+ program_ok (0, "pg_ctlcluster $v main stop -m i");
+ program_ok (0, "pg_ctlcluster $v main start");
+ my $c = 0; # fallback for when pg_isready is missing (PG < 9.3)
+ while (system ("pg_isready -q 2>&1") >> 8 == 1 and $c++ < 15) {
+ sleep(1);
+ }
+ program_ok ('postgres', "psql -c ''");
+
+ # try again with an write-protected file
+ program_ok (0, "pg_ctlcluster $v main stop -m i");
+ open F, ">/var/lib/postgresql/$v/main/foo";
+ print F "moo\n";
+ close F;
+ ok ((chmod 0444, "/var/lib/postgresql/$v/main/foo"),
+ "create write-protected file in data directory");
+ program_ok (0, "pg_ctlcluster $v main start");
+ $c = 0;
+ while (system ("pg_isready -q 2>&1") >> 8 == 1 and $c++ < 15) {
+ sleep(1);
+ }
+ program_ok ('postgres', "psql -c ''");
+
+ program_ok (0, "pg_dropcluster $v main --stop", 0,
+ 'pg_dropcluster removes cluster');
+
+ check_clean;
+}
+
+foreach (@MAJORS) {
+ check_major $_;
+}
+
+# vim: filetype=perl
diff --git a/t/025_logging.t b/t/025_logging.t
new file mode 100644
index 0000000..a7e5c50
--- /dev/null
+++ b/t/025_logging.t
@@ -0,0 +1,99 @@
+# Test various logging-related things
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Time::HiRes qw/usleep/;
+
+use Test::More tests => 55 * @MAJORS;
+
+my $syslog_works = 0;
+
+sub check_logging ($$)
+{
+ my ($text, $msg) = @_;
+ my $ls = `pg_lsclusters -h`;
+ $ls =~ s/\s+/ /g;
+ $ls =~ s/\s*$//;
+ like $ls, $text, $msg;
+}
+
+sub check_major {
+ my $v = $_[0];
+ note "Running tests for $v";
+ my $pgdata = "/var/lib/postgresql/$v/main";
+
+ # create cluster
+ ok ((system "pg_createcluster $v main --start >/dev/null") == 0,
+ "pg_createcluster $v main");
+
+ # default log setup
+ my $default_log = "/var/log/postgresql/postgresql-$v-main.log";
+ check_logging qr($v main 5432 online postgres $pgdata $default_log), "pg_lscluster reports logfile $default_log";
+ like_program_out 'postgres', "psql -qc \"'foobar_${v}_$$'\"", 1, qr/syntax error.*foobar_${v}_$$/, 'log an error';
+ usleep $delay;
+ like_program_out 'postgres', "grep --binary-files=text foobar_${v}_$$ $default_log", 0, qr/syntax error.*foobar_${v}_$$/, 'error appears in logfile';
+
+ # syslog
+ is_program_out 0, "pg_conftool $v main set log_destination syslog", 0, "", "set log_destination syslog";
+ is_program_out 0, "pg_ctlcluster $v main reload", 0, "", "$v main reload";
+ is_program_out 'postgres', "psql -Atc \"show log_destination\"", 0, "syslog\n", 'log_destination is syslog';
+ check_logging qr($v main 5432 online postgres $pgdata syslog), "pg_lscluster reports syslog";
+ SKIP: {
+ skip "/var/log/syslog not available", 2 unless ($syslog_works);
+ usleep $delay;
+ like_program_out 0, "grep --binary-files=text 'postgres.*parameter \"log_destination\" changed to \"syslog\"' /var/log/syslog", 0, qr/log_destination/, 'error appears in /var/log/syslog';
+ }
+
+ # turn logging_collector on, csvlog
+ my $pg_log = $v >= 10 ? 'log' : 'pg_log'; # log directory in PGDATA changed in PG 10
+ SKIP: {
+ skip "No logging collector in 8.2", 30 if ($v <= 8.2);
+ is_program_out 0, "pg_conftool $v main set logging_collector on", 0, "", "set logging_collector on";
+ is_program_out 0, "pg_conftool $v main set log_destination csvlog", 0, "", "set log_destination csvlog";
+ is_program_out 0, "pg_ctlcluster $v main restart", 0, "", "$v main restart";
+ is_program_out 'postgres', "psql -Atc \"show logging_collector\"", 0, "on\n", 'logging_collector is on';
+ is_program_out 'postgres', "psql -Atc \"show log_destination\"", 0, "csvlog\n", 'log_destination is csvlog';
+ check_logging qr($v main 5432 online postgres $pgdata $pg_log/.*\.csv), "pg_lscluster reports csvlog";
+ like_program_out 'postgres', "psql -qc \"'barbaz_${v}_$$'\"", 1, qr/syntax error.*barbaz_${v}_$$/, 'log an error';
+ usleep $delay;
+ like_program_out 'postgres', "grep --binary-files=text barbaz_${v}_$$ $pgdata/$pg_log/*.csv", 0, qr/syntax error.*barbaz_${v}_$$/, "error appears in $pg_log/*.csv";
+
+ # stderr,syslog,csvlog
+ is_program_out 0, "pg_conftool $v main set log_destination stderr,syslog,csvlog", 0, "", "set log_destination stderr,syslog,csvlog";
+ is_program_out 0, "pg_ctlcluster $v main reload", 0, "", "$v main reload";
+ is_program_out 'postgres', "psql -Atc \"show log_destination\"", 0, "stderr,syslog,csvlog\n", 'log_destination is stderr,syslog,csvlog';
+ check_logging qr($v main 5432 online postgres $pgdata $pg_log/.*\.log,syslog,$pg_log/.*\.csv), "pg_lscluster reports stderr,syslog,csvlog";
+ like_program_out 'postgres', "psql -qc \"'moo_${v}_$$'\"", 1, qr/syntax error.*moo_${v}_$$/, 'log an error';
+ usleep $delay;
+ like_program_out 'postgres', "grep --binary-files=text moo_${v}_$$ $pgdata/$pg_log/*.log", 0, qr/syntax error.*moo_${v}_$$/, "error appears in $pg_log/*.log";
+ SKIP: {
+ skip "/var/log/syslog not available", 2 unless ($syslog_works);
+ usleep $delay;
+ like_program_out 0, "grep --binary-files=text 'postgres.*moo_${v}_$$' /var/log/syslog", 0, qr/moo_${v}_$$/, 'error appears in /var/log/syslog';
+ }
+ like_program_out 'postgres', "grep --binary-files=text moo_${v}_$$ $pgdata/$pg_log/*.csv", 0, qr/syntax error.*moo_${v}_$$/, "error appears in $pg_log/*.csv";
+ }
+
+ # stop server, clean up, check for leftovers
+ is_program_out 0, "pg_dropcluster $v main --stop", 0, "", 'pg_dropcluster removes cluster';
+
+ check_clean;
+}
+
+system "logger -t '$0' 'test-logging-$$'";
+usleep $delay;
+if (system ("grep --binary-files=text -q 'test-logging-$$' /var/log/syslog 2> /dev/null") == 0) {
+ note 'Logging to /var/log/syslog works';
+ $syslog_works = 1;
+} else {
+ note 'Logging to /var/log/syslog does not work, skipping some syslog tests';
+}
+
+foreach (@MAJORS) {
+ check_major $_;
+}
+
+# vim: filetype=perl
diff --git a/t/030_errors.t b/t/030_errors.t
new file mode 100644
index 0000000..88d3700
--- /dev/null
+++ b/t/030_errors.t
@@ -0,0 +1,336 @@
+# Check all kinds of error conditions.
+
+use strict;
+
+require File::Temp;
+
+use lib 't';
+use TestLib;
+use Test::More tests => 156;
+use PgCommon;
+
+my $version = $MAJORS[-1];
+
+my $socketdir = '/tmp/postgresql-testsuite/';
+my ($pg_uid, $pg_gid) = (getpwnam 'postgres')[2,3];
+
+# create a pid file with content $1 and return its path
+sub create_pidfile {
+ my $fname = "/var/lib/postgresql/$version/main/postmaster.pid";
+ open F, ">$fname" or die "open: $!";
+ print F $_[0];
+ close F;
+ chown $pg_uid, $pg_gid, $fname or die "chown: $!";
+ chmod 0700, $fname or die "chmod: $!";
+ return $fname;
+}
+
+sub check_nonexisting_cluster_error {
+ my $outref;
+ my $result = exec_as 0, $_[0], $outref;
+ is $result, 1, "'$_[0]' fails";
+ like $$outref, qr/(invalid version|does not exist)/i, "$_[0] gives error message about nonexisting cluster";
+ unlike $$outref, qr/invalid symbolic link/i, "$_[0] does not print 'invalid symbolic link' gibberish";
+}
+
+# check if pg_lsclusters shows a cluster without binaries
+mkdir "/etc/postgresql/6.3";
+mkdir "/etc/postgresql/6.3/main";
+open F, ">/etc/postgresql/6.3/main/postgresql.conf";
+close F;
+is `pg_lsclusters -h`, "6.3 main <unknown> down,binaries_missing <unknown> <unknown> <unknown>\n",
+ 'pg_lscluster reports cluster without binaries';
+program_ok 0, "pg_dropcluster 6.3 main";
+
+# create cluster
+ok ((system "pg_createcluster --socketdir '$socketdir' $version main >/dev/null") == 0,
+ "pg_createcluster --socketdir");
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/$version\s*main.*5432.*down/, 'cluster was created';
+
+is ((get_cluster_port $version, 'main'), 5432, 'Port of created cluster is 5432');
+
+# creating cluster with the same name should fail
+like_program_out 'root', "pg_createcluster --socketdir '$socketdir' $version main", 1, qr/already exists/,
+ "pg_createcluster on existing cluster";
+# and the original one still exists
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/$version\s*main.*5432.*down/, 'original cluster still exists';
+
+# attempt to create clusters with an invalid port
+like_program_out 0, "pg_createcluster $version test -p foo", 1,
+ qr/invalid.*number expected/,
+ 'pg_createcluster -p checks that port option is numeric';
+like_program_out 0, "pg_createcluster $version test -p 42", 1,
+ qr/must be a positive integer between/,
+ 'pg_createcluster -p checks valid port range';
+
+# chown cluster to an invalid user to test error
+(system "chown -R 0 /var/lib/postgresql/$version/main") == 0 or die "chown failed: $!";
+like_program_out 0, "pg_ctlcluster $version main start", 1, qr/must not be owned by root/,
+ "pg_ctlcluster refuses to start root-owned cluster";
+my $badid = 98;
+(system "chown -R $badid /var/lib/postgresql/$version/main") == 0 or die "chown failed: $!";
+like_program_out 0, "pg_ctlcluster $version main start", 1, qr/owned by user id 98 which does not exist/,
+ 'pg_ctlcluster fails on invalid cluster owner uid';
+(system "chown -R postgres:$badid /var/lib/postgresql/$version/main") == 0 or die "chown failed: $!";
+like_program_out 0, "pg_ctlcluster $version main start", 1, qr/owned by group id 98 which does not exist/,
+ 'pg_ctlcluster as root fails on invalid cluster owner gid';
+like_program_out 'postgres', "pg_ctlcluster $version main start", 1, qr/owned by group id 98 which does not exist/,
+ 'pg_ctlcluster as postgres fails on invalid cluster owner gid';
+(system "chown -R postgres:postgres /var/lib/postgresql/$version/main") == 0 or die "chown failed: $!";
+program_ok 0, "pg_ctlcluster $version main start", 0,
+ 'pg_ctlcluster succeeds on valid cluster owner uid/gid';
+
+# check socket
+my @contents = ('.s.PGSQL.5432', '.s.PGSQL.5432.lock', "$version-main.pid", "$version-main.pg_stat_tmp");
+pop @contents if ($version < 8.4 or $version >= 15); # remove pg_stat_tmp
+ok_dir '/var/run/postgresql', [grep {/main/} @contents], 'No sockets in /var/run/postgresql';
+ok_dir $socketdir, ['.s.PGSQL.5432', '.s.PGSQL.5432.lock'], "Socket is in $socketdir";
+
+# stop cluster, check sockets
+ok ((system "pg_ctlcluster $version main stop") == 0,
+ 'cluster stops with custom unix_socket_dir');
+ok_dir $socketdir, [], "No sockets in $socketdir after stopping cluster";
+
+# remove default socket dir and check that the socket defaults to
+# /var/run/postgresql
+open F, "+</etc/postgresql/$version/main/postgresql.conf" or
+ die "could not open postgresql.conf for r/w: $!";
+my @lines = <F>;
+seek F, 0, 0 or die "seek: $!";
+truncate F, 0;
+@lines = grep !/^unix_socket_dir/, @lines; # <= 9.2: "_directory", >= 9.3: "_directories"
+print F @lines;
+close F;
+
+ok ((system "pg_ctlcluster $version main start") == 0,
+ 'cluster starts after removing unix_socket_dir');
+if ($PgCommon::rpm) {
+ ok ((grep { $_ eq '.s.PGSQL.5432' } @{TestLib::dircontent('/tmp')}) == 1, 'Socket is in /tmp');
+} else {
+ ok_dir '/var/run/postgresql', [@contents],
+ 'Socket is in default dir /var/run/postgresql';
+}
+ok_dir $socketdir, [], "No sockets in $socketdir";
+
+# server should not stop with corrupt file
+rename "/var/lib/postgresql/$version/main/postmaster.pid",
+ "/var/lib/postgresql/$version/main/postmaster.pid.orig" or die "rename: $!";
+create_pidfile 'foo';
+is_program_out 'postgres', "pg_ctlcluster $version main stop", 1,
+ "Error: pid file is invalid, please manually kill the stale server process.\n",
+ 'pg_ctlcluster fails with corrupted PID file';
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/online/, 'cluster is still online';
+
+# restore PID file
+(system "cp /var/lib/postgresql/$version/main/postmaster.pid.orig /var/lib/postgresql/$version/main/postmaster.pid") == 0 or die "cp: $!";
+is ((exec_as 'postgres', "pg_ctlcluster $version main stop"), 0,
+ 'pg_ctlcluster succeeds with restored PID file');
+mkdir $PgCommon::binroot . "foo"; # #940220: infinite recursion in get_program_path
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/down/, 'cluster is down';
+rmdir $PgCommon::binroot . "foo";
+
+# stop stopped server
+is_program_out 'postgres', "pg_ctlcluster $version main stop", 2,
+ "Cluster is not running.\n", 'pg_ctlcluster stop fails on stopped cluster';
+
+# simulate crashed server
+rename "/var/lib/postgresql/$version/main/postmaster.pid.orig",
+ "/var/lib/postgresql/$version/main/postmaster.pid" or die "rename: $!";
+is_program_out 'postgres', "pg_ctlcluster $version main start", 0,
+ "Removed stale pid file.\n", 'pg_ctlcluster succeeds with already existing PID file';
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/online/, 'cluster is online';
+is ((exec_as 'postgres', "pg_ctlcluster $version main stop"), 0,
+ 'pg_ctlcluster stop succeeds');
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/down/, 'cluster is down';
+ok (! -e "/var/lib/postgresql/$version/main/postmaster.pid", 'no pid file left');
+
+# trying to stop a stopped server cleans up corrupt and stale pid files
+my $pf = create_pidfile 'foo';
+is_program_out 'postgres', "pg_ctlcluster $version main stop", 2,
+ "Removed stale pid file.\nCluster is not running.\n",
+ 'pg_ctlcluster stop succeeds with corrupted PID file';
+ok (! -e $pf, 'pid file was cleaned up');
+
+create_pidfile 'foo';
+is_program_out 'postgres', "pg_ctlcluster --force $version main stop", 2,
+ "Removed stale pid file.\nCluster is not running.\n",
+ 'pg_ctlcluster --force stop succeeds with corrupted PID file';
+ok (! -e $pf, 'pid file was cleaned up');
+
+create_pidfile '99998';
+is_program_out 'postgres', "pg_ctlcluster $version main stop", 2,
+ "Removed stale pid file.\nCluster is not running.\n",
+ 'pg_ctlcluster stop succeeds with stale PID file';
+ok (! -e $pf, 'pid file was cleaned up');
+
+create_pidfile '99998';
+is_program_out 'postgres', "pg_ctlcluster --force $version main stop", 2,
+ "Removed stale pid file.\nCluster is not running.\n",
+ 'pg_ctlcluster --force stop succeeds with stale PID file';
+ok (! -e $pf, 'pid file was cleaned up');
+
+create_pidfile '';
+is_program_out 'postgres', "pg_ctlcluster --force $version main stop", 2,
+ "Removed stale pid file.\nCluster is not running.\n",
+ 'pg_ctlcluster stop succeeds with empty PID file';
+ok (! -e $pf, 'pid file was cleaned up');
+
+# corrupt PID file while server is down
+create_pidfile 'foo';
+is_program_out 'postgres', "pg_ctlcluster $version main start", 0,
+ "Removed stale pid file.\n", 'pg_ctlcluster succeeds with corrupted PID file';
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/online/, 'cluster is online';
+
+# start running server
+is_program_out 'postgres', "pg_ctlcluster $version main start", 2,
+ "Cluster is already running.\n", 'pg_ctlcluster start fails on running cluster';
+is ((exec_as 'postgres', "pg_ctlcluster $version main stop"), 0, 'pg_ctlcluster stop');
+
+# backup pg_hba.conf
+rename "/etc/postgresql/$version/main/pg_hba.conf",
+ "/etc/postgresql/$version/main/pg_hba.conf.orig" or die "rename: $!";
+
+# test check for invalid pg_hba.conf
+open F, ">/etc/postgresql/$version/main/pg_hba.conf" or die "could not create pg_hba.conf: $!";
+print F "foo\n";
+close F;
+chmod 0644, "/etc/postgresql/$version/main/pg_hba.conf" or die "chmod: $!";
+
+if ($version < '8.4') {
+ like_program_out 'postgres', "pg_ctlcluster $version main start", 0,
+ qr/WARNING.*connection to the database failed.*pg_hba.conf/is,
+ 'pg_ctlcluster start warns about invalid pg_hba.conf';
+ is_program_out 'postgres', "pg_ctlcluster $version main stop", 0, '', 'stopping cluster';
+} else {
+ like_program_out 'postgres', "pg_ctlcluster $version main start", 1,
+ qr/FATAL.*pg_hba.conf/is,
+ 'pg_ctlcluster start fails on invalid pg_hba.conf';
+ is_program_out 'postgres', "pg_ctlcluster $version main stop", 2,
+ "Cluster is not running.\n", 'stopping cluster';
+}
+
+# test check for pg_hba.conf with removed passwordless local superuser access
+open F, ">/etc/postgresql/$version/main/pg_hba.conf" or die "could not create pg_hba.conf: $!";
+print F "local all all md5\n";
+close F;
+chmod 0644, "/etc/postgresql/$version/main/pg_hba.conf" or die "chmod: $!";
+
+like_program_out 'postgres', "pg_ctlcluster $version main start", 0,
+ qr/Warning.*connection to the database failed.*(no password supplied|password authentication failed)/is,
+ 'pg_ctlcluster start warns about absence of passwordless superuser connection';
+is_program_out 'postgres', "pg_ctlcluster $version main stop", 0, '', 'stopping cluster';
+
+# restore pg_hba.conf
+unlink "/etc/postgresql/$version/main/pg_hba.conf";
+rename "/etc/postgresql/$version/main/pg_hba.conf.orig",
+ "/etc/postgresql/$version/main/pg_hba.conf" or die "rename: $!";
+
+# leftover files must not create confusion
+open F, '>/etc/postgresql/postgresql.conf';
+print F "data_directory = '/nonexisting'\n";
+close F;
+my @c = get_version_clusters $version;
+is_deeply (\@c, ['main'],
+ 'leftover /etc/postgresql/postgresql.conf is not regarded as a cluster');
+unlink '/etc/postgresql/postgresql.conf';
+
+# fails by default due to access restrictions
+# remove cluster and directory; this should work as user "postgres"
+is_program_out 'postgres', "pg_dropcluster $version main", 0, '',
+ , "pg_dropcluster works as user postgres";
+
+# graceful handling of absent data dir (might not be mounted)
+ok ((system "pg_createcluster $version main >/dev/null") == 0,
+ "pg_createcluster succeeds");
+rename "/var/lib/postgresql/$version", "/var/lib/postgresql/$version.orig" or die "rename: $!";
+my $outref;
+is ((exec_as 0, "pg_ctlcluster $version main start", $outref, 1), 1,
+ 'pg_ctlcluster fails on nonexisting /var/lib/postgresql');
+like $$outref, qr/^Error:.*\/var\/lib\/postgresql.*not accessible.*$/, 'proper error message for nonexisting /var/lib/postgresql';
+
+rename "/var/lib/postgresql/$version.orig", "/var/lib/postgresql/$version" or die "rename: $!";
+is_program_out 'postgres', "pg_ctlcluster $version main start", 0, '',
+ 'pg_ctlcluster start succeeds again with reappeared /var/lib/postgresql';
+is_program_out 'postgres', "pg_ctlcluster $version main stop", 0, '', 'stopping cluster';
+
+# pg_ctlcluster checks colliding ports
+ok ((system "pg_createcluster $version other >/dev/null") == 0,
+ "pg_createcluster other");
+set_cluster_port $version, 'other', '5432';
+is ((exec_as 'postgres', "pg_ctlcluster $version main start"), 0,
+ 'pg_ctlcluster: main cluster on conflicting port starts');
+
+# clusters can run side by side on different socket directories
+set_cluster_socketdir $version, 'other', $socketdir;
+PgCommon::set_conf_value $version, 'other', 'postgresql.conf',
+ 'listen_addresses', ''; # otherwise they will conflict on TCP socket
+is ((exec_as 'postgres', "pg_ctlcluster $version other start"), 0,
+ 'pg_ctlcluster: other cluster starts on conflicting port, but different socket dirs');
+is ((exec_as 'postgres', "pg_ctlcluster $version other stop"), 0);
+
+# ... but will give an error when running on the same port
+set_cluster_socketdir $version, 'other', ($PgCommon::rpm and $version < 9.4) ? '/tmp' : '/var/run/postgresql';
+like_program_out 'postgres', "pg_ctlcluster $version other start", 1,
+ qr/Port conflict:.*port 5432/,
+ 'pg_ctlcluster other cluster fails on conflicting port and same socket dir';
+is_program_out 'postgres', "pg_ctlcluster $version main stop", 0, '',
+ 'stopping main cluster';
+is ((exec_as 'postgres', "pg_ctlcluster $version other start"), 0,
+ 'pg_ctlcluster: other cluster on conflicting port starts after main is down');
+ok ((system "pg_dropcluster $version other --stop") == 0,
+ 'pg_dropcluster other');
+
+# clean up
+ok ((system "pg_dropcluster $version main") == 0,
+ 'pg_dropcluster');
+ok_dir $socketdir, [], 'No sockets any more';
+rmdir $socketdir or die "rmdir: $!";
+
+# ensure sane error messages for nonexisting clusters
+check_nonexisting_cluster_error 'pg_lsclusters 4.5 foo';
+check_nonexisting_cluster_error 'psql --cluster 4.5/foo';
+check_nonexisting_cluster_error "psql --cluster $MAJORS[0]/foo";
+check_nonexisting_cluster_error "pg_dropcluster 4.5 foo";
+check_nonexisting_cluster_error "pg_dropcluster $MAJORS[0] foo";
+check_nonexisting_cluster_error "pg_upgradecluster 4.5 foo";
+check_nonexisting_cluster_error "pg_upgradecluster $MAJORS[0] foo";
+check_nonexisting_cluster_error "pg_ctlcluster 4.5 foo stop";
+check_nonexisting_cluster_error "pg_ctlcluster $MAJORS[0] foo stop";
+
+check_clean;
+
+# check that pg_dropcluster copes with partially existing cluster
+# configurations (which can happen if the disk becomes full)
+
+mkdir '/etc/postgresql/';
+mkdir "/etc/postgresql/$MAJORS[-1]";
+mkdir "/etc/postgresql/$MAJORS[-1]/broken" or die "mkdir: $!";
+symlink "/var/lib/postgresql/$MAJORS[-1]/broken", "/etc/postgresql/$MAJORS[-1]/broken/pgdata" or die "symlink: $!";
+
+unlike_program_out 0, "pg_dropcluster $MAJORS[-1] broken", 0, qr/error/i,
+ 'pg_dropcluster cleans up broken cluster configuration (only /etc with pgdata)';
+
+check_clean;
+
+mkdir '/etc/postgresql/';
+mkdir '/var/lib/postgresql/';
+mkdir "/etc/postgresql/$MAJORS[-1]" and
+mkdir "/etc/postgresql/$MAJORS[-1]/broken";
+mkdir "/var/lib/postgresql/$MAJORS[-1]";
+mkdir "/var/lib/postgresql/$MAJORS[-1]/broken";
+chown $pg_uid, $pg_gid, "/var/lib/postgresql/$MAJORS[-1]/broken";
+mkdir "/var/lib/postgresql/$MAJORS[-1]/broken/base" or die "mkdir: $!";
+open F, ">/etc/postgresql/$MAJORS[-1]/broken/postgresql.conf" or die "open: $!";
+print F "data_directory = '/var/lib/postgresql/$MAJORS[-1]/broken'\n";
+close F;
+open F, ">/var/lib/postgresql/$MAJORS[-1]/broken/PG_VERSION" or die "open: $!";
+close F;
+
+unlike_program_out 0, "pg_dropcluster $MAJORS[-1] broken", 0, qr/error/i,
+ 'pg_dropcluster cleans up broken cluster configuration (/etc with pgdata and postgresql.conf and partial /var)';
+is -d "/etc/postgresql/$MAJORS[-1]", undef, "/etc/postgresql/$MAJORS[-1] was removed";
+is -d "/var/lib/postgresql/$MAJORS[-1]", undef, "/var/lib/postgresql/$MAJORS[-1] was removed";
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/031_errors_disk_full.t b/t/031_errors_disk_full.t
new file mode 100644
index 0000000..bc0a860
--- /dev/null
+++ b/t/031_errors_disk_full.t
@@ -0,0 +1,86 @@
+# Check for proper ENOSPC handling
+
+use strict;
+
+require File::Temp;
+
+use lib 't';
+use TestLib;
+use Test::More tests => $ENV{NO_TMPFS} ? 1 : 22;
+
+# skip tests if NO_TMPFS is set
+if ($ENV{NO_TMPFS}) {
+ pass 'Skipping disk full tests, NO_TMPFS is set';
+ exit;
+}
+
+# we are using unshare here, won't work with systemd
+$ENV{_SYSTEMCTL_SKIP_REDIRECT} = 1;
+
+my $outref;
+
+#
+note 'check that a failed pg_createcluster leaves no cruft behind: try creating a cluster on a 10 MB tmpfs';
+my $cmd = <<EOF;
+exec 2>&1
+set -e
+mount --make-rprivate / 2> /dev/null || :
+mkdir -p /var/lib/postgresql
+trap "umount /var/lib/postgresql" 0 HUP INT QUIT ILL ABRT PIPE TERM
+mount -t tmpfs -o size=10000000 none /var/lib/postgresql
+# this is supposed to fail
+LC_MESSAGES=C pg_createcluster $MAJORS[-1] test && exit 1 || true
+echo -n "ls>"
+# should not output anything
+ls /etc/postgresql
+ls /var/lib/postgresql
+echo "<ls"
+EOF
+
+my $result;
+$result = exec_as 'root', "echo '$cmd' | unshare -m sh", $outref;
+
+is $result, 0, 'script failed';
+like $$outref, qr/No space left on device/i,
+ 'pg_createcluster fails due to insufficient disk space';
+like $$outref, qr/\nls><ls\n/, 'does not leave files behind';
+
+check_clean;
+
+#
+note 'check disk full conditions on startup';
+my $cmd = <<EOF;
+set -e
+mount --make-rprivate / 2> /dev/null || :
+export LC_MESSAGES=C
+dirs="/etc/postgresql /var/lib/postgresql /var/log/postgresql"
+mkdir -p \$dirs
+trap "umount \$dirs" 0 HUP INT QUIT ILL ABRT PIPE TERM
+mount -t tmpfs -o size=1000000 none /etc/postgresql
+# an empty cluster needs 69MB on ppc64el, round up to 90
+mount -t tmpfs -o size=90000000 none /var/lib/postgresql
+mount -t tmpfs -o size=1000000 none /var/log/postgresql
+pg_createcluster $MAJORS[-1] test
+
+# fill up /var/lib/postgresql
+! cat < /dev/zero > /var/lib/postgresql/cruft 2>/dev/null
+echo '-- full lib --'
+! pg_ctlcluster $MAJORS[-1] test start
+echo '-- end full lib --'
+echo '-- full lib log --'
+cat /var/log/postgresql/postgresql-$MAJORS[-1]-test.log
+echo '-- end full lib log --'
+rm /var/lib/postgresql/cruft
+pg_dropcluster $MAJORS[-1] test --stop
+EOF
+
+$result = exec_as 'root', "echo '$cmd' | unshare -m sh", $outref;
+is $result, 0, 'script failed';
+like $$outref, qr/^-- full lib --.*No space left on device.*^-- end full lib --/ims,
+ 'pg_ctlcluster prints error message';
+like $$outref, qr/^-- full lib log --.*No space left on device.*^-- end full lib log --/ims,
+ 'log file has error message';
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/032_ssl_key_permissions.t b/t/032_ssl_key_permissions.t
new file mode 100644
index 0000000..929f08a
--- /dev/null
+++ b/t/032_ssl_key_permissions.t
@@ -0,0 +1,60 @@
+use strict;
+use warnings;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => $PgCommon::rpm ? 1 : 3 + 19 * @MAJORS;
+
+if ($PgCommon::rpm) { pass 'No ssl key checks on RedHat'; exit; }
+
+my ($pg_uid, $pg_gid) = (getpwnam 'postgres')[2,3];
+my $ssl_cert_gid = (getgrnam 'ssl-cert')[2]; # reset permissions
+die "Could not determine ssl-cert gid" unless ($ssl_cert_gid);
+
+my $snakekey = '/etc/ssl/private/ssl-cert-snakeoil.key';
+is ((stat $snakekey)[4], 0, "$snakekey is owned by root");
+is ((stat $snakekey)[5], $ssl_cert_gid, "$snakekey group is ssl-cert");
+is ((stat $snakekey)[2], 0100640, "$snakekey mode is 0640");
+
+foreach my $version (@MAJORS) {
+ my $pkgversion = `dpkg-query -f '\${Version}' -W postgresql-$version`;
+ note "$version ($pkgversion)";
+ if ($version <= 9.1) {
+ pass "no SSL support on $version" foreach (1..19);
+ next;
+ }
+SKIP: {
+ skip "No SSL key check on <= 9.0", 19 if ($version <= 9.0);
+ program_ok (0, "pg_createcluster $version main");
+
+ my $nobody_uid = (getpwnam 'nobody')[2];
+ chown $nobody_uid, 0, $snakekey;
+ like_program_out 'postgres', "pg_ctlcluster $version main start", 1,
+ qr/private key file.*must be owned by the database user or root/s,
+ 'ssl key owned by nobody refused';
+
+SKIP: {
+ skip "SSL key group check skipped on Debian oldstable packages", 4 if ($version <= 9.4 and $pkgversion !~ /pgdg/);
+ chown 0, 0, $snakekey;
+ chmod 0644, $snakekey;
+ like_program_out 'postgres', "pg_ctlcluster $version main start", 1,
+ qr/private key file.*has group or world access/,
+ 'ssl key with permissions root:root 0644 refused';
+
+ chown $pg_uid, $pg_gid, $snakekey;
+ chmod 0640, $snakekey;
+ like_program_out 'postgres', "pg_ctlcluster $version main start", 1,
+ qr/private key file.*has group or world access/,
+ 'ssl key with permissions postgres:postgres 0640 refused';
+}
+
+ chown 0, $ssl_cert_gid, $snakekey;
+
+ program_ok (0, "pg_dropcluster $version main --stop");
+ is ((stat $snakekey)[4], 0, "$snakekey is owned by root");
+ is ((stat $snakekey)[5], $ssl_cert_gid, "$snakekey group is ssl-cert");
+ is ((stat $snakekey)[2], 0100640, "$snakekey mode is 0640");
+ check_clean;
+}
+}
diff --git a/t/040_upgrade.t b/t/040_upgrade.t
new file mode 100644
index 0000000..6818ef8
--- /dev/null
+++ b/t/040_upgrade.t
@@ -0,0 +1,272 @@
+# Test upgrading from the oldest version to the latest, using the default
+# configuration file.
+
+# Lowest supported "upgrade from" version: 8.4 (lower versions don't have lo_import)
+# Lowest supported "upgrade to" version: 9.2 (lower versions don't have pg_upgrade -o)
+# Lowest supported "upgrade to" version with pg_dumpall: 9.1 (lower versions don't have pg_dumpall --quote-all-identifiers)
+
+use strict;
+
+use File::Temp qw/tempfile tempdir/;
+use POSIX qw/dup2/;
+use Time::HiRes qw/usleep/;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => (@MAJORS == 1) ? 1 : 123 * 3;
+
+if (@MAJORS == 1) {
+ pass 'only one major version installed, skipping upgrade tests';
+ exit 0;
+}
+
+foreach my $upgrade_options ('-m dump', '-m upgrade', '-m upgrade --link') {
+next if ($ENV{UPGRADE_METHOD} and $upgrade_options !~ /$ENV{UPGRADE_METHOD}$/); # hack to ease debugging individual methods
+note ("upgrade method \"$upgrade_options\", $MAJORS[0] -> $MAJORS[-1]");
+
+# create cluster
+ok ((system "pg_createcluster $MAJORS[0] upgr >/dev/null") == 0,
+ "pg_createcluster $MAJORS[0] upgr");
+exec_as 'root', "sed -i '/^local.*postgres/ s/\$/\\nlocal all foo trust/' /etc/postgresql/$MAJORS[0]/upgr/pg_hba.conf";
+is ((system "pg_ctlcluster $MAJORS[0] upgr start"), 0, 'Starting upgr cluster');
+
+# Create nobody user, test database, and put a table into it
+is ((exec_as 'postgres', 'createuser nobody -D -R -s && createdb -O nobody test && createdb -O nobody testnc && createdb -O nobody testro'),
+ 0, 'Create nobody user and test databases');
+is ((exec_as 'nobody', 'psql test -c "CREATE TABLE phone (name varchar(255) PRIMARY KEY, tel int NOT NULL)"'),
+ 0, 'create table');
+is ((exec_as 'nobody', 'psql test -c "INSERT INTO phone VALUES (\'Alice\', 2)"'), 0, 'insert Alice into phone table');
+SKIP: {
+ skip 'datallowconn = f not supported with pg_upgrade', 1 if $upgrade_options =~ /upgrade/;
+ is ((exec_as 'postgres', 'psql template1 -c "UPDATE pg_database SET datallowconn = \'f\' WHERE datname = \'testnc\'"'),
+ 0, 'disallow connection to testnc');
+}
+is ((exec_as 'nobody', 'psql testro -c "CREATE TABLE nums (num int NOT NULL); INSERT INTO nums VALUES (1)"'), 0, 'create table in testro');
+SKIP: {
+ skip 'read-only not supported with pg_upgrade', 2 if $upgrade_options =~ /upgrade/;
+ is ((exec_as 'postgres', 'psql template1 -c "ALTER DATABASE testro SET default_transaction_read_only TO on"'),
+ 0, 'set testro transaction default to readonly');
+ is ((exec_as 'nobody', 'psql testro -c "CREATE TABLE test(num int)"'),
+ 1, 'creating table in testro fails');
+}
+
+# create a schema and a table with a name that was un-reserved between 8.4 and 9.1
+is ((exec_as 'nobody', 'psql test -c "CREATE SCHEMA \"old\""'),
+ 0, 'create schema "old"');
+is ((exec_as 'nobody', 'psql test -c "CREATE TABLE \"old\".\"old\" (\"old\" text)"'),
+ 0, 'create table "old.old"');
+
+# create a sequence
+is ((exec_as 'nobody', 'psql test -c "CREATE SEQUENCE odd10 INCREMENT BY 2 MINVALUE 1 MAXVALUE 10 CYCLE"'),
+ 0, 'create sequence');
+is_program_out 'nobody', 'psql -Atc "SELECT nextval(\'odd10\')" test', 0, "1\n",
+ 'check next sequence value';
+is_program_out 'nobody', 'psql -Atc "SELECT nextval(\'odd10\')" test', 0, "3\n",
+ 'check next sequence value';
+
+# create a large object
+my ($fh, $filename) = tempfile("lo_import.XXXXXX", TMPDIR => 1, UNLINK => 1);
+print $fh "Hello world";
+close $fh;
+chmod 0644, $filename;
+is_program_out 'postgres', "psql -Atc \"SELECT lo_import('$filename', 1234)\"", 0, "1234\n",
+ 'create large object';
+
+# create stored procedures
+if ($MAJORS[0] < 9.0) {
+ is_program_out 'postgres', 'createlang plpgsql test', 0, '', 'createlang plpgsql test';
+} else {
+ pass '>= 9.0 enables PL/pgsql by default';
+ pass '...';
+}
+is_program_out 'nobody', 'psql test -c "CREATE FUNCTION inc2(integer) RETURNS integer LANGUAGE plpgsql AS \'BEGIN RETURN \$1 + 2; END;\';"',
+ 0, "CREATE FUNCTION\n", 'CREATE FUNCTION inc2';
+SKIP: {
+ skip 'hardcoded library paths not supported by pg_upgrade', 2 if $upgrade_options =~ /upgrade/;
+ is_program_out 'postgres', "psql -c \"UPDATE pg_proc SET probin = '$PgCommon::binroot$MAJORS[0]/lib/plpgsql.so' where proname = 'plpgsql_call_handler';\" test",
+ 0, "UPDATE 1\n", 'hardcoding plpgsql lib path';
+}
+is_program_out 'nobody', 'psql test -c "CREATE FUNCTION inc3(integer) RETURNS integer LANGUAGE plpgsql AS \'BEGIN RETURN \$1 + 3; END;\';"',
+ 0, "CREATE FUNCTION\n", 'create function inc3';
+is_program_out 'nobody', 'psql -Atc "SELECT inc2(3)" test', 0, "5\n",
+ 'call function inc2';
+is_program_out 'nobody', 'psql -Atc "SELECT inc3(3)" test', 0, "6\n",
+ 'call function inc3';
+
+# create user and group
+is_program_out 'postgres', "psql -qc 'CREATE USER foo' template1", 0, '',
+ 'create user foo';
+is_program_out 'postgres', "psql -qc 'CREATE GROUP gfoo' template1", 0, '',
+ 'create group gfoo';
+
+# create per-database and per-table ACL
+is_program_out 'postgres', "psql -qc 'GRANT CREATE ON DATABASE test TO foo'", 0, '',
+ 'GRANT CREATE ON DATABASE';
+is_program_out 'postgres', "psql -qc 'GRANT INSERT ON phone TO foo' test", 0, '',
+ 'GRANT INSERT';
+
+# exercise ACL on old database to ensure they are working
+is_program_out 'nobody', 'psql -U foo -qc "CREATE SCHEMA s_foo" test', 0, '',
+ 'CREATE SCHEMA on old cluster (ACL)';
+is_program_out 'nobody', 'psql -U foo -qc "INSERT INTO phone VALUES (\'Bob\', 1)" test',
+ 0, '', 'insert Bob into phone table (ACL)';
+
+# set config parameters
+is_program_out 'postgres', "pg_conftool $MAJORS[0] upgr set log_statement all",
+ 0, '', 'set postgresql.conf parameter';
+SKIP: {
+ skip 'postgresql.auto.conf not supported before 9.4', 6 if ($MAJORS[0] < 9.4);
+ is_program_out 'postgres', "psql -qc \"ALTER SYSTEM SET ident_file = '/etc/postgresql/$MAJORS[0]/upgr/pg_ident.conf'\"",
+ 0, '', 'set ident_file in postgresql.auto.conf';
+ is_program_out 'postgres', 'psql -qc "ALTER SYSTEM SET log_min_duration_statement = \'10s\'"',
+ 0, '', 'set log_min_duration_statement in postgresql.auto.conf';
+ is_program_out 'postgres', "echo \"data_directory = '/var/lib/postgresql/$MAJORS[0]/upgr'\" >> /var/lib/postgresql/$MAJORS[0]/upgr/postgresql.auto.conf", 0, "", "Append bogus data_directory setting to postgresql.auto.conf";
+}
+is_program_out 'postgres', 'psql -qc "ALTER DATABASE test SET DateStyle = \'ISO, YMD\'"',
+ 0, '', 'set database parameter';
+
+# create a tablespace
+my $tdir = tempdir (CLEANUP => 1);
+my ($p_uid, $p_gid) = (getpwnam 'postgres')[2,3];
+chown $p_uid, $p_gid, $tdir;
+is_program_out 'postgres', "psql -qc \"CREATE TABLESPACE myts LOCATION '$tdir'\"",
+ 0, '', "creating tablespace in $tdir";
+is_program_out 'postgres', "psql -qc 'CREATE TABLE tstab (a int) TABLESPACE myts'",
+ 0, '', "creating table in tablespace";
+
+# Check clusters
+like_program_out 'nobody', 'pg_lsclusters -h', 0,
+ qr/^$MAJORS[0]\s+upgr\s+5432 online postgres/;
+
+# Check SELECT in original cluster
+my $select_old;
+is ((exec_as 'nobody', 'psql -tAc "SELECT * FROM phone ORDER BY name" test', $select_old), 0, 'SELECT in original cluster succeeds');
+is ($$select_old, 'Alice|2
+Bob|1
+', 'check SELECT output in original cluster');
+
+# create inaccessible cwd, to check for confusing error messages
+rmdir '/tmp/pgtest';
+mkdir '/tmp/pgtest/' or die "Could not create temporary test directory /tmp/pgtest: $!";
+chmod 0100, '/tmp/pgtest/';
+chdir '/tmp/pgtest';
+
+# Upgrade to latest version
+my $outref;
+is ((exec_as 0, "(env LC_MESSAGES=C pg_upgradecluster -v $MAJORS[-1] $upgrade_options $MAJORS[0] upgr | sed -e 's/^/STDOUT: /')", $outref, 0), 0, 'pg_upgradecluster succeeds');
+like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+my @err = grep (!/^STDOUT: /, split (/\n/, $$outref));
+if (@err) {
+ fail 'no error messages during upgrade';
+ print (join ("\n", @err));
+} else {
+ pass "no error messages during upgrade";
+}
+
+# remove inaccessible test cwd
+chdir '/';
+rmdir '/tmp/pgtest/';
+
+# Check clusters
+like_program_out 'nobody', 'pg_lsclusters -h', 0,
+ qr"$MAJORS[0] +upgr 5433 down postgres /var/lib/postgresql/$MAJORS[0]/upgr +/var/log/postgresql/postgresql-$MAJORS[0]-upgr.log\n$MAJORS[-1] +upgr 5432 online postgres /var/lib/postgresql/$MAJORS[-1]/upgr +/var/log/postgresql/postgresql-$MAJORS[-1]-upgr.log", 'pg_lsclusters output';
+
+# Check that SELECT output is identical
+is_program_out 'nobody', 'psql -tAc "SELECT * FROM phone ORDER BY name" test', 0,
+ $$select_old, 'SELECT output is the same in original and upgraded test';
+is_program_out 'nobody', 'psql -tAc "SELECT * FROM nums" testro', 0,
+ "1\n", 'SELECT output is the same in original and upgraded testro';
+
+# Check that table was analyzed
+like_program_out 'nobody', "psql -XAtc \"select analyze_count from pg_stat_user_tables where relname = 'phone'\" test", 0, qr/^[1-3]$/,
+ 'check analyze count'; # --analyze-in-stages does 3 passes
+
+# Check sequence value
+is_program_out 'nobody', 'psql -Atc "SELECT nextval(\'odd10\')" test', 0, "5\n",
+ 'check next sequence value';
+is_program_out 'nobody', 'psql -Atc "SELECT nextval(\'odd10\')" test', 0, "7\n",
+ 'check next sequence value';
+is_program_out 'nobody', 'psql -Atc "SELECT nextval(\'odd10\')" test', 0, "9\n",
+ 'check next sequence value';
+is_program_out 'nobody', 'psql -Atc "SELECT nextval(\'odd10\')" test', 0, "1\n",
+ 'check next sequence value (wrap)';
+
+# check large objects
+is_program_out 'postgres', 'psql -Aqtc "SET bytea_output = \'escape\'; SELECT data FROM pg_largeobject WHERE loid = 1234"', 0, "Hello world\n",
+ 'check large object';
+
+# check stored procedures
+is_program_out 'nobody', 'psql -Atc "SELECT inc2(-3)" test', 0, "-1\n",
+ 'call function inc2';
+is_program_out 'nobody', 'psql -Atc "SELECT inc3(1)" test', 0, "4\n",
+ 'call function inc3 (formerly hardcoded path)';
+
+SKIP: {
+ skip 'upgrading databases with datallowcon = false not supported by pg_upgrade', 2 if $upgrade_options =~ /upgrade/;
+
+ # Check connection permissions
+ my $testnc_conn = $upgrade_options =~ /upgrade/ ? 't' : 'f';
+ is_program_out 'nobody', 'psql -tAc "SELECT datname, datallowconn FROM pg_database ORDER BY datname" template1', 0,
+ "postgres|t
+template0|f
+template1|t
+test|t
+testnc|$testnc_conn
+testro|t
+", 'dataallowconn values';
+}
+
+# check ACLs
+is_program_out 'nobody', 'psql -U foo -qc "CREATE SCHEMA s_bar" test', 0, '',
+ 'CREATE SCHEMA on new cluster (ACL)';
+is_program_out 'nobody', 'psql -U foo -qc "INSERT INTO phone VALUES (\'Chris\', 5)" test',
+ 0, '', 'insert Chris into phone table (ACL)';
+
+# check default transaction r/o
+is ((exec_as 'nobody', 'psql test -c "CREATE TABLE test(num int)"'),
+ 0, 'creating table in test succeeds');
+SKIP: {
+ skip 'read-only not supported by pg_upgrade', 2 if $upgrade_options =~ /upgrade/;
+ is ((exec_as 'nobody', 'psql testro -c "CREATE TABLE test(num int)"'),
+ 1, 'creating table in testro fails');
+ is ((exec_as 'postgres', 'psql testro -c "CREATE TABLE test(num int)"'),
+ 1, 'creating table in testro as superuser fails');
+}
+is ((exec_as 'nobody', 'psql testro -c "BEGIN READ WRITE; CREATE TABLE test(num int); COMMIT"'),
+ 0, 'creating table in testro succeeds with RW transaction');
+
+# check config parameters
+is_program_out 'postgres', 'psql -Atc "SHOW log_statement" test', 0, "all\n", 'check postgresql.conf parameters';
+SKIP: {
+ skip 'postgresql.auto.conf not supported before 9.4', 4 if ($MAJORS[0] < 9.4);
+ is_program_out 'postgres', 'psql -Atc "SHOW log_min_duration_statement" test', 0, "10s\n", 'check postgresql.auto.conf parameter';
+ is_program_out 'postgres', "cat /var/lib/postgresql/$MAJORS[-1]/upgr/postgresql.auto.conf", 0,
+ "# Do not edit this file manually!\n# It will be overwritten by the ALTER SYSTEM command.\nident_file = '/etc/postgresql/$MAJORS[-1]/upgr/pg_ident.conf'\nlog_min_duration_statement = '10s'\n#data_directory = '/var/lib/postgresql/$MAJORS[0]/upgr' #not valid in postgresql.auto.conf\n";
+}
+is_program_out 'postgres', 'psql -Atc "SHOW DateStyle" test', 0, "ISO, YMD\n", 'check database parameter';
+SKIP: {
+ skip "cluster name not supported in $MAJORS[0]", 1 if ($MAJORS[0] < 9.5);
+ is (PgCommon::get_conf_value ($MAJORS[-1], 'upgr', 'postgresql.conf', 'cluster_name'), "$MAJORS[-1]/upgr", "cluster_name is updated");
+}
+
+# check tablespace
+is_program_out 'postgres', "psql -Atc 'SELECT spcname FROM pg_tablespace ORDER BY spcname'",
+ 0, "myts\npg_default\npg_global\n", "check tablespace of upgraded table";
+is_program_out 'postgres', "psql -Atc \"SELECT spcname FROM pg_class c LEFT JOIN pg_tablespace t ON (c.reltablespace = t.oid) WHERE c.relname = 'tstab'\"",
+ 0, "myts\n", "check tablespace of upgraded table";
+
+# stop servers, clean up
+is ((system "pg_dropcluster $MAJORS[0] upgr --stop"), 0, 'Dropping original cluster');
+is ((system "pg_ctlcluster $MAJORS[-1] upgr restart"), 0, 'Restarting upgraded cluster');
+is_program_out 'nobody', 'psql -Atc "SELECT nextval(\'odd10\')" test', 0, "3\n",
+ 'upgraded cluster still works after removing old one';
+is ((system "pg_dropcluster $MAJORS[-1] upgr --stop"), 0, 'Dropping upgraded cluster');
+is ((system "rm -rf /var/log/postgresql/pg_upgradecluster-*"), 0, 'Cleaning pg_upgrade log files');
+
+check_clean;
+} # foreach method
+
+# vim: filetype=perl
diff --git a/t/041_upgrade_custompaths.t b/t/041_upgrade_custompaths.t
new file mode 100644
index 0000000..3416b55
--- /dev/null
+++ b/t/041_upgrade_custompaths.t
@@ -0,0 +1,51 @@
+# Test cluster upgrade with a custom data directory and custom log file.
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => (@MAJORS == 1) ? 1 : 28;
+
+if (@MAJORS == 1) {
+ pass 'only one major version installed, skipping upgrade tests';
+ exit 0;
+}
+
+ok ((system "pg_createcluster --start --datadir /tmp/postgresql-test -l /tmp/postgresql-test.log $MAJORS[0] upgr >/dev/null") == 0);
+
+# Upgrade to latest version
+my $outref;
+is ((exec_as 0, "pg_upgradecluster -v $MAJORS[-1] $MAJORS[0] upgr", $outref, 0), 0, 'pg_upgradecluster succeeds');
+like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+
+# Check clusters
+like_program_out 'nobody', 'pg_lsclusters -h', 0,
+ qr"$MAJORS[0] +upgr 5433 down postgres /tmp/postgresql-test +/tmp/postgresql-test.log\n$MAJORS[-1] +upgr 5432 online postgres /var/lib/postgresql/$MAJORS[-1]/upgr +/var/log/postgresql/postgresql-$MAJORS[-1]-upgr.log", 'pg_lsclusters output';
+
+# clean away new cluster and restart the old one
+is ((system "pg_dropcluster $MAJORS[-1] upgr --stop"), 0, 'Dropping upgraded cluster');
+is_program_out 0, "pg_ctlcluster $MAJORS[0] upgr start", 0, '', 'Restarting old cluster';
+is_program_out 'nobody', 'pg_lsclusters -h', 0,
+ "$MAJORS[0] upgr 5433 online postgres /tmp/postgresql-test /tmp/postgresql-test.log
+", 'pg_lsclusters output';
+
+# Do another upgrade with using a custom defined data directory (and in passing, test --keep-port)
+my $outref;
+is ((exec_as 0, "pg_upgradecluster --keep-port -v $MAJORS[-1] $MAJORS[0] upgr /tmp/psql-common-testsuite", $outref, 0), 0, 'pg_upgradecluster succeeds');
+unlike $$outref, qr/^pg_restore: /m, 'no pg_restore error messages during upgrade';
+unlike $$outref, qr/^[A-Z]+: /m, 'no server error messages during upgrade';
+like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+
+like_program_out 'nobody', 'pg_lsclusters -h', 0,
+ qr"$MAJORS[0] +upgr 5433 down postgres /tmp/postgresql-test +/tmp/postgresql-test.log\n$MAJORS[-1] +upgr 5432 online postgres /tmp/psql-common-testsuite +/var/log/postgresql/postgresql-$MAJORS[-1]-upgr.log", 'pg_lsclusters output';
+
+# stop servers, clean up
+is ((system "pg_dropcluster $MAJORS[0] upgr"), 0, 'Dropping original cluster');
+is ((system "pg_dropcluster $MAJORS[-1] upgr --stop"), 0, 'Dropping upgraded cluster');
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/042_upgrade_rename.t b/t/042_upgrade_rename.t
new file mode 100644
index 0000000..ef44f11
--- /dev/null
+++ b/t/042_upgrade_rename.t
@@ -0,0 +1,27 @@
+# Test in-version upgrading (usually used after catalog version bumps)
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => 15 * @MAJORS;
+
+foreach my $v (@MAJORS) {
+ SKIP: {
+ skip "pg_upgrade not supported on $v", 15 if ($v < 9.2);
+ note "PostgreSQL $v";
+
+ program_ok 0, "pg_createcluster $v main --start", 0;
+ program_ok 0, "pg_upgradecluster -m upgrade --old-bindir=$PgCommon::binroot$v/bin -v $v --rename upgr $v main", 0;
+ like_program_out 0, "pg_lsclusters -h", 0, qr/$v main 5433 down.*\n$v upgr 5432 online/;
+
+ program_ok 0, "pg_dropcluster $v main --stop", 0;
+ program_ok 0, "pg_dropcluster $v upgr --stop", 0;
+ is ((system "rm -rf /var/log/postgresql/pg_upgradecluster-$v-$v-upgr.*"), 0, 'Cleaning pg_upgrade log files');
+ check_clean;
+ }
+}
+
+# vim: filetype=perl
diff --git a/t/043_upgrade_ssl_cert.t b/t/043_upgrade_ssl_cert.t
new file mode 100644
index 0000000..d7dc79e
--- /dev/null
+++ b/t/043_upgrade_ssl_cert.t
@@ -0,0 +1,79 @@
+# Test cluster upgrade with a custom ssl certificate
+
+use strict;
+use File::Temp qw/tempdir/;
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => (@MAJORS == 1 or $PgCommon::rpm) ? 1 : 22;
+
+if (@MAJORS == 1) {
+ pass 'only one major version installed, skipping upgrade tests';
+ exit 0;
+}
+if ($PgCommon::rpm) {
+ pass 'SSL certificates not handled on RedHat';
+ exit 0;
+}
+
+
+ok ((system "pg_createcluster $MAJORS[0] upgr >/dev/null") == 0);
+
+my $tdir = tempdir (CLEANUP => 1);
+my ($p_uid, $p_gid) = (getpwnam 'postgres')[2,3];
+chown $p_uid, $p_gid, $tdir;
+
+my $tempcrt = "$tdir/ssl-cert-snakeoil.pem";
+my $oldcrt = "/var/lib/postgresql/$MAJORS[0]/upgr/server.crt";
+my $newcrt = "/var/lib/postgresql/$MAJORS[-1]/upgr/server.crt";
+
+# First upgrade
+note "upgrade test: server.crt is a symlink";
+(system "cp -p /etc/ssl/certs/ssl-cert-snakeoil.pem $tempcrt") == 0 or die "cp: $!";
+unlink $oldcrt; # remove file installed by pg_createcluster
+symlink $tempcrt, $oldcrt or die "symlink: $!";
+
+# Upgrade to latest version
+my $outref;
+is ((exec_as 0, "pg_upgradecluster --start -v $MAJORS[-1] $MAJORS[0] upgr", $outref, 0), 0, 'pg_upgradecluster succeeds');
+like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+
+if ($MAJORS[-1] >= 9.2) {
+ is ((-e $newcrt), undef, "new data directory does not contain server.crt");
+ is ((PgCommon::get_conf_value $MAJORS[-1], 'upgr', 'postgresql.conf', 'ssl_cert_file'),
+ $tempcrt, "symlink server.crt target is put into ssl_cert_file");
+} else {
+ is ((-l $newcrt), 1, "new data directory contains server.crt");
+ is ((readlink $newcrt), $tempcrt, "symlink server.crt points to correct location");
+}
+
+# Clean away new cluster
+is ((system "pg_dropcluster $MAJORS[-1] upgr --stop"), 0, 'Dropping upgraded cluster');
+unlink $oldcrt or die "unlink: $!";
+
+# Second upgrade
+note "upgrade test: server.crt is a plain file";
+(system "cp -p $tempcrt $oldcrt") == 0 or die "cp: $!";
+
+# Upgrade to latest version
+my $outref;
+is ((exec_as 0, "pg_upgradecluster --start -v $MAJORS[-1] $MAJORS[0] upgr", $outref, 0), 0, 'pg_upgradecluster succeeds');
+like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+
+is ((-f $newcrt), 1, "new data directory contains server.crt file");
+if ($MAJORS[-1] >= 9.2) {
+ is ((PgCommon::get_conf_value $MAJORS[-1], 'upgr', 'postgresql.conf', 'ssl_cert_file'),
+ $newcrt, "server.crt is put into ssl_cert_file");
+} else {
+ pass "...";
+}
+
+# Stop servers, clean up
+is ((system "pg_dropcluster $MAJORS[0] upgr"), 0, 'Dropping original cluster');
+is ((system "pg_dropcluster $MAJORS[-1] upgr --stop"), 0, 'Dropping upgraded cluster');
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/045_backup.t b/t/045_backup.t
new file mode 100644
index 0000000..1258ea3
--- /dev/null
+++ b/t/045_backup.t
@@ -0,0 +1,169 @@
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More;
+use Time::HiRes qw/usleep/;
+
+my ($pg_uid, $pg_gid) = (getpwnam 'postgres')[2,3];
+my $systemd = (-d "/run/systemd/system" and not $ENV{_SYSTEMCTL_SKIP_REDIRECT});
+note $systemd ? "We are running systemd" : "We are not running systemd";
+
+foreach my $v (@MAJORS) {
+ if ($v < 9.1) {
+ ok 1, "pg_backupcluster not supported on $v";
+ next;
+ }
+ note "PostgreSQL $v";
+
+ note "create cluster";
+ program_ok 0, "pg_createcluster --locale en_US.UTF-8 $v main --start";
+ like_program_out 0, "pg_lsclusters -h", 0, qr/$v main 5432 online/;
+ program_ok 0, "pg_conftool $v main set work_mem 11MB";
+ if ($v <= 9.6) {
+ open my $hba, ">>", "/etc/postgresql/$v/main/pg_hba.conf";
+ print $hba "local replication all peer\n";
+ close $hba;
+ program_ok 0, "pg_conftool $v main set max_wal_senders 10";
+ program_ok 0, "pg_conftool $v main set wal_level archive";
+ program_ok 0, "pg_conftool $v main set max_replication_slots 10" if ($v >= 9.4);
+ program_ok 0, "pg_conftool $v main set ssl off" if ($v <= 9.1); # cert symlinks not backed up in 9.1
+ program_ok 0, "pg_ctlcluster $v main restart";
+ }
+ my $locale_provider = $v >= 15 ? "--locale-provider libc " : "";
+ program_ok $pg_uid, "createdb -E SQL_ASCII $locale_provider-T template0 mydb";
+ program_ok $pg_uid, "psql -c 'alter database mydb set search_path=public'";
+ program_ok $pg_uid, "psql -c 'create table foo (t text)' mydb";
+ program_ok $pg_uid, "psql -c \"insert into foo values ('data from backup')\" mydb";
+ program_ok $pg_uid, "psql -c 'CREATE USER myuser'";
+ program_ok $pg_uid, "psql -c 'alter role myuser set search_path=public, myschema'";
+ program_ok $pg_uid, "createdb --locale-provider icu --icu-locale de -T template0 myicudb" if ($v >= 15);
+
+ SKIP: { # in PG 10, ARID is part of globals.sql which we try to restore before databases.sql
+ skip "alter role in database handling in PG <= 10 not supported", 1 if ($v <= 10);
+ program_ok $pg_uid, "psql -c 'alter role myuser in database mydb set search_path=public, myotherschema'";
+ }
+
+ note "create directory";
+ program_ok 0, "pg_backupcluster $v main createdirectory";
+ my $dir = "/var/backups/postgresql/$v-main";
+ my @stat = stat $dir;
+ is $stat[4], $pg_uid, "$dir owned by uid postgres";
+ is $stat[5], $pg_gid, "$dir owned by gid postgres";
+
+ my @backups = ();
+ my $dump = '';
+ SKIP: {
+ skip "dump not supported before 9.3", 1 if ($v < 9.3);
+ note "dump";
+ if ($systemd) {
+ program_ok 0, "systemctl start pg_dump\@$v-main";
+ } else {
+ program_ok 0, "pg_backupcluster $v main dump";
+ }
+ ($dump) = glob "$dir/*.dump";
+ ok -d $dump, "dump created in $dump";
+ @stat = stat $dump;
+ is $stat[4], $pg_uid, "$dump owned by uid postgres";
+ is $stat[5], $pg_gid, "$dump owned by gid postgres";
+ push @backups, $dump;
+ }
+
+ note "basebackup";
+ my $receivewal_pid;
+ if ($v >= 9.5) {
+ if ($systemd) {
+ program_ok 0, "systemctl start pg_receivewal\@$v-main";
+ } else {
+ $receivewal_pid = fork;
+ if ($receivewal_pid == 0) {
+ # suppress "not renaming "000000010000000000000003.gz.partial", segment is not complete"
+ exec "pg_backupcluster $v main receivewal 2>/dev/null";
+ }
+ }
+ program_ok $pg_uid, "psql -c 'create table poke_receivewal (t text)' mydb";
+ usleep($delay);
+ my $wal = "000000010000000000000001";
+ $wal .= ".gz" if ($v >= 10);
+ $wal .= ".partial";
+ TODO: {
+ local $TODO = "WAL test is unstable";
+ ok_dir "$dir/wal", [$wal], "$dir/wal contains $wal";
+ }
+ }
+ if ($systemd) {
+ program_ok 0, "systemctl start pg_basebackup\@$v-main";
+ } else {
+ program_ok 0, "pg_backupcluster --checkpoint=fast $v main basebackup";
+ }
+ my ($basebackup) = glob "$dir/*.backup";
+ ok -d $basebackup, "dump created in $basebackup";
+ @stat = stat $basebackup;
+ is $stat[4], $pg_uid, "$basebackup owned by uid postgres";
+ is $stat[5], $pg_gid, "$basebackup owned by gid postgres";
+ push @backups, $basebackup;
+
+ note "list";
+ like_program_out 0, "pg_backupcluster $v main list", 0, qr/$dump.*$basebackup/s;
+
+ note "more database changes";
+ program_ok $pg_uid, "psql -c \"insert into foo values ('data later deleted')\" mydb";
+ program_ok $pg_uid, "psql -c \"insert into foo values ('data from archive')\" mydb";
+ my $timestamp = `su -c "psql -XAtc 'select now()'" postgres`;
+ ok $timestamp, "retrieve recovery timestamp";
+ program_ok $pg_uid, "psql -c \"delete from foo where t = 'data later deleted'\" mydb";
+ usleep($delay);
+ if ($v >= 9.5) {
+ # since we are stopping pg_receivewal before postgresql, this implicitly tests restoring from .partial WAL files as well
+ if ($systemd) {
+ program_ok 0, "systemctl stop pg_receivewal\@$v-main";
+ } else {
+ is kill('INT', $receivewal_pid), 1, "stop receivewal";
+ }
+ }
+
+ for my $backup (@backups) {
+ note "restore $backup";
+ program_ok 0, "pg_dropcluster $v main --stop";
+ program_ok 0, "pg_restorecluster $v main $backup --start --datadir /var/lib/postgresql/$v/snowflake";
+ like_program_out 0, "pg_lsclusters -h", 0, qr/$v main 5432 online postgres .var.lib.postgresql.$v.snowflake/;
+ my $outref;
+ is exec_as($pg_uid, "psql -XAtl", $outref), 0, 'psql -XAtl';
+ like $$outref, qr/^mydb\|postgres\|SQL_ASCII\|(libc\|)?en_US.UTF-8\|en_US.UTF-8\|(\|libc\||\|\|)?$/m, "mydb locales";
+ like $$outref, qr/^myicudb\|postgres\|UTF8\|(icu\|)?en_US.UTF-8\|en_US.UTF-8\|(de\|icu\||de\|\|)?$/m, "myicudb locales" if ($v >= 15);
+ is_program_out $pg_uid, "psql -XAtc 'show work_mem'", 0, "11MB\n";
+ is_program_out $pg_uid, "psql -XAtc 'select * from foo' mydb", 0, "data from backup\n";
+ is_program_out $pg_uid, "psql -XAtc \"select analyze_count between 1 and 3 from pg_stat_user_tables where relname = 'foo'\" mydb", 0,
+ "t\n"; # --analyze-in-stages does 3 passes
+ SKIP: {
+ skip "alter role in database handling in PG <= 10 not supported", 1 if ($v <= 10);
+ like_program_out $pg_uid, "psql -XAtc '\\drds'", 0, qr/myuser\|mydb\|search_path=public, myotherschema.*
+myuser\|\|search_path=public, myschema.*
+\|mydb\|search_path=public.*\n/;
+ }
+ }
+
+ if ($v >= 9.5) {
+ note "restore $basebackup with WAL archive";
+ program_ok 0, "pg_dropcluster $v main --stop";
+ program_ok 0, "pg_restorecluster $v main $basebackup --start --archive --port 5430";
+ like_program_out 0, "pg_lsclusters -h", 0, qr/$v main 5430 online postgres .var.lib.postgresql.$v.main/;
+ is_program_out $pg_uid, "psql -XAtc 'select * from foo order by t' mydb", 0, "data from archive\ndata from backup\n";
+
+ note "restore $basebackup with PITR";
+ program_ok 0, "pg_dropcluster $v main --stop";
+ program_ok 0, "pg_restorecluster $v main $basebackup --start --pitr '$timestamp'";
+ like_program_out 0, "pg_lsclusters -h", 0, qr/$v main 5432 online postgres .var.lib.postgresql.$v.main/;
+ is_program_out $pg_uid, "psql -XAtc 'select * from foo order by t' mydb", 0, "data from archive\ndata from backup\ndata later deleted\n";
+ }
+
+ program_ok 0, "pg_dropcluster $v main --stop";
+ check_clean;
+
+} # foreach version
+
+done_testing();
+
+# vim: filetype=perl
diff --git a/t/050_encodings.t b/t/050_encodings.t
new file mode 100644
index 0000000..02dd2f7
--- /dev/null
+++ b/t/050_encodings.t
@@ -0,0 +1,117 @@
+# Test locale and encoding settings in pg_createcluster.
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => @MAJORS * 52 + 8;
+
+# create a test cluster with given locale, check the locale/encoding, and
+# remove it
+# Arguments: <version> <locale> [<encoding>]
+sub check_cluster {
+ my ($v, $locale, $enc) = @_;
+ note "Checking $v $locale";
+ my $cluster_name = $locale;
+ $cluster_name =~ s/-//g; # strip dashes so postgresql@.service likes it
+ if (defined $enc) {
+ $cluster_name .= "_$enc";
+ is ((system "LC_ALL='$locale' pg_createcluster --encoding $enc --start $v $cluster_name >/dev/null 2>&1"), 0,
+ "pg_createcluster version $v for $locale with --encoding succeeded");
+ } else {
+ is ((system "pg_createcluster --start --locale=$locale $v $cluster_name >/dev/null 2>&1"), 0,
+ "pg_createcluster version $v for $locale without --encoding succeeded");
+ }
+
+ # check encoding
+ sleep 1;
+ my $outref;
+ is ((exec_as 'postgres', "psql -Atl --cluster $v/$cluster_name", $outref, 0), 0,
+ 'psql -l succeeds');
+ my $is_unicode = 0;
+ $is_unicode = 1 if defined $enc && $enc =~ /(UNICODE|UTF-8)/;
+ $is_unicode = 1 if $locale =~ /UTF-8/;
+ if ($is_unicode) {
+ like $$outref, qr/template1.*(UNICODE|UTF8)/, 'template1 is UTF-8 encoded';
+ } else {
+ unlike $$outref, qr/template1.*(UNICODE|UTF8)/, 'template1 is not UTF-8 encoded';
+ }
+
+ # create a table and stuff some ISO-8859-5 characters into it (для)
+ is ((exec_as 'postgres', "createdb test", $outref), 0, 'creating test database');
+ is_program_out 'postgres', "printf '\324\333\357' | psql -qc \"set client_encoding='iso-8859-5';
+ create table t (x varchar); copy t from stdin\" test", 0, '',
+ 'creating table with ISO-8859-5 characters';
+ is_program_out 'postgres', "echo \"set client_encoding='utf8'; select * from t\" | psql -Atq test", 0,
+ "\320\264\320\273\321\217\n", 'correct string in UTF-8';
+ is_program_out 'postgres', "echo \"set client_encoding='iso-8859-5'; select * from t\" | psql -Atq test", 0,
+ "\324\333\357\n", 'correct string in ISO-8859-5';
+
+ # do the same test with using UTF-8 as input
+ is_program_out 'postgres', "printf '\320\264\320\273\321\217' | psql -qc \"set client_encoding='utf8';
+ delete from t; copy t from stdin\" test", 0, '',
+ 'creating table with UTF-8 characters';
+ is_program_out 'postgres', "echo \"set client_encoding='utf8'; select * from t\" | psql -Atq test", 0,
+ "\320\264\320\273\321\217\n", 'correct string in UTF-8';
+ is_program_out 'postgres', "echo \"set client_encoding='iso-8859-5'; select * from t\" | psql -Atq test", 0,
+ "\324\333\357\n", 'correct string in ISO-8859-1';
+
+ # check encoding of server error messages (breaks in locale/encoding mismatches, so skip that)
+ if (!defined $enc) {
+ # temporarily disable and accept English text, since Russian translations are disabled now
+ like_program_out 'postgres', 'psql test -c "set client_encoding = \'UTF-8\'; select sqrt(-1)"', 1,
+ qr/^[^?]*(брать|отрицательного|cannot take square root)[^?]*$/, 'Server error message has correct language and encoding';
+ }
+
+ # check that we do not run into 'ignoring unconvertible UTF-8 character'
+ # breakage on nonmatching lc_messages and client_encoding
+ PgCommon::set_conf_value $v, $cluster_name, 'postgresql.conf',
+ 'client_encoding', 'UTF-8';
+ PgCommon::set_conf_value $v, $cluster_name, 'postgresql.conf',
+ 'lc_messages', 'POSIX';
+ is_program_out 0, "pg_ctlcluster $v $cluster_name restart", 0, '',
+ 'cluster starts correctly with nonmatching lc_messages and client_encoding';
+
+ # check interception of invalidly encoded/escaped strings
+ if ($is_unicode) {
+ like_program_out 'postgres',
+ 'printf "set client_encoding=\'UTF-8\'; select \'\\310\\\\\'a\'" | psql -Atq template1',
+ 0, qr/(UNICODE|UTF8).*0x(c8.*5c|c8.*27)/,
+ 'Server rejects incorrect encoding (CVE-2006-2313)';
+ like_program_out 'postgres',
+ 'printf "set client_encoding=\'SJIS\'; select \'\\\\\\\'a\'" | psql -Atq template1',
+ 0, qr/(\\' is insecure)|(unterminated quoted string)/,
+ 'Server rejects \\\' escaping in unsafe client encoding (CVE-2006-2314)';
+ if ($v >= '9.1') {
+ like_program_out 'postgres',
+ "printf \"set client_encoding='UTF-8'; set escape_string_warning='off'; select '\\\\\\'a'\" | psql -Atq template1",
+ 0, qr/unterminated quoted string/,
+ 'Server rejects obsolete \\\' escaping in unsafe client encoding (CVE-2006-2314)';
+ } else {
+ is_program_out 'postgres',
+ "printf \"set client_encoding='UTF-8'; set escape_string_warning='off'; select '\\\\\\'a'\" | psql -Atq template1",
+ 0, "'a\n", 'Server accepts \\\' escaping in safe client encoding (CVE-2006-2314)';
+ }
+ }
+
+ # drop cluster
+ is ((system "pg_dropcluster $v $cluster_name --stop"), 0, 'Dropping cluster');
+}
+
+foreach my $v (@MAJORS) {
+ check_cluster $v, 'ru_RU';
+ check_cluster $v, 'ru_RU.UTF-8';
+
+ note "Check $v locale environment variables";
+ # check LC_* over LANG domination
+ is ((system "LANGUAGE= LC_ALL=C LANG=bo_GUS.UTF-8 pg_createcluster --start $v main >/dev/null 2>&1"), 0,
+ "pg_createcluster: LC_ALL dominates LANG");
+ like_program_out 'postgres', "psql -Atl --cluster $v/main", 0,
+ qr/template1.*ASCII/, 'template1 is ASCII encoded';
+ is ((system "pg_dropcluster $v main --stop"), 0, 'Dropping cluster');
+}
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/052_upgrade_encodings.t b/t/052_upgrade_encodings.t
new file mode 100644
index 0000000..e7a7125
--- /dev/null
+++ b/t/052_upgrade_encodings.t
@@ -0,0 +1,98 @@
+# Test default and explicit encoding on upgrades
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => (@MAJORS == 1) ? 1 : 46;
+
+if (@MAJORS == 1) {
+ pass 'only one major version installed, skipping upgrade tests';
+ exit 0;
+}
+
+my $outref;
+my $oldv = $MAJORS[0];
+my $newv = $MAJORS[-1];
+
+is ((exec_as 0, "pg_createcluster --start --locale=ru_RU $oldv main", $outref), 0,
+ "creating ru_RU $oldv cluster");
+
+is ((exec_as 'postgres', 'psql -c "create database latintest" template1', $outref), 0,
+ "creating latintest DB with LATIN encoding");
+if ($oldv <= '8.3') {
+ is ((exec_as 'postgres', 'psql -c "create database asctest encoding = \'SQL_ASCII\'" template1', $outref), 0,
+ "creating asctest DB with ASCII encoding");
+} else {
+ is ((exec_as 'postgres', 'psql -c "create database asctest template = template0 lc_collate = \'C\' lc_ctype = \'C\' encoding = \'SQL_ASCII\'" template1', $outref), 0,
+ "creating asctest DB with C locale");
+}
+if ($oldv >= 15) {
+ program_ok 'postgres', 'psql -c "create database icutest template template0 locale_provider icu icu_locale de"', 0, "creating database with ICU locale";
+} else {
+ program_ok 'postgres', 'psql -c "create database icutest"', 0, "creating placeholder icutest database";
+}
+
+is ((exec_as 'postgres', "printf 'A\\324B' | psql -c \"create table t(x varchar); copy t from stdin\" latintest", $outref),
+ 0, 'write LATIN database content to latintest');
+is ((exec_as 'postgres', "printf 'A\\324B' | psql -c \"create table t(x varchar); copy t from stdin\" asctest", $outref),
+ 0, 'write LATIN database content to asctest');
+
+is_program_out 'postgres', "echo \"select * from t\" | psql -Atq latintest",
+ 0, "A\324B\n", 'old latintest DB has correctly encoded string';
+is_program_out 'postgres', "echo \"select * from t\" | psql -Atq asctest",
+ 0, "A\324B\n", 'old asctest DB has correctly encoded string';
+
+is ((exec_as 'postgres', 'psql -Atl', $outref), 0, 'psql -Atl on old cluster');
+ok ((index $$outref, 'latintest|postgres|ISO_8859_5') >= 0, 'latintest is LATIN encoded');
+ok ((index $$outref, 'asctest|postgres|SQL_ASCII') >= 0, 'asctest is ASCII encoded');
+if ($oldv >= 15) {
+ like $$outref, qr/icutest\|postgres\|ISO_8859_5\|icu\|ru_RU\|ru_RU\|de/, 'icutest has proper icu locale';
+} else {
+ like $$outref, qr/icutest\|postgres\|ISO_8859_5/, 'icutest is LATIN encoded';
+}
+ok ((index $$outref, 'template1|postgres|ISO_8859_5') >= 0, 'template1 is LATIN encoded');
+
+# upgrade without specifying locales, should be kept
+like_program_out 0, "pg_upgradecluster -v $newv $oldv main", 0, qr/^Success. Please check/m;
+
+is ((exec_as 'postgres', "psql --cluster $newv/main -Atl", $outref), 0, 'psql -Atl on upgraded cluster');
+ok ((index $$outref, 'latintest|postgres|ISO_8859_5') >= 0, 'latintest is LATIN encoded');
+ok ((index $$outref, 'asctest|postgres|SQL_ASCII') >= 0, 'asctest is ASCII encoded');
+if ($oldv >= 15) {
+ like $$outref, qr/icutest\|postgres\|ISO_8859_5\|icu\|ru_RU\|ru_RU\|de/, 'icutest has proper icu locale';
+} else {
+ like $$outref, qr/icutest\|postgres\|ISO_8859_5/, 'icutest is LATIN encoded';
+}
+ok ((index $$outref, 'template1|postgres|ISO_8859_5') >= 0, 'template1 is LATIN encoded');
+is_program_out 'postgres', "echo \"select * from t\" | psql --cluster $newv/main -Atq latintest",
+ 0, "A\324B\n", 'new latintest DB has correctly encoded string';
+
+is ((system "pg_dropcluster --stop $newv main"), 0, 'dropping upgraded cluster');
+is ((system "pg_ctlcluster $oldv main start"), 0, 'restarting old cluster');
+
+# upgrade with explicitly specifying other locale
+like_program_out 0, "pg_upgradecluster --locale ru_RU.UTF-8 -v $newv $oldv main", 0, qr/^Success. Please check/m;
+
+is ((exec_as 'postgres', "psql --cluster $newv/main -Atl", $outref), 0, 'psql -Atl on upgraded cluster');
+if ($newv >= 11) {
+ ok ((index $$outref, 'latintest|postgres|ISO_8859_5') >= 0, 'latintest is still LATIN encoded');
+} else {
+ like $$outref, qr/latintest\|postgres\|(UTF8|UNICODE)/, 'latintest is now UTF8 encoded';
+}
+ok ((index $$outref, 'asctest|postgres|SQL_ASCII') >= 0, 'asctest is ASCII encoded');
+like $$outref, qr/template1\|postgres\|(UTF8|UNICODE)/, 'template1 is UTF8 encoded';
+is_program_out 'postgres', "echo \"select * from t\" | psql --cluster $newv/main -Atq latintest",
+ 0, ($newv >= 11 ? "A\324B\n": "AдB\n"), 'new latintest DB has correctly encoded string';
+# ASCII databases don't do automatic encoding conversion, so this remains LATIN
+is_program_out 'postgres', "echo \"select * from t\" | psql --cluster $newv/main -Atq asctest",
+ 0, "A\324B\n", 'new asctest DB has correctly encoded string';
+
+is ((system "pg_dropcluster --stop $newv main"), 0, 'dropping upgraded cluster');
+
+is ((system "pg_dropcluster $oldv main"), 0, 'dropping old cluster');
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/060_obsolete_confparams.t b/t/060_obsolete_confparams.t
new file mode 100644
index 0000000..c276431
--- /dev/null
+++ b/t/060_obsolete_confparams.t
@@ -0,0 +1,83 @@
+# Test upgrading from the oldest version to all majors with all possible
+# configuration parameters set. This checks that they are correctly
+# transitioned.
+
+use strict;
+
+use lib 't';
+use TestLib;
+
+use Test::More;
+
+if (@MAJORS == 1) {
+ pass 'only one major version installed, skipping upgrade tests';
+ done_testing();
+ exit 0;
+}
+
+$ENV{_SYSTEMCTL_SKIP_REDIRECT} = 1; # FIXME: testsuite is hanging otherwise
+
+# Test one particular upgrade (old version, new version)
+sub do_upgrade {
+ my $cur = $_[0];
+ my $new = $_[1];
+ note "Testing upgrade $cur -> $new";
+
+ # Upgrade cluster
+ like_program_out 0, "env LC_MESSAGES=C pg_upgradecluster -v $new $cur main", 0, qr/^Success. Please check/m;
+ like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/$new.*online/,
+ "New $new cluster is online";
+}
+
+# create cluster for oldest version
+is_program_out 0, "pg_createcluster $MAJORS[0] main >/dev/null", 0, "";
+
+# generate configuration file with all settings and start cluster
+is_program_out 0, "sed -i -e 's/^#\\([a-z]\\)/\\1/' /etc/postgresql/$MAJORS[0]/main/postgresql.conf",
+ 0, "", "Enabling all settings in /etc/postgresql/$MAJORS[0]/main/postgresql.conf";
+like PgCommon::get_conf_value($MAJORS[0], 'main', 'postgresql.conf', 'work_mem'), qr/MB/, "work_mem is set";
+
+# tweak invalid settings
+PgCommon::set_conf_value $MAJORS[0], 'main', 'postgresql.conf', 'log_timezone', 'UTC';
+PgCommon::set_conf_value $MAJORS[0], 'main', 'postgresql.conf', 'timezone', 'UTC';
+PgCommon::disable_conf_value $MAJORS[0], 'main', 'postgresql.conf', 'include_dir', "Disable placeholder value";
+PgCommon::disable_conf_value $MAJORS[0], 'main', 'postgresql.conf', 'include_if_exists', "Disable placeholder value";
+PgCommon::disable_conf_value $MAJORS[0], 'main', 'postgresql.conf', 'include', "Disable placeholder value";
+# older versions (<= 9.1 as of 2019-03) do not support ssl anymore
+my $postgres = PgCommon::get_program_path('postgres', $MAJORS[0]);
+my $ldd = `ldd $postgres 2>/dev/null`;
+if ($ldd and $ldd !~ /libssl/) {
+ is_program_out 0, "sed -i -e 's/^ssl/#ssl/' /etc/postgresql/$MAJORS[0]/main/postgresql.conf",
+ 0, "", "Disabling ssl settings on server that does not support SSL";
+}
+
+# start server
+is_program_out 0, "pg_ctlcluster $MAJORS[0] main start", 0, "";
+
+# Loop over all but the latest major version, testing N->N+1 upgrades
+for my $index (0 .. @MAJORS - 2) {
+ do_upgrade $MAJORS[$index], $MAJORS[$index + 1]
+}
+# remove all clusters except for the first one
+for my $index (1 .. @MAJORS - 1) {
+ is_program_out 0, "pg_dropcluster $MAJORS[$index] main --stop", 0, "", "Dropping $MAJORS[$index]/main";
+}
+
+# now test a direct upgrade from oldest to newest, to also catch parameters
+# which changed several times, like syslog -> redirect_stderr ->
+# logging_collector
+if ($#MAJORS > 1) {
+ is_program_out 0, "pg_ctlcluster $MAJORS[0] main start", 0, "";
+ do_upgrade $MAJORS[0], $MAJORS[-1];
+ is_program_out 0, "pg_dropcluster $MAJORS[-1] main --stop", 0, "", "Dropping $MAJORS[-1]/main";
+} else {
+ pass 'only two available versions, skipping tests...';
+}
+
+# remove first cluster
+is_program_out 0, "pg_dropcluster $MAJORS[0] main --stop", 0, "", "Dropping $MAJORS[0]/main";
+
+check_clean;
+done_testing();
+
+# vim: filetype=perl
diff --git a/t/070_non_postgres_clusters.t b/t/070_non_postgres_clusters.t
new file mode 100644
index 0000000..06d7f13
--- /dev/null
+++ b/t/070_non_postgres_clusters.t
@@ -0,0 +1,116 @@
+# Test successful operation of clusters which are not owned by
+# postgres. Only check the oldest and newest version.
+
+use strict;
+
+use lib 't';
+use TestLib;
+
+use Test::More tests => 40;
+
+$ENV{_SYSTEMCTL_SKIP_REDIRECT} = 1; # FIXME: testsuite is hanging otherwise
+
+my $owner = 'nobody';
+my $v = $MAJORS[0];
+
+# create cluster
+is ((system "pg_createcluster -u $owner $v main >/dev/null"), 0,
+ "pg_createcluster $v main for owner $owner");
+
+# check if start is refused when config and data owner do not match
+my $pgconf = "/etc/postgresql/$v/main/postgresql.conf";
+my ($origuid, $origgid) = (stat $pgconf)[4,5];
+chown 1, 1, $pgconf;
+like_program_out 0, "pg_ctlcluster $v main start", 1, qr/do not match/, "start refused when config and data owners mismatch";
+chown $origuid, $origgid, $pgconf;
+is ((system "pg_ctlcluster $v main start"), 0, "pg_ctlcluster succeeds with owner $owner");
+
+# Check cluster
+like_program_out $owner, 'pg_lsclusters -h', 0,
+ qr/^$v\s+main\s+5432\s+online\s+$owner/,
+ 'pg_lsclusters shows running cluster';
+
+like ((ps 'postgres'), qr/^$owner.*bin\/postgres .*\/var\/lib\/postgresql\/$v\/main/m,
+ "postgres is running as user $owner");
+
+is_program_out $owner, 'ls /tmp/.s.PGSQL.*', 0, "/tmp/.s.PGSQL.5432\n/tmp/.s.PGSQL.5432.lock\n", 'socket is in /tmp';
+
+ok_dir '/var/run/postgresql', [], '/var/run/postgresql is empty';
+
+# verify owner of configuration files
+my @st;
+my $confdir = "/etc/postgresql/$v/main";
+my ($owneruid, $ownergid) = (getpwnam $owner)[2,3];
+@st = stat $confdir;
+is $st[4], $owneruid, 'conf dir is owned by user';
+is $st[5], $ownergid, 'conf dir is owned by user\'s primary group';
+my ($ok_uid, $ok_gid) = (1, 1);
+opendir D, $confdir or die "opendir: $!";
+for my $f (readdir D) {
+ next if $f eq '.' or $f eq '..';
+ @st = stat "$confdir/$f" or die "stat: $!";
+ if ($st[4] != $owneruid) {
+ note "$f is not owned by user";
+ $ok_uid = 0;
+ }
+ if ($st[5] != $ownergid) {
+ note "$f is not owned by user's primary group";
+ $ok_gid = 0;
+ }
+}
+closedir D;
+is $ok_uid, 1, "files are owned by user";
+is $ok_gid, 1, "files are owned by user's primary group";
+
+# verify log file properties
+@st = stat "/var/log/postgresql/postgresql-$v-main.log";
+is $st[2], 0100640, 'log file has 0640 permissions';
+is $st[4], $owneruid, 'log file is owned by user';
+# the log file gid setting works on RedHat, but nobody has gid 99 there (and
+# there's not good alternative for testing)
+my $loggid = $PgCommon::rpm ? (getgrnam 'adm')[2] : $ownergid;
+is $st[5], $loggid, 'log file is owned by user\'s primary group';
+
+if ($#MAJORS > 0) {
+ my $newv = $MAJORS[-1];
+
+ my $outref;
+ is ((exec_as 0, "(pg_upgradecluster -v $newv $v main | sed -e 's/^/STDOUT: /')", $outref, 0), 0,
+ 'pg_upgradecluster succeeds');
+ like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+ like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+ my @err = grep (!/^STDOUT: /, split (/\n/, $$outref));
+ if (@err) {
+ fail 'no error messages during upgrade';
+ print (join ("\n", @err));
+ } else {
+ pass "no error messages during upgrade";
+ }
+
+ # verify file permissions
+ @st = stat "/etc/postgresql/$newv/main";
+ is $st[4], $owneruid, 'upgraded conf dir is owned by user';
+ is $st[5], $ownergid, 'upgraded conf dir is owned by user\'s primary group';
+ @st = stat "/etc/postgresql/$newv/main/postgresql.conf";
+ is $st[4], $owneruid, 'upgraded postgresql.conf dir is owned by user';
+ is $st[5], $ownergid, 'upgraded postgresql.conf dir is owned by user\'s primary group';
+ @st = stat "/var/log/postgresql/postgresql-$v-main.log";
+ is $st[4], $owneruid, 'upgraded log file is owned by user';
+ is $st[5], $loggid, 'upgraded log file is owned by user\'s primary group';
+
+ is ((system "pg_dropcluster $newv main --stop"), 0, 'pg_dropcluster');
+} else {
+ pass 'only one major version installed, skipping upgrade test';
+ for (my $i = 0; $i < 10; ++$i) {
+ pass '...';
+ }
+}
+
+# Check proper cleanup
+is ((system "pg_dropcluster $v main --stop"), 0, 'pg_dropcluster');
+is_program_out $owner, 'pg_lsclusters -h', 0, '', 'No clusters left';
+is ((ps 'postgres'), '', "No postgres processes left");
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/080_start.conf.t b/t/080_start.conf.t
new file mode 100644
index 0000000..2e4d4cf
--- /dev/null
+++ b/t/080_start.conf.t
@@ -0,0 +1,145 @@
+# Check start.conf handling.
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => 73;
+
+my $systemd = -d '/run/systemd/system';
+
+# Do test with oldest version
+my $v = $MAJORS[0];
+
+# create cluster
+is ((system "pg_createcluster $v main >/dev/null"), 0, "pg_createcluster $v main");
+
+# Check that we start with 'auto'
+note "start.conf auto";
+is ((get_cluster_start_conf $v, 'main'), 'auto',
+ 'get_cluster_start_conf returns auto');
+is_program_out 'nobody', "grep '^[^\\s#]' /etc/postgresql/$v/main/start.conf",
+ 0, "auto\n", 'start.conf contains auto';
+SKIP: {
+ skip 'not running under systemd', 2 unless ($systemd);
+ ok_dir '/run/systemd/generator/postgresql.service.wants',
+ ["postgresql\@$v-main.service"],
+ "systemd generator links cluster";
+ is ((readlink "/run/systemd/generator/postgresql.service.wants/postgresql\@$v-main.service"),
+ "/lib/systemd/system/postgresql@.service",
+ "systemd generator links correct service file");
+}
+
+# init script should handle auto cluster
+like_program_out 0, "/etc/init.d/postgresql start $v", 0, qr/Start.*($v|systemctl)/;
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/online/, 'cluster is online';
+like_program_out 0, "/etc/init.d/postgresql stop $v", 0, qr/Stop.*($v|systemctl)/;
+sleep 3 if ($systemd); # FIXME: systemctl stop postgresql is not yet synchronous (#759725)
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/down/, 'cluster is down';
+
+# change to manual, verify start.conf contents
+note "start.conf manual";
+set_cluster_start_conf $v, 'main', 'manual';
+
+is ((get_cluster_start_conf $v, 'main'), 'manual',
+ 'get_cluster_start_conf returns manual');
+is_program_out 'nobody', "grep '^[^\\s#]' /etc/postgresql/$v/main/start.conf",
+ 0, "manual\n", 'start.conf contains manual';
+SKIP: {
+ skip 'not running under systemd', 1 unless ($systemd);
+ system "systemctl daemon-reload";
+ ok_dir '/run/systemd/generator/postgresql.service.wants',
+ [], "systemd generator doesn't link cluster";
+}
+
+# init script should not handle manual cluster ...
+like_program_out 0, "/etc/init.d/postgresql start $v", 0, qr/Start.*($v|systemctl)/;
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/down/, 'cluster is down';
+
+# pg_ctlcluster should handle manual cluster
+is_program_out 'postgres', "pg_ctlcluster $v main start", 0, '';
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/online/, 'cluster is online';
+is_program_out 'postgres', "pg_ctlcluster $v main stop", 0, '';
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/down/, 'cluster is down';
+
+# change to disabled, verify start.conf contents
+note "start.conf disabled";
+set_cluster_start_conf $v, 'main', 'disabled';
+
+is ((get_cluster_start_conf $v, 'main'), 'disabled',
+ 'get_cluster_start_conf returns disabled');
+SKIP: {
+ skip 'not running under systemd', 1 unless ($systemd);
+ system "systemctl daemon-reload";
+ ok_dir '/run/systemd/generator/postgresql.service.wants',
+ [], "systemd generator doesn't link cluster";
+}
+
+# init script should not handle disabled cluster
+like_program_out 0, "/etc/init.d/postgresql start $v", 0, qr/Start.*($v|systemctl)/;
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/down/, 'cluster is down';
+
+# pg_ctlcluster should not start disabled cluster
+is_program_out 'postgres', "pg_ctlcluster $v main start", 1,
+ "Error: Cluster is disabled\n";
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/down/, 'cluster is down';
+
+# change back to manual, start cluster
+set_cluster_start_conf $v, 'main', 'manual';
+is_program_out 'postgres', "pg_ctlcluster $v main start", 0, '';
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/online/, 'cluster is online';
+
+# however, we want to stop disabled clusters
+set_cluster_start_conf $v, 'main', 'disabled';
+is_program_out 'postgres', "pg_ctlcluster $v main stop", 0, '';
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/down/, 'cluster is down';
+
+# set back to manual
+set_cluster_start_conf $v, 'main', 'manual';
+is_program_out 'postgres', "pg_ctlcluster $v main start", 0, '';
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/online/, 'cluster is online';
+
+# upgrade cluster
+note "test upgrade";
+if ($#MAJORS == 0) {
+ pass 'only one major version installed, skipping upgrade test';
+ pass '...';
+} else {
+ like_program_out 0, "pg_upgradecluster -v $MAJORS[-1] $v main", 0, qr/Success. Please check/;
+}
+
+# check start.conf of old and upgraded cluster
+is ((get_cluster_start_conf $v, 'main'), 'manual',
+ 'get_cluster_start_conf for old cluster returns manual');
+is ((get_cluster_start_conf $MAJORS[-1], 'main'), 'manual',
+ 'get_cluster_start_conf for new cluster returns manual');
+
+# clean up
+if ($#MAJORS == 0) {
+ pass '...';
+} else {
+ is ((system "pg_dropcluster $v main"), 0,
+ 'dropping old cluster');
+}
+
+is ((system "pg_dropcluster $MAJORS[-1] main --stop"), 0,
+ 'dropping upgraded cluster');
+
+is_program_out 'postgres', 'pg_lsclusters -h', 0, '', 'no clusters any more';
+
+# create cluster with --start-conf option
+is_program_out 0, "pg_createcluster $v main --start-conf foo", 1,
+ "Error: Invalid --start-conf value: foo\n",
+ 'pg_createcluster checks --start-conf validity';
+is ((system "pg_createcluster $v main --start-conf manual >/dev/null"), 0,
+ 'pg_createcluster checks --start-conf manual');
+is ((get_cluster_start_conf $v, 'main'), 'manual',
+ 'get_cluster_start_conf returns manual');
+is ((system "pg_dropcluster $v main"), 0,
+ 'dropping cluster');
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/085_pg_ctl.conf.t b/t/085_pg_ctl.conf.t
new file mode 100644
index 0000000..dca2700
--- /dev/null
+++ b/t/085_pg_ctl.conf.t
@@ -0,0 +1,51 @@
+# Check pg_ctl.conf handling.
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => $MAJORS[-1] >= '8.3' ? 33 : 1;
+
+# Do test with newest version
+my $v = $MAJORS[-1];
+if ($v < '8.3') {
+ pass 'Skipping core limit tests for versions < 8.3';
+ exit 0;
+}
+
+# enable core dumps
+# sudo and salsa-ci set the hard limit to 0 by default, undo that
+is_program_out 0, "prlimit --core=0:unlimited --pid=$$", 0, '', "set core file size to unlimited";
+is_program_out 'postgres', "sh -c 'ulimit -Hc'", 0, "unlimited\n", "core file size is unlimited";
+
+# create cluster
+is ((system "pg_createcluster $v main >/dev/null"), 0, "pg_createcluster $v main");
+ok (-f "/etc/postgresql/$v/main/pg_ctl.conf", "/etc/postgresql/$v/main/pg_ctl.conf exists");
+
+# Default behaviour, core size=0
+is_program_out 0, "pg_ctlcluster $v main start", 0, '', "starting cluster as root";
+is_program_out 'postgres', "xargs -i awk '/core/ {print \$5}' /proc/{}/limits < /var/run/postgresql/$v-main.pid", 0, "0\n", "soft core size is 0";
+my $hard_limit = `xargs -i awk '/core/ {print \$6}' /proc/{}/limits < /var/run/postgresql/$v-main.pid`;
+chomp $hard_limit;
+note "hard core file size limit of root-started postgres process is $hard_limit";
+
+# -c in pg_ctl.conf, core size=unlimited
+ok (set_cluster_pg_ctl_conf($v, 'main', '-c'), "set pg_ctl default option to -c");
+is_program_out 0, "pg_ctlcluster $v main restart", 0, '', "restarting cluster as root";
+is_program_out 'postgres', "xargs -i awk '/core/ {print \$5}' /proc/{}/limits < /var/run/postgresql/$v-main.pid", 0, "$hard_limit\n", "soft core size is $hard_limit";
+
+# Back to default behaviour, core size=0
+is_program_out 0, "pg_ctlcluster $v main stop", 0, '', "stopping cluster";
+ok (set_cluster_pg_ctl_conf($v, 'main', ''), "restored pg_ctl default option");
+
+# pg_ctl -c, core size=unlimited
+is_program_out 'postgres', "pg_ctlcluster $v main start -- -c", 0, '', "starting cluster with -c on the command line as postgres";
+is_program_out 'postgres', "xargs -i awk '/core/ {print \$5}' /proc/{}/limits < /var/run/postgresql/$v-main.pid", 0, "unlimited\n", "soft core size is unlimited";
+is_program_out 'postgres', "pg_ctlcluster $v main stop", 0, '', "stopping cluster";
+
+is ((system "pg_dropcluster $v main --stop"), 0, 'dropping cluster');
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/090_multicluster.t b/t/090_multicluster.t
new file mode 100644
index 0000000..8322ace
--- /dev/null
+++ b/t/090_multicluster.t
@@ -0,0 +1,286 @@
+# Check operation with multiple clusters
+
+use strict;
+
+use lib 't';
+use TestLib;
+use Socket;
+use PgCommon;
+
+use Test::More tests => 125;
+
+# create fake socket at 5433 to verify that this port is skipped
+socket (SOCK, PF_INET, SOCK_STREAM, getprotobyname('tcp')) or die "socket: $!";
+setsockopt(SOCK, Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1) or die "setsockopt: $!";
+bind (SOCK, sockaddr_in(5433, INADDR_ANY)) || die "bind: $! ";
+listen (SOCK, 0) or die "listen: $!";
+
+# create clusters
+is ((system "pg_createcluster $MAJORS[0] old -- -A trust >/dev/null"), 0, "pg_createcluster $MAJORS[0] old");
+is ((system "pg_createcluster $MAJORS[-1] new1 -- -A trust >/dev/null"), 0, "pg_createcluster $MAJORS[-1] new1");
+is ((system "pg_createcluster $MAJORS[-1] new2 -p 5440 -- -A trust >/dev/null"), 0, "pg_createcluster $MAJORS[-1] new2");
+close SOCK;
+
+my $old = "$MAJORS[0]/old";
+my $new1 = "$MAJORS[-1]/new1";
+my $new2 = "$MAJORS[-1]/new2";
+
+is ((system "pg_ctlcluster $MAJORS[0] old start >/dev/null"), 0, "starting cluster $old");
+is ((system "pg_ctlcluster $MAJORS[-1] new1 start >/dev/null"), 0, "starting cluster $new1");
+is ((system "pg_ctlcluster $MAJORS[-1] new2 start >/dev/null"), 0, "starting cluster $new2");
+
+like_program_out 'postgres', 'pg_lsclusters -h | sort -k3', 0, qr/.*5432.*5434.*5440.*/s,
+ 'clusters have the correct ports, skipping used 5433';
+
+# move user_clusters aside for the test; this will ensure that client programs
+# work correctly without any file at all
+if (-f '/etc/postgresql-common/user_clusters') {
+ ok ((rename '/etc/postgresql-common/user_clusters',
+ '/etc/postgresql-common/user_clusters.psqltestsuite'),
+ 'Temporarily moving away /etc/postgresql-common/user_clusters');
+} else {
+ pass '/etc/postgresql-common/user_clusters does not exist';
+}
+
+# check basic cluster selection
+like_program_out 0, 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[0]/,
+ 'pg_wrapper selects port 5432 as default cluster';
+like_program_out 0, "createdb --cluster $new1 --version", 0,
+ qr/^createdb \(PostgreSQL\) $MAJORS[-1]/,
+ 'pg_wrapper --cluster works';
+like_program_out 0, "createdb --cluster $MAJORS[-1]/foo --version", 1,
+ qr/Cluster .* does not exist/,
+ 'pg_wrapper --cluster errors out for invalid cluster';
+
+# create a database in new1 and check that it doesn't appear in new2
+is_program_out 'postgres', "createdb --cluster $new1 test", 0, ($MAJORS[-1] < 8.3 ? "CREATE DATABASE\n" : '');
+like_program_out 'postgres', "psql -Atl --cluster $new1", 0,
+ qr/test\|postgres\|/,
+ 'test db appears in cluster new1';
+unlike_program_out 'postgres', "psql -Atl --cluster $new2", 0,
+ qr/test\|postgres\|/,
+ 'test db does not appear in cluster new2';
+unlike_program_out 'postgres', "psql -Atl", 0, qr/test\|postgres\|/,
+ 'test db does not appear in default cluster';
+
+# check network cluster selection
+is_program_out 'postgres', "psql --cluster $MAJORS[0]/127.0.0.1: -Atc 'show port' template1", 0, "5432\n",
+ "psql --cluster $MAJORS[0]/127.0.0.1: defaults to port 5432";
+like_program_out 'postgres', "createdb --cluster $MAJORS[-1]/127.0.0.1:5432 --version", 0,
+ qr/^createdb \(PostgreSQL\) $MAJORS[-1]/,
+ "createdb --cluster $MAJORS[-1]/127.0.0.1:5432 uses latest client version";
+like_program_out 'postgres', "psql -Atl --cluster $MAJORS[-1]/localhost:5434", 0,
+ qr/test\|postgres\|/, "test db appears in cluster $MAJORS[-1]/localhost:5434";
+unlike_program_out 'postgres', "psql -Atl --cluster $MAJORS[-1]/localhost:5440", 0,
+ qr/test\|postgres\|/, "test db does not appear in cluster $MAJORS[-1]/localhost:5440";
+
+# check some erroneous cluster specifications
+like_program_out 'postgres', "LC_MESSAGES=C psql -Atl --cluster $MAJORS[-1]/localhost:5435", 2,
+ qr/could not connect|connection to server .* failed/, "psql --cluster $MAJORS[-1]/localhost:5435 fails due to nonexisting port";
+like_program_out 'postgres', "LC_MESSAGES=C psql -Atl --cluster $MAJORS[-1]/localhost:a", 1,
+ qr/Cluster .* does not exist/, "psql --cluster $MAJORS[-1]/localhost:a fails due to invalid syntax";
+like_program_out 'postgres', "LC_MESSAGES=C psql -Atl --cluster $MAJORS[-1]/doesnotexi.st", 1,
+ qr/Cluster .* does not exist/, "psql --cluster $MAJORS[-1]/doesnotexi.st fails due to invalid syntax";
+like_program_out 'postgres', "psql -Atl --cluster 6.4/localhost:", 1,
+ qr/Invalid version/, "psql --cluster 6.4/localhost: fails due to invalid version";
+
+# check that environment variables work
+$ENV{'PGCLUSTER'} = $new1;
+like_program_out 'postgres', "psql -Atl", 0, qr/test\|postgres\|/,
+ 'PGCLUSTER selection (1)';
+$ENV{'PGCLUSTER'} = $new2;
+unlike_program_out 'postgres', "psql -Atl", 0, qr/test\|postgres\|/,
+ 'PGCLUSTER selection (2)';
+$ENV{'PGCLUSTER'} = 'foo';
+like_program_out 'postgres', "psql -l", 1,
+ qr/Invalid version .* specified in PGCLUSTER/,
+ 'invalid PGCLUSTER value';
+$ENV{'PGCLUSTER'} = "$MAJORS[-1]/127.0.0.1:";
+like_program_out 0, 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[-1]/,
+ 'PGCLUSTER network cluster selection (1)';
+$ENV{'PGCLUSTER'} = "$MAJORS[-1]/localhost:5434";
+like_program_out 'postgres', 'psql -Atl', 0,
+ qr/test\|postgres\|/, 'PGCLUSTER network cluster selection (2)';
+$ENV{'PGCLUSTER'} = "$MAJORS[-1]/localhost:5440";
+unlike_program_out 'postgres', 'psql -Atl', 0,
+ qr/test\|postgres\|/, 'PGCLUSTER network cluster selection (3)';
+$ENV{'PGCLUSTER'} = "$MAJORS[-1]/localhost:5435";
+like_program_out 'postgres', 'LC_MESSAGES=C psql -Atl', 2,
+ qr/could not connect|connection to server .* failed/, "psql --cluster $MAJORS[-1]/localhost:5435 fails due to nonexisting port";
+delete $ENV{'PGCLUSTER'};
+
+# check that PGPORT works
+$ENV{'PGPORT'} = '5434';
+is_program_out 'postgres', 'psql -Atc "show port" template1', 0, "5434\n",
+ 'PGPORT selection (1)';
+$ENV{'PGPORT'} = '5432';
+is_program_out 'postgres', 'psql -Atc "show port" template1', 0, "5432\n",
+ 'PGPORT selection (2)';
+$ENV{'PGCLUSTER'} = $new2;
+delete $ENV{'PGPORT'};
+$ENV{'PGPORT'} = '5432';
+like_program_out 'postgres', 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[-1]/,
+ 'PGPORT+PGCLUSTER, PGCLUSTER selects version';
+is_program_out 'postgres', 'psql -Atc "show port" template1', 0, "5432\n",
+ 'PGPORT+PGCLUSTER, PGPORT selects port';
+delete $ENV{'PGPORT'};
+delete $ENV{'PGCLUSTER'};
+
+# check that PGDATABASE works
+$ENV{'PGDATABASE'} = 'test';
+is_program_out 'postgres', "psql --cluster $new1 -Atc 'select current_database()'", 0, "test\n",
+ 'PGDATABASE environment variable works';
+delete $ENV{'PGDATABASE'};
+
+# check cluster selection with an empty user_clusters
+open F, '>/etc/postgresql-common/user_clusters' or die "Could not create user_clusters: $!";
+close F;
+chmod 0644, '/etc/postgresql-common/user_clusters';
+like_program_out 0, 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[0]/,
+ 'pg_wrapper selects port 5432 as default cluster with empty user_clusters';
+like_program_out 0, "createdb --cluster $new1 --version", 0,
+ qr/^createdb \(PostgreSQL\) $MAJORS[-1]/,
+ 'pg_wrapper --cluster works with empty user_clusters';
+
+# check default cluster selection with user_clusters
+open F, '>/etc/postgresql-common/user_clusters' or die "Could not create user_clusters: $!";
+print F "* * $MAJORS[-1] new1 *\n";
+close F;
+chmod 0644, '/etc/postgresql-common/user_clusters';
+like_program_out 'postgres', 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[-1]/,
+ "pg_wrapper selects correct cluster with user_clusters '* * $MAJORS[-1] new1 *'";
+
+# check default database selection with user_clusters
+open F, '>/etc/postgresql-common/user_clusters' or die "Could not create user_clusters: $!";
+print F "* * $MAJORS[-1] new1 test\n";
+close F;
+chmod 0644, '/etc/postgresql-common/user_clusters';
+is_program_out 'postgres', 'psql -Atc "select current_database()"', 0, "test\n",
+ "pg_wrapper selects correct database with user_clusters '* * $MAJORS[-1] new1 test'";
+$ENV{'PGDATABASE'} = 'template1';
+is_program_out 'postgres', "psql -Atc 'select current_database()'", 0, "template1\n",
+ 'PGDATABASE environment variable is not overridden by user_clusters';
+delete $ENV{'PGDATABASE'};
+
+# check by-user cluster selection with user_clusters
+# (also check invalid cluster reporting)
+open F, '>/etc/postgresql-common/user_clusters' or die "Could not create user_clusters: $!";
+print F "postgres * $MAJORS[-1] new1 *\nnobody * $MAJORS[0] old *\n* * 5.5 * *";
+close F;
+chmod 0644, '/etc/postgresql-common/user_clusters';
+like_program_out 'postgres', 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[-1]/,
+ 'pg_wrapper selects correct cluster with per-user user_clusters';
+like_program_out 'nobody', 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[0]/,
+ 'pg_wrapper selects correct cluster with per-user user_clusters';
+like_program_out 0, 'createdb --version', 0, qr/user_clusters.*line 3.*version.*not exist/i,
+ 'pg_wrapper warning for invalid per-user user_clusters line';
+
+# check by-user network cluster selection with user_clusters
+# (also check invalid cluster reporting)
+open F, '>/etc/postgresql-common/user_clusters' or die "Could not create user_clusters: $!";
+print F "postgres * $MAJORS[0] localhost: *\nnobody * $MAJORS[-1] new1 *\n* * $MAJORS[-1] localhost:a *";
+close F;
+chmod 0644, '/etc/postgresql-common/user_clusters';
+like_program_out 'postgres', 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[0]/,
+ 'pg_wrapper selects correct version with per-user user_clusters';
+like_program_out 'nobody', 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[-1]/,
+ 'pg_wrapper selects correct version with per-user user_clusters';
+like_program_out 0, 'createdb --version', 0, qr/user_clusters.*line 3.*cluster.*not exist/i,
+ 'pg_wrapper warning for invalid per-user user_clusters line';
+# check PGHOST environment variable precedence
+$ENV{'PGHOST'} = '127.0.0.2';
+like_program_out 'postgres', 'psql -Atl', 2, qr/127.0.0.2/, '$PGHOST overrides user_clusters';
+is_program_out 'postgres', "psql --cluster $MAJORS[-1]/localhost:5434 -Atc 'select current_database()' test",
+ 0, "test\n", '--cluster overrides $PGHOST';
+delete $ENV{'PGHOST'};
+
+# check invalid user_clusters
+open F, '>/etc/postgresql-common/user_clusters' or die "Could not create user_clusters: $!";
+print F 'foo';
+close F;
+chmod 0644, '/etc/postgresql-common/user_clusters';
+like_program_out 'postgres', 'createdb --version', 0, qr/ignoring invalid line 1/,
+ 'pg_wrapper ignores invalid lines in user_clusters';
+
+# remove test user_clusters
+unlink '/etc/postgresql-common/user_clusters' or die
+ "unlink user_clusters: $!";
+
+# check that pg_service.conf works
+open F, '>/etc/postgresql-common/pg_service.conf' or die "Could not create pg_service.conf: $!";
+print F "[old_t1]
+user=postgres
+dbname=template1
+port=5432
+
+[new1_test]
+user=postgres
+dbname=test
+port=5434
+
+# these do not exist
+[new2_test]
+user=postgres
+dbname=test
+port=5440
+";
+close F;
+chmod 0644, '/etc/postgresql-common/pg_service.conf';
+
+$ENV{'PGSERVICE'} = 'old_t1';
+is_program_out 'postgres', "psql -Atc 'select current_database()'", 0,
+ "template1\n", 'pg_service conf selection 1';
+$ENV{'PGSERVICE'} = 'new1_test';
+is_program_out 'postgres', "psql -Atc 'select current_database()'", 0,
+ "test\n", 'pg_service conf selection 2';
+$ENV{'PGSERVICE'} = 'new2_test';
+like_program_out 'postgres', "psql -Atc 'select current_database()'", 2,
+ qr/FATAL.*test/, 'pg_service conf selection 3';
+delete $ENV{'PGSERVICE'};
+unlink '/etc/postgresql-common/pg_service.conf';
+
+# check proper error message if no cluster could be determined as default for
+# pg_wrapper
+is ((system "pg_ctlcluster $MAJORS[0] old stop >/dev/null"), 0, "stopping cluster $old");
+PgCommon::set_conf_value $MAJORS[0], 'old', 'postgresql.conf', 'port', '5435';
+is ((system "pg_ctlcluster $MAJORS[0] old start >/dev/null"), 0, "restarting cluster $old");
+like_program_out 'postgres', 'pg_lsclusters -h | sort -k3', 0, qr/.*5434.*5435.*5440.*/s,
+ 'port of first cluster was successfully changed';
+like_program_out 'postgres', "psql -l", 2,
+ qr/no.*default.*man pg_wrapper.*psql:.*\.s\.PGSQL.5432/is,
+ 'proper pg_wrapper warning and psql error if no cluster is suitable as default target';
+like_program_out 'postgres', "psql -Atl --cluster $new1", 0,
+ qr/test\|postgres\|/,
+ '--cluster selects appropriate cluster';
+like_program_out 'postgres', "psql -Atl -p 5434", 0,
+ qr/test\|postgres\|/,
+ '-p selects appropriate cluster';
+like_program_out 'postgres', "psql -Atlp 5434", 0,
+ qr/test\|postgres\|/,
+ '-Atlp selects appropriate cluster';
+like_program_out 'postgres', "psql -Atl --port 5434", 0,
+ qr/test\|postgres\|/,
+ '--port selects appropriate cluster';
+like_program_out 'postgres', "env PGPORT=5434 psql -Atl", 0,
+ qr/test\|postgres\|/,
+ '$PGPORT selects appropriate cluster';
+
+# but specifying -p explicitly should work
+
+# restore original user_clusters
+if (-f '/etc/postgresql-common/user_clusters.psqltestsuite') {
+ ok ((rename '/etc/postgresql-common/user_clusters.psqltestsuite',
+ '/etc/postgresql-common/user_clusters'),
+ 'Restoring original /etc/postgresql-common/user_clusters');
+} else {
+ pass '/etc/postgresql-common/user_clusters did not exist, not restoring';
+}
+
+# clean up
+is ((system "pg_dropcluster $MAJORS[-1] new1 --stop"), 0, "dropping $new1");
+is ((system "pg_dropcluster $MAJORS[-1] new2 --stop"), 0, "dropping $new2");
+is ((system "pg_dropcluster $MAJORS[0] old --stop"), 0, "dropping $old");
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/110_integrate_cluster.t b/t/110_integrate_cluster.t
new file mode 100644
index 0000000..4201a58
--- /dev/null
+++ b/t/110_integrate_cluster.t
@@ -0,0 +1,44 @@
+# Check integration of an already existing cluster
+
+use strict;
+
+use lib 't';
+use TestLib;
+use File::Temp qw/tempdir/;
+use Time::HiRes qw(usleep);
+
+my $version = $MAJORS[-1];
+
+use Test::More tests => 32;
+use PgCommon;
+
+delete $ENV{'LANG'};
+delete $ENV{'LANGUAGE'};
+$ENV{'LC_ALL'} = 'C';
+
+my $wdir = tempdir (CLEANUP => 1);
+chmod 0755, $wdir or die "Could not chmod $wdir: $!";
+
+# create clusters for different owners and check their integration
+for my $o ('postgres', 'nobody') {
+ my $cdir = "$wdir/c";
+ mkdir $cdir;
+ my $oid = getpwnam $o;
+ chown $oid, 0, $cdir or die "Could not chown $cdir to $oid: $!";
+ like_program_out $o, "$PgCommon::binroot$version/bin/initdb $cdir/$o",
+ 0, qr/Success/, "creating raw initdb cluster for user $o";
+ like_program_out 0, "pg_createcluster $version $o -d $cdir/$o", 0,
+ qr/Configuring already existing cluster/i, "integrating $o cluster";
+ like_program_out 0, "pg_lsclusters", 0,
+ qr/$version\s+$o\s+5432\s+down\s+$o\s/, 'correct pg_lsclusters output';
+ is_program_out $o, "pg_ctlcluster $version $o start", 0, '', "starting cluster $o";
+ like_program_out 0, "pg_lsclusters", 0,
+ qr/$version\s+$o\s+5432\s+online\s+$o\s/, 'correct pg_lsclusters output';
+ is ((system "pg_dropcluster $version $o --stop"), 0, "dropping cluster $o");
+ ok_dir $cdir, [], 'No files in temporary cluster dir left behind';
+ rmdir $cdir;
+}
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/120_pg_upgradecluster_scripts.t b/t/120_pg_upgradecluster_scripts.t
new file mode 100644
index 0000000..d31e52c
--- /dev/null
+++ b/t/120_pg_upgradecluster_scripts.t
@@ -0,0 +1,114 @@
+# Check /etc/p-c/pg_upgradecluster.d/ scripts and proper handling of already
+# existing tables in the target cluster.
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => (@MAJORS == 1) ? 1 : 31;
+
+if (@MAJORS == 1) {
+ pass 'only one major version installed, skipping upgrade tests';
+ exit 0;
+}
+
+
+# create old cluster
+is ((system "pg_createcluster $MAJORS[0] main --start >/dev/null"), 0, "pg_createcluster $MAJORS[0] main");
+
+# add data table, auxtable with 'old...' values, and an unrelated auxtable in
+# another schema
+is_program_out 'postgres',
+ 'psql template1 -qc "create table auxdata (x varchar(10)); insert into auxdata values (\'old1\'); insert into auxdata values (\'old2\')"',
+ 0, '', 'adding auxdata to template1 and fill in some "old..." values';
+is_program_out 'postgres', "createdb test", 0, '';
+is_program_out 'postgres', 'psql test -qc "create table userdata(x int); insert into userdata values(42); insert into userdata values(256)"',
+ 0, '', 'creating userdata table';
+is_program_out 'postgres',
+ 'psql test -qc "create schema s; create table s.auxdata (x varchar(10)); insert into s.auxdata values (\'schema1\')"',
+ 0, '', 'adding schema s and s.auxdata to test and fill in some values';
+
+if (not -d '/etc/postgresql-common/pg_upgradecluster.d') {
+ mkdir '/etc/postgresql-common/pg_upgradecluster.d' or die "mkdir: $!";
+}
+
+# move existing files away
+for my $f (glob("/etc/postgresql-common/pg_upgradecluster.d/*")) {
+ next if ($f =~ /\.disabled$/);
+ rename $f, "$f.disabled";
+}
+
+# create test scripts
+chmod 0755, '/etc/postgresql-common/pg_upgradecluster.d' or die "chmod: $!";
+open F, '>/etc/postgresql-common/pg_upgradecluster.d/auxdata' or die "open: $!";
+print F <<EOS;
+#!/bin/sh -e
+# Arguments: <old version> <cluster name> <new version> <phase>
+oldver=\$1
+cluster=\$2
+newver=\$3
+phase=\$4
+
+if [ \$phase = init ]; then
+ createdb --cluster \$newver/\$cluster idb
+fi
+
+if [ \$phase = finish ]; then
+ psql --cluster \$newver/\$cluster template1 <<EOF
+drop table if exists auxdata;
+create table auxdata (x varchar(10));
+insert into auxdata values ('new1');
+insert into auxdata values ('new2');
+EOF
+fi
+
+EOS
+close F;
+chmod 0755, '/etc/postgresql-common/pg_upgradecluster.d/auxdata' or die "chmod: $!";
+
+open F, '>/etc/postgresql-common/pg_upgradecluster.d/badscript' or die "open: $!";
+print F <<EOS;
+#!/bin/false
+EOS
+close F;
+chmod 0755, '/etc/postgresql-common/pg_upgradecluster.d/badscript' or die "chmod: $!";
+
+# upgrade cluster
+my $outref;
+is ((exec_as 0, "pg_upgradecluster -v $MAJORS[-1] $MAJORS[0] main", $outref, 1), 1, 'pg_upgradecluster fails with bad script');
+like $$outref, qr/error|fail/i, 'server error messages during upgrade';
+unlink '/etc/postgresql-common/pg_upgradecluster.d/badscript';
+
+is ((exec_as 0, "pg_upgradecluster -v $MAJORS[-1] $MAJORS[0] main", $outref, 0), 0, 'pg_upgradecluster succeeds');
+unlike $$outref, qr/error|fail/i, 'no server error messages during upgrade';
+like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+
+is ((system "pg_dropcluster $MAJORS[0] main --stop"), 0, 'Dropping old cluster');
+
+# check new version cluster
+is_program_out 'postgres', 'psql template1 -Atc "select * from auxdata order by x"', 0,
+ "new1\nnew2\n", 'new cluster\'s template1/auxdata table is the script\'s version';
+
+like_program_out 'postgres', 'psql -Atl', 0, qr/^idb\b.*^test\b/ms,
+ 'upgraded cluster has idb and test databases';
+
+is_program_out 'postgres', 'psql test -Atc "select * from s.auxdata"', 0,
+ "schema1\n", 'new cluster\'s test/auxdata table in schema s was upgraded normally';
+
+# remove test script
+unlink '/etc/postgresql-common/pg_upgradecluster.d/auxdata' or die "unlink: $!";
+
+# restore original contents
+for my $f (glob("/etc/postgresql-common/pg_upgradecluster.d/*.disabled")) {
+ my $f2 = $f;
+ $f2 =~ s/\.disabled$//;
+ rename $f, $f2;
+}
+
+# clean up
+is ((system "pg_dropcluster $MAJORS[-1] main --stop"), 0, "pg_dropcluster $MAJORS[-1] main");
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/130_nonroot_admin.t b/t/130_nonroot_admin.t
new file mode 100644
index 0000000..5375b73
--- /dev/null
+++ b/t/130_nonroot_admin.t
@@ -0,0 +1,50 @@
+# Check that cluster administration works as non-root if the invoker has
+# sufficient permissions on directories.
+
+use strict;
+
+use lib 't';
+use TestLib;
+
+my $version = $MAJORS[-1];
+my $oldversion = $MAJORS[0];
+
+use Test::More tests => 22;
+use PgCommon;
+
+my $testuser = 'postgres';
+
+# pg_createcluster and pg_ctlcluster
+is ((exec_as $testuser, "pg_createcluster $version main --start"), 0,
+ "pg_createcluster succeeds as user $testuser with appropriate owner permissions");
+
+like_program_out $testuser, 'pg_lsclusters -h', 0, qr/^$version\s+main.*online/m;
+like_program_out 'postgres', 'psql -Atl', 0, qr/template1.*UTF8/;
+
+# pg_dropcluster
+is ((exec_as $testuser, "pg_dropcluster $version main --stop"), 0,
+ "pg_dropcluster succeeds as user $testuser with appropriate directory owner permissions");
+
+# pg_upgradecluster
+SKIP: {
+ skip 'Only one major version installed, skipping pg_upgradecluster tests', 8 if ($oldversion eq $version);
+
+ is ((exec_as $testuser, "pg_createcluster $oldversion main --start"), 0,
+ "pg_createcluster succeeds as user $testuser with appropriate group permissions");
+ my $outref;
+ is ((exec_as $testuser, "pg_upgradecluster -v $version $oldversion main", $outref, 0), 0,
+ "pg_upgradecluster succeeds as user $testuser");
+ like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+ like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+
+ like_program_out $testuser, 'pg_lsclusters -h', 0,
+ qr/^$oldversion\s+main.*down.*\n^$version\s+main.*online/m;
+
+ # clean up
+ is ((exec_as $testuser, "pg_dropcluster $oldversion main"), 0);
+ is ((exec_as $testuser, "pg_dropcluster $version main --stop"), 0);
+}
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/135_pg_virtualenv.t b/t/135_pg_virtualenv.t
new file mode 100644
index 0000000..1662e5b
--- /dev/null
+++ b/t/135_pg_virtualenv.t
@@ -0,0 +1,35 @@
+# check if pg_virtualenv runs ok, even under fakeroot
+
+use strict;
+use warnings;
+
+use lib 't';
+use TestLib;
+
+use Test::More tests => 12 * @MAJORS + 8;
+
+foreach my $v (@MAJORS) {
+ my $args = 'sh -c \'echo "id|$(id -un)"; psql -AtXxc "SELECT current_user"\'';
+ my $virtualenv = "pg_virtualenv -v $v $args";
+
+ $ENV{USER} = 'root';
+ like_program_out 'root', $virtualenv, 0, qr!id.root\ncurrent_user.postgres!, "running pg_virtualenv as root";
+ $ENV{USER} = 'postgres';
+ like_program_out 'postgres', $virtualenv, 0, qr!id.postgres\ncurrent_user.postgres!, "running pg_virtualenv as postgres";
+ $ENV{USER} = 'nobody';
+ like_program_out 'nobody', $virtualenv, 0, qr!id.nobody\ncurrent_user.nobody!, "running pg_virtualenv as nobody";
+
+ SKIP: {
+ skip "/usr/bin/fakeroot not available", 6 unless (-x "/usr/bin/fakeroot"); # CentOS doesn't have fakeroot
+ $ENV{USER} = 'root';
+ like_program_out 'root', "fakeroot $virtualenv", 0, qr!id.root\ncurrent_user.postgres!, "running fakeroot pg_virtualenv as root";
+ $ENV{USER} = 'postgres';
+ like_program_out 'postgres', "fakeroot $virtualenv", 0, qr!id.root\ncurrent_user.postgres!, "running fakeroot pg_virtualenv as postgres";
+ $ENV{USER} = 'nobody';
+ like_program_out 'nobody', "fakeroot $virtualenv", 0, qr!id.root\ncurrent_user.nobody!, "running fakeroot pg_virtualenv as nobody";
+ }
+}
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/140_pg_config.t b/t/140_pg_config.t
new file mode 100644
index 0000000..770c75c
--- /dev/null
+++ b/t/140_pg_config.t
@@ -0,0 +1,89 @@
+# Check pg_config output
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => 14 * @MAJORS + ($PgCommon::rpm ? 1 : 2) * 12;
+
+my $multiarch = '';
+unless ($PgCommon::rpm) {
+ $multiarch = `dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null`;
+ chomp $multiarch;
+}
+note "Multiarch is " . ($multiarch ? 'enabled' : 'disabled');
+
+my $version;
+foreach $version (@MAJORS) {
+ note "checking version specific output for $version";
+ if ($version < '8.2') {
+ pass "Skipping known-broken pg_config check for version $version";
+ for (my $i = 0; $i < 13; ++$i) { pass '...'; }
+ next;
+ }
+ is_program_out 'postgres', "$PgCommon::binroot$version/bin/pg_config --pgxs", 0,
+ "$PgCommon::binroot$version/lib/pgxs/src/makefiles/pgxs.mk\n";
+ my $libdir = "/usr/lib" . ($version >= 9.3 and $multiarch ? "/$multiarch" : "") . "\n";
+ $libdir = "$PgCommon::binroot$version/lib\n" if ($PgCommon::rpm);
+ is_program_out 'postgres', "$PgCommon::binroot$version/bin/pg_config --libdir", 0,
+ $libdir;
+ is_program_out 'postgres', "$PgCommon::binroot$version/bin/pg_config --pkglibdir", 0,
+ "$PgCommon::binroot$version/lib\n";
+ is_program_out 'postgres', "$PgCommon::binroot$version/bin/pg_config --bindir", 0,
+ "$PgCommon::binroot$version/bin\n";
+ # mkdir should be in /bin on Debian. If /bin was linked to /usr/bin at build time, usrmerge was installed
+ SKIP: {
+ skip 'MKDIR_P not present before 9.0', 2 if ($version < 9.0);
+ skip 'MKDIR_P not checked on RedHat', 2 if ($PgCommon::rpm); # varies across builds/versions
+ is_program_out 'postgres', "grep ^MKDIR_P $PgCommon::binroot$version/lib/pgxs/src/Makefile.global", 0,
+ "MKDIR_P = /bin/mkdir -p\n";
+ }
+ SKIP: {
+ skip 'build path not canonicalized on RedHat', 4 if ($PgCommon::rpm);
+ my $pkgversion = `dpkg-query -f '\${Version}' -W postgresql-server-dev-$version`;
+ # check that we correctly canonicalized the build paths
+ SKIP: {
+ skip 'abs_top_builddir introduced in 9.5', 2 if ($version < 9.5);
+ skip 'abs_top_builddir not patched in Debian (old)stable', 2 if ($version < 10 and $pkgversion !~ /pgdg/);
+ is_program_out 'postgres', "grep ^abs_top_builddir $PgCommon::binroot$version/lib/pgxs/src/Makefile.global", 0,
+ "abs_top_builddir = /build/postgresql-$version$ENV{PG_FLAVOR}/build\n";
+ }
+ SKIP: {
+ skip 'abs_top_srcdir not patched before 9.3', 2 if ($version < 9.3);
+ skip 'abs_top_srcdir not patched in Debian (old)stable', 2 if ($version < 10 and $pkgversion !~ /pgdg/);
+ is_program_out 'postgres', "grep ^abs_top_srcdir $PgCommon::binroot$version/lib/pgxs/src/Makefile.global", 0,
+ "abs_top_srcdir = /build/postgresql-$version$ENV{PG_FLAVOR}/build/..\n";
+ }
+ }
+}
+
+my @pg_configs = $PgCommon::rpm ? qw(pg_config) : qw(pg_config pg_config.libpq-dev);
+for my $pg_config (@pg_configs) {
+ if ($pg_config eq 'pg_config' or $PgCommon::rpm) { # pg_config should point at newest installed postgresql-server-dev-$version
+ $version = $ALL_MAJORS[-1];
+ } else { # pg_config.libpq-dev should point at postgresql-server-dev-$(version of libpq-dev)
+ my $libpqdev_version = `dpkg-query --showformat '\${Version}' --show libpq-dev`;
+ $libpqdev_version =~ /^([89].\d|1.)/ or die "could not determine libpq-dev version";
+ $version = $1;
+ }
+ note "checking $pg_config output (should behave like version $version)";
+
+ SKIP: {
+ my $pgc = "$PgCommon::binroot$version/bin/pg_config";
+ skip "$pgc not installed, can't check full $pg_config output", 2 unless (-x $pgc);
+ my $full_output = `$pgc`;
+ is_program_out 'postgres', "$pg_config", 0, $full_output;
+ }
+ like_program_out 'postgres', "$pg_config --help", 0, qr/--includedir-server/;
+ is_program_out 'postgres', "$pg_config --pgxs", 0,
+ "$PgCommon::binroot$version/lib/pgxs/src/makefiles/pgxs.mk\n";
+ my $libdir = "/usr/lib" . ($version >= 9.3 and $multiarch ? "/$multiarch" : "") . "\n";
+ $libdir = "$PgCommon::binroot$version/lib\n" if ($PgCommon::rpm);
+ is_program_out 'postgres', "$pg_config --libdir", 0,
+ $libdir;
+ is_program_out 'postgres', "$pg_config --pkglibdir", 0,
+ "$PgCommon::binroot$version/lib\n";
+ is_program_out 'postgres', "$pg_config --bindir", 0,
+ "$PgCommon::binroot$version/bin\n";
+}
diff --git a/t/150_tsearch_stemming.t b/t/150_tsearch_stemming.t
new file mode 100644
index 0000000..a2a6253
--- /dev/null
+++ b/t/150_tsearch_stemming.t
@@ -0,0 +1,108 @@
+# Check tsearch, and stemming with dynamic creation of .affix/.dict files
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+my $version = $MAJORS[-1];
+
+use Test::More tests => ($MAJORS[-1] < 8.3 or $PgCommon::rpm) ? 1 : 37;
+if ($version < 8.3) {
+ pass 'tsearch dictionaries not tested before 8.3';
+ exit;
+}
+if ($PgCommon::rpm) {
+ pass 'tsearch dictionaries not handled by postgresql-common on RedHat';
+ exit;
+}
+
+# test pg_updatedicts
+unlink '/var/cache/postgresql/dicts/en_us.affix';
+unlink '/var/cache/postgresql/dicts/en_us.dict';
+unlink "/usr/share/postgresql/$version/tsearch_data/en_us.affix";
+unlink "/usr/share/postgresql/$version/tsearch_data/en_us.dict";
+is ((exec_as 0, 'pg_updatedicts'), 0, 'pg_updatedicts succeeded');
+ok -f '/var/cache/postgresql/dicts/en_us.affix',
+ 'pg_updatedicts created en_us.affix';
+ok -f '/var/cache/postgresql/dicts/en_us.dict',
+ 'pg_updatedicts created en_us.dict';
+ok -l "/usr/share/postgresql/$version/tsearch_data/en_us.affix",
+ "pg_updatedicts created $version en_us.affix symlink";
+ok -l "/usr/share/postgresql/$version/tsearch_data/en_us.dict",
+ "pg_updatedicts created $version en_us.dict symlink";
+
+# create cluster
+is ((system "pg_createcluster $version main --start >/dev/null"), 0, "pg_createcluster $version main");
+
+# create DB with en_US text search configuration
+is_program_out 'postgres', 'createdb fts', 0, '';
+
+my $outref;
+
+is ((exec_as 'postgres', 'psql -qd fts -c "
+ CREATE TEXT SEARCH CONFIGURATION public.sc_english ( COPY = pg_catalog.english );
+ CREATE TEXT SEARCH DICTIONARY english_ispell (TEMPLATE = ispell, DictFile = en_US,
+ AffFile = en_US, StopWords = english);
+ SET default_text_search_config = \'public.sc_english\';
+ ALTER TEXT SEARCH CONFIGURATION public.sc_english
+ ALTER MAPPING FOR asciiword WITH english_ispell, english_stem;"', $outref),
+ 0, 'creating en_US full text search configuration ' . $$outref);
+
+# create test table and index
+my $outref;
+is ((exec_as 'postgres', 'psql -qd fts -c "
+ CREATE TABLE stuff (id SERIAL PRIMARY KEY, text TEXT, textsearch tsvector);
+ UPDATE stuff SET textsearch = to_tsvector(\'public.sc_english\', coalesce(text, \'\'));
+ CREATE INDEX textsearch_idx ON stuff USING gin(textsearch);
+ CREATE TRIGGER textsearch_update_trigger BEFORE INSERT OR UPDATE
+ ON stuff FOR EACH ROW EXECUTE PROCEDURE
+ tsvector_update_trigger(textsearch, \'public.sc_english\', text);
+ INSERT INTO stuff (text) VALUES (\'PostgreSQL rocks\');
+ INSERT INTO stuff (text) VALUES (\'Linux rocks\');
+ INSERT INTO stuff (text) VALUES (\'I am your father\'\'s nephew\'\'s former roommate\');
+ INSERT INTO stuff (text) VALUES (\'3 cafés\');
+ "'), 0, 'creating data table and search index');
+
+# test stemming
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT dictionary, lexemes FROM ts_debug(\'public.sc_english\', \'friendliest\')"',
+ 0, "english_ispell|{friendly}\n", 'stem search of correct word';
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT dictionary, lexemes FROM ts_debug(\'public.sc_english\', \'father\'\'s\')"',
+ 0, "english_ispell|{father}\n|\nenglish_ispell|{}\n", 'stem search of correct word';
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT dictionary, lexemes FROM ts_debug(\'public.sc_english\', \'duffles\')"',
+ 0, "english_stem|{duffl}\n", 'stem search of unknown word';
+
+# test searching
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT text FROM stuff, to_tsquery(\'rocks\') query WHERE query @@ to_tsvector(text)"',
+ 0, "PostgreSQL rocks\nLinux rocks\n", 'full text search, exact word';
+
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT text FROM stuff, to_tsquery(\'rock\') query WHERE query @@ to_tsvector(text)"',
+ 0, "PostgreSQL rocks\nLinux rocks\n", 'full text search for word stem';
+
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT text FROM stuff, to_tsquery(\'roc\') query WHERE query @@ to_tsvector(text)"',
+ 0, '', 'full text search for word substring fails';
+
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT text FROM stuff, to_tsquery(\'cafés\') query WHERE query @@ to_tsvector(text)"',
+ 0, "3 cafés\n", 'full text search, exact unicode word';
+
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT text FROM stuff, to_tsquery(\'café\') query WHERE query @@ to_tsvector(text)"',
+ 0, "3 cafés\n", 'full text search for unicode word stem';
+
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT text FROM stuff, to_tsquery(\'afé\') query WHERE query @@ to_tsvector(text)"',
+ 0, '', 'full text search for unicode word substring fails';
+
+# clean up
+is ((system "pg_dropcluster $version main --stop"), 0);
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/160_alternate_confroot.t b/t/160_alternate_confroot.t
new file mode 100644
index 0000000..e2a9604
--- /dev/null
+++ b/t/160_alternate_confroot.t
@@ -0,0 +1,57 @@
+# Check that we can do all operations using a per-user $PG_CLUSTER_CONF_ROOT
+
+use strict;
+
+use lib 't';
+use TestLib;
+
+my $version = $MAJORS[0];
+
+use Test::More tests => 28;
+
+# prepare nobody-owned root dir for $PG_CLUSTER_CONF_ROOT
+my $rootdir=`su -s /bin/sh -c 'mktemp -d' nobody`;
+chomp $rootdir;
+($rootdir) = $rootdir =~ m!^([a-zA-Z0-9._/]+)$!; # untaint
+$ENV{'PG_CLUSTER_CONF_ROOT'} = "$rootdir/etc";
+
+is ((exec_as 'nobody', "pg_createcluster $version test -d $rootdir/data/test -l $rootdir/test.log --start"), 0);
+
+is_program_out 'nobody', 'env -u PG_CLUSTER_CONF_ROOT pg_lsclusters -h', 0, '';
+like_program_out 'nobody', "pg_lsclusters -h", 0,
+ qr!^$version\s+test.*online\s+nobody\s+$rootdir/data/test\s+$rootdir/test.log$!;
+
+like_program_out 'nobody', "psql -Atl", 0, qr/template1.*UTF8/;
+
+# pg_upgradecluster
+if ($MAJORS[0] ne $MAJORS[-1]) {
+ my $outref;
+ is ((exec_as 'nobody', "pg_upgradecluster --logfile $rootdir/testupgr.log -v $MAJORS[-1] $version test $rootdir/data/testupgr", $outref, 0), 0);
+ like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+ like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+
+ like_program_out 'nobody', 'pg_lsclusters -h', 0,
+ qr!^$version\s+test.*down.*\n^$MAJORS[-1]\s+test.*online\s+nobody\s+$rootdir/data/testupgr\s+$rootdir/testupgr.log$!m;
+
+ # clean up
+ is_program_out 'nobody', "pg_dropcluster $version test", 0, '';
+ is_program_out 'nobody', "pg_dropcluster $MAJORS[-1] test --stop", 0, '';
+} else {
+ pass 'Only one major version installed, skipping pg_upgradecluster tests';
+ for (my $i = 0; $i < 6; ++$i) { pass '...'; }
+
+ is_program_out 'nobody', "pg_dropcluster $version test --stop", 0, '';
+}
+
+# pg_dropcluster
+is_program_out 'nobody', "pg_lsclusters -h", 0, '';
+
+ok_dir "$rootdir/data", [], 'No files in root/data left behind';
+ok_dir "$rootdir", ['etc', 'data'], 'No cruft in root dir left behind';
+
+system "rm -rf $rootdir";
+
+delete $ENV{'PG_CLUSTER_CONF_ROOT'};
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/170_extensions.t b/t/170_extensions.t
new file mode 100644
index 0000000..1abb330
--- /dev/null
+++ b/t/170_extensions.t
@@ -0,0 +1,87 @@
+# Check that all extensions install successfully.
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More 0.87; # needs libtest-simple-perl backport on lenny
+
+foreach my $v (@MAJORS) {
+note "Running tests for $v";
+
+if ($v < '9.1') {
+ pass 'No extensions for version < 9.1';
+ next;
+}
+
+# create cluster
+is ((system "pg_createcluster $v main --start >/dev/null"), 0, "pg_createcluster $v main");
+
+# plpgsql is installed by default
+is_program_out 'postgres', "psql -Atc 'SELECT extname FROM pg_extension'", 0, "plpgsql\n";
+
+my %depends = (
+ bool_plperl => [qw(plperl)],
+ bool_plperlu => [qw(plperlu)],
+ earthdistance => [qw(cube)],
+ hstore_plperl => [qw(hstore plperl)],
+ hstore_plperlu => [qw(hstore plperlu)],
+ hstore_plpython2u => [qw(hstore plpython2u)],
+ hstore_plpython3u => [qw(hstore plpython3u)],
+ hstore_plpythonu => [qw(hstore plpythonu)],
+ jsonb_plperl => [qw(plperl)], # PG 11
+ jsonb_plperlu => [qw(plperlu)], # PG 11
+ jsonb_plpython2u => [qw(plpython2u)], # PG 11
+ jsonb_plpython3u => [qw(plpython3u)], # PG 11
+ jsonb_plpythonu => [qw(plpythonu)], # PG 11
+ ltree_plpython2u => [qw(ltree plpython2u)],
+ ltree_plpython3u => [qw(ltree plpython3u)],
+ ltree_plpythonu => [qw(ltree plpythonu)],
+);
+
+foreach (</usr/share/postgresql/$v/extension/*.control>) {
+ my ($extname) = $_ =~ /^.*\/(.*)\.control$/;
+ next if ($extname eq 'plpgsql');
+
+ if ($depends{$extname}) {
+ for my $dep (@{$depends{$extname}}) {
+ is_program_out 'postgres', "psql -qc 'CREATE EXTENSION $dep'", 0, '',
+ "$extname dependency $dep installs without error";
+ }
+ }
+
+ if ($extname eq 'hstore' && $v eq '9.1') {
+ # EXFAIL: hstore in 9.1 throws a warning about obsolete => operator
+ like_program_out 'postgres', "psql -qc 'CREATE EXTENSION \"$extname\"'", 0,
+ qr/=>/, "extension $extname installs (with warning)";
+ } elsif ($extname eq 'chkpass' && $v >= '9.5') {
+ # chkpass is slightly broken, see
+ # http://www.postgresql.org/message-id/20141117162116.GA3565@msg.df7cb.de
+ like_program_out 'postgres', "psql -qc 'CREATE EXTENSION \"$extname\"'", 0,
+ qr/WARNING: type input function chkpass_in should not be volatile/,
+ "extension $extname installs (with warning)";
+ } else {
+ is_program_out 'postgres', "psql -qc 'CREATE EXTENSION \"$extname\"'", 0, '',
+ "extension $extname installs without error";
+ }
+
+ is_program_out 'postgres', "psql -qc 'DROP EXTENSION \"$extname\"'", 0, '',
+ "extension $extname removes without error";
+
+ if ($depends{$extname}) {
+ for my $dep (@{$depends{$extname}}) {
+ is_program_out 'postgres', "psql -qc 'DROP EXTENSION $dep'", 0, '',
+ "$extname dependency extension $dep removes without error";
+ }
+ }
+}
+
+# clean up
+is ((system "pg_dropcluster $v main --stop"), 0, "pg_dropcluster $v main");
+check_clean;
+}
+
+done_testing();
+
+# vim: filetype=perl
diff --git a/t/180_ecpg.t b/t/180_ecpg.t
new file mode 100644
index 0000000..5d3f7a8
--- /dev/null
+++ b/t/180_ecpg.t
@@ -0,0 +1,56 @@
+# Check that ecpg works
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => 14;
+
+my $v = $MAJORS[-1];
+
+# prepare nobody-owned work dir
+my $workdir=`su -s /bin/sh -c 'mktemp -d' nobody`;
+chomp $workdir;
+chdir $workdir or die "could not chdir to $workdir: $!";
+
+# create test code
+open F, '>test.pgc' or die "Could not open $workdir/test.pgc: $!";
+print F <<EOF;
+#include <stdio.h>
+#include <stdlib.h>
+
+EXEC SQL WHENEVER SQLWARNING SQLPRINT;
+EXEC SQL WHENEVER SQLERROR SQLPRINT;
+
+EXEC SQL BEGIN DECLARE SECTION;
+ char output[1024];
+EXEC SQL END DECLARE SECTION;
+
+int main() {
+ ECPGdebug(1, stderr);
+ EXEC SQL CONNECT TO template1;
+ EXEC SQL SELECT 'Database is ' || current_database() INTO :output;
+ puts(output);
+ EXEC SQL DISCONNECT ALL;
+ return 0;
+}
+EOF
+close F;
+chmod 0644, 'test.pgc';
+
+is_program_out 'nobody', 'ecpg test.pgc', 0, '', 'ecpg processing';
+
+is_program_out 'nobody', 'cc -I$(pg_config --includedir) -L$(pg_config --libdir) -o test test.c -lecpg',
+ 0, '', 'compiling ecpg output';
+chdir '/' or die "could not chdir to /: $!";
+
+# run program
+like_program_out 'nobody', "pg_virtualenv $workdir/test", 0, qr/Database is template1/,
+ 'test program runs and gives correct output';
+
+# clean up
+system "rm -rf $workdir";
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/190_pg_buildext.t b/t/190_pg_buildext.t
new file mode 100644
index 0000000..9a50712
--- /dev/null
+++ b/t/190_pg_buildext.t
@@ -0,0 +1,104 @@
+# Check pg_buildext and that our debhelper integration works
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More;
+
+if ($PgCommon::rpm) {
+ pass 'No pg_buildext tests on RedHat';
+ done_testing();
+ exit;
+}
+
+# when invoked from the postgresql-NN package tests, postgresql-server-dev-all is not installed
+if (! -x '/usr/bin/dh_make_pgxs') {
+ pass "Skipping pg_buildext tests, /usr/bin/dh_make_pgxs is not installed";
+ done_testing();
+ exit;
+}
+
+my $arch = `dpkg-architecture -qDEB_HOST_ARCH`;
+chomp $arch;
+
+if ($ENV{PG_VERSIONS}) {
+ note "PG_VERSIONS=$ENV{PG_VERSIONS}";
+ $ENV{PG_SUPPORTED_VERSIONS} = join ' ', (grep { $_ >= 9.1 } split /\s+/, $ENV{PG_VERSIONS});
+ unless ($ENV{PG_SUPPORTED_VERSIONS}) {
+ ok 1, 'No versions with extension support to test';
+ done_testing();
+ exit;
+ }
+ note "PG_SUPPORTED_VERSIONS=$ENV{PG_SUPPORTED_VERSIONS}";
+}
+my @versions = split /\s+/, `/usr/share/postgresql-common/supported-versions`;
+
+# prepare build environment
+chmod 0777, 't/foo', 't/foo/foo-123', 't/bar/debian';
+umask 0022;
+chdir 't/foo';
+
+program_ok 0, 'make clean';
+program_ok 'nobody', 'make tar';
+program_ok 'nobody', 'cd foo-123 && echo y | EDITOR=true dh_make_pgxs';
+
+note "testing 'dh --with pgxs'";
+program_ok 'nobody', 'cd foo-123 && DEB_BUILD_OPTIONS=nocheck dpkg-buildpackage -us -uc';
+
+foreach my $ver (@versions) {
+ my $deb = "postgresql-$ver-foo_123-1_$arch.deb";
+ ok (-f $deb, "$deb was built");
+ SKIP: {
+ my $have_extension_destdir = `grep extension_destdir /usr/share/postgresql/$ver/postgresql.conf.sample`;
+ skip "No in-tree installcheck on PG $ver (missing extension_destdir)", 2 unless ($have_extension_destdir);
+ like_program_out 'nobody', "cd foo-123 && PG_SUPPORTED_VERSIONS=$ver dh_pgxs_test",
+ 0, qr/PostgreSQL $ver installcheck.*(test foo * \.\.\. ok|ok 1 * - foo)/s; # old/new PG 16 syntax
+ }
+ program_ok 0, "dpkg -i $deb";
+ like_program_out 'nobody', "cd foo-123 && pg_buildext installcheck",
+ 0, qr/PostgreSQL $ver installcheck.*(test foo * \.\.\. ok|ok 1 * - foo)/s;
+ like_program_out 'nobody', "cd foo-123 && echo 'SELECT 3*41, version()' | pg_buildext psql", 0, qr/123.*PostgreSQL $ver/;
+ like_program_out 'nobody', "cd foo-123 && echo 'echo --\$PGVERSION--' | pg_buildext virtualenv", 0, qr/--$ver--/;
+ like_program_out 'nobody', "cd foo-123 && pg_buildext run echo --%v--", 0, qr/--$ver--/;
+ program_ok 0, "dpkg -r postgresql-$ver-foo";
+}
+
+note "testing 'dh --with pgxs_loop'";
+system "rm -f postgresql-*.deb";
+
+program_ok 'nobody', 'sed -i -e s/pgxs/pgxs_loop/ foo-123/debian/rules';
+program_ok 'nobody', 'cd foo-123 && DEB_BUILD_OPTIONS=nocheck dpkg-buildpackage -us -uc';
+
+foreach my $ver (@versions) {
+ my $deb = "postgresql-$ver-foo_123-1_$arch.deb";
+ ok (-f $deb, "$deb was built");
+}
+
+program_ok 'nobody', 'make clean';
+
+note "testing pg_buildext updatecontrol";
+chdir '../bar';
+program_ok 'nobody', 'PG_SUPPORTED_VERSIONS="0.9 1.0 1.1 2 3" pg_buildext updatecontrol';
+is `cat debian/control`, "Source: bar
+Build-Depends: whatever, postgresql-1.0-moo (>= 1), postgresql-1.1-moo (>= 1), postgresql-2-moo (>= 1), more,
+ postgresql-2-new,
+ postgresql-1.0, postgresql-1.1, postgresql-2
+
+Package: postgresql-1.0-bar
+Architecture: some
+Depends: postgresql-1.0-moo
+
+Package: postgresql-1.1-bar
+Architecture: some
+Depends: postgresql-1.1-moo
+
+Package: postgresql-2-bar
+Architecture: some
+Depends: postgresql-2-moo
+", "PGVERSION and PGVERSIONS were correctly replaced";
+
+done_testing();
+
+# vim: filetype=perl
diff --git a/t/200_maintscripts.t b/t/200_maintscripts.t
new file mode 100644
index 0000000..3fc5018
--- /dev/null
+++ b/t/200_maintscripts.t
@@ -0,0 +1,46 @@
+# This test runs last since we are reinstalling postgresql-common, and we want
+# to avoid spoiling the other tests with any version skew.
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => $PgCommon::rpm ? 1 : 15;
+
+if ($PgCommon::rpm) {
+ pass 'No maintainer script tests on rpm';
+ exit;
+}
+
+my $v = $MAJORS[-1];
+
+note -d "/run/systemd/system" ? "We are running systemd" : "We are not running systemd";
+
+# create cluster
+program_ok 0, "pg_createcluster $v main --start";
+
+# get postmaster PID
+my $postmaster_pid = `head -1 /var/lib/postgresql/$v/main/postmaster.pid`;
+chomp $postmaster_pid;
+ok $postmaster_pid > 0, "postmaster PID is $postmaster_pid";
+
+# "upgrade" postgresql-common to check if postgresql.service is left alone
+program_ok 0, 'apt-get update -q', 0, '';
+note `apt-cache policy postgresql-common`;
+program_ok 0, 'apt-get install -y --reinstall -o DPkg::Options::=--force-confnew postgresql-common', 0, '';
+note `apt-cache policy postgresql-common`;
+
+# get postmaster PID again, compare
+my $postmaster_pid2 = `head -1 /var/lib/postgresql/$v/main/postmaster.pid`;
+chomp $postmaster_pid2;
+ok $postmaster_pid2 > 0, "postmaster PID is $postmaster_pid2";
+is $postmaster_pid, $postmaster_pid2, "postmaster was not restarted";
+
+# stop server, clean up, check for leftovers
+program_ok 0, "pg_dropcluster $v main --stop";
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/TestLib.pm b/t/TestLib.pm
new file mode 100644
index 0000000..408c9f5
--- /dev/null
+++ b/t/TestLib.pm
@@ -0,0 +1,270 @@
+# Common functionality for postgresql-common self tests
+#
+# (C) 2005-2009 Martin Pitt <mpitt@debian.org>
+# (C) 2013-2022 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.
+
+package TestLib;
+use strict;
+use Exporter;
+use Test::More;
+use PgCommon qw/get_versions change_ugid next_free_port/;
+
+our $VERSION = 1.00;
+our @ISA = ('Exporter');
+our @EXPORT = qw/os_release ps ok_dir exec_as deb_installed rpm_installed package_version
+ version_ge program_ok is_program_out like_program_out unlike_program_out
+ pidof pid_env check_clean
+ @ALL_MAJORS @MAJORS $delay/;
+
+our @ALL_MAJORS = get_versions(); # not affected by PG_VERSIONS/-v
+our @MAJORS = $ENV{PG_VERSIONS} ? split (/\s+/, $ENV{PG_VERSIONS}) : @ALL_MAJORS;
+our $delay = 500_000; # 500ms
+
+# called if a test fails; spawn a shell if the environment variable
+# FAILURE=shell is set
+sub fail_debug {
+ if ($ENV{'FAILURE'} eq 'shell') {
+ if ((system 'bash') != 0) {
+ exit 1;
+ }
+ }
+}
+
+# parse /etc/os-release and return (os, version number)
+sub os_release {
+ open OS, "/etc/os-release" or return (undef, undef);
+ my ($os, $osversion);
+ while (<OS>) {
+ $os = $1 if /^ID=(.*)/;
+ $osversion = $1 if /^VERSION_ID="?([^"]*)/;
+ }
+ close OS;
+ $osversion = 'unstable' if ($os eq 'debian' and not defined $osversion);
+ return ($os, $osversion);
+}
+
+# Return whether a given deb is installed.
+# Arguments: <deb name>
+sub deb_installed {
+ open (DPKG, "dpkg -s $_[0] 2>/dev/null|") or die "call dpkg: $!";
+ my $result = 0;
+ while (<DPKG>) {
+ if (/^Status: install ok installed/) {
+ $result = 1;
+ last;
+ }
+ }
+ close DPKG;
+
+ return $result;
+}
+
+# Return whether a given rpm is installed.
+# Arguments: <rpm name>
+sub rpm_installed {
+ open (RPM, "rpm -qa $_[0] 2>/dev/null|") or die "call rpm: $!";
+ my $out = <RPM>; # returns void or the package name
+ close RPM;
+ return ($out =~ /./);
+}
+
+# Return a package version
+# Arguments: <package>
+sub package_version {
+ my $package = shift;
+ if ($PgCommon::rpm) {
+ return `rpm --queryformat '%{VERSION}' -q $package`;
+ } else {
+ my $version = `dpkg-query -f '\${Version}' --show $package`;
+ chomp $version;
+ return $version;
+ }
+}
+
+# Return whether a version is greater or equal to another one
+# Arguments: <ver1> <ver2>
+sub version_ge {
+ my ($v1, $v2) = @_;
+ use IPC::Open2;
+ open2(\*CHLD_OUT, \*CHLD_IN, 'sort', '-Vr');
+ print CHLD_IN "$v1\n";
+ print CHLD_IN "$v2\n";
+ close CHLD_IN;
+ my $v_ge = <CHLD_OUT>;
+ chomp $v_ge;
+ return $v_ge eq $v1;
+}
+
+# Return the user, group, and command line of running processes for the given
+# program.
+sub ps {
+ return `ps h -o user,group,args -C $_[0] | grep '$_[0]' | sort -u`;
+}
+
+# Return array of pids that match the given command name (we require a leading
+# slash so the postgres children are filtered out)
+sub pidof {
+ my $prg = shift;
+ open F, '-|', 'ps', 'h', '-C', $prg, '-o', 'pid,cmd' or die "open: $!";
+ my @pids;
+ while (<F>) {
+ if ((index $_, "/$prg") >= 0) {
+ push @pids, (split)[0];
+ }
+ }
+ close F;
+ return @pids;
+}
+
+# Return an reference to an array of all entries but . and .. of the given directory.
+sub dircontent {
+ my $dir = $_[0];
+ opendir D, $dir or return ["opendir $dir: $!"];
+ my @e = grep { $_ ne '.' && $_ ne '..' } readdir (D);
+ closedir D;
+ return [sort @e];
+}
+
+# Return environment of given PID
+sub pid_env {
+ my ($user, $pid) = @_;
+ my $path = "/proc/$pid/environ";
+ my @lines;
+ open E, "su -c 'cat $path' $user |" or warn "open $path: $!";
+ {
+ local $/;
+ @lines = split '\0', <E>;
+ }
+ close E;
+ my %env;
+ foreach (@lines) {
+ my ($k, $v) = (split '=');
+ $env{$k} = $v;
+ }
+ return %env;
+}
+
+# Check the contents of a directory.
+# Arguments: <directory name> <ref to expected dir content> <test description>
+sub ok_dir {
+ my $content = dircontent $_[0];
+ if (eq_set $content, $_[1]) {
+ pass $_[2];
+ } else {
+ diag "Expected directory contents: [@{$_[1]}], actual contents: [@$content]\n";
+ fail $_[2];
+ }
+}
+
+# Execute a command as a different user and return the output. Prints the
+# output of the command if exit code differs from expected one.
+# Arguments: <user> <system command> <ref to output> [<expected exit code>]
+# Returns: Program exit code
+sub exec_as {
+ my $uid;
+ if ($_[0] =~ /\d+/) {
+ $uid = int($_[0]);
+ } else {
+ $uid = getpwnam $_[0];
+ defined($uid) or die "TestLib::exec_as: target user '$_[0]' does not exist";
+ }
+ change_ugid ($uid, (getpwuid $uid)[3]);
+ die "changing euid: $!" if $> != $uid;
+ my $out = `$_[1] 2>&1`;
+ my $result = $? >> 8;
+ $< = $> = 0;
+ $( = $) = 0;
+ die "changing euid back to root: $!" if $> != 0;
+ $_[2] = \$out;
+
+ if (defined $_[3] && $_[3] != $result) {
+ print "command '$_[1]' did not exit with expected code $_[3] but with $result:\n";
+ print $out;
+ fail_debug;
+ }
+ return $result;
+}
+
+# Execute a command as a particular user, and check the exit code
+# Arguments: <user> <command> [<expected exit code>] [<description>]
+sub program_ok {
+ my ($user, $cmd, $exit, $description) = @_;
+ $exit ||= 0;
+ $description ||= $cmd;
+ my $outref;
+ ok ((exec_as $user, $cmd, \$outref, $exit) == $exit, $description);
+}
+
+# Execute a command as a particular user, and check the exit code and output
+# (merged stdout/stderr).
+# Arguments: <user> <command> <expected exit code> <expected output> [<description>]
+sub is_program_out {
+ my $outref;
+ my $result = exec_as $_[0], $_[1], $outref;
+ is $result, $_[2], $_[1] or fail_debug;
+ is ($$outref, $_[3], (defined $_[4] ? $_[4] : "correct output of $_[1]")) or fail_debug;
+}
+
+# Execute a command as a particular user, and check the exit code and output
+# against a regular expression (merged stdout/stderr).
+# Arguments: <user> <command> <expected exit code> <expected output re> [<description>]
+sub like_program_out {
+ my $outref;
+ my $result = exec_as $_[0], $_[1], $outref;
+ is $result, $_[2], $_[1] or fail_debug;
+ like ($$outref, $_[3], (defined $_[4] ? $_[4] : "correct output of $_[1]")) or fail_debug;
+}
+
+# Execute a command as a particular user, check the exit code, and check that
+# the output does not match a regular expression (merged stdout/stderr).
+# Arguments: <user> <command> <expected exit code> <expected output re> [<description>]
+sub unlike_program_out {
+ my $outref;
+ my $result = exec_as $_[0], $_[1], $outref;
+ is $result, $_[2], $_[1] or fail_debug;
+ unlike ($$outref, $_[3], (defined $_[4] ? $_[4] : "correct output of $_[1]")) or fail_debug;
+}
+
+# Check that all PostgreSQL related directories are empty and no
+# postgres processes are running. Should be called at the end
+# of all tests. Does 8 tests.
+sub check_clean {
+ note "Cleanup";
+ is (`pg_lsclusters -h`, '', 'Cleanup: No clusters left behind');
+ is ((ps 'postgres'), '', 'No postgres processes left behind');
+
+ my @check_dirs = ('/etc/postgresql', '/var/lib/postgresql',
+ '/var/run/postgresql');
+ foreach (@check_dirs) {
+ if (-d) {
+ ok_dir $_, [], "No files in $_ left behind";
+ } else {
+ pass "Directory $_ does not exist";
+ }
+ }
+ # we always want /var/log/postgresql/ to exist, so that logrotate does not
+ # complain about missing directories
+ ok_dir '/var/log/postgresql', [], "No files in /var/log/postgresql left behind";
+
+ # prefer ss over netstat (until all debian/tests/control files in postgresql-* have been updated)
+ unless (-x '/bin/netstat' and not -x '/bin/ss') {
+ is `ss --no-header -tlp 'sport >= 5432 and sport <= 5439'`, '',
+ 'PostgreSQL TCP ports are closed';
+ } else {
+ is `netstat -avptn 2>/dev/null | grep ":543[2-9]\\b.*LISTEN"`, '',
+ 'PostgreSQL TCP ports are closed';
+ }
+ is next_free_port(), 5432, "Next free port is 5432";
+}
+
+1;
diff --git a/t/bar/debian/control.in b/t/bar/debian/control.in
new file mode 100644
index 0000000..86b8069
--- /dev/null
+++ b/t/bar/debian/control.in
@@ -0,0 +1,8 @@
+Source: bar
+Build-Depends: whatever, postgresql-PGVERSIONS-moo (>= 1), more,
+ postgresql-PGVERSION-new,
+ postgresql-PGVERSIONS
+
+Package: postgresql-PGVERSION-bar
+Architecture: some
+Depends: postgresql-PGVERSION-moo
diff --git a/t/bar/debian/pgversions b/t/bar/debian/pgversions
new file mode 100644
index 0000000..41f0ed3
--- /dev/null
+++ b/t/bar/debian/pgversions
@@ -0,0 +1,3 @@
+1.0
+1.1
+2
diff --git a/t/foo/Makefile b/t/foo/Makefile
new file mode 100644
index 0000000..75e95e1
--- /dev/null
+++ b/t/foo/Makefile
@@ -0,0 +1,12 @@
+TAR = foo_123.orig.tar.gz
+
+test: $(TAR)
+ cd foo-123 && echo y | EDITOR=true dh_make_pgxs
+ cd foo-123 && dpkg-buildpackage -us -uc
+
+tar $(TAR):
+ tar cfz $(TAR) foo-123/
+
+clean:
+ rm -f *.*
+ rm -rf foo-123/build-* foo-123/debian foo-123/*.o*
diff --git a/t/foo/foo-123/Makefile b/t/foo/foo-123/Makefile
new file mode 100644
index 0000000..9f9a2f8
--- /dev/null
+++ b/t/foo/foo-123/Makefile
@@ -0,0 +1,10 @@
+MODULE_big = foo
+OBJS = foo.o
+EXTENSION = foo
+DATA = foo--100.sql foo--100--123.sql foo--123.sql
+REGRESS = foo upgrade
+
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+
diff --git a/t/foo/foo-123/README.md b/t/foo/foo-123/README.md
new file mode 100644
index 0000000..14235f9
--- /dev/null
+++ b/t/foo/foo-123/README.md
@@ -0,0 +1,5 @@
+The PostgreSQL foo extension
+============================
+
+This extension exists for testing postgresql-common's `dh_make_pgxs` and
+`dh --with pgxs` mechanisms.
diff --git a/t/foo/foo-123/expected/foo.out b/t/foo/foo-123/expected/foo.out
new file mode 100644
index 0000000..62afef7
--- /dev/null
+++ b/t/foo/foo-123/expected/foo.out
@@ -0,0 +1,8 @@
+CREATE EXTENSION foo;
+SELECT foo();
+ foo
+-----
+ bar
+(1 row)
+
+DROP EXTENSION foo;
diff --git a/t/foo/foo-123/expected/upgrade.out b/t/foo/foo-123/expected/upgrade.out
new file mode 100644
index 0000000..98fc8e6
--- /dev/null
+++ b/t/foo/foo-123/expected/upgrade.out
@@ -0,0 +1,14 @@
+CREATE EXTENSION foo VERSION "100";
+SELECT foo();
+ foo
+---------
+ old bar
+(1 row)
+
+ALTER EXTENSION foo UPDATE TO "123";
+SELECT foo();
+ foo
+-----
+ bar
+(1 row)
+
diff --git a/t/foo/foo-123/foo--100--123.sql b/t/foo/foo-123/foo--100--123.sql
new file mode 100644
index 0000000..a6e7adc
--- /dev/null
+++ b/t/foo/foo-123/foo--100--123.sql
@@ -0,0 +1,3 @@
+CREATE OR REPLACE FUNCTION foo ()
+RETURNS text LANGUAGE C
+AS '$libdir/foo';
diff --git a/t/foo/foo-123/foo--100.sql b/t/foo/foo-123/foo--100.sql
new file mode 100644
index 0000000..5ed8546
--- /dev/null
+++ b/t/foo/foo-123/foo--100.sql
@@ -0,0 +1,3 @@
+CREATE OR REPLACE FUNCTION foo ()
+RETURNS text LANGUAGE SQL
+AS $$ SELECT 'old bar'::text $$;
diff --git a/t/foo/foo-123/foo--123.sql b/t/foo/foo-123/foo--123.sql
new file mode 100644
index 0000000..a6e7adc
--- /dev/null
+++ b/t/foo/foo-123/foo--123.sql
@@ -0,0 +1,3 @@
+CREATE OR REPLACE FUNCTION foo ()
+RETURNS text LANGUAGE C
+AS '$libdir/foo';
diff --git a/t/foo/foo-123/foo.c b/t/foo/foo-123/foo.c
new file mode 100644
index 0000000..8677acc
--- /dev/null
+++ b/t/foo/foo-123/foo.c
@@ -0,0 +1,13 @@
+#include "postgres.h"
+#include "fmgr.h"
+#include "utils/builtins.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1 (foo);
+
+Datum
+foo (PG_FUNCTION_ARGS)
+{
+ PG_RETURN_TEXT_P(cstring_to_text("bar"));
+}
diff --git a/t/foo/foo-123/foo.control b/t/foo/foo-123/foo.control
new file mode 100644
index 0000000..81c95c8
--- /dev/null
+++ b/t/foo/foo-123/foo.control
@@ -0,0 +1,2 @@
+default_version = '123'
+comment = 'The foo extension'
diff --git a/t/foo/foo-123/sql/foo.sql b/t/foo/foo-123/sql/foo.sql
new file mode 100644
index 0000000..155f63e
--- /dev/null
+++ b/t/foo/foo-123/sql/foo.sql
@@ -0,0 +1,5 @@
+CREATE EXTENSION foo;
+
+SELECT foo();
+
+DROP EXTENSION foo;
diff --git a/t/foo/foo-123/sql/upgrade.sql b/t/foo/foo-123/sql/upgrade.sql
new file mode 100644
index 0000000..b4427b4
--- /dev/null
+++ b/t/foo/foo-123/sql/upgrade.sql
@@ -0,0 +1,7 @@
+CREATE EXTENSION foo VERSION "100";
+
+SELECT foo();
+
+ALTER EXTENSION foo UPDATE TO "123";
+
+SELECT foo();
diff --git a/t/template b/t/template
new file mode 100644
index 0000000..fa1df59
--- /dev/null
+++ b/t/template
@@ -0,0 +1,26 @@
+# Check XXX
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => 14;
+
+my @versions = ($MAJORS[-1]);
+
+# create clusters
+foreach (@versions) {
+ is ((system "pg_createcluster $_ main --start >/dev/null"), 0, "pg_createcluster $_ main");
+ is_program_out 'postgres', "createdb --cluster $_/main XXX", 0, '';
+}
+
+# XXX
+
+# clean up
+foreach (@versions) {
+ is ((system "pg_dropcluster $_ main --stop"), 0, "pg_dropcluster $_ main");
+}
+check_clean;
+
+# vim: filetype=perl
diff --git a/testsuite b/testsuite
new file mode 100755
index 0000000..d3ff77d
--- /dev/null
+++ b/testsuite
@@ -0,0 +1,222 @@
+#!/bin/bash
+
+# Run integration tests (on the installed packages).
+#
+# (C) 2005-2012 Martin Pitt <mpitt@debian.org>
+# (C) 2012-2023 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.
+
+set -eu
+
+# default config
+TESTSDIR="$(dirname $0)/t"
+: ${PG_UMASKS="077"} # default umask(s) to try
+
+help ()
+{
+ echo "postgresql-common testsuite"
+ echo "Syntax: $0 [options] [test ...]"
+ echo " -D drop all existing clusters (USE WITH CARE)"
+ echo " -f number run tests starting at this number"
+ echo " -F suffix flavor suffix on package names, e.g. 'ee'"
+ echo " -i install packages for versions specified by -v"
+ echo " -M don't mount tmpfses, run in the host system"
+ echo " -s start a shell in the testbed on failure"
+ echo " -u 'umask ...' umasks to run testsuite with [default: 077]"
+ echo " -v 'version ...' PostgreSQL versions to test [default: client versions installed]"
+ echo " -V debug output"
+ exit ${1:-0}
+}
+
+# option parsing
+while getopts "Dhf:F:iMsu:v:V" opt ; do
+ case $opt in
+ D) DROP_ALL_CLUSTERS=1 ;;
+ f) FROM="$OPTARG" ;;
+ F) export PG_FLAVOR="$OPTARG" ;;
+ i) INSTALL=1 ;;
+ h) help ;;
+ M) export NO_TMPFS=1 ;;
+ s) export FAILURE="shell" ;;
+ u) PG_UMASKS="$OPTARG" ;;
+ v) export PG_VERSIONS="$OPTARG" ;; # used in t/TestLib.pm
+ V) VERBOSE=1 ;;
+ *) help 1 ;;
+ esac
+done
+
+if [ "$(id -u)" != 0 ]; then
+ echo "Error: this test suite needs to be run as root" >&2
+ exit 1
+fi
+
+# install packages for versions specified by -v
+# needs network for apt, so run before unshare
+if [ "${INSTALL:-}" ] && [ -z "${UNSHARED:-}" ]; then
+ . /etc/os-release
+ case $ID in
+ debian|ubuntu)
+ for v in $PG_VERSIONS; do
+ case $v in 8.*|9.*|10|11)
+ [ "$(perl -I. -le 'use PgCommon; print $PgCommon::have_python2')" = "1" ] && PYTHON2_PACKAGE=postgresql-plpython-$v${PG_FLAVOR:=}
+ ;;
+ esac
+ apt-get install -y \
+ postgresql-contrib-$v${PG_FLAVOR:=} \
+ postgresql-plperl-$v$PG_FLAVOR \
+ ${PYTHON2_PACKAGE:-} \
+ $(dpkg --compare-versions $v ge 9.1 && echo postgresql-plpython3-$v$PG_FLAVOR) \
+ postgresql-pltcl-$v$PG_FLAVOR \
+ postgresql-server-dev-$v$PG_FLAVOR \
+ postgresql-doc-$v$PG_FLAVOR
+ done
+ apt-get install -y \
+ debhelper \
+ libecpg-dev \
+ locales-all \
+ procps systemd \
+ netcat-openbsd \
+ hunspell-en-us \
+ gcc
+ ;;
+ redhat|centos)
+ for v in $PG_VERSIONS; do
+ vv=$(echo $v | tr -d .)
+ yum install -y \
+ postgresql$vv${PG_FLAVOR:=}-contrib \
+ postgresql$vv$PG_FLAVOR-plperl \
+ postgresql$vv$PG_FLAVOR-plpython3 \
+ postgresql$vv$PG_FLAVOR-pltcl \
+ postgresql$vv$PG_FLAVOR-devel \
+ postgresql$vv$PG_FLAVOR-doc
+ done
+ yum install -y \
+ nmap-ncat \
+ perl-Test-Simple perl-Time-HiRes \
+ gcc
+ ;;
+ *)
+ echo "Unknown distribution ID $ID in /etc/os-release"
+ exit 1
+ ;;
+ esac
+fi
+
+# shift away args
+shift $(($OPTIND - 1))
+
+# stop currently running clusters
+if [ -d /run/systemd/system ] ; then
+ # stop postgresql@* explicitly (#759725)
+ systemctl stop postgresql "postgresql@*" "pg_receivewal@*"
+else
+ service postgresql stop
+fi
+
+# drop all existing clusters if the user requested it
+if [ "${DROP_ALL_CLUSTERS:-}" ]; then
+ pg_lsclusters -h | while read ver cluster rest; do
+ pg_dropcluster $ver $cluster
+ done
+fi
+
+# set up test directories
+dirs="/etc/postgresql /var/lib/postgresql /var/log/postgresql /var/run/postgresql /var/backups/postgresql"
+
+if [ -z "${NO_TMPFS:-}" ]; then
+ # clean up after us
+ cleanup () {
+ set +e
+ umount -l $dirs
+ sed -i -e '/# by pg-testsuite/d' /etc/postgresql-common/createcluster.conf
+ ./pg_updateaptconfig
+ systemctl daemon-reload 2> /dev/null || : # poke generator to handle the system's clusters again
+ exit
+ }
+ trap "cleanup" 0 HUP INT QUIT ILL ABRT PIPE TERM
+
+ for d in $dirs; do
+ mkdir -p $d
+ mount -t tmpfs tmpfs $d
+ done
+
+ chown postgres:postgres /etc/postgresql ; chmod 755 /etc/postgresql
+ chown postgres:postgres /var/lib/postgresql ; chmod 755 /var/lib/postgresql
+ chown root:postgres /var/log/postgresql ; chmod 1775 /var/log/postgresql
+ chown postgres:postgres /var/run/postgresql ; chmod 2775 /var/run/postgresql /var/backups/postgresql
+fi
+
+if [ -d /run/systemd/system ]; then
+ systemctl daemon-reload # poke generator to forget the system's clusters
+fi
+
+# the RPM packages enable the logging_collector by default, which the testsuite
+# doesn't like. We disable it unconditionally here.
+if ! grep -q logging_collector /etc/postgresql-common/createcluster.conf; then
+ echo "logging_collector = off # by pg-testsuite" >> /etc/postgresql-common/createcluster.conf
+fi
+
+# reset core limit for pg_ctl tests
+ulimit -S -c 0
+
+# set environment
+unset TMPDIR
+unset LC_ALL
+export LANG=en_US.utf8
+
+# set variables which cause taint check errors
+export IFS
+export CDPATH=/usr
+export ENV=/nonexisting
+export BASH_ENV=/nonexisting
+
+if [ $# -eq 0 ]; then
+ set -- $TESTSDIR/*.t
+fi
+
+# dump environment for debugging
+if [ "${VERBOSE:-}" ]; then
+ echo "Environment:"
+ env | sort
+ echo "Mounts:"
+ cat /proc/mounts
+ echo "Namespaces:"
+ ls -l /proc/self/ns
+ echo
+fi
+
+for U in $PG_UMASKS; do
+ echo "====== Running all tests with umask $U ======="
+ umask $U
+ for T; do
+ TBASE=${T##*/}
+ [ "${FROM:-}" ] && [ "${TBASE%%_*}" -lt "${FROM:-}" ] && continue
+ echo "### PostgreSQL test $TBASE ###"
+ perl -I. $T || {
+ EXIT=$?
+ FAILED_TESTS="${FAILED_TESTS:-} $T"
+ if [ "${FAILURE:-}" ]; then
+ echo "*** $TBASE failed with status $EXIT, dropping you into a shell in the testbed ***"
+ ${SHELL:-/bin/sh}
+ fi
+ }
+ echo "### End test $TBASE ###"
+ echo
+ done
+done
+
+if [ "${FAILED_TESTS:-}" ]; then
+ echo "Failed tests: $FAILED_TESTS"
+ echo
+fi
+
+exit ${EXIT:-0}
diff --git a/user_clusters b/user_clusters
new file mode 100644
index 0000000..82d2d70
--- /dev/null
+++ b/user_clusters
@@ -0,0 +1,22 @@
+# This file maps users against the database clusters to which they
+# will connect by default. Any user may create ~/.postgresqlrc which
+# will supersede the defaults stored here. If a database is
+# specified, that will be the one connected to by client tools if none
+# is specified on the command line. If the database specified here is
+# "*", this is interpreted as the database whose name is the same as
+# the user's login. (Setting the database to "*" will provide the
+# current default upstream behaviour for command line tools.)
+#
+# When pg_wrapper scans this file, the first matching line is used.
+# It is a good idea to provide a default explicitly, with a final line
+# where both user and group are set to "*". If there is no default,
+# the implicit default is to connect to the cluster listening on
+# port 5432 and to the database matching the user's login name.
+#
+# In the context of this file, user and group refer to the Unix login
+# or group, not to PostgreSQL users and groups.
+#
+# Please see user_clusters(5) and postgresqlrc(5) for more information.
+#
+# USER GROUP VERSION CLUSTER DATABASE
+
diff --git a/user_clusters.5 b/user_clusters.5
new file mode 100644
index 0000000..a173fde
--- /dev/null
+++ b/user_clusters.5
@@ -0,0 +1,63 @@
+.TH USER_CLUSTERS 5 "Feburary 2005" "Debian" "Debian PostgreSQL infrastructure"
+
+.SH NAME
+user_clusters \- File linking users to PostgreSQL clusters
+
+.SH DESCRIPTION
+The file
+.B /etc/postgresql-common/user_clusters
+maps users against the database clusters to which they will
+connect by default. However, every user can override these settings in
+.B ~/.postgresqlrc\fR.
+
+When scanning this file, the first matching line will be used. It is a
+good idea to provide a default explicitly, with a final line where both
+user and group are set to
+.BR * .
+
+If there is no default, the implicit default is to connect to the cluster
+listening on port 5432 and to the database matching the user's
+login name.
+
+.SH FORMAT
+Comments are introduced by the character
+.BR # .
+Comments may follow data on a line; the first comment character terminates
+the data. Leading whitespace and blank lines are ignored.
+
+Each uncommented, non\-blank line must describe a user, group or the
+default (where both user and group are set to \fB*\fR).
+
+Fields must be given in the following order, separated by white space:
+
+.TP
+.B USER
+The login id of the Unix user to whom this line applies. The wildcard character
+.B *
+means any user.
+.TP
+.B GROUP
+The group name of the Unix group to which this line applies. The wildcard character
+.B *
+means any group.
+.TP
+.B VERSION
+The major PostgreSQL version of the cluster to connect to.
+.TP
+.B CLUSTER
+The name of a cluster to connect to. A remote cluster is specified
+with \fIhost\fR:\fIport\fR. If port is empty, it defaults to 5432.
+.TP
+.B DATABASE
+Within the cluster, the database to which the user will connect by default
+if he does not specify a database on the command line. If this is
+.BR * ,
+the default database will be the one named by the user's login id.
+
+.SH NOTES
+.P
+Since the first matching line is used, the default line must come last.
+.P
+
+.SH SEE ALSO
+.BR pg_wrapper (1), " postgresqlrc" (5)