use strict;
 use warnings;
+use PostgresNode;
 use TestLib;
 use Test::More tests => 14;
 
 
 use warnings;
 use Cwd;
 use Config;
+use PostgresNode;
 use TestLib;
 use Test::More tests => 51;
 
 program_version_ok('pg_basebackup');
 program_options_handling_ok('pg_basebackup');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $tempdir = TestLib::tempdir;
 
-command_fails(['pg_basebackup'],
+my $node = get_new_node();
+
+# Initialize node without replication settings
+$node->init(hba_permit_replication => 0);
+$node->start;
+my $pgdata = $node->data_dir;
+
+$node->command_fails(['pg_basebackup'],
    'pg_basebackup needs target directory specified');
-command_fails(
+$node->command_fails(
    [ 'pg_basebackup', '-D', "$tempdir/backup" ],
    'pg_basebackup fails because of hba');
 
    close BADCHARS;
 }
 
-configure_hba_for_replication "$tempdir/pgdata";
-system_or_bail 'pg_ctl', '-D', "$tempdir/pgdata", 'reload';
+$node->set_replication_conf();
+system_or_bail 'pg_ctl', '-D', $pgdata, 'reload';
 
-command_fails(
+$node->command_fails(
    [ 'pg_basebackup', '-D', "$tempdir/backup" ],
    'pg_basebackup fails because of WAL configuration');
 
-open CONF, ">>$tempdir/pgdata/postgresql.conf";
+open CONF, ">>$pgdata/postgresql.conf";
 print CONF "max_replication_slots = 10\n";
 print CONF "max_wal_senders = 10\n";
 print CONF "wal_level = archive\n";
 close CONF;
-restart_test_server;
+$node->restart;
 
-command_ok([ 'pg_basebackup', '-D', "$tempdir/backup" ],
+$node->command_ok([ 'pg_basebackup', '-D', "$tempdir/backup" ],
    'pg_basebackup runs');
 ok(-f "$tempdir/backup/PG_VERSION", 'backup was created');
 
-is_deeply([sort(slurp_dir("$tempdir/backup/pg_xlog/"))],
-         [sort qw(. .. archive_status)],
-         'no WAL files copied');
+is_deeply(
+   [ sort(slurp_dir("$tempdir/backup/pg_xlog/")) ],
+   [ sort qw(. .. archive_status) ],
+   'no WAL files copied');
 
-command_ok(
+$node->command_ok(
    [   'pg_basebackup', '-D', "$tempdir/backup2", '--xlogdir',
        "$tempdir/xlog2" ],
    'separate xlog directory');
 ok(-f "$tempdir/backup2/PG_VERSION", 'backup was created');
 ok(-d "$tempdir/xlog2/",             'xlog directory was created');
 
-command_ok([ 'pg_basebackup', '-D', "$tempdir/tarbackup", '-Ft' ],
+$node->command_ok([ 'pg_basebackup', '-D', "$tempdir/tarbackup", '-Ft' ],
    'tar format');
 ok(-f "$tempdir/tarbackup/base.tar", 'backup tar was created');
 
-command_fails(
+$node->command_fails(
    [ 'pg_basebackup', '-D', "$tempdir/backup_foo", '-Fp', "-T=/foo" ],
    '-T with empty old directory fails');
-command_fails(
+$node->command_fails(
    [ 'pg_basebackup', '-D', "$tempdir/backup_foo", '-Fp', "-T/foo=" ],
    '-T with empty new directory fails');
-command_fails(
+$node->command_fails(
    [   'pg_basebackup', '-D', "$tempdir/backup_foo", '-Fp',
        "-T/foo=/bar=/baz" ],
    '-T with multiple = fails');
-command_fails(
+$node->command_fails(
    [ 'pg_basebackup', '-D', "$tempdir/backup_foo", '-Fp', "-Tfoo=/bar" ],
    '-T with old directory not absolute fails');
-command_fails(
+$node->command_fails(
    [ 'pg_basebackup', '-D', "$tempdir/backup_foo", '-Fp', "-T/foo=bar" ],
    '-T with new directory not absolute fails');
-command_fails(
+$node->command_fails(
    [ 'pg_basebackup', '-D', "$tempdir/backup_foo", '-Fp', "-Tfoo" ],
    '-T with invalid format fails');
 
 # Tar format doesn't support filenames longer than 100 bytes.
 my $superlongname = "superlongname_" . ("x" x 100);
-my $superlongpath = "$tempdir/pgdata/$superlongname";
+my $superlongpath = "$pgdata/$superlongname";
 
 open FILE, ">$superlongpath" or die "unable to create file $superlongpath";
 close FILE;
-command_fails([ 'pg_basebackup', '-D', "$tempdir/tarbackup_l1", '-Ft' ],
+$node->command_fails(
+   [ 'pg_basebackup', '-D', "$tempdir/tarbackup_l1", '-Ft' ],
    'pg_basebackup tar with long name fails');
-unlink "$tempdir/pgdata/$superlongname";
+unlink "$pgdata/$superlongname";
 
 # The following tests test symlinks. Windows doesn't have symlinks, so
 # skip on Windows.
-SKIP: {
-    skip "symlinks not supported on Windows", 10 if ($windows_os);
+SKIP:
+{
+   skip "symlinks not supported on Windows", 10 if ($windows_os);
 
    # Create a temporary directory in the system location and symlink it
    # to our physical temp location.  That way we can use shorter names
    # for the tablespace directories, which hopefully won't run afoul of
    # the 99 character length limit.
-   my $shorter_tempdir = tempdir_short . "/tempdir";
+   my $shorter_tempdir = TestLib::tempdir_short . "/tempdir";
    symlink "$tempdir", $shorter_tempdir;
 
    mkdir "$tempdir/tblspc1";
-   psql 'postgres',
-   "CREATE TABLESPACE tblspc1 LOCATION '$shorter_tempdir/tblspc1';";
-   psql 'postgres', "CREATE TABLE test1 (a int) TABLESPACE tblspc1;";
-   command_ok([ 'pg_basebackup', '-D', "$tempdir/tarbackup2", '-Ft' ],
-              'tar format with tablespaces');
+   $node->psql('postgres',
+       "CREATE TABLESPACE tblspc1 LOCATION '$shorter_tempdir/tblspc1';");
+   $node->psql('postgres', "CREATE TABLE test1 (a int) TABLESPACE tblspc1;");
+   $node->command_ok([ 'pg_basebackup', '-D', "$tempdir/tarbackup2", '-Ft' ],
+       'tar format with tablespaces');
    ok(-f "$tempdir/tarbackup2/base.tar", 'backup tar was created');
    my @tblspc_tars = glob "$tempdir/tarbackup2/[0-9]*.tar";
    is(scalar(@tblspc_tars), 1, 'one tablespace tar was created');
 
-   command_fails(
+   $node->command_fails(
        [ 'pg_basebackup', '-D', "$tempdir/backup1", '-Fp' ],
        'plain format with tablespaces fails without tablespace mapping');
 
-   command_ok(
+   $node->command_ok(
        [   'pg_basebackup', '-D', "$tempdir/backup1", '-Fp',
            "-T$shorter_tempdir/tblspc1=$tempdir/tbackup/tblspc1" ],
        'plain format with tablespaces succeeds with tablespace mapping');
    ok(-d "$tempdir/tbackup/tblspc1", 'tablespace was relocated');
-   opendir(my $dh, "$tempdir/pgdata/pg_tblspc") or die;
+   opendir(my $dh, "$pgdata/pg_tblspc") or die;
    ok( (   grep {
-       -l "$tempdir/backup1/pg_tblspc/$_"
-           and readlink "$tempdir/backup1/pg_tblspc/$_" eq
-           "$tempdir/tbackup/tblspc1"
-           } readdir($dh)),
+               -l "$tempdir/backup1/pg_tblspc/$_"
+                 and readlink "$tempdir/backup1/pg_tblspc/$_" eq
+                 "$tempdir/tbackup/tblspc1"
+             } readdir($dh)),
        "tablespace symlink was updated");
    closedir $dh;
 
    mkdir "$tempdir/tbl=spc2";
-   psql 'postgres', "DROP TABLE test1;";
-   psql 'postgres', "DROP TABLESPACE tblspc1;";
-   psql 'postgres',
-   "CREATE TABLESPACE tblspc2 LOCATION '$shorter_tempdir/tbl=spc2';";
-   command_ok(
+   $node->psql('postgres', "DROP TABLE test1;");
+   $node->psql('postgres', "DROP TABLESPACE tblspc1;");
+   $node->psql('postgres',
+       "CREATE TABLESPACE tblspc2 LOCATION '$shorter_tempdir/tbl=spc2';");
+   $node->command_ok(
        [   'pg_basebackup', '-D', "$tempdir/backup3", '-Fp',
            "-T$shorter_tempdir/tbl\\=spc2=$tempdir/tbackup/tbl\\=spc2" ],
        'mapping tablespace with = sign in path');
-   ok(-d "$tempdir/tbackup/tbl=spc2", 'tablespace with = sign was relocated');
-   psql 'postgres', "DROP TABLESPACE tblspc2;";
+   ok(-d "$tempdir/tbackup/tbl=spc2",
+       'tablespace with = sign was relocated');
+   $node->psql('postgres', "DROP TABLESPACE tblspc2;");
 
    mkdir "$tempdir/$superlongname";
-   psql 'postgres',
-   "CREATE TABLESPACE tblspc3 LOCATION '$tempdir/$superlongname';";
-   command_ok([ 'pg_basebackup', '-D', "$tempdir/tarbackup_l3", '-Ft' ],
-              'pg_basebackup tar with long symlink target');
-   psql 'postgres', "DROP TABLESPACE tblspc3;";
+   $node->psql('postgres',
+       "CREATE TABLESPACE tblspc3 LOCATION '$tempdir/$superlongname';");
+   $node->command_ok(
+       [ 'pg_basebackup', '-D', "$tempdir/tarbackup_l3", '-Ft' ],
+       'pg_basebackup tar with long symlink target');
+   $node->psql('postgres', "DROP TABLESPACE tblspc3;");
 }
 
-command_ok([ 'pg_basebackup', '-D', "$tempdir/backupR", '-R' ],
+$node->command_ok([ 'pg_basebackup', '-D', "$tempdir/backupR", '-R' ],
    'pg_basebackup -R runs');
 ok(-f "$tempdir/backupR/recovery.conf", 'recovery.conf was created');
 my $recovery_conf = slurp_file "$tempdir/backupR/recovery.conf";
+
 # using a character class for the final "'" here works around an apparent
 # bug in several version of the Msys DTK perl
-like($recovery_conf, qr/^standby_mode = 'on[']$/m, 'recovery.conf sets standby_mode');
-like($recovery_conf, qr/^primary_conninfo = '.*port=$ENV{PGPORT}.*'$/m, 'recovery.conf sets primary_conninfo');
-
-command_ok([ 'pg_basebackup', '-D', "$tempdir/backupxf", '-X', 'fetch' ],
+like(
+   $recovery_conf,
+   qr/^standby_mode = 'on[']$/m,
+   'recovery.conf sets standby_mode');
+like(
+   $recovery_conf,
+   qr/^primary_conninfo = '.*port=$ENV{PGPORT}.*'$/m,
+   'recovery.conf sets primary_conninfo');
+
+$node->command_ok(
+   [ 'pg_basebackup', '-D', "$tempdir/backupxf", '-X', 'fetch' ],
    'pg_basebackup -X fetch runs');
-ok(grep(/^[0-9A-F]{24}$/, slurp_dir("$tempdir/backupxf/pg_xlog")), 'WAL files copied');
-command_ok([ 'pg_basebackup', '-D', "$tempdir/backupxs", '-X', 'stream' ],
+ok(grep(/^[0-9A-F]{24}$/, slurp_dir("$tempdir/backupxf/pg_xlog")),
+   'WAL files copied');
+$node->command_ok(
+   [ 'pg_basebackup', '-D', "$tempdir/backupxs", '-X', 'stream' ],
    'pg_basebackup -X stream runs');
-ok(grep(/^[0-9A-F]{24}$/, slurp_dir("$tempdir/backupxf/pg_xlog")), 'WAL files copied');
+ok(grep(/^[0-9A-F]{24}$/, slurp_dir("$tempdir/backupxf/pg_xlog")),
+   'WAL files copied');
 
-command_fails([ 'pg_basebackup', '-D', "$tempdir/fail", '-S', 'slot1' ],
+$node->command_fails(
+   [ 'pg_basebackup', '-D', "$tempdir/fail", '-S', 'slot1' ],
    'pg_basebackup with replication slot fails without -X stream');
-command_fails([ 'pg_basebackup', '-D', "$tempdir/backupxs_sl_fail", '-X', 'stream', '-S', 'slot1' ],
+$node->command_fails(
+   [   'pg_basebackup',             '-D',
+       "$tempdir/backupxs_sl_fail", '-X',
+       'stream',                    '-S',
+       'slot1' ],
    'pg_basebackup fails with nonexistent replication slot');
 
-psql 'postgres', q{SELECT * FROM pg_create_physical_replication_slot('slot1')};
-my $lsn = psql 'postgres', q{SELECT restart_lsn FROM pg_replication_slots WHERE slot_name = 'slot1'};
+$node->psql('postgres',
+   q{SELECT * FROM pg_create_physical_replication_slot('slot1')});
+my $lsn = $node->psql('postgres',
+   q{SELECT restart_lsn FROM pg_replication_slots WHERE slot_name = 'slot1'}
+);
 is($lsn, '', 'restart LSN of new slot is null');
-command_ok([ 'pg_basebackup', '-D', "$tempdir/backupxs_sl", '-X', 'stream', '-S', 'slot1' ],
+$node->command_ok(
+   [   'pg_basebackup', '-D', "$tempdir/backupxs_sl", '-X',
+       'stream',        '-S', 'slot1' ],
    'pg_basebackup -X stream with replication slot runs');
-$lsn = psql 'postgres', q{SELECT restart_lsn FROM pg_replication_slots WHERE slot_name = 'slot1'};
+$lsn = $node->psql('postgres',
+   q{SELECT restart_lsn FROM pg_replication_slots WHERE slot_name = 'slot1'}
+);
 like($lsn, qr!^0/[0-9A-Z]{7,8}$!, 'restart LSN of slot has advanced');
 
-command_ok([ 'pg_basebackup', '-D', "$tempdir/backupxs_sl_R", '-X', 'stream', '-S', 'slot1', '-R' ],
+$node->command_ok(
+   [   'pg_basebackup', '-D', "$tempdir/backupxs_sl_R", '-X',
+       'stream',        '-S', 'slot1',                  '-R' ],
    'pg_basebackup with replication slot and -R runs');
-like(slurp_file("$tempdir/backupxs_sl_R/recovery.conf"),
-    qr/^primary_slot_name = 'slot1'$/m,
-    'recovery.conf sets primary_slot_name');
+like(
+   slurp_file("$tempdir/backupxs_sl_R/recovery.conf"),
+   qr/^primary_slot_name = 'slot1'$/m,
+   'recovery.conf sets primary_slot_name');
 
 use strict;
 use warnings;
+use PostgresNode;
 use TestLib;
 use Test::More tests => 13;
 
-my $tempdir = TestLib::tempdir;
-
 program_help_ok('pg_controldata');
 program_version_ok('pg_controldata');
 program_options_handling_ok('pg_controldata');
 command_fails(['pg_controldata'], 'pg_controldata without arguments fails');
 command_fails([ 'pg_controldata', 'nonexistent' ],
    'pg_controldata with nonexistent directory fails');
-standard_initdb "$tempdir/data";
-command_like([ 'pg_controldata', "$tempdir/data" ],
+
+my $node = get_new_node();
+$node->init;
+$node->start;
+
+command_like([ 'pg_controldata', $node->data_dir ],
    qr/checkpoint/, 'pg_controldata produces output');
 
 use strict;
 use warnings;
+
 use Config;
+use PostgresNode;
 use TestLib;
 use Test::More tests => 17;
 
 
 command_ok([ 'pg_ctl', 'initdb', '-D', "$tempdir/data", '-o', '-N' ],
    'pg_ctl initdb');
-command_ok(
-   [ $ENV{PG_REGRESS}, '--config-auth',
-       "$tempdir/data" ],
+command_ok([ $ENV{PG_REGRESS}, '--config-auth', "$tempdir/data" ],
    'configure authentication');
 open CONF, ">>$tempdir/data/postgresql.conf";
 print CONF "fsync = off\n";
-if (! $windows_os)
+if (!$windows_os)
 {
    print CONF "listen_addresses = ''\n";
    print CONF "unix_socket_directories = '$tempdir_short'\n";
 close CONF;
 command_ok([ 'pg_ctl', 'start', '-D', "$tempdir/data", '-w' ],
    'pg_ctl start -w');
+
 # sleep here is because Windows builds can't check postmaster.pid exactly,
 # so they may mistake a pre-existing postmaster.pid for one created by the
 # postmaster they start.  Waiting more than the 2 seconds slop time allowed
 
 use strict;
 use warnings;
+
+use PostgresNode;
 use TestLib;
 use Test::More tests => 3;
 
 command_exit_is([ 'pg_ctl', 'status', '-D', "$tempdir/nonexistent" ],
    4, 'pg_ctl status with nonexistent directory');
 
-standard_initdb "$tempdir/data";
+my $node = get_new_node();
+$node->init;
 
-command_exit_is([ 'pg_ctl', 'status', '-D', "$tempdir/data" ],
+command_exit_is([ 'pg_ctl', 'status', '-D', $node->data_dir ],
    3, 'pg_ctl status with server not running');
 
 system_or_bail 'pg_ctl', '-l', "$tempdir/logfile", '-D',
-  "$tempdir/data", '-w', 'start';
-command_exit_is([ 'pg_ctl', 'status', '-D', "$tempdir/data" ],
+  $node->data_dir, '-w', 'start';
+command_exit_is([ 'pg_ctl', 'status', '-D', $node->data_dir ],
    0, 'pg_ctl status with server running');
 
-system_or_bail 'pg_ctl', 'stop', '-D', "$tempdir/data", '-m', 'fast';
+system_or_bail 'pg_ctl', 'stop', '-D', $node->data_dir, '-m', 'fast';
 
 # To run a test, the test script (in t/ subdirectory) calls the functions
 # in this module. These functions should be called in this sequence:
 #
-# 1. init_rewind_test - sets up log file etc.
+# 1. setup_cluster - creates a PostgreSQL cluster that runs as the master
 #
-# 2. setup_cluster - creates a PostgreSQL cluster that runs as the master
+# 2. start_master - starts the master server
 #
-# 3. start_master - starts the master server
-#
-# 4. create_standby - runs pg_basebackup to initialize a standby server, and
+# 3. create_standby - runs pg_basebackup to initialize a standby server, and
 #    sets it up to follow the master.
 #
-# 5. promote_standby - runs "pg_ctl promote" to promote the standby server.
+# 4. promote_standby - runs "pg_ctl promote" to promote the standby server.
 # The old master keeps running.
 #
-# 6. run_pg_rewind - stops the old master (if it's still running) and runs
+# 5. run_pg_rewind - stops the old master (if it's still running) and runs
 # pg_rewind to synchronize it with the now-promoted standby server.
 #
-# 7. clean_rewind_test - stops both servers used in the test, if they're
+# 6. clean_rewind_test - stops both servers used in the test, if they're
 # still running.
 #
 # The test script can use the helper functions master_psql and standby_psql
 use strict;
 use warnings;
 
-use TestLib;
-use Test::More;
-
 use Config;
+use Exporter 'import';
 use File::Copy;
 use File::Path qw(rmtree);
-use IPC::Run qw(run start);
+use IPC::Run qw(run);
+use PostgresNode;
+use TestLib;
+use Test::More;
 
-use Exporter 'import';
 our @EXPORT = qw(
-  $connstr_master
-  $connstr_standby
-  $test_master_datadir
-  $test_standby_datadir
+  $node_master
+  $node_standby
 
-  append_to_file
   master_psql
   standby_psql
   check_query
 
-  init_rewind_test
   setup_cluster
   start_master
   create_standby
   clean_rewind_test
 );
 
-our $test_master_datadir  = "$tmp_check/data_master";
-our $test_standby_datadir = "$tmp_check/data_standby";
-
-# Define non-conflicting ports for both nodes.
-my $port_master  = $ENV{PGPORT};
-my $port_standby = $port_master + 1;
-
-my $connstr_master  = "port=$port_master";
-my $connstr_standby = "port=$port_standby";
-
-$ENV{PGDATABASE} = "postgres";
+# Our nodes.
+our $node_master;
+our $node_standby;
 
 sub master_psql
 {
    my $cmd = shift;
 
-   system_or_bail 'psql', '-q', '--no-psqlrc', '-d', $connstr_master,
-     '-c', "$cmd";
+   system_or_bail 'psql', '-q', '--no-psqlrc', '-d',
+     $node_master->connstr('postgres'), '-c', "$cmd";
 }
 
 sub standby_psql
 {
    my $cmd = shift;
 
-   system_or_bail 'psql', '-q', '--no-psqlrc', '-d', $connstr_standby,
-     '-c', "$cmd";
+   system_or_bail 'psql', '-q', '--no-psqlrc', '-d',
+     $node_standby->connstr('postgres'), '-c', "$cmd";
 }
 
 # Run a query against the master, and check that the output matches what's
 
    # we want just the output, no formatting
    my $result = run [
-       'psql',          '-q', '-A', '-t', '--no-psqlrc', '-d',
-       $connstr_master, '-c', $query ],
+       'psql', '-q', '-A', '-t', '--no-psqlrc', '-d',
+       $node_master->connstr('postgres'),
+       '-c', $query ],
      '>', \$stdout, '2>', \$stderr;
 
    # We don't use ok() for the exit code and stderr, because we want this
    }
 }
 
-# Run a query once a second, until it returns 't' (i.e. SQL boolean true).
-sub poll_query_until
-{
-   my ($query, $connstr) = @_;
-
-   my $max_attempts = 30;
-   my $attempts     = 0;
-   my ($stdout, $stderr);
-
-   while ($attempts < $max_attempts)
-   {
-       my $cmd = [ 'psql', '-At', '-c', "$query", '-d', "$connstr" ];
-       my $result = run $cmd, '>', \$stdout, '2>', \$stderr;
-
-       chomp($stdout);
-       $stdout =~ s/\r//g if $Config{osname} eq 'msys';
-       if ($stdout eq "t")
-       {
-           return 1;
-       }
-
-       # Wait a second before retrying.
-       sleep 1;
-       $attempts++;
-   }
-
-   # The query result didn't change in 30 seconds. Give up. Print the stderr
-   # from the last attempt, hopefully that's useful for debugging.
-   diag $stderr;
-   return 0;
-}
-
-sub append_to_file
-{
-   my ($filename, $str) = @_;
-
-   open my $fh, ">>", $filename or die "could not open file $filename";
-   print $fh $str;
-   close $fh;
-}
-
 sub setup_cluster
 {
+
    # Initialize master, data checksums are mandatory
-   rmtree($test_master_datadir);
-   standard_initdb($test_master_datadir);
+   $node_master = get_new_node();
+   $node_master->init;
 
    # Custom parameters for master's postgresql.conf
-   append_to_file(
-       "$test_master_datadir/postgresql.conf", qq(
+   $node_master->append_conf(
+       "postgresql.conf", qq(
 wal_level = hot_standby
 max_wal_senders = 2
 wal_keep_segments = 20
 autovacuum = off
 max_connections = 10
 ));
-
-   # Accept replication connections on master
-   configure_hba_for_replication $test_master_datadir;
 }
 
 sub start_master
 {
-   system_or_bail('pg_ctl' , '-w',
-                  '-D' , $test_master_datadir,
-                  '-l',  "$log_path/master.log",
-                  "-o", "-p $port_master", 'start');
+   $node_master->start;
 
    #### Now run the test-specific parts to initialize the master before setting
    # up standby
 
 sub create_standby
 {
+   $node_standby = get_new_node();
+   $node_master->backup('my_backup');
+   $node_standby->init_from_backup($node_master, 'my_backup');
+   my $connstr_master = $node_master->connstr('postgres');
 
-   # Set up standby with necessary parameter
-   rmtree $test_standby_datadir;
-
-   # Base backup is taken with xlog files included
-   system_or_bail('pg_basebackup', '-D', $test_standby_datadir,
-                  '-p', $port_master, '-x');
-   append_to_file(
-       "$test_standby_datadir/recovery.conf", qq(
+   $node_standby->append_conf(
+       "recovery.conf", qq(
 primary_conninfo='$connstr_master application_name=rewind_standby'
 standby_mode=on
 recovery_target_timeline='latest'
 ));
 
    # Start standby
-   system_or_bail('pg_ctl', '-w', '-D', $test_standby_datadir,
-                  '-l', "$log_path/standby.log",
-                  '-o', "-p $port_standby", 'start');
+   $node_standby->start;
 
    # The standby may have WAL to apply before it matches the primary.  That
    # is fine, because no test examines the standby before promotion.
    # Wait for the standby to receive and write all WAL.
    my $wal_received_query =
 "SELECT pg_current_xlog_location() = write_location FROM pg_stat_replication WHERE application_name = 'rewind_standby';";
-   poll_query_until($wal_received_query, $connstr_master)
+   $node_master->poll_query_until('postgres', $wal_received_query)
      or die "Timed out while waiting for standby to receive and write WAL";
 
    # Now promote slave and insert some new data on master, this will put
    # the master out-of-sync with the standby. Wait until the standby is
    # out of recovery mode, and is ready to accept read-write connections.
-   system_or_bail('pg_ctl', '-w', '-D', $test_standby_datadir, 'promote');
-   poll_query_until("SELECT NOT pg_is_in_recovery()", $connstr_standby)
+   system_or_bail('pg_ctl', '-w', '-D', $node_standby->data_dir, 'promote');
+   $node_standby->poll_query_until('postgres',
+       "SELECT NOT pg_is_in_recovery()")
      or die "Timed out while waiting for promotion of standby";
 
    # Force a checkpoint after the promotion. pg_rewind looks at the control
 
 sub run_pg_rewind
 {
-   my $test_mode = shift;
+   my $test_mode       = shift;
+   my $master_pgdata   = $node_master->data_dir;
+   my $standby_pgdata  = $node_standby->data_dir;
+   my $standby_connstr = $node_standby->connstr('postgres');
+   my $tmp_folder      = TestLib::tempdir;
 
    # Stop the master and be ready to perform the rewind
-   system_or_bail('pg_ctl', '-D', $test_master_datadir, '-m', 'fast', 'stop');
+   $node_master->stop;
 
    # At this point, the rewind processing is ready to run.
    # We now have a very simple scenario with a few diverged WAL record.
 
    # Keep a temporary postgresql.conf for master node or it would be
    # overwritten during the rewind.
-   copy("$test_master_datadir/postgresql.conf",
-        "$tmp_check/master-postgresql.conf.tmp");
+   copy(
+       "$master_pgdata/postgresql.conf",
+       "$tmp_folder/master-postgresql.conf.tmp");
 
    # Now run pg_rewind
    if ($test_mode eq "local")
    {
+
        # Do rewind using a local pgdata as source
        # Stop the master and be ready to perform the rewind
-       system_or_bail('pg_ctl', '-D', $test_standby_datadir,
-                      '-m', 'fast', 'stop');
-       command_ok(['pg_rewind',
-                   "--debug",
-                   "--source-pgdata=$test_standby_datadir",
-                   "--target-pgdata=$test_master_datadir"],
-                  'pg_rewind local');
+       $node_standby->stop;
+       command_ok(
+           [   'pg_rewind',
+               "--debug",
+               "--source-pgdata=$standby_pgdata",
+               "--target-pgdata=$master_pgdata" ],
+           'pg_rewind local');
    }
    elsif ($test_mode eq "remote")
    {
+
        # Do rewind using a remote connection as source
-       command_ok(['pg_rewind',
-                   "--debug",
-                   "--source-server",
-                   "port=$port_standby dbname=postgres",
-                   "--target-pgdata=$test_master_datadir"],
-                  'pg_rewind remote');
+       command_ok(
+           [   'pg_rewind',       "--debug",
+               "--source-server", $standby_connstr,
+               "--target-pgdata=$master_pgdata" ],
+           'pg_rewind remote');
    }
    else
    {
    }
 
    # Now move back postgresql.conf with old settings
-   move("$tmp_check/master-postgresql.conf.tmp",
-        "$test_master_datadir/postgresql.conf");
+   move(
+       "$tmp_folder/master-postgresql.conf.tmp",
+       "$master_pgdata/postgresql.conf");
 
    # Plug-in rewound node to the now-promoted standby node
-   append_to_file(
-       "$test_master_datadir/recovery.conf", qq(
+   my $port_standby = $node_standby->port;
+   $node_master->append_conf(
+       'recovery.conf', qq(
 primary_conninfo='port=$port_standby'
 standby_mode=on
 recovery_target_timeline='latest'
 ));
 
    # Restart the master to check that rewind went correctly
-   system_or_bail('pg_ctl', '-w', '-D', $test_master_datadir,
-                  '-l', "$log_path/master.log",
-                  '-o', "-p $port_master", 'start');
+   $node_master->start;
 
    #### Now run the test-specific parts to check the result
 }
 # Clean up after the test. Stop both servers, if they're still running.
 sub clean_rewind_test
 {
-   if ($test_master_datadir)
-   {
-       system
-         'pg_ctl', '-D', $test_master_datadir, '-m', 'immediate', 'stop';
-   }
-   if ($test_standby_datadir)
-   {
-       system
-         'pg_ctl', '-D', $test_standby_datadir, '-m', 'immediate', 'stop';
-   }
+   $node_master->teardown_node  if defined $node_master;
+   $node_standby->teardown_node if defined $node_standby;
 }
 
-# Stop the test servers, just in case they're still running.
-END
-{
-   my $save_rc = $?;
-   clean_rewind_test();
-   $? = $save_rc;
-}
+1;
 
    RewindTest::setup_cluster();
    RewindTest::start_master();
 
-   my $test_master_datadir = $RewindTest::test_master_datadir;
+   my $test_master_datadir = $node_master->data_dir;
 
    # Create a subdir and files that will be present in both
    mkdir "$test_master_datadir/tst_both_dir";
    RewindTest::create_standby();
 
    # Create different subdirs and files in master and standby
+   my $test_standby_datadir = $node_standby->data_dir;
 
    mkdir "$test_standby_datadir/tst_standby_dir";
    append_to_file "$test_standby_datadir/tst_standby_dir/standby_file1",
 
 {
    my $test_mode = shift;
 
-   my $master_xlogdir = "$tmp_check/xlog_master";
+   my $master_xlogdir = "${TestLib::tmp_check}/xlog_master";
 
    rmtree($master_xlogdir);
    RewindTest::setup_cluster();
 
+   my $test_master_datadir = $node_master->data_dir;
+
    # turn pg_xlog into a symlink
    print("moving $test_master_datadir/pg_xlog to $master_xlogdir\n");
    move("$test_master_datadir/pg_xlog", $master_xlogdir) or die;
 
 use strict;
 use warnings;
+
+use PostgresNode;
 use TestLib;
 use Test::More tests => 13;
 
 program_version_ok('clusterdb');
 program_options_handling_ok('clusterdb');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
 
-issues_sql_like(
-   [ 'clusterdb', 'postgres' ],
+$node->issues_sql_like(
+   ['clusterdb'],
    qr/statement: CLUSTER;/,
    'SQL CLUSTER run');
 
-command_fails([ 'clusterdb', '-t', 'nonexistent', 'postgres' ],
+$node->command_fails([ 'clusterdb', '-t', 'nonexistent' ],
    'fails with nonexistent table');
 
-psql 'postgres',
-'CREATE TABLE test1 (a int); CREATE INDEX test1x ON test1 (a); CLUSTER test1 USING test1x';
-issues_sql_like(
-   [ 'clusterdb', '-t', 'test1', 'postgres' ],
+$node->psql('postgres',
+'CREATE TABLE test1 (a int); CREATE INDEX test1x ON test1 (a); CLUSTER test1 USING test1x'
+);
+$node->issues_sql_like(
+   [ 'clusterdb', '-t', 'test1' ],
    qr/statement: CLUSTER test1;/,
    'cluster specific table');
 
 use strict;
 use warnings;
+
+use PostgresNode;
 use TestLib;
 use Test::More tests => 2;
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
+
+# clusterdb -a is not compatible with -d, hence enforce environment variable
+# correctly.
+$ENV{PGDATABASE} = 'postgres';
 
-issues_sql_like(
+$node->issues_sql_like(
    [ 'clusterdb', '-a' ],
    qr/statement: CLUSTER.*statement: CLUSTER/s,
    'cluster all databases');
 
 use strict;
 use warnings;
+
+use PostgresNode;
 use TestLib;
 use Test::More tests => 13;
 
 program_version_ok('createdb');
 program_options_handling_ok('createdb');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
 
-issues_sql_like(
+$node->issues_sql_like(
    [ 'createdb', 'foobar1' ],
    qr/statement: CREATE DATABASE foobar1/,
    'SQL CREATE DATABASE run');
-issues_sql_like(
+$node->issues_sql_like(
    [ 'createdb', '-l', 'C', '-E', 'LATIN1', '-T', 'template0', 'foobar2' ],
    qr/statement: CREATE DATABASE foobar2 ENCODING 'LATIN1'/,
    'create database with encoding');
 
-command_fails([ 'createdb', 'foobar1' ], 'fails if database already exists');
+$node->command_fails([ 'createdb', 'foobar1' ],
+   'fails if database already exists');
 
 use strict;
 use warnings;
+
+use PostgresNode;
 use TestLib;
 use Test::More tests => 14;
 
 program_version_ok('createlang');
 program_options_handling_ok('createlang');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
 
-command_fails(
-   [ 'createlang', 'plpgsql', 'postgres' ],
+$node->command_fails([ 'createlang', 'plpgsql' ],
    'fails if language already exists');
 
-psql 'postgres', 'DROP EXTENSION plpgsql';
-issues_sql_like(
-   [ 'createlang', 'plpgsql', 'postgres' ],
+$node->psql('postgres', 'DROP EXTENSION plpgsql');
+$node->issues_sql_like(
+   [ 'createlang', 'plpgsql' ],
    qr/statement: CREATE EXTENSION "plpgsql"/,
    'SQL CREATE EXTENSION run');
 
-command_like([ 'createlang', '--list', 'postgres' ],
-   qr/plpgsql/, 'list output');
+$node->command_like([ 'createlang', '--list' ], qr/plpgsql/, 'list output');
 
 use strict;
 use warnings;
+
+use PostgresNode;
 use TestLib;
 use Test::More tests => 17;
 
 program_version_ok('createuser');
 program_options_handling_ok('createuser');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
 
-issues_sql_like(
+$node->issues_sql_like(
    [ 'createuser', 'user1' ],
 qr/statement: CREATE ROLE user1 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN;/,
    'SQL CREATE USER run');
-issues_sql_like(
+$node->issues_sql_like(
    [ 'createuser', '-L', 'role1' ],
 qr/statement: CREATE ROLE role1 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOLOGIN;/,
    'create a non-login role');
-issues_sql_like(
+$node->issues_sql_like(
    [ 'createuser', '-r', 'user2' ],
 qr/statement: CREATE ROLE user2 NOSUPERUSER NOCREATEDB CREATEROLE INHERIT LOGIN;/,
    'create a CREATEROLE user');
-issues_sql_like(
+$node->issues_sql_like(
    [ 'createuser', '-s', 'user3' ],
 qr/statement: CREATE ROLE user3 SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;/,
    'create a superuser');
 
-command_fails([ 'createuser', 'user1' ], 'fails if role already exists');
+$node->command_fails([ 'createuser', 'user1' ],
+   'fails if role already exists');
 
 use strict;
 use warnings;
+
+use PostgresNode;
 use TestLib;
 use Test::More tests => 11;
 
 program_version_ok('dropdb');
 program_options_handling_ok('dropdb');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
 
-psql 'postgres', 'CREATE DATABASE foobar1';
-issues_sql_like(
+$node->psql('postgres', 'CREATE DATABASE foobar1');
+$node->issues_sql_like(
    [ 'dropdb', 'foobar1' ],
    qr/statement: DROP DATABASE foobar1/,
    'SQL DROP DATABASE run');
 
-command_fails([ 'dropdb', 'nonexistent' ], 'fails with nonexistent database');
+$node->command_fails([ 'dropdb', 'nonexistent' ],
+   'fails with nonexistent database');
 
 use strict;
 use warnings;
+
+use PostgresNode;
 use TestLib;
 use Test::More tests => 11;
 
 program_version_ok('droplang');
 program_options_handling_ok('droplang');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
 
-issues_sql_like(
+$node->issues_sql_like(
    [ 'droplang', 'plpgsql', 'postgres' ],
    qr/statement: DROP EXTENSION "plpgsql"/,
    'SQL DROP EXTENSION run');
 
-command_fails(
+$node->command_fails(
    [ 'droplang', 'nonexistent', 'postgres' ],
    'fails with nonexistent language');
 
 use strict;
 use warnings;
+
+use PostgresNode;
 use TestLib;
 use Test::More tests => 11;
 
 program_version_ok('dropuser');
 program_options_handling_ok('dropuser');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
 
-psql 'postgres', 'CREATE ROLE foobar1';
-issues_sql_like(
+$node->psql('postgres', 'CREATE ROLE foobar1');
+$node->issues_sql_like(
    [ 'dropuser', 'foobar1' ],
    qr/statement: DROP ROLE foobar1/,
    'SQL DROP ROLE run');
 
-command_fails([ 'dropuser', 'nonexistent' ], 'fails with nonexistent user');
+$node->command_fails([ 'dropuser', 'nonexistent' ],
+   'fails with nonexistent user');
 
 use strict;
 use warnings;
+
+use PostgresNode;
 use TestLib;
 use Test::More tests => 10;
 
 
 command_fails(['pg_isready'], 'fails with no server running');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
 
-command_ok(['pg_isready'], 'succeeds with server running');
+$node->command_ok(['pg_isready'], 'succeeds with server running');
 
 use strict;
 use warnings;
+
+use PostgresNode;
 use TestLib;
 use Test::More tests => 20;
 
 program_version_ok('reindexdb');
 program_options_handling_ok('reindexdb');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
 
 $ENV{PGOPTIONS} = '--client-min-messages=WARNING';
 
-issues_sql_like(
+$node->issues_sql_like(
    [ 'reindexdb', 'postgres' ],
    qr/statement: REINDEX DATABASE postgres;/,
    'SQL REINDEX run');
 
-psql 'postgres',
-  'CREATE TABLE test1 (a int); CREATE INDEX test1x ON test1 (a);';
-issues_sql_like(
+$node->psql('postgres',
+   'CREATE TABLE test1 (a int); CREATE INDEX test1x ON test1 (a);');
+$node->issues_sql_like(
    [ 'reindexdb', '-t', 'test1', 'postgres' ],
    qr/statement: REINDEX TABLE test1;/,
    'reindex specific table');
-issues_sql_like(
+$node->issues_sql_like(
    [ 'reindexdb', '-i', 'test1x', 'postgres' ],
    qr/statement: REINDEX INDEX test1x;/,
    'reindex specific index');
-issues_sql_like(
+$node->issues_sql_like(
    [ 'reindexdb', '-S', 'pg_catalog', 'postgres' ],
    qr/statement: REINDEX SCHEMA pg_catalog;/,
    'reindex specific schema');
-issues_sql_like(
+$node->issues_sql_like(
    [ 'reindexdb', '-s', 'postgres' ],
    qr/statement: REINDEX SYSTEM postgres;/,
    'reindex system tables');
-issues_sql_like(
+$node->issues_sql_like(
    [ 'reindexdb', '-v', '-t', 'test1', 'postgres' ],
    qr/statement: REINDEX \(VERBOSE\) TABLE test1;/,
    'reindex with verbose output');
 
 use strict;
 use warnings;
-use TestLib;
+
+use PostgresNode;
 use Test::More tests => 2;
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
 
 $ENV{PGOPTIONS} = '--client-min-messages=WARNING';
 
-issues_sql_like(
+$node->issues_sql_like(
    [ 'reindexdb', '-a' ],
    qr/statement: REINDEX.*statement: REINDEX/s,
    'reindex all databases');
 
 use strict;
 use warnings;
+
+use PostgresNode;
 use TestLib;
 use Test::More tests => 18;
 
 program_version_ok('vacuumdb');
 program_options_handling_ok('vacuumdb');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
 
-issues_sql_like(
+$node->issues_sql_like(
    [ 'vacuumdb', 'postgres' ],
    qr/statement: VACUUM;/,
    'SQL VACUUM run');
-issues_sql_like(
+$node->issues_sql_like(
    [ 'vacuumdb', '-f', 'postgres' ],
    qr/statement: VACUUM \(FULL\);/,
    'vacuumdb -f');
-issues_sql_like(
+$node->issues_sql_like(
    [ 'vacuumdb', '-F', 'postgres' ],
    qr/statement: VACUUM \(FREEZE\);/,
    'vacuumdb -F');
-issues_sql_like(
+$node->issues_sql_like(
    [ 'vacuumdb', '-z', 'postgres' ],
    qr/statement: VACUUM \(ANALYZE\);/,
    'vacuumdb -z');
-issues_sql_like(
+$node->issues_sql_like(
    [ 'vacuumdb', '-Z', 'postgres' ],
    qr/statement: ANALYZE;/,
    'vacuumdb -Z');
 
 use strict;
 use warnings;
-use TestLib;
+
+use PostgresNode;
 use Test::More tests => 2;
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
 
-issues_sql_like(
+$node->issues_sql_like(
    [ 'vacuumdb', '-a' ],
    qr/statement: VACUUM.*statement: VACUUM/s,
    'vacuum all databases');
 
 use strict;
 use warnings;
-use TestLib;
+
+use PostgresNode;
 use Test::More tests => 4;
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
 
-issues_sql_like(
+$node->issues_sql_like(
    [ 'vacuumdb', '--analyze-in-stages', 'postgres' ],
 qr/.*statement:\ SET\ default_statistics_target=1;\ SET\ vacuum_cost_delay=0;
                    .*statement:\ ANALYZE.*
                    .*statement:\ ANALYZE/sx,
    'analyze three times');
 
-
-issues_sql_like(
+$node->issues_sql_like(
    [ 'vacuumdb', '--analyze-in-stages', '--all' ],
 qr/.*statement:\ SET\ default_statistics_target=1;\ SET\ vacuum_cost_delay=0;
                    .*statement:\ ANALYZE.*
 
--- /dev/null
+# PostgresNode, class representing a data directory and postmaster.
+#
+# This contains a basic set of routines able to work on a PostgreSQL node,
+# allowing to start, stop, backup and initialize it with various options.
+# The set of nodes managed by a given test is also managed by this module.
+
+package PostgresNode;
+
+use strict;
+use warnings;
+
+use Config;
+use Cwd;
+use Exporter 'import';
+use File::Basename;
+use File::Spec;
+use File::Temp ();
+use IPC::Run;
+use PostgresNode;
+use RecursiveCopy;
+use Test::More;
+use TestLib ();
+
+our @EXPORT = qw(
+  get_new_node
+);
+
+our ($test_pghost, $last_port_assigned, @all_nodes);
+
+BEGIN
+{
+
+   # PGHOST is set once and for all through a single series of tests when
+   # this module is loaded.
+   $test_pghost =
+     $TestLib::windows_os ? "127.0.0.1" : TestLib::tempdir_short;
+   $ENV{PGHOST}     = $test_pghost;
+   $ENV{PGDATABASE} = 'postgres';
+
+   # Tracking of last port value assigned to accelerate free port lookup.
+   # XXX: Should this use PG_VERSION_NUM?
+   $last_port_assigned = 90600 % 16384 + 49152;
+
+   # Node tracking
+   @all_nodes = ();
+}
+
+sub new
+{
+   my $class  = shift;
+   my $pghost = shift;
+   my $pgport = shift;
+   my $self   = {
+       _port     => $pgport,
+       _host     => $pghost,
+       _basedir  => TestLib::tempdir,
+       _applname => "node_$pgport",
+       _logfile  => "$TestLib::log_path/node_$pgport.log" };
+
+   bless $self, $class;
+   $self->dump_info;
+
+   return $self;
+}
+
+sub port
+{
+   my ($self) = @_;
+   return $self->{_port};
+}
+
+sub host
+{
+   my ($self) = @_;
+   return $self->{_host};
+}
+
+sub basedir
+{
+   my ($self) = @_;
+   return $self->{_basedir};
+}
+
+sub applname
+{
+   my ($self) = @_;
+   return $self->{_applname};
+}
+
+sub logfile
+{
+   my ($self) = @_;
+   return $self->{_logfile};
+}
+
+sub connstr
+{
+   my ($self, $dbname) = @_;
+   my $pgport = $self->port;
+   my $pghost = $self->host;
+   if (!defined($dbname))
+   {
+       return "port=$pgport host=$pghost";
+   }
+   return "port=$pgport host=$pghost dbname=$dbname";
+}
+
+sub data_dir
+{
+   my ($self) = @_;
+   my $res = $self->basedir;
+   return "$res/pgdata";
+}
+
+sub archive_dir
+{
+   my ($self) = @_;
+   my $basedir = $self->basedir;
+   return "$basedir/archives";
+}
+
+sub backup_dir
+{
+   my ($self) = @_;
+   my $basedir = $self->basedir;
+   return "$basedir/backup";
+}
+
+# Dump node information
+sub dump_info
+{
+   my ($self) = @_;
+   print "Data directory: " . $self->data_dir . "\n";
+   print "Backup directory: " . $self->backup_dir . "\n";
+   print "Archive directory: " . $self->archive_dir . "\n";
+   print "Connection string: " . $self->connstr . "\n";
+   print "Application name: " . $self->applname . "\n";
+   print "Log file: " . $self->logfile . "\n";
+}
+
+sub set_replication_conf
+{
+   my ($self) = @_;
+   my $pgdata = $self->data_dir;
+
+   open my $hba, ">>$pgdata/pg_hba.conf";
+   print $hba "\n# Allow replication (set up by PostgresNode.pm)\n";
+   if (!$TestLib::windows_os)
+   {
+       print $hba "local replication all trust\n";
+   }
+   else
+   {
+       print $hba
+"host replication all 127.0.0.1/32 sspi include_realm=1 map=regress\n";
+   }
+   close $hba;
+}
+
+# Initialize a new cluster for testing.
+#
+# Authentication is set up so that only the current OS user can access the
+# cluster. On Unix, we use Unix domain socket connections, with the socket in
+# a directory that's only accessible to the current user to ensure that.
+# On Windows, we use SSPI authentication to ensure the same (by pg_regress
+# --config-auth).
+sub init
+{
+   my ($self, %params) = @_;
+   my $port   = $self->port;
+   my $pgdata = $self->data_dir;
+   my $host   = $self->host;
+
+   $params{hba_permit_replication} = 1
+     if (!defined($params{hba_permit_replication}));
+
+   mkdir $self->backup_dir;
+   mkdir $self->archive_dir;
+
+   TestLib::system_or_bail('initdb', '-D', $pgdata, '-A', 'trust', '-N');
+   TestLib::system_or_bail($ENV{PG_REGRESS}, '--config-auth', $pgdata);
+
+   open my $conf, ">>$pgdata/postgresql.conf";
+   print $conf "\n# Added by PostgresNode.pm)\n";
+   print $conf "fsync = off\n";
+   print $conf "log_statement = all\n";
+   print $conf "port = $port\n";
+   if ($TestLib::windows_os)
+   {
+       print $conf "listen_addresses = '$host'\n";
+   }
+   else
+   {
+       print $conf "unix_socket_directories = '$host'\n";
+       print $conf "listen_addresses = ''\n";
+   }
+   close $conf;
+
+   $self->set_replication_conf if ($params{hba_permit_replication});
+}
+
+sub append_conf
+{
+   my ($self, $filename, $str) = @_;
+
+   my $conffile = $self->data_dir . '/' . $filename;
+
+   TestLib::append_to_file($conffile, $str);
+}
+
+sub backup
+{
+   my ($self, $backup_name) = @_;
+   my $backup_path = $self->backup_dir . '/' . $backup_name;
+   my $port        = $self->port;
+
+   print "# Taking backup $backup_name from node with port $port\n";
+   TestLib::system_or_bail("pg_basebackup -D $backup_path -p $port -x");
+   print "# Backup finished\n";
+}
+
+sub init_from_backup
+{
+   my ($self, $root_node, $backup_name) = @_;
+   my $backup_path = $root_node->backup_dir . '/' . $backup_name;
+   my $port        = $self->port;
+   my $root_port   = $root_node->port;
+
+   print
+"Initializing node $port from backup \"$backup_name\" of node $root_port\n";
+   die "Backup $backup_path does not exist" unless -d $backup_path;
+
+   mkdir $self->backup_dir;
+   mkdir $self->archive_dir;
+
+   my $data_path = $self->data_dir;
+   rmdir($data_path);
+   RecursiveCopy::copypath($backup_path, $data_path);
+   chmod(0700, $data_path);
+
+   # Base configuration for this node
+   $self->append_conf(
+       'postgresql.conf',
+       qq(
+port = $port
+));
+   $self->set_replication_conf;
+}
+
+sub start
+{
+   my ($self) = @_;
+   my $port   = $self->port;
+   my $pgdata = $self->data_dir;
+   print("### Starting test server in $pgdata\n");
+   my $ret = TestLib::system_log('pg_ctl', '-w', '-D', $self->data_dir, '-l',
+       $self->logfile, 'start');
+
+   if ($ret != 0)
+   {
+       print "# pg_ctl failed; logfile:\n";
+       print TestLib::slurp_file($self->logfile);
+       BAIL_OUT("pg_ctl failed");
+   }
+
+   $self->_update_pid;
+
+}
+
+sub stop
+{
+   my ($self, $mode) = @_;
+   my $port   = $self->port;
+   my $pgdata = $self->data_dir;
+   $mode = 'fast' if (!defined($mode));
+   print "### Stopping node in $pgdata with port $port using mode $mode\n";
+   TestLib::system_log('pg_ctl', '-D', $pgdata, '-m', $mode, 'stop');
+   $self->{_pid} = undef;
+   $self->_update_pid;
+}
+
+sub restart
+{
+   my ($self)  = @_;
+   my $port    = $self->port;
+   my $pgdata  = $self->data_dir;
+   my $logfile = $self->logfile;
+   TestLib::system_log('pg_ctl', '-D', $pgdata, '-w', '-l', $logfile,
+       'restart');
+   $self->_update_pid;
+}
+
+sub _update_pid
+{
+   my $self = shift;
+
+   # If we can open the PID file, read its first line and that's the PID we
+   # want.  If the file cannot be opened, presumably the server is not
+   # running; don't be noisy in that case.
+   open my $pidfile, $self->data_dir . "/postmaster.pid";
+   if (not defined $pidfile)
+   {
+       $self->{_pid} = undef;
+       print "# No postmaster PID\n";
+       return;
+   }
+
+   $self->{_pid} = <$pidfile>;
+   print "# Postmaster PID is $self->{_pid}\n";
+   close $pidfile;
+}
+
+#
+# Cluster management functions
+#
+
+# Build a new PostgresNode object, assigning a free port number.
+#
+# We also register the node, to avoid the port number from being reused
+# for another node even when this one is not active.
+sub get_new_node
+{
+   my $found = 0;
+   my $port  = $last_port_assigned;
+
+   while ($found == 0)
+   {
+       $port++;
+       print "# Checking for port $port\n";
+       my $devnull = $TestLib::windows_os ? "nul" : "/dev/null";
+       if (!TestLib::run_log([ 'pg_isready', '-p', $port ]))
+       {
+           $found = 1;
+
+           # Found a potential candidate port number.  Check first that it is
+           # not included in the list of registered nodes.
+           foreach my $node (@all_nodes)
+           {
+               $found = 0 if ($node->port == $port);
+           }
+       }
+   }
+
+   print "# Found free port $port\n";
+
+   # Lock port number found by creating a new node
+   my $node = new PostgresNode($test_pghost, $port);
+
+   # Add node to list of nodes
+   push(@all_nodes, $node);
+
+   # And update port for next time
+   $last_port_assigned = $port;
+
+   return $node;
+}
+
+sub DESTROY
+{
+   my $self = shift;
+   return if not defined $self->{_pid};
+   print "# signalling QUIT to $self->{_pid}\n";
+   kill 'QUIT', $self->{_pid};
+}
+
+sub teardown_node
+{
+   my $self = shift;
+
+   $self->stop('immediate');
+}
+
+sub psql
+{
+   my ($self, $dbname, $sql) = @_;
+
+   my ($stdout, $stderr);
+   print("# Running SQL command: $sql\n");
+
+   IPC::Run::run [ 'psql', '-XAtq', '-d', $self->connstr($dbname), '-f',
+       '-' ], '<', \$sql, '>', \$stdout, '2>', \$stderr
+     or die;
+
+   if ($stderr ne "")
+   {
+       print "#### Begin standard error\n";
+       print $stderr;
+       print "#### End standard error\n";
+   }
+   chomp $stdout;
+   $stdout =~ s/\r//g if $Config{osname} eq 'msys';
+   return $stdout;
+}
+
+# Run a query once a second, until it returns 't' (i.e. SQL boolean true).
+sub poll_query_until
+{
+   my ($self, $dbname, $query) = @_;
+
+   my $max_attempts = 30;
+   my $attempts     = 0;
+   my ($stdout, $stderr);
+
+   while ($attempts < $max_attempts)
+   {
+       my $cmd =
+         [ 'psql', '-At', '-c', $query, '-d', $self->connstr($dbname) ];
+       my $result = IPC::Run::run $cmd, '>', \$stdout, '2>', \$stderr;
+
+       chomp($stdout);
+       $stdout =~ s/\r//g if $Config{osname} eq 'msys';
+       if ($stdout eq "t")
+       {
+           return 1;
+       }
+
+       # Wait a second before retrying.
+       sleep 1;
+       $attempts++;
+   }
+
+   # The query result didn't change in 30 seconds. Give up. Print the stderr
+   # from the last attempt, hopefully that's useful for debugging.
+   diag $stderr;
+   return 0;
+}
+
+sub command_ok
+{
+   my $self = shift;
+
+   local $ENV{PGPORT} = $self->port;
+
+   TestLib::command_ok(@_);
+}
+
+sub command_fails
+{
+   my $self = shift;
+
+   local $ENV{PGPORT} = $self->port;
+
+   TestLib::command_fails(@_);
+}
+
+sub command_like
+{
+   my $self = shift;
+
+   local $ENV{PGPORT} = $self->port;
+
+   TestLib::command_like(@_);
+}
+
+# Run a command on the node, then verify that $expected_sql appears in the
+# server log file.
+sub issues_sql_like
+{
+   my ($self, $cmd, $expected_sql, $test_name) = @_;
+
+   local $ENV{PGPORT} = $self->port;
+
+   truncate $self->logfile, 0;
+   my $result = TestLib::run_log($cmd);
+   ok($result, "@$cmd exit code 0");
+   my $log = TestLib::slurp_file($self->logfile);
+   like($log, $expected_sql, "$test_name: SQL found in server log");
+}
+
+1;
 
--- /dev/null
+# RecursiveCopy, a simple recursive copy implementation
+package RecursiveCopy;
+
+use strict;
+use warnings;
+
+use File::Basename;
+use File::Copy;
+
+sub copypath
+{
+   my $srcpath  = shift;
+   my $destpath = shift;
+
+   die "Cannot operate on symlinks" if -l $srcpath or -l $destpath;
+
+   # This source path is a file, simply copy it to destination with the
+   # same name.
+   die "Destination path $destpath exists as file" if -f $destpath;
+   if (-f $srcpath)
+   {
+       copy($srcpath, $destpath)
+         or die "copy $srcpath -> $destpath failed: $!";
+       return 1;
+   }
+
+   die "Destination needs to be a directory" unless -d $srcpath;
+   mkdir($destpath) or die "mkdir($destpath) failed: $!";
+
+   # Scan existing source directory and recursively copy everything.
+   opendir(my $directory, $srcpath) or die "could not opendir($srcpath): $!";
+   while (my $entry = readdir($directory))
+   {
+       next if ($entry eq '.' || $entry eq '..');
+       RecursiveCopy::copypath("$srcpath/$entry", "$destpath/$entry")
+         or die "copypath $srcpath/$entry -> $destpath/$entry failed";
+   }
+   closedir($directory);
+   return 1;
+}
+
+1;
 
+# TestLib, low-level routines and actions regression tests.
+#
+# This module contains a set of routines dedicated to environment setup for
+# a PostgreSQL regression test tun, and includes some low-level routines
+# aimed at controlling command execution, logging and test functions. This
+# module should never depend on any other PostgreSQL regression test modules.
+
 package TestLib;
 
 use strict;
 
 use Config;
 use Exporter 'import';
+use File::Basename;
+use File::Spec;
+use File::Temp ();
+use IPC::Run;
+use SimpleTee;
+use Test::More;
+
 our @EXPORT = qw(
-  tempdir
-  tempdir_short
-  standard_initdb
-  configure_hba_for_replication
-  start_test_server
-  restart_test_server
-  psql
   slurp_dir
   slurp_file
+  append_to_file
   system_or_bail
   system_log
   run_log
   program_version_ok
   program_options_handling_ok
   command_like
-  issues_sql_like
 
-  $tmp_check
-  $log_path
   $windows_os
 );
 
-use Cwd;
-use File::Basename;
-use File::Spec;
-use File::Temp ();
-use IPC::Run qw(run start);
+our ($windows_os, $tmp_check, $log_path, $test_logfile);
 
-use SimpleTee;
-
-use Test::More;
-
-our $windows_os = $Config{osname} eq 'MSWin32' || $Config{osname} eq 'msys';
-
-# Open log file. For each test, the log file name uses the name of the
-# file launching this module, without the .pl suffix.
-our ($tmp_check, $log_path);
-$tmp_check = $ENV{TESTDIR} ? "$ENV{TESTDIR}/tmp_check" : "tmp_check";
-$log_path = "$tmp_check/log";
-mkdir $tmp_check;
-mkdir $log_path;
-my $test_logfile = basename($0);
-$test_logfile =~ s/\.[^.]+$//;
-$test_logfile = "$log_path/regress_log_$test_logfile";
-open TESTLOG, '>', $test_logfile or die "Cannot open STDOUT to logfile: $!";
-
-# Hijack STDOUT and STDERR to the log file
-open(ORIG_STDOUT, ">&STDOUT");
-open(ORIG_STDERR, ">&STDERR");
-open(STDOUT, ">&TESTLOG");
-open(STDERR, ">&TESTLOG");
-
-# The test output (ok ...) needs to be printed to the original STDOUT so
-# that the 'prove' program can parse it, and display it to the user in
-# real time. But also copy it to the log file, to provide more context
-# in the log.
-my $builder = Test::More->builder;
-my $fh = $builder->output;
-tie *$fh, "SimpleTee", *ORIG_STDOUT, *TESTLOG;
-$fh = $builder->failure_output;
-tie *$fh, "SimpleTee", *ORIG_STDERR, *TESTLOG;
-
-# Enable auto-flushing for all the file handles. Stderr and stdout are
-# redirected to the same file, and buffering causes the lines to appear
-# in the log in confusing order.
-autoflush STDOUT 1;
-autoflush STDERR 1;
-autoflush TESTLOG 1;
-
-# Set to untranslated messages, to be able to compare program output
-# with expected strings.
-delete $ENV{LANGUAGE};
-delete $ENV{LC_ALL};
-$ENV{LC_MESSAGES} = 'C';
-
-delete $ENV{PGCONNECT_TIMEOUT};
-delete $ENV{PGDATA};
-delete $ENV{PGDATABASE};
-delete $ENV{PGHOSTADDR};
-delete $ENV{PGREQUIRESSL};
-delete $ENV{PGSERVICE};
-delete $ENV{PGSSLMODE};
-delete $ENV{PGUSER};
-
-if (!$ENV{PGPORT})
+BEGIN
 {
-   $ENV{PGPORT} = 65432;
+
+   # Set to untranslated messages, to be able to compare program output
+   # with expected strings.
+   delete $ENV{LANGUAGE};
+   delete $ENV{LC_ALL};
+   $ENV{LC_MESSAGES} = 'C';
+
+   delete $ENV{PGCONNECT_TIMEOUT};
+   delete $ENV{PGDATA};
+   delete $ENV{PGDATABASE};
+   delete $ENV{PGHOSTADDR};
+   delete $ENV{PGREQUIRESSL};
+   delete $ENV{PGSERVICE};
+   delete $ENV{PGSSLMODE};
+   delete $ENV{PGUSER};
+   delete $ENV{PGPORT};
+   delete $ENV{PGHOST};
+
+   # Must be set early
+   $windows_os = $Config{osname} eq 'MSWin32' || $Config{osname} eq 'msys';
 }
 
-$ENV{PGPORT} = int($ENV{PGPORT}) % 65536;
+INIT
+{
 
+   # Determine output directories, and create them.  The base path is the
+   # TESTDIR environment variable, which is normally set by the invoking
+   # Makefile.
+   $tmp_check = $ENV{TESTDIR} ? "$ENV{TESTDIR}/tmp_check" : "tmp_check";
+   $log_path = "$tmp_check/log";
+
+   mkdir $tmp_check;
+   mkdir $log_path;
+
+   # Open the test log file, whose name depends on the test name.
+   $test_logfile = basename($0);
+   $test_logfile =~ s/\.[^.]+$//;
+   $test_logfile = "$log_path/regress_log_$test_logfile";
+   open TESTLOG, '>', $test_logfile
+     or die "could not open STDOUT to logfile \"$test_logfile\": $!";
+
+   # Hijack STDOUT and STDERR to the log file
+   open(ORIG_STDOUT, ">&STDOUT");
+   open(ORIG_STDERR, ">&STDERR");
+   open(STDOUT,      ">&TESTLOG");
+   open(STDERR,      ">&TESTLOG");
+
+   # The test output (ok ...) needs to be printed to the original STDOUT so
+   # that the 'prove' program can parse it, and display it to the user in
+   # real time. But also copy it to the log file, to provide more context
+   # in the log.
+   my $builder = Test::More->builder;
+   my $fh      = $builder->output;
+   tie *$fh, "SimpleTee", *ORIG_STDOUT, *TESTLOG;
+   $fh = $builder->failure_output;
+   tie *$fh, "SimpleTee", *ORIG_STDERR, *TESTLOG;
+
+   # Enable auto-flushing for all the file handles. Stderr and stdout are
+   # redirected to the same file, and buffering causes the lines to appear
+   # in the log in confusing order.
+   autoflush STDOUT 1;
+   autoflush STDERR 1;
+   autoflush TESTLOG 1;
+}
 
 #
 # Helper functions
 #
-
-
 sub tempdir
 {
    return File::Temp::tempdir(
    return File::Temp::tempdir(CLEANUP => 1);
 }
 
-# Initialize a new cluster for testing.
-#
-# The PGHOST environment variable is set to connect to the new cluster.
-#
-# Authentication is set up so that only the current OS user can access the
-# cluster. On Unix, we use Unix domain socket connections, with the socket in
-# a directory that's only accessible to the current user to ensure that.
-# On Windows, we use SSPI authentication to ensure the same (by pg_regress
-# --config-auth).
-sub standard_initdb
-{
-   my $pgdata = shift;
-   system_or_bail('initdb', '-D', "$pgdata", '-A' , 'trust', '-N');
-   system_or_bail($ENV{PG_REGRESS}, '--config-auth', $pgdata);
-
-   my $tempdir_short = tempdir_short;
-
-   open CONF, ">>$pgdata/postgresql.conf";
-   print CONF "\n# Added by TestLib.pm)\n";
-   print CONF "fsync = off\n";
-   if ($windows_os)
-   {
-       print CONF "listen_addresses = '127.0.0.1'\n";
-   }
-   else
-   {
-       print CONF "unix_socket_directories = '$tempdir_short'\n";
-       print CONF "listen_addresses = ''\n";
-   }
-   close CONF;
-
-   $ENV{PGHOST}         = $windows_os ? "127.0.0.1" : $tempdir_short;
-}
-
-# Set up the cluster to allow replication connections, in the same way that
-# standard_initdb does for normal connections.
-sub configure_hba_for_replication
-{
-   my $pgdata = shift;
-
-   open HBA, ">>$pgdata/pg_hba.conf";
-   print HBA "\n# Allow replication (set up by TestLib.pm)\n";
-   if (! $windows_os)
-   {
-       print HBA "local replication all trust\n";
-   }
-   else
-   {
-       print HBA "host replication all 127.0.0.1/32 sspi include_realm=1 map=regress\n";
-   }
-   close HBA;
-}
-
-my ($test_server_datadir, $test_server_logfile);
-
-
-# Initialize a new cluster for testing in given directory, and start it.
-sub start_test_server
-{
-   my ($tempdir) = @_;
-   my $ret;
-
-   print("### Starting test server in $tempdir\n");
-   standard_initdb "$tempdir/pgdata";
-
-   $ret = system_log('pg_ctl', '-D', "$tempdir/pgdata", '-w', '-l',
-     "$log_path/postmaster.log", '-o', "--log-statement=all",
-     'start');
-
-   if ($ret != 0)
-   {
-       print "# pg_ctl failed; logfile:\n";
-       system('cat', "$log_path/postmaster.log");
-       BAIL_OUT("pg_ctl failed");
-   }
-
-   $test_server_datadir = "$tempdir/pgdata";
-   $test_server_logfile = "$log_path/postmaster.log";
-}
-
-sub restart_test_server
+sub system_log
 {
-   print("### Restarting test server\n");
-   system_log('pg_ctl', '-D', $test_server_datadir, '-w', '-l',
-     $test_server_logfile, 'restart');
+   print("# Running: " . join(" ", @_) . "\n");
+   return system(@_);
 }
 
-END
+sub system_or_bail
 {
-   if ($test_server_datadir)
+   if (system_log(@_) != 0)
    {
-       system_log('pg_ctl', '-D', $test_server_datadir, '-m',
-         'immediate', 'stop');
+       BAIL_OUT("system $_[0] failed");
    }
 }
 
-sub psql
+sub run_log
 {
-   my ($dbname, $sql) = @_;
-   my ($stdout, $stderr);
-   print("# Running SQL command: $sql\n");
-   run [ 'psql', '-X', '-A', '-t', '-q', '-d', $dbname, '-f', '-' ], '<', \$sql, '>', \$stdout, '2>', \$stderr or die;
-   chomp $stdout;
-   $stdout =~ s/\r//g if $Config{osname} eq 'msys';
-   return $stdout;
+   print("# Running: " . join(" ", @{ $_[0] }) . "\n");
+   return run(@_);
 }
 
 sub slurp_dir
 {
    my ($dir) = @_;
-   opendir(my $dh, $dir) or die;
+   opendir(my $dh, $dir)
+     or die "could not opendir \"$dir\": $!";
    my @direntries = readdir $dh;
    closedir $dh;
    return @direntries;
    return $contents;
 }
 
-sub system_or_bail
-{
-   if (system_log(@_) != 0)
-   {
-       BAIL_OUT("system $_[0] failed: $?");
-   }
-}
-
-sub system_log
+sub append_to_file
 {
-   print("# Running: " . join(" ", @_) ."\n");
-   return system(@_);
-}
+   my ($filename, $str) = @_;
 
-sub run_log
-{
-   print("# Running: " . join(" ", @{$_[0]}) ."\n");
-   return run (@_);
+   open my $fh, ">>", $filename or die "could not open \"$filename\": $!";
+   print $fh $str;
+   close $fh;
 }
 
-
 #
 # Test functions
 #
-
-
 sub command_ok
 {
    my ($cmd, $test_name) = @_;
 sub command_exit_is
 {
    my ($cmd, $expected, $test_name) = @_;
-   print("# Running: " . join(" ", @{$cmd}) ."\n");
-   my $h = start $cmd;
+   print("# Running: " . join(" ", @{$cmd}) . "\n");
+   my $h = IPC::Run::start $cmd;
    $h->finish();
 
    # On Windows, the exit status of the process is returned directly as the
    # assuming the Unix convention, which will always return 0 on Windows as
    # long as the process was not terminated by an exception. To work around
    # that, use $h->full_result on Windows instead.
-   my $result = ($Config{osname} eq "MSWin32") ?
-       ($h->full_results)[0] : $h->result(0);
+   my $result =
+       ($Config{osname} eq "MSWin32")
+     ? ($h->full_results)[0]
+     : $h->result(0);
    is($result, $expected, $test_name);
 }
 
    my ($cmd) = @_;
    my ($stdout, $stderr);
    print("# Running: $cmd --help\n");
-   my $result = run [ $cmd, '--help' ], '>', \$stdout, '2>', \$stderr;
+   my $result = IPC::Run::run [ $cmd, '--help' ], '>', \$stdout, '2>',
+     \$stderr;
    ok($result, "$cmd --help exit code 0");
    isnt($stdout, '', "$cmd --help goes to stdout");
    is($stderr, '', "$cmd --help nothing to stderr");
    my ($cmd) = @_;
    my ($stdout, $stderr);
    print("# Running: $cmd --version\n");
-   my $result = run [ $cmd, '--version' ], '>', \$stdout, '2>', \$stderr;
+   my $result = IPC::Run::run [ $cmd, '--version' ], '>', \$stdout, '2>',
+     \$stderr;
    ok($result, "$cmd --version exit code 0");
    isnt($stdout, '', "$cmd --version goes to stdout");
    is($stderr, '', "$cmd --version nothing to stderr");
    my ($cmd) = @_;
    my ($stdout, $stderr);
    print("# Running: $cmd --not-a-valid-option\n");
-   my $result = run [ $cmd, '--not-a-valid-option' ], '>', \$stdout, '2>',
-     \$stderr;
+   my $result = IPC::Run::run [ $cmd, '--not-a-valid-option' ], '>',
+     \$stdout,
+     '2>', \$stderr;
    ok(!$result, "$cmd with invalid option nonzero exit code");
    isnt($stderr, '', "$cmd with invalid option prints error message");
 }
    my ($cmd, $expected_stdout, $test_name) = @_;
    my ($stdout, $stderr);
    print("# Running: " . join(" ", @{$cmd}) . "\n");
-   my $result = run $cmd, '>', \$stdout, '2>', \$stderr;
+   my $result = IPC::Run::run $cmd, '>', \$stdout, '2>', \$stderr;
    ok($result, "@$cmd exit code 0");
    is($stderr, '', "@$cmd no stderr");
    like($stdout, $expected_stdout, "$test_name: matches");
 }
 
-sub issues_sql_like
-{
-   my ($cmd, $expected_sql, $test_name) = @_;
-   truncate $test_server_logfile, 0;
-   my $result = run_log($cmd);
-   ok($result, "@$cmd exit code 0");
-   my $log = slurp_file($test_server_logfile);
-   like($log, $expected_sql, "$test_name: SQL found in server log");
-}
-
 1;
 
 
 use strict;
 use warnings;
+use PostgresNode;
 use TestLib;
 use File::Basename;
 use File::Copy;
 
 sub configure_test_server_for_ssl
 {
-   my $tempdir    = $_[0];
+   my $node       = $_[0];
    my $serverhost = $_[1];
 
+   my $pgdata = $node->data_dir;
+
    # Create test users and databases
-   psql 'postgres', "CREATE USER ssltestuser";
-   psql 'postgres', "CREATE USER anotheruser";
-   psql 'postgres', "CREATE DATABASE trustdb";
-   psql 'postgres', "CREATE DATABASE certdb";
+   $node->psql('postgres', "CREATE USER ssltestuser");
+   $node->psql('postgres', "CREATE USER anotheruser");
+   $node->psql('postgres', "CREATE DATABASE trustdb");
+   $node->psql('postgres', "CREATE DATABASE certdb");
 
    # enable logging etc.
-   open CONF, ">>$tempdir/pgdata/postgresql.conf";
+   open CONF, ">>$pgdata/postgresql.conf";
    print CONF "fsync=off\n";
    print CONF "log_connections=on\n";
    print CONF "log_hostname=on\n";
    close CONF;
 
 # Copy all server certificates and keys, and client root cert, to the data dir
-   copy_files("ssl/server-*.crt", "$tempdir/pgdata");
-   copy_files("ssl/server-*.key", "$tempdir/pgdata");
-   chmod(0600, glob "$tempdir/pgdata/server-*.key") or die $!;
-   copy_files("ssl/root+client_ca.crt", "$tempdir/pgdata");
-   copy_files("ssl/root+client.crl",    "$tempdir/pgdata");
+   copy_files("ssl/server-*.crt", $pgdata);
+   copy_files("ssl/server-*.key", $pgdata);
+   chmod(0600, glob "$pgdata/server-*.key") or die $!;
+   copy_files("ssl/root+client_ca.crt", $pgdata);
+   copy_files("ssl/root+client.crl",    $pgdata);
 
   # Only accept SSL connections from localhost. Our tests don't depend on this
   # but seems best to keep it as narrow as possible for security reasons.
   #
   # When connecting to certdb, also check the client certificate.
-   open HBA, ">$tempdir/pgdata/pg_hba.conf";
+   open HBA, ">$pgdata/pg_hba.conf";
    print HBA
 "# TYPE  DATABASE        USER            ADDRESS                 METHOD\n";
    print HBA
 # the server so that the configuration takes effect.
 sub switch_server_cert
 {
-   my $tempdir  = $_[0];
+   my $node     = $_[0];
    my $certfile = $_[1];
+   my $pgdata   = $node->data_dir;
 
    diag "Restarting server with certfile \"$certfile\"...";
 
-   open SSLCONF, ">$tempdir/pgdata/sslconfig.conf";
+   open SSLCONF, ">$pgdata/sslconfig.conf";
    print SSLCONF "ssl=on\n";
    print SSLCONF "ssl_ca_file='root+client_ca.crt'\n";
    print SSLCONF "ssl_cert_file='$certfile.crt'\n";
    close SSLCONF;
 
    # Stop and restart server to reload the new config.
-   restart_test_server();
+   $node->restart;
 }
 
 use strict;
 use warnings;
+use PostgresNode;
+use TestLib;
 use TestLib;
 use Test::More tests => 38;
 use ServerSetup;
 # postgresql-ssl-regression.test.
 my $SERVERHOSTADDR = '127.0.0.1';
 
-my $tempdir = TestLib::tempdir;
-
 # Define a couple of helper functions to test connecting to the server.
 
 my $common_connstr;
 
 #### Part 0. Set up the server.
 
-diag "setting up data directory in \"$tempdir\"...";
-start_test_server($tempdir);
-configure_test_server_for_ssl($tempdir, $SERVERHOSTADDR);
-switch_server_cert($tempdir, 'server-cn-only');
+diag "setting up data directory...";
+my $node = get_new_node();
+$node->init;
+
+# PGHOST is enforced here to set up the node, subsequent connections
+# will use a dedicated connection string.
+$ENV{PGHOST} = $node->host;
+$ENV{PGPORT} = $node->port;
+$node->start;
+configure_test_server_for_ssl($node, $SERVERHOSTADDR);
+switch_server_cert($node, 'server-cn-only');
 
 ### Part 1. Run client-side tests.
 ###
 test_connect_fails("sslmode=verify-full host=wronghost.test");
 
 # Test Subject Alternative Names.
-switch_server_cert($tempdir, 'server-multiple-alt-names');
+switch_server_cert($node, 'server-multiple-alt-names');
 
 diag "test hostname matching with X509 Subject Alternative Names";
 $common_connstr =
 
 # Test certificate with a single Subject Alternative Name. (this gives a
 # slightly different error message, that's all)
-switch_server_cert($tempdir, 'server-single-alt-name');
+switch_server_cert($node, 'server-single-alt-name');
 
 diag "test hostname matching with a single X509 Subject Alternative Name";
 $common_connstr =
 
 # Test server certificate with a CN and SANs. Per RFCs 2818 and 6125, the CN
 # should be ignored when the certificate has both.
-switch_server_cert($tempdir, 'server-cn-and-alt-names');
+switch_server_cert($node, 'server-cn-and-alt-names');
 
 diag "test certificate with both a CN and SANs";
 $common_connstr =
 
 # Finally, test a server certificate that has no CN or SANs. Of course, that's
 # not a very sensible certificate, but libpq should handle it gracefully.
-switch_server_cert($tempdir, 'server-no-names');
+switch_server_cert($node, 'server-no-names');
 $common_connstr =
 "user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
 
 
 # Test that the CRL works
 diag "Testing client-side CRL";
-switch_server_cert($tempdir, 'server-revoked');
+switch_server_cert($node, 'server-revoked');
 
 $common_connstr =
 "user=ssltestuser dbname=trustdb sslcert=invalid hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
 test_connect_fails(
 "user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked.key"
 );
-
-
-# All done! Save the log, before the temporary installation is deleted
-copy("$tempdir/client-log", "./client-log");