diff --git a/src/bin/pg_rewind/Makefile b/src/bin/pg_rewind/Makefile index 7a97df6..b214501 100644 --- a/src/bin/pg_rewind/Makefile +++ b/src/bin/pg_rewind/Makefile @@ -18,9 +18,12 @@ include $(top_builddir)/src/Makefile.global override CPPFLAGS := -I$(libpq_srcdir) -DFRONTEND $(CPPFLAGS) LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport) +ENCRYPTIONSOURCES = $(sort $(notdir $(wildcard $(top_srcdir)/src/backend/storage/encryption/*.c))) +ENCRYPTIONOBJS = $(patsubst %.c,%.o,$(ENCRYPTIONSOURCES)) + OBJS = pg_rewind.o parsexlog.o xlogreader.o datapagemap.o timeline.o \ - fetch.o file_ops.o copy_fetch.o libpq_fetch.o filemap.o \ - $(WIN32RES) + fetch.o file_ops.o copy_fetch.o libpq_fetch.o filemap.o compat.o \ + $(ENCRYPTIONOBJS) $(WIN32RES) EXTRA_CLEAN = xlogreader.c @@ -32,6 +35,9 @@ pg_rewind: $(OBJS) | submake-libpq submake-libpgport xlogreader.c: % : $(top_srcdir)/src/backend/access/transam/% rm -f $@ && $(LN_S) $< . +$(ENCRYPTIONSOURCES): % : $(top_srcdir)/src/backend/storage/encryption/% + rm -f $@ && $(LN_S) $< . + install: all installdirs $(INSTALL_PROGRAM) pg_rewind$(X) '$(DESTDIR)$(bindir)/pg_rewind$(X)' @@ -42,7 +48,7 @@ uninstall: rm -f '$(DESTDIR)$(bindir)/pg_rewind$(X)' clean distclean maintainer-clean: - rm -f pg_rewind$(X) $(OBJS) xlogreader.c + rm -f pg_rewind$(X) $(OBJS) $(ENCRYPTIONSOURCES) xlogreader.c rm -rf tmp_check check: diff --git a/src/bin/pg_rewind/compat.c b/src/bin/pg_rewind/compat.c new file mode 100644 index 0000000..1555176 --- /dev/null +++ b/src/bin/pg_rewind/compat.c @@ -0,0 +1,125 @@ +/*------------------------------------------------------------------------- + * + * compat.c + * Reimplementations of various backend functions. + * + * Portions Copyright (c) 2013-2019, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/bin/pg_waldump/compat.c + * + * This file contains client-side implementations for various backend + * functions that the rm_desc functions in *desc.c files rely on. + * + *------------------------------------------------------------------------- + */ + +/* ugly hack, same as in e.g pg_controldata */ +#define FRONTEND 1 +#include "postgres.h" + +#include + +#include "lib/stringinfo.h" +#include "utils/datetime.h" +#include "common/logging.h" + +/* copied from timestamp.c */ +pg_time_t +timestamptz_to_time_t(TimestampTz t) +{ + pg_time_t result; + + result = (pg_time_t) (t / USECS_PER_SEC + + ((POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY)); + return result; +} + +/* + * Stopgap implementation of timestamptz_to_str that doesn't depend on backend + * infrastructure. This will work for timestamps that are within the range + * of the platform time_t type. (pg_time_t is compatible except for possibly + * being wider.) + * + * XXX the return value points to a static buffer, so beware of using more + * than one result value concurrently. + * + * XXX: The backend timestamp infrastructure should instead be split out and + * moved into src/common. That's a large project though. + */ +const char * +timestamptz_to_str(TimestampTz dt) +{ + static char buf[MAXDATELEN + 1]; + char ts[MAXDATELEN + 1]; + char zone[MAXDATELEN + 1]; + time_t result = (time_t) timestamptz_to_time_t(dt); + struct tm *ltime = localtime(&result); + + strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", ltime); + strftime(zone, sizeof(zone), "%Z", ltime); + + snprintf(buf, sizeof(buf), "%s.%06d %s", + ts, (int) (dt % USECS_PER_SEC), zone); + + return buf; +} + +/* + * Provide a hacked up compat layer for StringInfos so xlog desc functions can + * be linked/called. + */ +void +initStringInfo(StringInfo str) +{ + int size = 1024; /* initial default buffer size */ + + str->data = (char *) palloc(size); + str->maxlen = size; + str->data[0] = '\0'; + str->len = 0; + str->cursor = 0; +} + +void +appendStringInfo(StringInfo str, const char *fmt,...) +{ + va_list args; + + va_start(args, fmt); + if (str) + { + int avail; + size_t nprinted; + + avail = str->maxlen - str->len; + nprinted = pvsnprintf(str->data + str->len, (size_t) avail, fmt, args); + + if (nprinted < (size_t) avail) + { + /* Success. Note nprinted does not include trailing null. */ + str->len += (int) nprinted; + } + else + { + pg_log_error("the string is too long"); + exit(1); + } + + } + else + vprintf(fmt, args); + va_end(args); +} + +void +appendStringInfoString(StringInfo str, const char *string) +{ + appendStringInfo(str, "%s", string); +} + +void +appendStringInfoChar(StringInfo str, char ch) +{ + appendStringInfo(str, "%c", ch); +} diff --git a/src/bin/pg_rewind/parsexlog.c b/src/bin/pg_rewind/parsexlog.c index 1d03375..e3557de 100644 --- a/src/bin/pg_rewind/parsexlog.c +++ b/src/bin/pg_rewind/parsexlog.c @@ -21,6 +21,8 @@ #include "commands/dbcommands_xlog.h" #include "filemap.h" #include "pg_rewind.h" +#include "catalog/pg_control.h" +#include "storage/encryption.h" /* * RmgrNames is an array of resource manager names, to make error messages @@ -312,6 +314,18 @@ SimpleXLogPageRead(XLogReaderState *xlogreader, XLogRecPtr targetPagePtr, return -1; } + if (DataEncryptionEnabled()) + { + XLogSegNo readSegNo; + uint32 readSegOff; + + XLByteToSeg(targetPagePtr, readSegNo, WalSegSz); + readSegOff = targetPagePtr % WalSegSz; + + DecryptXLog(readBuf, XLOG_BLCKSZ, readSegNo, readSegOff); + } + + Assert(targetSegNo == xlogreadsegno); xlogreader->seg.ws_tli = targetHistory[private->tliIndex].tli; diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c index e75c7ee..370d00e 100644 --- a/src/bin/pg_rewind/pg_rewind.c +++ b/src/bin/pg_rewind/pg_rewind.c @@ -7,6 +7,7 @@ * *------------------------------------------------------------------------- */ +#define FRONTEND 1 #include "postgres_fe.h" #include @@ -74,6 +75,8 @@ usage(const char *progname) printf(_("%s resynchronizes a PostgreSQL cluster with another copy of the cluster.\n\n"), progname); printf(_("Usage:\n %s [OPTION]...\n\n"), progname); printf(_("Options:\n")); + printf(_(" -c --cluster-passphrase-command=COMMAND\n" + " set command to obtain passphrase for data encryption key\n")); printf(_(" -D, --target-pgdata=DIRECTORY existing data directory to modify\n")); printf(_(" --source-pgdata=DIRECTORY source data directory to synchronize with\n")); printf(_(" --source-server=CONNSTR source server to synchronize with\n")); @@ -96,6 +99,7 @@ main(int argc, char **argv) { static struct option long_options[] = { {"help", no_argument, NULL, '?'}, + {"cluster-passphrase-command", required_argument, NULL, 'c'}, {"target-pgdata", required_argument, NULL, 'D'}, {"write-recovery-conf", no_argument, NULL, 'R'}, {"source-pgdata", required_argument, NULL, 1}, @@ -123,6 +127,7 @@ main(int argc, char **argv) TimeLineID endtli; ControlFileData ControlFile_new; bool writerecoveryconf = false; + char *cluster_passphrase = NULL; pg_logging_init(argv[0]); set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_rewind")); @@ -143,7 +148,7 @@ main(int argc, char **argv) } } - while ((c = getopt_long(argc, argv, "D:nNPR", long_options, &option_index)) != -1) + while ((c = getopt_long(argc, argv, "c:D:nNPR", long_options, &option_index)) != -1) { switch (c) { @@ -151,6 +156,9 @@ main(int argc, char **argv) fprintf(stderr, _("Try \"%s --help\" for more information.\n"), progname); exit(1); + case 'c': + cluster_passphrase = pg_strdup(optarg); + break; case 'P': showprogress = true; break; @@ -242,6 +250,12 @@ main(int argc, char **argv) } #endif + if (datadir_target) + getKmgr(datadir_target, cluster_passphrase, progname); + if (datadir_source) + getKmgr(datadir_source, cluster_passphrase, progname); + + get_restricted_token(); /* Set mask based on PGDATA permissions */ diff --git a/src/bin/pg_rewind/t/007_basic_tde.pl b/src/bin/pg_rewind/t/007_basic_tde.pl new file mode 100644 index 0000000..127dbf6 --- /dev/null +++ b/src/bin/pg_rewind/t/007_basic_tde.pl @@ -0,0 +1,175 @@ +use strict; +use warnings; +use TestLib; +use Test::More tests => 11; + +use FindBin; +use lib $FindBin::RealBin; + +use RewindTest; + +sub run_test +{ + my $test_mode = shift; + + RewindTest::setup_cluster($test_mode); + RewindTest::start_master(); + + # Create a test table and insert a row in master. + master_psql("CREATE TABLE tbl1 (d text)"); + master_psql("INSERT INTO tbl1 VALUES ('in master')"); + + # This test table will be used to test truncation, i.e. the table + # is extended in the old master after promotion + master_psql("CREATE TABLE trunc_tbl (d text)"); + master_psql("INSERT INTO trunc_tbl VALUES ('in master')"); + + # This test table will be used to test the "copy-tail" case, i.e. the + # table is truncated in the old master after promotion + master_psql("CREATE TABLE tail_tbl (id integer, d text)"); + master_psql("INSERT INTO tail_tbl VALUES (0, 'in master')"); + + master_psql("CHECKPOINT"); + + RewindTest::create_standby($test_mode); + + # Insert additional data on master that will be replicated to standby + master_psql("INSERT INTO tbl1 values ('in master, before promotion')"); + master_psql( + "INSERT INTO trunc_tbl values ('in master, before promotion')"); + master_psql( + "INSERT INTO tail_tbl SELECT g, 'in master, before promotion: ' || g FROM generate_series(1, 10000) g" + ); + + master_psql('CHECKPOINT'); + + RewindTest::promote_standby(); + + # Insert a row in the old master. This causes the master and standby + # to have "diverged", it's no longer possible to just apply the + # standy's logs over master directory - you need to rewind. + master_psql("INSERT INTO tbl1 VALUES ('in master, after promotion')"); + + # Also insert a new row in the standby, which won't be present in the + # old master. + standby_psql("INSERT INTO tbl1 VALUES ('in standby, after promotion')"); + + # Insert enough rows to trunc_tbl to extend the file. pg_rewind should + # truncate it back to the old size. + master_psql( + "INSERT INTO trunc_tbl SELECT 'in master, after promotion: ' || g FROM generate_series(1, 10000) g" + ); + + # Truncate tail_tbl. pg_rewind should copy back the truncated part + # (We cannot use an actual TRUNCATE command here, as that creates a + # whole new relfilenode) + master_psql("DELETE FROM tail_tbl WHERE id > 10"); + master_psql("VACUUM tail_tbl"); + + # Before running pg_rewind, do a couple of extra tests with several + # option combinations. As the code paths taken by those tests + # do not change for the "local" and "remote" modes, just run them + # in "local" mode for simplicity's sake. + if ($test_mode eq 'local') + { + my $master_pgdata = $node_master->data_dir; + my $standby_pgdata = $node_standby->data_dir; + + # First check that pg_rewind fails if the target cluster is + # not stopped as it fails to start up for the forced recovery + # step. + command_fails( + [ + 'pg_rewind', '--debug', + '--source-pgdata', $standby_pgdata, + '--target-pgdata', $master_pgdata, + '--no-sync' + ], + 'pg_rewind with running target'); + + # Again with --no-ensure-shutdown, which should equally fail. + # This time pg_rewind complains without attempting to perform + # recovery once. + command_fails( + [ + 'pg_rewind', '--debug', + '--source-pgdata', $standby_pgdata, + '--target-pgdata', $master_pgdata, + '--no-sync', '--no-ensure-shutdown' + ], + 'pg_rewind --no-ensure-shutdown with running target'); + + # Stop the target, and attempt to run with a local source + # still running. This fails as pg_rewind requires to have + # a source cleanly stopped. + $node_master->stop; + command_fails( + [ + 'pg_rewind', '--debug', + '--source-pgdata', $standby_pgdata, + '--target-pgdata', $master_pgdata, + '--no-sync', '--no-ensure-shutdown' + ], + 'pg_rewind with unexpected running source'); + + # Stop the target cluster cleanly, and run again pg_rewind + # with --dry-run mode. If anything gets generated in the data + # folder, the follow-up run of pg_rewind will most likely fail, + # so keep this test as the last one of this subset. + $node_standby->stop; + command_ok( + [ + 'pg_rewind', '--debug', + '--source-pgdata', $standby_pgdata, + '--target-pgdata', $master_pgdata, + '--no-sync', '--dry-run' + ], + 'pg_rewind --dry-run'); + + # Both clusters need to be alive moving forward. + $node_standby->start; + $node_master->start; + } + + RewindTest::run_pg_rewind($test_mode); + + check_query( + 'SELECT * FROM tbl1', + qq(in master +in master, before promotion +in standby, after promotion +), + 'table content'); + + check_query( + 'SELECT * FROM trunc_tbl', + qq(in master +in master, before promotion +), + 'truncation'); + + check_query( + 'SELECT count(*) FROM tail_tbl', + qq(10001 +), + 'tail-copy'); + + # Permissions on PGDATA should be default + SKIP: + { + skip "unix-style permissions not supported on Windows", 1 + if ($windows_os); + + ok(check_mode_recursive($node_master->data_dir(), 0700, 0600), + 'check PGDATA permissions'); + } + + RewindTest::clean_rewind_test(); + return; +} + +# Run the test in both modes +run_test('local-tde'); +run_test('remote-tde'); + +exit(0); diff --git a/src/bin/pg_rewind/t/RewindTest.pm b/src/bin/pg_rewind/t/RewindTest.pm index 82fa220..43ea17a 100644 --- a/src/bin/pg_rewind/t/RewindTest.pm +++ b/src/bin/pg_rewind/t/RewindTest.pm @@ -129,10 +129,21 @@ sub setup_cluster # Set up pg_hba.conf and pg_ident.conf for the role running # pg_rewind. This role is used for all the tests, and has # minimal permissions enough to rewind from an online source. - $node_master->init( - allows_streaming => 1, - extra => $extra, - auth_extra => [ '--create-role', 'rewind_user' ]); + if (($extra_name eq "local-tde") or ($extra_name eq "remote-tde")) + { + $node_master->init( + allows_streaming => 1, + extra => $extra, + auth_extra => [ '--create-role', 'rewind_user' ], + enable_encryption => 1); + } + else + { + $node_master->init( + allows_streaming => 1, + extra => $extra, + auth_extra => [ '--create-role', 'rewind_user' ]); + } # Set wal_keep_segments to prevent WAL segment recycling after enforced # checkpoints in the tests. @@ -284,6 +295,40 @@ sub run_pg_rewind $node_standby->safe_psql('postgres', "ALTER ROLE rewind_user WITH REPLICATION;"); } + elsif ($test_mode eq "local-tde") + { + + $node_standby->stop; + command_ok( + [ + 'pg_rewind', + "--debug", + "--cluster-passphrase-command", 'echo "password"', + "--source-pgdata=$standby_pgdata", + "--target-pgdata=$master_pgdata", + "--no-sync" + ], + 'pg_rewind local'); + } + elsif ($test_mode eq "remote-tde") + { + + command_ok( + [ + 'pg_rewind', "--debug", + "--cluster-passphrase-command", 'echo "password"', + "--source-server", $standby_connstr, + "--target-pgdata=$master_pgdata", "--no-sync", + "--write-recovery-conf" + ], + 'pg_rewind remote'); + + ok( -e "$master_pgdata/standby.signal", + 'standby.signal created after pg_rewind'); + + $node_standby->safe_psql('postgres', + "ALTER ROLE rewind_user WITH REPLICATION;"); + } else {