From e308bcff5a610d6a3bbe33b3769f03f6d4533b16 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 18:02:19 +0200 Subject: Adding upstream version 248. Signed-off-by: Daniel Baumann --- .vimrc | 3 + Makefile | 64 + PgCommon.pm | 1665 ++++++++++++++++++++ README.md | 114 ++ TODO | 39 + cleanpg | 24 + createcluster.conf | 41 + debhelper/Debian/Debhelper/Buildsystem/pgxs.pm | 58 + .../Debian/Debhelper/Buildsystem/pgxs_loop.pm | 33 + debhelper/Debian/Debhelper/Sequence/pgxs.pm | 22 + debhelper/Debian/Debhelper/Sequence/pgxs_loop.pm | 23 + debhelper/Debian/Debhelper/pgxs.pm | 38 + debhelper/dh_pgxs_test | 11 + debhelper/dh_pgxs_test.pod | 44 + dh_make_pgxs/debian/control.in | 23 + dh_make_pgxs/debian/copyright | 24 + dh_make_pgxs/debian/gitlab-ci.yml | 1 + dh_make_pgxs/debian/pgversions | 1 + dh_make_pgxs/debian/rules | 36 + dh_make_pgxs/debian/source/format | 1 + dh_make_pgxs/debian/tests/control | 5 + dh_make_pgxs/debian/tests/installcheck | 3 + dh_make_pgxs/debian/watch | 2 + dh_make_pgxs/dh_make_pgxs | 136 ++ dh_make_pgxs/dh_make_pgxs.pod | 43 + doc/dependencies.dia | Bin 0 -> 3144 bytes doc/dependencies.svg | 206 +++ gitlab/gitlab-ci.yml | 13 + pg_backupcluster | 614 ++++++++ pg_buildext | 467 ++++++ pg_buildext.pod | 344 ++++ pg_checksystem | 59 + pg_config | 33 + pg_conftool | 233 +++ pg_createcluster | 952 +++++++++++ pg_ctlcluster | 649 ++++++++ pg_dropcluster | 226 +++ pg_getwal | 96 ++ pg_hba | 144 ++ pg_lsclusters | 184 +++ pg_renamecluster | 176 +++ pg_restorecluster | 435 +++++ pg_updateaptconfig | 41 + pg_updatedicts | 139 ++ pg_upgradecluster | 943 +++++++++++ pg_virtualenv | 280 ++++ pg_virtualenv.pod | 122 ++ pg_wrapper | 329 ++++ pgdg/Makefile | 2 + pgdg/apt.postgresql.org.asc | 77 + pgdg/apt.postgresql.org.gpg | Bin 0 -> 3494 bytes pgdg/apt.postgresql.org.sh | 283 ++++ pgdg/update | 12 + pgxs_debian_control.mk | 13 + postgresqlrc.5 | 42 + rpm/README | 52 + rpm/init-functions-compat | 12 + rpm/postgresql-common.spec | 139 ++ run-upgrade-scripts | 114 ++ server/pg_config.pl | 76 + server/postgresql.mk | 263 ++++ server/test-with-jit.conf | 8 + systemd/README.systemd | 55 + systemd/system-generators/postgresql-generator | 38 + systemd/system/pg_basebackup@.service | 14 + systemd/system/pg_basebackup@.timer | 12 + systemd/system/pg_compresswal@.service | 9 + systemd/system/pg_compresswal@.timer | 12 + systemd/system/pg_dump@.service | 14 + systemd/system/pg_dump@.timer | 12 + systemd/system/pg_receivewal@.service | 21 + systemd/system/postgresql.service | 18 + systemd/system/postgresql@.service | 40 + t/001_packages.t | 79 + t/002_existing_clusters.t | 11 + t/005_PgCommon.t | 311 ++++ t/006_next_free_port.t | 49 + t/007_pg_conftool.t | 85 + t/010_defaultport_cluster.t | 33 + t/015_start_stop.t | 174 ++ t/020_create_sql_remove.t | 448 ++++++ t/021_pg_renamecluster.t | 43 + t/022_recovery.t | 54 + t/025_logging.t | 99 ++ t/030_errors.t | 336 ++++ t/031_errors_disk_full.t | 86 + t/032_ssl_key_permissions.t | 60 + t/040_upgrade.t | 268 ++++ t/041_upgrade_custompaths.t | 51 + t/042_upgrade_rename.t | 27 + t/043_upgrade_ssl_cert.t | 79 + t/045_backup.t | 168 ++ t/050_encodings.t | 117 ++ t/052_upgrade_encodings.t | 83 + t/060_obsolete_confparams.t | 83 + t/070_non_postgres_clusters.t | 116 ++ t/080_start.conf.t | 145 ++ t/085_pg_ctl.conf.t | 51 + t/090_multicluster.t | 286 ++++ t/100_upgrade_scripts.t | 149 ++ t/110_integrate_cluster.t | 44 + t/120_pg_upgradecluster_scripts.t | 114 ++ t/130_nonroot_admin.t | 50 + t/135_pg_virtualenv.t | 35 + t/140_pg_config.t | 89 ++ t/150_tsearch_stemming.t | 108 ++ t/160_alternate_confroot.t | 57 + t/170_extensions.t | 87 + t/180_ecpg.t | 56 + t/190_pg_buildext.t | 82 + t/200_maintscripts.t | 46 + t/TestLib.pm | 270 ++++ t/foo/Makefile | 12 + t/foo/foo-123/Makefile | 10 + t/foo/foo-123/README.md | 5 + t/foo/foo-123/expected/foo.out | 8 + t/foo/foo-123/expected/upgrade.out | 14 + t/foo/foo-123/foo--100--123.sql | 3 + t/foo/foo-123/foo--100.sql | 3 + t/foo/foo-123/foo--123.sql | 3 + t/foo/foo-123/foo.c | 13 + t/foo/foo-123/foo.control | 2 + t/foo/foo-123/sql/foo.sql | 5 + t/foo/foo-123/sql/upgrade.sql | 7 + t/template | 26 + testsuite | 216 +++ upgrade-scripts/SPECIFICATION | 59 + user_clusters | 22 + user_clusters.5 | 63 + 129 files changed, 15477 insertions(+) create mode 100644 .vimrc create mode 100644 Makefile create mode 100644 PgCommon.pm create mode 100644 README.md create mode 100644 TODO create mode 100755 cleanpg create mode 100644 createcluster.conf create mode 100644 debhelper/Debian/Debhelper/Buildsystem/pgxs.pm create mode 100644 debhelper/Debian/Debhelper/Buildsystem/pgxs_loop.pm create mode 100644 debhelper/Debian/Debhelper/Sequence/pgxs.pm create mode 100644 debhelper/Debian/Debhelper/Sequence/pgxs_loop.pm create mode 100644 debhelper/Debian/Debhelper/pgxs.pm create mode 100755 debhelper/dh_pgxs_test create mode 100644 debhelper/dh_pgxs_test.pod create mode 100644 dh_make_pgxs/debian/control.in create mode 100644 dh_make_pgxs/debian/copyright create mode 100644 dh_make_pgxs/debian/gitlab-ci.yml create mode 100644 dh_make_pgxs/debian/pgversions create mode 100755 dh_make_pgxs/debian/rules create mode 100644 dh_make_pgxs/debian/source/format create mode 100644 dh_make_pgxs/debian/tests/control create mode 100755 dh_make_pgxs/debian/tests/installcheck create mode 100644 dh_make_pgxs/debian/watch create mode 100755 dh_make_pgxs/dh_make_pgxs create mode 100644 dh_make_pgxs/dh_make_pgxs.pod create mode 100644 doc/dependencies.dia create mode 100644 doc/dependencies.svg create mode 100644 gitlab/gitlab-ci.yml create mode 100755 pg_backupcluster create mode 100755 pg_buildext create mode 100644 pg_buildext.pod create mode 100755 pg_checksystem create mode 100755 pg_config create mode 100755 pg_conftool create mode 100755 pg_createcluster create mode 100755 pg_ctlcluster create mode 100755 pg_dropcluster create mode 100755 pg_getwal create mode 100755 pg_hba create mode 100755 pg_lsclusters create mode 100755 pg_renamecluster create mode 100755 pg_restorecluster create mode 100755 pg_updateaptconfig create mode 100755 pg_updatedicts create mode 100755 pg_upgradecluster create mode 100755 pg_virtualenv create mode 100644 pg_virtualenv.pod create mode 100755 pg_wrapper create mode 100644 pgdg/Makefile create mode 100644 pgdg/apt.postgresql.org.asc create mode 100644 pgdg/apt.postgresql.org.gpg create mode 100755 pgdg/apt.postgresql.org.sh create mode 100755 pgdg/update create mode 100644 pgxs_debian_control.mk create mode 100644 postgresqlrc.5 create mode 100644 rpm/README create mode 100644 rpm/init-functions-compat create mode 100644 rpm/postgresql-common.spec create mode 100755 run-upgrade-scripts create mode 100755 server/pg_config.pl create mode 100644 server/postgresql.mk create mode 100644 server/test-with-jit.conf create mode 100644 systemd/README.systemd create mode 100755 systemd/system-generators/postgresql-generator create mode 100644 systemd/system/pg_basebackup@.service create mode 100644 systemd/system/pg_basebackup@.timer create mode 100644 systemd/system/pg_compresswal@.service create mode 100644 systemd/system/pg_compresswal@.timer create mode 100644 systemd/system/pg_dump@.service create mode 100644 systemd/system/pg_dump@.timer create mode 100644 systemd/system/pg_receivewal@.service create mode 100644 systemd/system/postgresql.service create mode 100644 systemd/system/postgresql@.service create mode 100644 t/001_packages.t create mode 100644 t/002_existing_clusters.t create mode 100644 t/005_PgCommon.t create mode 100644 t/006_next_free_port.t create mode 100644 t/007_pg_conftool.t create mode 100644 t/010_defaultport_cluster.t create mode 100644 t/015_start_stop.t create mode 100644 t/020_create_sql_remove.t create mode 100644 t/021_pg_renamecluster.t create mode 100644 t/022_recovery.t create mode 100644 t/025_logging.t create mode 100644 t/030_errors.t create mode 100644 t/031_errors_disk_full.t create mode 100644 t/032_ssl_key_permissions.t create mode 100644 t/040_upgrade.t create mode 100644 t/041_upgrade_custompaths.t create mode 100644 t/042_upgrade_rename.t create mode 100644 t/043_upgrade_ssl_cert.t create mode 100644 t/045_backup.t create mode 100644 t/050_encodings.t create mode 100644 t/052_upgrade_encodings.t create mode 100644 t/060_obsolete_confparams.t create mode 100644 t/070_non_postgres_clusters.t create mode 100644 t/080_start.conf.t create mode 100644 t/085_pg_ctl.conf.t create mode 100644 t/090_multicluster.t create mode 100644 t/100_upgrade_scripts.t create mode 100644 t/110_integrate_cluster.t create mode 100644 t/120_pg_upgradecluster_scripts.t create mode 100644 t/130_nonroot_admin.t create mode 100644 t/135_pg_virtualenv.t create mode 100644 t/140_pg_config.t create mode 100644 t/150_tsearch_stemming.t create mode 100644 t/160_alternate_confroot.t create mode 100644 t/170_extensions.t create mode 100644 t/180_ecpg.t create mode 100644 t/190_pg_buildext.t create mode 100644 t/200_maintscripts.t create mode 100644 t/TestLib.pm create mode 100644 t/foo/Makefile create mode 100644 t/foo/foo-123/Makefile create mode 100644 t/foo/foo-123/README.md create mode 100644 t/foo/foo-123/expected/foo.out create mode 100644 t/foo/foo-123/expected/upgrade.out create mode 100644 t/foo/foo-123/foo--100--123.sql create mode 100644 t/foo/foo-123/foo--100.sql create mode 100644 t/foo/foo-123/foo--123.sql create mode 100644 t/foo/foo-123/foo.c create mode 100644 t/foo/foo-123/foo.control create mode 100644 t/foo/foo-123/sql/foo.sql create mode 100644 t/foo/foo-123/sql/upgrade.sql create mode 100644 t/template create mode 100755 testsuite create mode 100644 upgrade-scripts/SPECIFICATION create mode 100644 user_clusters create mode 100644 user_clusters.5 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..e51d89e --- /dev/null +++ b/PgCommon.pm @@ -0,0 +1,1665 @@ +=head1 NAME + +PgCommon - Common functions for the postgresql-common framework + +=head1 COPYRIGHT AND LICENSE + + (C) 2008-2009 Martin Pitt + (C) 2012-2022 Christoph Berg + +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, +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"; +} + +=head2 prepare_exec, restore_exec + + Functions for configuration + +=cut + +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; + +{ + my %saved_env; + + # untaint the environment for executing an external program + # Optional arguments: list of additional variables + 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'} = ''; + } + + # 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: + 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: + 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: + 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 () { + 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/ 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: + 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: +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: + +=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: + +=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 (); + 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: + +=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 if given. + + Arguments: + +=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 (); + 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 if given. + + Arguments: + +=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 + appended, if given. The new parameter is inserted directly after the old one. + + Arguments: + + +=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 (); + 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: + +=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: + +=cut + +sub set_cluster_port { + set_conf_value $_[0], $_[1], 'postgresql.conf', 'port', $_[2]; +} + + +=head2 cluster_data_directory + + Return cluster data directory. + + Arguments: [] + +=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: + +=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: + +=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: [] + +=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: + +=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: + 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 () { + 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: + = 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 () { + 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: + = 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: + +=cut + +sub read_pidfile { + return undef unless -e $_[0]; + + if (open PIDFILE, $_[0]) { + my $pid = ; + 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: + + 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 = ; + 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: + + 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 = ; + 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: + 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 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: + +=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: + +=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 () { + 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 () { + 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: + +=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: + +=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: + +=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: + 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 = ; + 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: + Returns: (LC_CTYPE, LC_COLLATE) or (undef,undef) if it cannot be determined. + +=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); + + # 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', + 'SHOW lc_ctype', $db or + die "Internal error: could not call $psql to determine db lc_ctype: $!"; + my $out = // error 'could not determine db lc_ctype'; + close PSQL; + ($ctype) = $out =~ /^([\w.\@-]+)$/; # untaint + open PSQL, '-|', $psql, '-h', $socketdir, '-p', $port, '-AXtc', + 'SHOW lc_collate', $db or + die "Internal error: could not call $psql to determine db lc_collate: $!"; + $out = // error 'could not determine db lc_collate'; + close PSQL; + ($collate) = $out =~ /^([\w.\@-]+)$/; # untaint + $> = $orig_euid; + restore_exec; + chomp $ctype; + chomp $collate; + return ($ctype, $collate) 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: + 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 () { + 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: + 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 () { + 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: + 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 () { + 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: + 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 () { + 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: + 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: + 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 () { + 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 +# (C) 2018 Christoph Berg +# +# 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, look for the package name containing the +B placeholder, and return it in the format suitable for passing to +B, i.e. with B replaced by B<%v>. + +For B it will return B. + +Errors out if more than one package with the B placeholder is found. + +=cut + +sub package_pattern () { + open F, "debian/control.in" or die "debian/control.in: $!"; + my $pattern; + while () { + 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 [B] + +=head1 DESCRIPTION + +B extensions need to be installed before they can be tested and +hence the usual B way of invoking tests from dh_auto_test(1) does +not work. + +B is a dh(1) sequence point created by the B and +B B extensions that is executed after dh_auto_install(1). +It calls B after a B extension module has +been built and installed into the CI directory. + +Users wishing to change the action called by B should call +B or similar commands. + + override_dh_pgxs_test: + echo "CREATE EXTENSION foo" | pg_buildext psql . . postgresql-%v-foo + +=head1 OPTIONS + +=over 4 + +=item B + +B builds packages in C subdirectories. The B +options corresponds to B 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 Lmyon@debian.orgE> 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 +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 +# +# 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 [B<-f>] [B<-h> I] [B<-n> I] [B<-v> I] + +=head1 DESCRIPTION + +B creates a F directory tree for PostgreSQL extension +packages using the PGXS build system. The B tool is used for the +build process. + +=head1 OPTIONS + +=over 4 + +=item B<-f> + +Overwrite existing files. + +=item B<-h> I + +Package upstream homepage. + +=item B<-n> I + +Package name to use. Default is to extract it from the current directory's name. + +=item B<-v> I + +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 Lmyon@debian.orgE> diff --git a/doc/dependencies.dia b/doc/dependencies.dia new file mode 100644 index 0000000..ba23097 Binary files /dev/null and b/doc/dependencies.dia 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 @@ + + + + + + + + + + + postgresql:all + M-A:none + + + + + + postgresql-client:all + M-A:foreign + + + + + + + + + + postgresql-NN:any + M-A:none + Provides: postgresql-contrib-NN + + + + + + postgresql-client-NN:any + M-A:foreign + + + + + + postgresql-common:all + M-A:foreign + + + + + + postgresql-client-common:all + M-A:foreign + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + libpq5:amd64 + M-A:same + + + + + + libpq-dev:amd64 + M-A:none + + + + + + + + + + postgresql-doc-NN:all + M-A:foreign + + + + + + postgresql-doc:all + M-A:foreign + + + + + + + + + + postgresql-contrib:all + M-A:none + + + + + + + + + + postgresql-server-dev-NN:any + M-A:none + + + + + + postgresql-server-dev-all:any + M-A:same + + + + + + + + + + + + postgresql-common + + + postgresql-NN + + + + + + + + + + + + + + + + + + + + + postgresql-all:all + M-A:none + + + + + + + + + + + + + + postgresql-plXX:any + M-A:none + Provides: postgresql-plXX + + + + + + + + + + + + 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..8fa49f2 --- /dev/null +++ b/pg_backupcluster @@ -0,0 +1,614 @@ +#!/usr/bin/perl -wT + +# simple pg_basebackup front-end +# +# Copyright (C) 2021 Christoph Berg +# +# 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] +Actions: + createdirectory Create /var/backups/version-cluster + basebackup Backup using pg_basebackup + dump Backup using pg_dump + expiredumps Remove all but last N dumps + expirebasebackups 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: + -k --keep-on-error Keep faulty backup directory on error + -v --verbose Verbose output +"; +} + +my $keep_on_error; +my $verbose; + +exit 1 unless GetOptions ( + '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 $suffix = shift; + my $timestamp = strftime("%FT%H%M%SZ", gmtime); + 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" : ()), + "--format", "tar", "--gzip", + ($version < 10 ? "--xlog" : ()), + "-D", $backupdir; +} + +sub create_dumpall($) { + my $backupdir = shift; + mkdir($backupdir, 0750) or error "mkdir $backupdir: $!"; + + my $clusterquery = "SELECT concat( + format('--encoding %s --lc-collate %s --lc-ctype %s', pg_catalog.pg_encoding_to_char(encoding), datcollate, datctype), + CASE WHEN current_setting('data_checksums')::boolean THEN ' -- --data-checksums' END) +FROM pg_database WHERE datname = 'template0'"; + system_or_error "psql", + "--cluster", "$version/$cluster", + "-XAtc", $clusterquery, + "-o", "$backupdir/createcluster.opts"; + + system_or_error "pg_dumpall", + "--cluster", "$version/$cluster", + "--globals-only", + "--file", "$backupdir/globals.sql"; + + 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) 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 => scalar(gmtime($endtime)), + start => scalar(gmtime($starttime)), + status => $status, + type => $type, + version => $version, + }; + + open F, '>', $statusfile or error "$statusfile: $!"; + print F encode_json($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('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('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 [I] I I I + +=head1 DESCRIPTION + +B provides a simple interface to create PostgreSQL cluster +backups using L and L. + +To ease integration with B operation, the alternative syntax +"B IB<->I I" is also supported. + +=head1 ACTIONS + +=over 4 + +=item B + +Create /var/backups and /var/backups/I-I. +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 + +Backup using L. The resulting basebackup contains the WAL +files required to run recovery on startup. + +=item B + +Backup using L. Global objects (users, tablespaces) are dumped +using L B<--globals-only>. Individual databases are dumped into +PostgreSQL's custom format. + +=item B I + +Remove all but last the I basebackups. + +=item B I + +Remove all but last the I dumps. + +=item B + +Launch pg_receivewal. WAL files are gzip-compressed in PG 10+. + +=item B + +Compress WAL files in archive. + +=item B + +Remove obsolete WAL files from archive using L. + +=item B + +Show dumps, basebackups, and WAL, with size. + +=back + +=head1 OPTIONS + +=over 4 + +=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-I + +Default directory for cluster backups. + +=item /var/backups/I-I/IB<.basebackup> + +Backup from B. + +=over 4 + +=item C + +Tarball of cluster configuration directory (postgresql.conf, pg_hba.conf, ...) +in /etc/postgresql. + +=item IC<.tar.gz>, C, C + +Tablespace and WAL tarballs and backup info written by B. + +=item C + +Completion timestamp of backup run. + +=back + +=item /var/backups/I-I/IB<.dump> + +Backup from B. + +=over 4 + +=item C + +Tarball of cluster configuration directory (postgresql.conf, pg_hba.conf, ...) +in /etc/postgresql. + +=item C + +Options (encoding, locale, data checksums) to be passed to B +for restoring this cluster. + +=item C + +Global objects (roles, tablespaces) from B. + +=item C + +SQL commands to create databases and restore database-level options. + +=item IC<.dump> + +Database dumps from B. + +=item C + +Completion timestamp of backup run. + +=back + +=item /var/backups/I-I/B + +WAL files from B. + +=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 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 (and hence archive recovery) are supported in 9.5 and later. + +=head1 SEE ALSO + +L, +L, L, +L, L, L. + +=head1 AUTHOR + +Christoph Berg Lmyon@debian.orgE> diff --git a/pg_buildext b/pg_buildext new file mode 100755 index 0000000..d986dfc --- /dev/null +++ b/pg_buildext @@ -0,0 +1,467 @@ +#!/bin/sh +# +# build a PostgreSQL module based on PGXS for given list of supported major +# versions +# +# (C) 2010 Dimitri Fontaine +# (C) 2011-2022 Christoph Berg +# +# 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" +if [ -d "${2:-}" ] && [ "${3:-}" ]; then # optional: $2 is source directory + srcdir="${2:-}" + [ "$srcdir" = "." ] && srcdir="$PWD" + shift +else + srcdir="$PWD" +fi +target="${2:-}" +opt="${3:-}" + +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_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 $newest_pgversion = $ENV{PGVERSIONS} =~ s/.*\s//sr; + while (<>) { + chomp; + if (/^Package: .*PGVERSION/) { + foreach my $version (split /\n/, $ENV{PGVERSIONS}) { + push @out, s/PGVERSION/$version/rg; + } + } else { + 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 +} + +# 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 + ;; + + 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 [ -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 [] []" + 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..37063f9 --- /dev/null +++ b/pg_buildext.pod @@ -0,0 +1,344 @@ +=head1 NAME + +pg_buildext - Build and install a PostgreSQL extension + +=head1 SYNOPSIS + +B [I] I [I] [I] + +=head1 DESCRIPTION + +B is a script that will build a PostgreSQL extension in a C +way, for potentially several PostgreSQL server versions in parallel. +It builds for the intersection of versions known in +C (versions supported by the package) and in +C (versions supported in this +release). + +Many PostgreSQL extension packages require no special handling at build time +and can use B or B to +automatically execute the steps outlined below. + +=head1 USAGE + +Packages using B 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. + +As the set of binary packages depends on the target PostgreSQL versions, +C is generated from a template in C when +B is run. +Package sections that contain B in the package name are replaced by +a list of sections, filling in the supported PostgreSQL versions. +Package sections that contain B outside the package name have the +newest supported PostgreSQL version filled in (useful for meta packages). +Include +C in C to +run a check at build time if updating debian/control is required. + +As B invokes B for the B, B, and B +actions, invocations from C (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 via the B<-m> option. + +Many extensions support B testing using B. As +this needs the package to be installed, it cannot be run at build time. +Instead, the tests should be run using B from C. + +If C exists, occurrences of package names containing +B are replaced by lists of package names with the target PostgreSQL +versions filled in. (If no replacing is needed in C, it +is fine to provide the tests control file directly.) + +=head1 OPTIONS + +=over 4 + +=item B<-cio> I + +=item B<-s> + +Passed to B when running B. + +=item B<-m> I + +Passed to B. + +=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 contains a C<%v> +sign, it will get replaced by the specific version of PostgreSQL being built +against. (Usually this parameter is C.) + +=over 4 + +=item B + +Print effective list of supported versions, i.e. the intersection of the sets +of versions supported by the system +(from C) and the package +(from C). + +Use this when building packages. + +=item B + +In the list of installed packages, look for packages matching the B +package name templates from C, and print the PostgreSQL +major version number part. + +Use this when testing packages. + +=item B + +Check if C needs updating from C. This is +invoked from C. When +building for a B or B suite as determined by +C, this action also updates the control file. Otherwise, +B needs to be run manually. + +=item B + +Update C from C, and C +from C if the latter exists. + +=item B [I] I [I] + +For every supported version, call B<../configure> from the I +directory. (Most PostgreSQL extensions do not have a configure script.) + +=item B [I] I [I] + +Build the extension in the I directory. + +=item B [I] I I + +Invoke B from the I directory. +The third parameter specifies the package name to use. Most packages +use B. Make will be +called with DESTDIR="$(CURDIR)/debian/I". + +The B substitution variable B is set to depend +on the required PostgreSQL server package. For compatibility with previous +packaging standards, the dependency is also added to B if +postgresql:Depends is not used. + +=item B [I] [I] [I] + +Clean the build directories. + +=item B [I] I + +As a variant to calling B and B 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, it should be placed were installation happens in debian/rules, +rather than where build would normally be called. + +=item B [I] [I] [I] + +Use B to run the extension regression tests. +This is meant to be run from C using B. If +I is omitted, the top source directory is used. + +If I is given, options are passed to B to set +up the temporary PostgreSQL instance to find extension files in +C. + +Other than the other actions which run on the "supported" versions, if C exists, this one +runs on the "installed" versions as reported by B (unless +I is provided, which means we are called during a build). + +=item B [I] [I] [I] + +=item B [I] [I] [I] + +Like B, but invokes B, or a shell, both wrapped in +B. Input is read from stdin. + +=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 +IB<->I. See the installcheck example below. + +=head1 SUPPORTED VERSIONS + +B reads C 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 + +Support all versions. This is recommended unless there are known incompatibilities. + +=item I + +Support this version. + +=item IB<+> + +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. See this file for how to +configure the list of supported versions on your system. + +=head1 EXAMPLE + +=over 4 + +=item B + + Source: postgresql-foobar + Rules-Requires-Root: no + Build-Depends: + debhelper, + postgresql-all , + postgresql-server-dev-all (>= 217~), + + Package: postgresql-PGVERSION-foobar + Architecture: any + Depends: + ${misc:Depends}, + ${postgresql:Depends}, + ${shlibs:Depends}, + +=item B + + all + + # alternatives: + #9.6 + #11+ + +=item B using B: + + #!/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: + + #!/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 using B 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 + + Depends: @, postgresql-server-dev-all + Tests: installcheck + Restrictions: allow-stderr + +=item B (optional) + + Depends: @, postgresql-contrib-PGVERSION, postgresql-PGVERSION-bar + Tests: installcheck + Restrictions: allow-stderr + +=item B + + #!/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 as subdirectory), use the I argument, where +I must be an absolute path. Example: + + override_dh_auto_build: + +pg_buildext build $(CURDIR)/postgresql-module build-%v + +=head1 COMPATIBILITY + +B 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 was introduced in postgresql-server-dev-all (>= +153~). + +B was introduced in postgresql-common (>= 170~). + +Handling of C with B replacement was +introduced in postgresql-common (>= 171~). + +The action B was introduced in postgresql-common (>= 208~). +B was switched to use it in the same version. + +B and B, the corresponding B<--buildsystem>, and +the B and B actions were introduced in postgresql-server-dev-all (>= 217~). + +=head1 SEE ALSO + +C, autopkgtest(1), +pg_virtualenv(1). + +=head1 AUTHORS + +Dimitri Fontaine Ldim@tapoueh.orgE>, with extensions by +Christoph Berg Lmyon@debian.orgE>. 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 +# +# 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: +# 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 ". +'; +} 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 +# (C) 2014-2018 Christoph Berg +# +# 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 +# +# 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] [ ] [] + +Options: + -b --boolean Format output as boolean + -s --short Print only value + -v --verbose Verbose output + --help This help + +Commands: + show |all + set + remove + 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 [I] [I I] [I] B + +=head1 DESCRIPTION + +B allows showing and setting parameters in PostgreSQL configuration +files. + +If I I is omitted, it defaults to the default cluster (see +user_clusters(5) and postgresqlrc(5)). If I is omitted, it defaults +to B. I can also be a path, in which case +I I is ignored. + +=head1 OPTIONS + +=over 4 + +=item B<-b>, B<--boolean> + +Format boolean value as B or B (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 I|B + +Show a parameter, or all present in this config file. + +=item B I I + +Set or update a parameter. + +=item B I + +Remove (comment out) a parameter from a config file. + +=item B + +Open the config file in an editor. Unless B<$EDITOR> is set, B is used. + +=back + +=head1 SEE ALSO + +user_clusters(5), postgresqlrc(5) + +=head1 AUTHOR + +Christoph Berg Lmyon@debian.orgE> diff --git a/pg_createcluster b/pg_createcluster new file mode 100755 index 0000000..4f9eb6a --- /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 +# (C) 2012-2021 Christoph Berg +# +# 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'} = '/bin:/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 in +# postgresql.conf. +# Arguments: +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] [-- ] + +Options: + -u cluster owner and superuser (default: 'postgres') + -g group for data files (default: primary group of owner) + -d data directory (default: + /var/lib/postgresql//) + -s socket directory (default: /var/run/postgresql for clusters + owned by 'postgres', /tmp for other clusters) + -l path to desired log file (default: + /var/log/postgresql/postgresql--.log) + --locale + set cluster locale (default: inherit from environment) + --lc-collate/ctype/messages/monetary/numeric/time + like --locale, but only set for a particular category + -e Default encoding (default: derived from locale) + -p 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 + 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 [I] I I [B<--> I] + +=head1 DESCRIPTION + +B creates a new PostgreSQL server cluster (i. e. a +collection of databases served by a L instance) and +integrates it into the multi-version/multi-cluster architecture of the +B 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
. However, you might wish to create other clusters for +testing, with other superusers, a cluster for each user on a shared server, +etc. C 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 service units, the cluster name should not +contain any dashes (B<->). B will warn about the problem, but +succeed with the operation. + +Given a major PostgreSQL I (like "8.2" or "8.3") and a cluster +I, it creates the necessary configuration files in +CICIC; in particular these are +C, C, C, a postgresql-common +specific configuration file C (see B below), +C, and a symbolic link C which points to the log file (by +default, CIC<->IC<.log>). + +C 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 command is +used to generate a new cluster structure. If the data directory already exists, +it is integrated into the B structure by moving the +configuration file and setting the data_directory option. Please note that this +I works for data directories which were created directly with B, i. +e. all the configuration files (C 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 can be customized to specify C and/or +C; if at least one of these options is present, then the symbolic +link C in the cluster configuration directory is ignored. + +If the default snakeoil SSL certificate exists +(C and +C), and the C user is in the +C Unix group, B 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 and C); for 9.2 and later, the +appropriate C options will be set (C and +C). Of course you can replace this with a cluster specific +certificate. Similarly for C and +C, these files will be configured as client +certificate CA and revocation list, when present. (C 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, B<--user=>I + +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. A cluster must +not be owned by root. + +=item B<-g> I, B<--group=>I + +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, B<--datadir=>I + +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 +CICI. + +=item B<-s> I, B<--socketdir=>I + +Explicitly set the directory where the L server stores the Unix +socket for local connections. Defaults to C for clusters +owned by the user B, and C for clusters owned by other users. +Please be aware that C 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, B<--logfile=>I + +Explicitly set the path for the L server log file. Defaults to +CIC<->IC<.log>. + +=item B<--locale=>I + +Set the default locale for the database cluster. If this option is not +specified, the locale is inherited from the environment that +B runs in. + +=item B<--lc-collate=>I + +=item B<--lc-ctype=>I + +=item B<--lc-messages=>I + +=item B<--lc-monetary=>I + +=item B<--lc-numeric=>I + +=item B<--lc-time=>I + +Like B<--locale>, but only sets the locale in the specified category. + +=item B<-e> I, B<--encoding=>I + +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: It is not recommended to set this option directly! Set the locale +instead. + +=item B<-p> I, B<--port=>I + +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 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 I C on it). By default, the cluster is +not started. + +=item B<--start-conf=>B|B|B + +Set the initial value in the C configuration file. See B below. By default, B is used, which means that the cluster is +handled by C, i. e. starts and stops +automatically on system boot. + +=item B<-o> IB<=>I, B<--pgoption> IB<=>I + +Configuration option to set in the new C file. + +=item B<--createclusterconf=>I + +Alternative B file to use. Default is +C (or +C<$PGSYSCONFDIR/createcluster.conf>). + +=item B<--environment=>I + +Alternative default B file to use. Default is +C (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 + +Options passed directly to L. + +Per default, B will update the C 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 file +will be left untouched. + +I If only one of B<--auth-host> and B<--auth-local> is provided, the +other setting will default to B as per B's defaults, opening a +potential security risk. + +=back + +=head1 STARTUP CONTROL + +The C 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 + +The postgres process is started/stopped automatically in the init script. + +When running from B, the cluster is started/stopped when +B is started/stopped. +This is also the default if the file is missing. + +=item B + +The postgres process is not handled by the init script, but manually +controlling the cluster with L is permitted. + +When running from B, the cluster is not started automatically when +B is started. However, stopping/restarting +B will stop/restart the cluster. The cluster can be started +using BIB<->I. + +=item B + +Neither the init script, L, nor B 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, invoke B after editing +C. + +The C file in the cluster configuration directory can contain +additional options passed to B of that cluster. + +=head1 DEFAULT VALUES + +Some default values used by B can be modified in +C. 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 (Default: B) + +Create a B
cluster when a new postgresql-NN server package is installed. + +=item B (Default: B) + +Default C value to use. + +=back + +=over 4 + +=item B (Default: B) + +Default data directory. + +=item B (Default: unset) + +Default directory for transaction logs. When used, B will create a +symlink from C (PostgreSQL 9.6 and earlier: C) 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 (Default: unset) + +Other options to pass to B. + +=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 + +Only added to postgresql.conf if the default snakeoil certificates exist and +are readable for the cluster owner as detailed above. + +=item B + +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 + +=item B + +=item B + +B supports the same include directives as +B. + +=item B + +=item B + +=item B + +To add include directives to the new postgresql.conf file, use the B +directives. The B prefix is removed. + +=back + +=back + +=head1 SEE ALSO + +L, L, L, L + +=head1 AUTHORS + +Martin Pitt Lmpitt@debian.orgE>, Christoph Berg Lmyon@debian.orgE> 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 +# (C) 2009 Cyril Bouthors +# (C) 2013-2021 Christoph Berg +# +# 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: +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 ; + } + 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 ; + } 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 [-- ]"; +} + +@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 [I] I I I [B<--> I] + +where I = B|B|B|B|B|B + +=head1 DESCRIPTION + +This program controls the B server for a particular cluster. It +essentially wraps the L command. It determines the cluster version +and data path and calls the right version of B 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 operation, the alternative syntax +"B IB<->I I" is also supported, +as well as putting the action first (matching the ordering used by B). + +=head1 ACTIONS + +=over 4 + +=item B + +A log file for this specific cluster is created if it does not exist yet (by +default, +CIC<->IC<.log>), +and a PostgreSQL server process (L) 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 + +Stops the L server of the given cluster. By default, "fast" +shutdown mode is used. + +=item B + +Stops the server if it is running and starts it (again). + +=item B + +Causes the configuration files to be re-read without a full shutdown of the +server. + +=item B + +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 + +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 and B, 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 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|B|B] + +Shutdown mode to use for B and B actions, default is B. +See pg_ctl(1) for documentation. + +=item B<--foreground> + +Start B in foreground, without daemonizing via B. + +=item B<--stdlog> + +When B<--foreground> is in use, redirect stderr to the standard logfile in +C. (Default when not run in foreground.) + +=item B<--skip-systemctl-redirect> + +When running as root, B redirects actions to B so +running clusters are properly supervised by B. This option skips the +redirect; it is used in the B unit file. The redirect is +also skipped if additional B or B options are provided. + +=item B<--bindir> I + +Path to B. (Default is CIC.) + +=item B<-o>|B<--options> I + +This configuration file controls the start/stop behavior of the cluster. See +section "STARTUP CONTROL" in L 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, L, L, L, +L + +=head1 AUTHOR + +Martin Pitt Lmpitt@debian.orgE> + diff --git a/pg_dropcluster b/pg_dropcluster new file mode 100755 index 0000000..9b2c7d0 --- /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 +# (C) 2015-2021 Christoph Berg +# +# 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'} = '/bin:/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] \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 () { + 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 [B<--stop>] I I + +=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. If the configuration directory +(CICI) is empty after this, it is +removed as well. +An empty socket directory other than B or B 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, L + +=head1 AUTHOR + +Martin Pitt Lmpitt@debian.orgE> + 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 +# +# 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 I I<%p> + +=head1 DESCRIPTION + +B retrieves and decompresses files from a WAL archive maintained by +B and B. It is put into PostgreSQL's +B by B. + +=head1 SEE ALSO + +L, L. + +=head1 AUTHOR + +Christoph Berg Lmyon@debian.orgE> + +=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 +# +# 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: +sub match_all { + return ($_[1] eq 'all' || $_[0] eq $_[1]); +} + +# Check if given IP matches the specification in the HBA record. +# Arguments: +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] \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 +# (C) 2013-2018 Christoph Berg +# +# 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} // ''; # 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'} // '', $status, + defined $info{'owneruid'} ? (getpwuid $info{'owneruid'})[0] : '', + $info{'pgdata'} || '', $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 [I] [I [I]] + +=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 information in status column. + +=item B<--help> + +Print usage help. + +=back + +=head1 NOTES + +The cluster status is shown as B or B. If a F 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 Lmpitt@debian.orgE> diff --git a/pg_renamecluster b/pg_renamecluster new file mode 100755 index 0000000..b8e66c6 --- /dev/null +++ b/pg_renamecluster @@ -0,0 +1,176 @@ +#!/usr/bin/perl -wT + +# Rename a PostgreSQL cluster +# +# (C) 2014-2021 Christoph Berg +# +# 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'} = '/bin:/usr/bin'; +delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; + +if (@ARGV != 3) { + print "Usage: $0 [OPTIONS] \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: , , +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 I I I + +=head1 DESCRIPTION + +B changes the name of a PostgreSQL cluster, i. e. the name of +the config directory in /etc/postgresql/I/ along with the data +directory in /var/lib/postgresql/I/. Existing log files in +/var/log/postgresql/ are also renamed. The cluster is stopped and started for +the operation. + +The following B config options are updated to refer to the +changed path names: B, B, B, +B, B, B. + +=head1 OPTIONS + +None. + +=head1 SEE ALSO + +L, L, L, L + +=head1 AUTHOR + +Christoph Berg Lmyon@debian.orgE> diff --git a/pg_restorecluster b/pg_restorecluster new file mode 100755 index 0000000..95a4d6d --- /dev/null +++ b/pg_restorecluster @@ -0,0 +1,435 @@ +#!/usr/bin/perl -wT + +# pg_restorecluster: restore from a pg_backupcluster backup +# +# Copyright (C) 2021 Christoph Berg +# +# 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] +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 /../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 [I] I I I + +=head1 DESCRIPTION + +B restores a PostgreSQL cluster from a backup created by +B. 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 name passed must end in either B<.basebackup> or B<.dump>; +usually this will be the full path to a backup directory in +C as reported by +B. + +Basebackups are restored as-is. For dumps, B is used to +create a new cluster, and schema and data are restored via B. + +=head1 OPTIONS + +=over 4 + +=item B<-d --datadir> I + +Use I as data directory for the restored cluster (default per +createcluster.conf, by default /var/lib/postgresql/I/I). + +=item B<-p --port> I + +Use port I 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 is run on all databases. + +=item B<--archive> + +Configure cluster for recovery from WAL archive. This sets B +to retrieve WAL files from IB. + +=item B<--pitr> I + +=item B<--recovery-target-time> I + +Additionally to setting B, set B to +I for point-in-time recovery. Also sets +B. + +=item B<--wal-archive> I + +For archive recovery, read WAL from archive I (default is +IB). + +=back + +=head1 FILES + +=over 4 + +=item /var/backups + +Default root directory for cluster backup directories. + +=back + +See L for a description of files. + +=head1 SEE ALSO + +L, L, L. + +=head1 AUTHOR + +Christoph Berg Lmyon@debian.orgE> 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 <> $TMPCONF +done + +cat >> $TMPCONF < +# (C) 2012-2017 Christoph Berg +# +# 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 + +=head1 DESCRIPTION + +B 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/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 Lmpitt@debian.orgE> diff --git a/pg_upgradecluster b/pg_upgradecluster new file mode 100755 index 0000000..4a4fd17 --- /dev/null +++ b/pg_upgradecluster @@ -0,0 +1,943 @@ +#!/usr/bin/perl -wT + +# Upgrade a PostgreSQL cluster to a newer major version. +# +# (C) 2005-2009 Martin Pitt +# (C) 2013 Peter Eisentraut +# (C) 2013-2022 Christoph Berg +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +use strict; +use warnings; +use PgCommon; +use File::Temp qw(tempfile); +use Getopt::Long; +use POSIX qw(lchown); + +# untaint environment +$ENV{'PATH'} = '/bin:/usr/bin'; +delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; + +# global variables +my ($version, $newversion, $cluster, $newcluster); +my (%info, %newinfo); +my ($encoding, $old_lc_ctype, $old_lc_collate); # old cluster encoding +my $maintenance_db = 'template1'; + +# do not trip over cwd not being accessible to postgres superuser +chdir '/'; + +# update the new cluster's conffiles +sub adapt_conffiles { + my ($newversion, $newcluster, $configfile) = @_; + my %c = read_cluster_conf_file $newversion, $newcluster, $configfile; + + # Arguments: + my $deprecate = sub { + my ($conf, $guc, $comment) = @_; + if (defined $conf->{$guc}) { + PgCommon::disable_conf_value $newversion, $newcluster, + $configfile, $guc, $comment; + } + }; + + # Arguments: + 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: + 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'); + } +} + +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 () { + 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: +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: +sub get_encoding { + my ($version, $cluster) = @_; + $encoding = get_db_encoding $version, $cluster, $maintenance_db; + if ($version <= '8.3') { + ($old_lc_ctype, $old_lc_collate) = get_cluster_locales $version, $cluster; + } else { + ($old_lc_ctype, $old_lc_collate) = get_db_locales $version, $cluster, $maintenance_db; + } + unless ($encoding && $old_lc_ctype && $old_lc_collate) { + error 'could not get cluster locales'; + } +} + +# RedHat's run-parts doesn't support any options, supply a minimalistic implementation here +# BUG: we don't care about validating the filenames yet +# Arguments: +sub run_parts { + my ($dir, @argv) = @_; + for my $script (<$dir/*>) { + my ($s) = $script =~ /(.*)/; # untaint + system ($s, @argv); + error "$s failed: $?" if ($?); + } +} + + +# +# Execution starts here +# + +# command line arguments + +my $newest_version = get_newest_version('postgres'); +$newversion = $newest_version; + +my $method = 'dump'; +my $link = 0; +my $clone = 0; +my $keep_port = 0; +my $start = -1; # -1 = auto + +my ($locale, $lc_collate, $lc_ctype, $lc_messages, $lc_monetary, $lc_numeric, + $lc_time, $logfile, $old_bindir, $jobs); +GetOptions ('v|version=s' => \$newversion, + 'locale=s' => \$locale, + 'lc-collate=s' => \$lc_collate, + 'lc-ctype=s' => \$lc_ctype, + 'lc-messages=s' => \$lc_messages, + 'lc-monetary=s' => \$lc_monetary, + 'lc-numeric=s' => \$lc_numeric, + 'lc-time=s' => \$lc_time, + 'logfile=s' => \$logfile, + 'm|method=s' => \$method, + 'j|jobs=s', => \$jobs, + 'k|link' => \$link, + 'clone' => \$clone, + 'keep-port' => \$keep_port, + 'rename=s' => \$newcluster, + 'old-bindir=s' => \$old_bindir, + 'maintenance-db=s' => \$maintenance_db, + 'start!' => \$start, + ) or exit 1; + +if ($method eq 'dump') { + error 'cannot use --link with --method=dump' if ($link); + error 'cannot use --clone with --method=dump' if ($clone); + error 'cannot use --jobs with --method=dump' if ($jobs); +} elsif ($method eq 'link') { + $method = 'upgrade'; + $link = 1; +} elsif ($method eq 'clone') { + $method = 'upgrade'; + $clone = 1; +} elsif ($method ne 'upgrade') { + error 'method must be "dump", "upgrade", "link", or "clone"'; +} + +# untaint +($newversion) = $newversion =~ /^(\d+\.?\d+)$/; +($locale) = $locale =~ /^([\w@._-]+)$/ if $locale; +($lc_collate) = $lc_collate =~ /^([\w@._-]+)$/ if $lc_collate; +($lc_ctype) = $lc_ctype =~ /^([\w@._-]+)$/ if $lc_ctype; +($lc_messages) = $lc_messages =~ /^([\w@._-]+)$/ if $lc_messages; +($lc_monetary) = $lc_monetary =~ /^([\w@._-]+)$/ if $lc_monetary; +($lc_numeric) = $lc_numeric =~ /^([\w@._-]+)$/ if $lc_numeric; +($lc_time) = $lc_time =~ /^([\w@._-]+)$/ if $lc_time; +($logfile) = $logfile =~ /^([^\n]+)$/ if $logfile; +($old_bindir) = $old_bindir =~ /^(\/.*)$/ if $old_bindir; +($maintenance_db) = $maintenance_db =~ /^([\w-]+)$/ if $maintenance_db; +($jobs) = $jobs =~ /^(\d+)$/ if $jobs; + +if ($#ARGV < 1) { + print "Usage: $0 [OPTIONS] []\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; +delete $ENV{'LC_ALL'}; +push @argv, ('--', '--data-checksums') if $oldcontrol->{'Data page checksum version'}; # 0 = off +error "Could not create target cluster" if system @argv; +print "\n"; + +# 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 +if ($upgrade_scripts) { + print "Running phase upgrade hook scripts...\n"; + if (!fork) { + change_ugid $info{'owneruid'}, $info{'ownergid'}; + + if ($PgCommon::rpm) { + run_parts ("$PgCommon::common_confdir/pg_upgradecluster.d", + $version, $newcluster, $newversion, 'init'); + exit; + } + + @argv = ('run-parts', '--lsbsysinit', '-a', $version, '-a', $newcluster, + '-a', $newversion, '-a', 'init', + "$PgCommon::common_confdir/pg_upgradecluster.d"); + error "$PgCommon::common_confdir/pg_upgradecluster.d script failed" if system @argv; + exit 0; + } + wait; + if ($? > 0) { + print STDERR "Error during running upgrade hooks, removing new cluster\n"; + system 'pg_dropcluster', '--stop', $newversion, $newcluster; + exit 1; + } +} + +# dump cluster; drop to cluster owner privileges + +if (!fork) { + change_ugid $info{'owneruid'}, $info{'ownergid'}; + my $pg_dumpall = get_program_path 'pg_dumpall', $newversion; + my $pg_dump = get_program_path 'pg_dump', $newversion; + my $psql = get_program_path 'psql'; + my $newsocket = get_cluster_socketdir $newversion, $newcluster; + + if ($method eq 'dump') { + # get list of databases (value = datallowconn) + my %databases; + open F, '-|', $psql, '-h', $oldsocket, '-p', $info{'port'}, + '-F|', '-d', $maintenance_db, '-AXtc', + 'SELECT datname, datallowconn FROM pg_database' or + error 'Could not get pg_database list'; + while () { + chomp; + my ($n, $a) = split '\|'; + $databases{$n} = ($a eq 't'); + } + close F; + error 'could not get list of databases' if $?; + + # Temporarily enable access to all DBs, so that we can upgrade them + for my $db (keys %databases) { + next if $db eq 'template0'; + + unless ($databases{$db}) { + print "Temporarily enabling access to database $db\n"; + (system $psql, '-h', $oldsocket, '-p', $info{'port'}, '-qX', + '-d', $maintenance_db, '-c', + "BEGIN READ WRITE; UPDATE pg_database SET datallowconn = 't' WHERE datname = '$db'; COMMIT") == 0 or + error 'Could not enable access to database'; + } + } + + # dump schemas + print "Roles, databases, schemas, ACLs...\n"; + open SOURCE, '-|', $pg_dumpall, '-h', $oldsocket, '-p', $info{'port'}, + '-s', '--quote-all-identifiers' or error 'Could not execute pg_dumpall for old cluster'; + my $data = ''; + my $buffer; + while (read SOURCE, $buffer, 1048576) { + $data .= $buffer; + } + close SOURCE; + ($? == 0) or exit 1; + + # remove creation of db superuser role to avoid error message + $data =~ s/^CREATE (ROLE|USER) "\Q$owner\E";\s*$//m; + + # create global objects in target cluster + open SINK, '|-', $psql, '-h', $newsocket, '-p', $newinfo{'port'}, + '-qX', '-d', $maintenance_db or + error 'Could not execute psql for new cluster'; + + # ensure that we can upgrade tables for DBs with default read-only + # transactions + print SINK "BEGIN READ WRITE; ALTER USER $owner SET default_transaction_read_only to off; COMMIT;\n"; + + print SINK $data; + + close SINK; + ($? == 0) or exit 1; + + + # Upgrade databases + for my $db (keys %databases) { + next if $db eq 'template0'; + + print "Fixing hardcoded library paths for stored procedures...\n"; + # starting from 9.0, replace() works on strings; for earlier versions it + # works on bytea + if ($version >= '9.0') { + (system $psql, '-h', $oldsocket, '-p', $info{'port'}, '-qX', '-d', + $db, '-c', "BEGIN READ WRITE; \ + UPDATE pg_proc SET probin = replace(\ + replace(probin, '/usr/lib/postgresql/lib', '\$libdir'), \ + '/usr/lib/postgresql/$version/lib', '\$libdir'); COMMIT") == 0 or + error 'Could not fix library paths'; + } else { + (system $psql, '-h', $oldsocket, '-p', $info{'port'}, '-qX', '-d', + $db, '-c', "BEGIN READ WRITE; \ + UPDATE pg_proc SET probin = decode(replace(\ + replace(encode(probin, 'escape'), '/usr/lib/postgresql/lib', '\$libdir'), \ + '/usr/lib/postgresql/$version/lib', '\$libdir'), 'escape'); COMMIT") == 0 or + error 'Could not fix library paths'; + } + + print 'Upgrading database ', $db, "...\n"; + open SOURCE, '-|', $pg_dump, '-h', $oldsocket, '-p', $info{'port'}, + '-Fc', '--quote-all-identifiers', $db or + error 'Could not execute pg_dump for old cluster'; + + # start pg_restore and copy over everything + my @restore_argv = ($pg_restore, '-h', $newsocket, '-p', + $newinfo{'port'}, '--data-only', '-d', $db, + '--disable-triggers', '--no-data-for-failed-tables'); + open SINK, '|-', @restore_argv or + error 'Could not execute pg_restore for new cluster'; + + my $buffer; + while (read SOURCE, $buffer, 1048576) { + print SINK $buffer; + } + close SOURCE; + ($? == 0) or exit 1; + close SINK; + + # clean up + print 'Analyzing database ', $db, "...\n"; + (system $psql, '-h', $newsocket, '-p', $newinfo{'port'}, '-qX', + '-d', $db, '-c', 'ANALYZE') == 0 or + error 'Could not ANALYZE database'; + + unless ($databases{$db}) { + print "Disabling access to database $db again\n"; + (system $psql, '-h', $oldsocket, '-p', $info{'port'}, '-qX', + '-d', $maintenance_db, '-c', + "BEGIN READ WRITE; UPDATE pg_database SET datallowconn = 'f' where datname = '$db'; COMMIT") == 0 or + error 'Could not disable access to database in old cluster'; + (system $psql, '-h', $newsocket, '-p', $newinfo{'port'}, '-qX', + '-d', $maintenance_db, '-c', + "BEGIN READ WRITE; UPDATE pg_database SET datallowconn = 'f' where datname = '$db'; COMMIT") == 0 or + error 'Could not disable access to database in new cluster'; + } + } + + # reset owner specific override for default read-only transactions + (system $psql, '-h', $newsocket, '-p', $newinfo{'port'}, '-qX', $maintenance_db, '-c', + "BEGIN READ WRITE; ALTER USER $owner RESET default_transaction_read_only; COMMIT;\n") == 0 or + error 'Could not reset default_transaction_read_only value for superuser'; + } else { + # pg_upgrade + + use File::Temp qw(tempdir); + + my $pg_upgrade = get_program_path 'pg_upgrade', $newversion; + $pg_upgrade or error "pg_upgrade $newversion not found"; + my @argv = ($pg_upgrade, + '-b', ($old_bindir || "$PgCommon::binroot$version/bin"), + '-B', "$PgCommon::binroot$newversion/bin", + '-p', $info{'port'}, + '-P', $newinfo{'port'}, + ); + if ($version <= 9.1) { + push @argv, '-d', $info{pgdata}, '-o', "-D $info{configdir}"; # -o and -D configdir require $newversion >= 9.2 + } else { + push @argv, '-d', $info{configdir}; + } + push @argv, '-D', $newinfo{configdir}; + push @argv, "--link" if $link; + push @argv, "--clone" if $clone; + push @argv, '-j', $jobs if $jobs; + + # Make a directory for pg_upgrade to store its reports and log + # files. It will not be removed. + my $logdir = tempdir("/var/log/postgresql/pg_upgradecluster-$version-$newversion-$newcluster.XXXX"); + chdir $logdir; + + # Run pg_upgrade. + print "@argv\n"; + my $status = system @argv; + + # Remove the PID file of the old cluster (normally removed by + # pg_ctlcluster, but not by pg_upgrade). + unlink "/var/run/postgresql/$version-$cluster.pid"; + + $status == 0 or error "pg_upgrade run failed. Logfiles are in $logdir"; + print "pg_upgrade output scripts are in $logdir\n"; + } + + exit 0; +} + +wait; + +if ($?) { + print STDERR "Error during cluster dumping, removing new cluster\n"; + system 'pg_dropcluster', '--stop', $newversion, $newcluster; + + # Restart old cluster to allow connections again (two steps because we started without systemd) + if (system 'pg_ctlcluster', $version, $cluster, 'stop') { + error 'could not stop old cluster, please do that manually'; + } + if (system 'pg_ctlcluster', $version, $cluster, 'start') { + error 'could not start old cluster, please do that manually'; + } + exit 1; +} + +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) { + print "Running phase upgrade hook scripts...\n"; + if (!fork) { + change_ugid $info{'owneruid'}, $info{'ownergid'}; + + if ($PgCommon::rpm) { + run_parts ("$PgCommon::common_confdir/pg_upgradecluster.d", + $version, $newcluster, $newversion, 'finish'); + exit; + } + + @argv = ('run-parts', '--lsbsysinit', '-a', $version, '-a', $newcluster, + '-a', $newversion, '-a', 'finish', + "$PgCommon::common_confdir/pg_upgradecluster.d"); + error "$PgCommon::common_confdir/pg_upgradecluster.d script failed" if system @argv; + exit 0; + } + wait; + exit $? >> 8 if ($?); +} + +print "\nSuccess. Please check that the upgraded cluster works. If it does, +you can remove the old cluster with + pg_dropcluster $version $cluster\n\n"; + +system 'pg_lsclusters', $version, $cluster; +system 'pg_lsclusters', $newversion, $newcluster; + +__END__ + +=head1 NAME + +pg_upgradecluster - upgrade an existing PostgreSQL cluster to a new major version. + +=head1 SYNOPSIS + +B [B<-v> I] I I [I] + +=head1 DESCRIPTION + +B upgrades an existing PostgreSQL server cluster (i. e. a +collection of databases served by a B instance) to a new version +specified by I (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. 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 to start/stop it. See section "STARTUP CONTROL" in +L for details. + +The I argument can be used to specify a non-default data directory +of the upgraded cluster. It is passed to B. If not specified, +this defaults to /var/lib/postgresql/I/I. + +=head1 OPTIONS + +=over 4 + +=item B<-v> I + +Set the version to upgrade to (default: latest available). + +=item B<--logfile> I + +Set a custom log file path for the upgraded database cluster. + +=item B<--locale=>I + +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 was changed to +retain database encodings.) + +=item B<--lc-collate=>I + +=item B<--lc-ctype=>I + +=item B<--lc-messages=>I + +=item B<--lc-monetary=>I + +=item B<--lc-numeric=>I + +=item B<--lc-time=>I + +Like B<--locale>, but only sets the locale in the specified category. + +=item B<-m>, B<--method=>B|B|B|B + +Specify the upgrade method. B uses L and +L, B uses L. The default is +B. + +B and B 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 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 for details. + +=item B<-j>, B<--jobs> + +In pg_upgrade mode, number of simultaneous processes to use. This +option is merely passed on to pg_upgrade. See L +for details. + +=item B<--keep-port> + +By default, the old cluster is moved to a new port, and the new cluster is +moved to the original port so clients will see the upgraded cluster. This +option disables that. + +=item B<--rename=>I + +Use a different name for the upgraded cluster. + +=item B<--old-bindir=>I + +Passed to B. + +=item B<--maintenance-db=>I + +Database to connect to for maintenance queries. The default is B. + +=item B<--[no-]start> + +Start the new database cluster after upgrading. The default is to start the new +cluster if the old cluster was running, or if upgrade hook scripts are present. + +=back + +=head1 HOOK SCRIPTS + +Some PostgreSQL extensions like PostGIS need metadata in auxiliary tables which +must not be upgraded from the old version, but rather initialized for the new +version before copying the table data. For this purpose, extensions (as well as +administrators, of course) can drop upgrade hook scripts into +C. 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: + + + +Phases: + +=over + +=item B + +A virgin cluster of version I has been created, i. e. this new +cluster will already have B and B, 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 operation. + +=item B + +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, L, L, L + +=head1 AUTHORS + +Martin Pitt Lmpitt@debian.orgE>, Christoph Berg Lmyon@debian.orgE> 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 +# (C) 2012-2020 Christoph Berg +# +# 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 < /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 [I] [B<-v> 'I'] [I] + +=head1 DESCRIPTION + +B creates a virtual PostgreSQL server environment, and sets +environment variables such that I can access the PostgreSQL database +server(s). The servers are destroyed when I exits. + +The environment variables B, B, B, and +B 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 will be set. +B 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 and B are not set. The clusters are +named I/regress. To access a cluster, set +BIB. For ease of access, the clusters are also +registered in F, with the version +number as cluster name. Clusters can be accessed by passing the connection +string "BI", e.g. B. + +When invoked as root, the clusters are created in F as usual; +for other users, B and B are +set to a temporary directory where all files belonging to the clusters are +created. + +If I fails, the tail of the PostgreSQL server log is shown. +Additionally, if B 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 + +Use these versions (space-separated list). + +=item B<-c> I + +Extra options to pass to B. + +=item B<-i> I + +Extra initdb options to pass to B. + +=item B<-o> IB<=>I + +Configuration option to set in the C file, passed to +B. + +=item B<-p> I + +Set B and B in cluster to enable +loading and testing extensions at build-time from BIB. + +This is a Debian-specific PostgreSQL patch. + +=item B<-s> + +Launch a shell inside the virtual environment when I 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 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=yes + +When non-empty, B will re-exec itself using newpid(1). + +=item B=I + +When non-empty, B will re-exec itself using unshare(1) using +these flags. + +=item B=I + +When set, the value is used for the (single) cluster created. + +=back + +=head1 COMPATIBILITY + +B is set in postgresql-common (>= 219~). + +=head1 SEE ALSO + +initdb(1), pg_createcluster(1). + +=head1 AUTHOR + +Christoph Berg Lmyon@debian.orgE> 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 +# (C) 2013-2022 Christoph Berg +# +# 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 (/)' 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- 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(); + + 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 [B<--cluster> I/I] [...] + +(I: B, B, B, and all other client +programs installed in CIC). + +=head1 DESCRIPTION + +This program is run only as a link to names which correspond to PostgreSQL +programs in CIC. 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 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, or default 5432) will be used. + +=item + +explicit specification with the B environment variable + +=item + +if a port is given (either via B<-p>, B<--port>, or B), and no host is +given, the local cluster matching that port number is used + +=item + +matching entry in C<~/.postgresqlrc> (see L), if that +file exists + +=item + +matching entry in C (see +L), 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 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, B, and B, B 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 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 in C and +pg_wrapper is invoked as any user other than B and B. + +=head1 OPTIONS + +=over + +=item B<--cluster> IBI + +=item B<--cluster> IBIB<:>[I] + +I is either the name of a cluster on the local system, or takes the form +I:I for a remote cluster. If I is left empty (i. e. you just +specify I), it defaults to 5432. + +=back + +=head1 ENVIRONMENT + +=over + +=item B + +If C<$PGCLUSTER> is set, its value (of the form I/I) +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 + +This specifies an alternative base directory for cluster configurations. This +is usually C, 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 + +This is the location of PostgreSQL's and postgresql-common's global +configuration (e. g. C, L). The default is +C. + +=back + +=head1 FILES + +=over + +=item C + +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, L + +=head1 AUTHOR + +Martin Pitt Lmpitt@debian.orgE> 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 Binary files /dev/null and b/pgdg/apt.postgresql.org.gpg differ diff --git a/pgdg/apt.postgresql.org.sh b/pgdg/apt.postgresql.org.sh new file mode 100755 index 0000000..20797f8 --- /dev/null +++ b/pgdg/apt.postgresql.org.sh @@ -0,0 +1,283 @@ +#!/bin/sh + +# script to add apt.postgresql.org to sources.list.d + +# Copyright (C) 2013-2022 Christoph Berg +# +# 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="" +PG_DEVEL_VERSION="16" +PG_REPOSITORY_DISTS="sid bookworm bullseye buster kinetic jammy focal bionic" +PG_ARCHIVE_DISTS="sid bookworm bullseye buster stretch jessie wheezy squeeze lenny etch 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 < $KEYRING < $SOURCESLIST <> $SOURCESLIST < +# +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 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 + +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 217-1 +- Drop postgresql-server-dev-all package, it's debian-specific only. +* Fri Dec 09 2016 Bernd Helmle 177-1 +- New upstream release 177 +* Fri Jun 03 2016 Bernd Helmle 174-2 +- Fix package dependencies and systemd integration +* Thu Aug 7 2014 Christoph Berg 160-1 +- Omit the LD_PRELOAD logic in pg_wrapper +* Thu Jun 5 2014 Christoph Berg 158-1 +- Initial specfile version diff --git a/run-upgrade-scripts b/run-upgrade-scripts new file mode 100755 index 0000000..aae3e90 --- /dev/null +++ b/run-upgrade-scripts @@ -0,0 +1,114 @@ +#!/usr/bin/perl -w +# Run all upgrade scripts. +# +# (C) 2005-2009 Martin Pitt +# +# 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; + +error "Usage: $0 " if $#ARGV != 0; + +# Return the cluster's databases that match the given scope. +# Arguments: +sub dbs_from_scope { + my ($v, $c, $scope) = @_; + my @dbs = get_cluster_databases $v, $c; + unless (defined $dbs[0]) { + print ' Error: cluster is not running'; + return (); + } + + # filter out the postgres database + @dbs = grep { $_ ne 'postgres' } @dbs; + + return @dbs if $scope eq 't0'; + return grep { $_ ne 'template0' } @dbs if $scope eq 't1'; + return grep { $_ ne 'template0' && $_ ne 'template1' } @dbs if $scope eq 'db'; + return grep { $_ eq 'template1' } @dbs if $scope eq 'cluster'; +} + +# Arguments: