diff --git a/patroni/config.py b/patroni/config.py index 1b454704..f9716b4c 100644 --- a/patroni/config.py +++ b/patroni/config.py @@ -15,14 +15,14 @@ from requests.structures import CaseInsensitiveDict logger = logging.getLogger(__name__) -AUTH_ALLOWED_PARAMETERS = ( +_AUTH_ALLOWED_PARAMETERS = ( 'username', 'password', 'sslmode', 'sslcert', 'sslkey', 'sslrootcert', - 'sslcrl', + 'sslcrl' ) @@ -260,9 +260,9 @@ class Config(object): if value: ret['log']['loggers'] = value - def _get_auth(name): + def _get_auth(name, params=None): ret = {} - for param in AUTH_ALLOWED_PARAMETERS: + for param in params or _AUTH_ALLOWED_PARAMETERS[:2]: value = _popenv(name + '_' + param) if value: ret[param] = value @@ -274,7 +274,7 @@ class Config(object): authentication = {} for user_type in ('replication', 'superuser', 'rewind'): - entry = _get_auth(user_type) + entry = _get_auth(user_type, _AUTH_ALLOWED_PARAMETERS) if entry: authentication[user_type] = entry @@ -366,7 +366,7 @@ class Config(object): # handle setting additional connection parameters that may be available # in the configuration file, such as SSL connection parameters for name, value in pg_config['authentication'].items(): - pg_config['authentication'][name] = {n: v for n, v in value.items() if n in AUTH_ALLOWED_PARAMETERS} + pg_config['authentication'][name] = {n: v for n, v in value.items() if n in _AUTH_ALLOWED_PARAMETERS} # no 'name' in config if 'name' not in config and 'name' in pg_config: diff --git a/patroni/dcs/__init__.py b/patroni/dcs/__init__.py index 8acc2354..aafcfd46 100644 --- a/patroni/dcs/__init__.py +++ b/patroni/dcs/__init__.py @@ -161,8 +161,7 @@ class Member(namedtuple('Member', 'index,name,session,data')): if auth and isinstance(auth, dict): ret.update(auth) if 'username' in auth: - ret['user'] = auth['username'] - del ret['username'] + ret['user'] = ret.pop('username') return ret @property diff --git a/patroni/postgresql/__init__.py b/patroni/postgresql/__init__.py index 1896906c..2f2a87ec 100644 --- a/patroni/postgresql/__init__.py +++ b/patroni/postgresql/__init__.py @@ -77,7 +77,6 @@ class Postgresql(object): self.slots_handler = SlotsHandler(self) - self._pgpass = config.get('pgpass') or os.path.join(os.path.expanduser('~'), 'pgpass') self._callback_executor = CallbackExecutor() self.__cb_called = False self.__cb_pending = None @@ -258,19 +257,6 @@ class Postgresql(object): def data_directory_empty(self): return not os.path.exists(self._data_dir) or os.listdir(self._data_dir) == [] - def write_pgpass(self, record): - if 'user' not in record or 'password' not in record: - return os.environ.copy() - - with open(self._pgpass, 'w') as f: - if os.name != 'nt': - os.fchmod(f.fileno(), 0o600) - f.write('{host}:{port}:*:{user}:{password}\n'.format(**record)) - - env = os.environ.copy() - env['PGPASSFILE'] = self._pgpass - return env - def replica_method_options(self, method): return deepcopy(self.config.get(method, {})) @@ -645,12 +631,10 @@ class Postgresql(object): @contextmanager def get_replication_connection_cursor(self, host='localhost', port=5432, database=None, **kwargs): - replication = self.config.replication - extra_kwargs = {k: v for k, v in replication.items() - if k not in ('username', 'password', 'connect_timeout', 'options')} - with get_connection_cursor(host=host, port=int(port), database=database or self._database, replication=1, - user=replication['username'], password=replication.get('password'), - connect_timeout=3, options='-c statement_timeout=2000', **extra_kwargs) as cur: + conn_kwargs = self.config.replication.copy() + conn_kwargs.update(host=host, port=int(port), database=database or self._database, connect_timeout=3, + user=conn_kwargs.pop('username'), replication=1, options='-c statement_timeout=2000') + with get_connection_cursor(**conn_kwargs) as cur: yield cur def get_local_timeline_lsn_from_replication_connection(self): @@ -696,7 +680,7 @@ class Postgresql(object): min_apply_delay = is_remote_master and member.recovery_min_apply_delay archive_cleanup = is_remote_master and member.archive_cleanup_command - primary_conninfo = self.config.primary_conninfo(member) + primary_conninfo = self.config.primary_conninfo_params(member) change_role = self.cb_called and (self.role in ('master', 'demoted') or not {'standby_leader', 'replica'} - {self.role, role}) diff --git a/patroni/postgresql/bootstrap.py b/patroni/postgresql/bootstrap.py index 78c8c98a..2f693443 100644 --- a/patroni/postgresql/bootstrap.py +++ b/patroni/postgresql/bootstrap.py @@ -135,16 +135,8 @@ class Bootstrap(object): # (pghost empty or the default socket directory) connections coming from the local machine. r['host'] = 'localhost' # set it to localhost to write into pgpass - if 'user' in r: - user = r['user'] - else: - user = '' - if 'password' in r: - import getpass - r.setdefault('user', os.environ.get('PGUSER', getpass.getuser())) - - connstring = uri('postgres', (host, r['port']), r['database'], user) - env = self._postgresql.write_pgpass(r) if 'password' in r else None + connstring = uri('postgres', (host, r['port']), r['database'], r.get('user')) + env = self._postgresql.config.write_pgpass(r) if 'password' in r else None try: ret = self._postgresql.cancellable.call(shlex.split(cmd) + [connstring], env=env) @@ -178,7 +170,7 @@ class Bootstrap(object): r = clone_member.conn_kwargs(self._postgresql.config.replication) connstring = uri('postgres', (r['host'], r['port']), r['database'], r['user']) # add the credentials to connect to the replica origin to pgpass. - env = self._postgresql.write_pgpass(r) + env = self._postgresql.config.write_pgpass(r) else: connstring = '' env = os.environ.copy() diff --git a/patroni/postgresql/config.py b/patroni/postgresql/config.py index 93241542..76aa9f56 100644 --- a/patroni/postgresql/config.py +++ b/patroni/postgresql/config.py @@ -154,6 +154,37 @@ def mtime(filename): return None +class ConfigWriter(object): + + def __init__(self, filename): + self._filename = filename + self._fd = None + + def __enter__(self): + self._fd = open(self._filename, 'w') + self.writeline('# Do not edit this file manually!\n# It will be overwritten by Patroni!') + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if self._fd: + self._fd.close() + + def writeline(self, line): + self._fd.write(line) + self._fd.write('\n') + + def writelines(self, lines): + for line in lines: + self.writeline(line) + + @staticmethod + def escape(value): # Escape (by doubling) any single quotes or backslashes in given string + return re.sub(r'([\'\\])', r'\1\1', str(value)) + + def write_param(self, param, value): + self.writeline("{0} = '{1}'".format(param, self.escape(value))) + + class ConfigHandler(object): # List of parameters which must be always passed to postmaster as command line options @@ -187,23 +218,6 @@ class ConfigHandler(object): 'wal_log_hints': ('on', lambda _: False, 90400) }) - # A list of keywords that can be found in a conninfo string. Follows what - # is acceptable by libpq - _CONNINFO_KEYWORDS = ( - 'user', - 'password', - 'host', - 'port', - 'sslmode', - 'sslcompression', - 'sslcert', - 'sslkey', - 'sslrootcert', - 'sslcrl', - 'application_name', - 'krbsrvname', - ) - _RECOVERY_PARAMETERS = { 'archive_cleanup_command', 'restore_command', @@ -223,8 +237,6 @@ class ConfigHandler(object): 'trigger_file' } - _CONFIG_WARNING_HEADER = '# Do not edit this file manually!\n# It will be overwritten by Patroni!\n' - def __init__(self, postgresql, config): self._postgresql = postgresql self._config_dir = os.path.abspath(config.get('config_dir') or postgresql.data_dir) @@ -241,6 +253,9 @@ class ConfigHandler(object): self._standby_signal = os.path.join(postgresql.data_dir, 'standby.signal') self._auto_conf = os.path.join(postgresql.data_dir, 'postgresql.auto.conf') self._auto_conf_mtime = None + self._pgpass = config.get('pgpass') or os.path.join(os.path.expanduser('~'), 'pgpass') + self._passfile = None + self._passfile_mtime = None self._synchronous_standby_names = None self._postmaster_ctime = None self._primary_conninfo = None @@ -300,28 +315,26 @@ class ConfigHandler(object): if 'custom_conf' not in self._config and not os.path.exists(self._postgresql_base_conf): os.rename(self._postgresql_conf, self._postgresql_base_conf) - with open(self._postgresql_conf, 'w') as f: - os.chmod(self._postgresql_conf, stat.S_IWRITE | stat.S_IREAD) - f.write(self._CONFIG_WARNING_HEADER) - f.write("include '{0}'\n\n".format(self._config.get('custom_conf') or self._postgresql_base_conf_name)) + with ConfigWriter(self._postgresql_conf) as f: + include = self._config.get('custom_conf') or self._postgresql_base_conf_name + f.writeline("include '{0}'\n".format(ConfigWriter.escape(include))) for name, value in sorted((configuration or self._server_parameters).items()): if (not self._postgresql.bootstrap.running_custom_bootstrap or name != 'hba_file') \ and name not in self._RECOVERY_PARAMETERS: - f.write("{0} = '{1}'\n".format(name, value)) + f.write_param(name, value) # when we are doing custom bootstrap we assume that we don't know superuser password # and in order to be able to change it, we are opening trust access from a certain address # therefore we need to make sure that hba_file is not overriden # after changing superuser password we will "revert" all these "changes" if self._postgresql.bootstrap.running_custom_bootstrap or 'hba_file' not in self._server_parameters: - f.write("hba_file = '{0}'\n".format(self._pg_hba_conf.replace('\\', '\\\\'))) + f.write_param('hba_file', self._pg_hba_conf) if 'ident_file' not in self._server_parameters: - f.write("ident_file = '{0}'\n".format(self._pg_ident_conf.replace('\\', '\\\\'))) + f.write_param('ident_file', self._pg_ident_conf) if self._postgresql.major_version >= 120000: if self._recovery_params: - f.write('\n# recovery.conf\n') - for name, value in sorted(self._recovery_params.items()): - f.write("{0} = '{1}'\n".format(name, value)) + f.writeline('\n# recovery.conf') + self._write_recovery_params(f, self._recovery_params) if not self._postgresql.bootstrap.keep_existing_recovery_conf: self._sanitize_auto_conf() @@ -349,18 +362,15 @@ class ConfigHandler(object): self.local_replication_address['host'], self.local_replication_address['port'], 0, socket.SOCK_STREAM, socket.IPPROTO_TCP)}) - with open(self._pg_hba_conf, 'w') as f: - f.write(self._CONFIG_WARNING_HEADER) + with ConfigWriter(self._pg_hba_conf) as f: for address, t in addresses.items(): - f.write(( + f.writeline(( '{0}\treplication\t{1}\t{3}\ttrust\n' - '{0}\tall\t{2}\t{3}\ttrust\n' + '{0}\tall\t{2}\t{3}\ttrust' ).format(t, self.replication['username'], self._superuser.get('username') or 'all', address)) elif not self.hba_file and self._config.get('pg_hba'): - with open(self._pg_hba_conf, 'w') as f: - f.write(self._CONFIG_WARNING_HEADER) - for line in self._config['pg_hba']: - f.write('{0}\n'.format(line)) + with ConfigWriter(self._pg_hba_conf) as f: + f.writelines(self._config['pg_hba']) return True def replace_pg_ident(self): @@ -372,29 +382,44 @@ class ConfigHandler(object): """ if not self._server_parameters.get('ident_file') and self._config.get('pg_ident'): - with open(self._pg_ident_conf, 'w') as f: - f.write(self._CONFIG_WARNING_HEADER) - for line in self._config['pg_ident']: - f.write('{0}\n'.format(line)) + with ConfigWriter(self._pg_ident_conf) as f: + f.writelines(self._config['pg_ident']) return True def primary_conninfo_params(self, member): - name = self._postgresql.name - if not (member and member.conn_url) or member.name == name: + if not (member and member.conn_url) or member.name == self._postgresql.name: return None ret = member.conn_kwargs(self.replication) - ret.update(application_name=name, sslmode='prefer') + ret['application_name'] = self._postgresql.name + ret.setdefault('sslmode', 'prefer') if self._krbsrvname: ret['krbsrvname'] = self._krbsrvname if 'database' in ret: del ret['database'] return ret - def primary_conninfo(self, member): - r = self.primary_conninfo_params(member) - if not r: - return None - return ' '.join('{0}={{{0}}}'.format(kw) for kw in self._CONNINFO_KEYWORDS if r.get(kw)).format(**r) + def format_dsn(self, params, include_dbname=False): + # A list of keywords that can be found in a conninfo string. Follows what is acceptable by libpq + keywords = ('user', 'passfile', 'host', 'port', 'sslmode', 'sslcompression', 'sslcert', + 'sslkey', 'sslrootcert', 'sslcrl', 'application_name', 'krbsrvname') + if include_dbname: + params['dbname'] = params.get('database') or self._postgresql.database + keywords = ('dbname',) + keywords + + def escape(value): + return re.sub(r'([\'\\ ])', r'\\\1', str(value)) + + return ' '.join('{0}={1}'.format(kw, escape(params[kw])) for kw in keywords if params.get(kw) is not None) + + def _write_recovery_params(self, fd, recovery_params): + for name, value in sorted(recovery_params.items()): + if name == 'primary_conninfo': + if 'password' in value: + self.write_pgpass(value) + value['passfile'] = self._passfile = self._pgpass + self._passfile_mtime = mtime(self._pgpass) + value = self.format_dsn(value) + fd.write_param(name, value) def recovery_conf_exists(self): if self._postgresql.major_version >= 120000: @@ -404,12 +429,13 @@ class ConfigHandler(object): def _read_primary_conninfo(self): pg_conf_mtime = mtime(self._postgresql_conf) auto_conf_mtime = mtime(self._auto_conf) + passfile_mtime = mtime(self._passfile) if self._passfile else False postmaster_ctime = self._postgresql.is_running() if postmaster_ctime: postmaster_ctime = postmaster_ctime.create_time() if self._postgresql_conf_mtime == pg_conf_mtime and self._auto_conf_mtime == auto_conf_mtime \ - and self._postmaster_ctime == postmaster_ctime: + and self._passfile_mtime == passfile_mtime and self._postmaster_ctime == postmaster_ctime: return None, False try: @@ -423,7 +449,8 @@ class ConfigHandler(object): def _read_primary_conninfo_pre_v12(self): recovery_conf_mtime = mtime(self._recovery_conf) - if recovery_conf_mtime == self._recovery_conf_mtime: + passfile_mtime = mtime(self._passfile) if self._passfile else False + if recovery_conf_mtime == self._recovery_conf_mtime and passfile_mtime == self._passfile_mtime: return None, False primary_conninfo = '' @@ -439,6 +466,27 @@ class ConfigHandler(object): self._recovery_conf_mtime = recovery_conf_mtime return primary_conninfo, True + def _check_passfile(self, wanted_primary_conninfo): + # If there is a passfile in the primary_conninfo try to figure out that + # the passfile contains the line allowing connection to the given node. + # We assume that the passfile was created by Patroni and therefore doing + # the full match and not covering cases when host, port or user are set to '*' + passfile = self._primary_conninfo['passfile'] + passfile_mtime = mtime(passfile) + if passfile_mtime: + try: + with open(passfile) as f: + wanted_line = self._pgpass_line(wanted_primary_conninfo).strip() + for raw_line in f: + if raw_line.strip() == wanted_line: + self._primary_conninfo['password'] = wanted_primary_conninfo['password'] + self._passfile = passfile + self._passfile_mtime = passfile_mtime + return True + except Exception: + logger.info('Failed to read %s', passfile) + return False + def check_recovery_conf(self, member): # Name is confusing. In fact it checks the value of primary_conninfo # TODO: recovery.conf could be stale, would be nice to detect that. if self._postgresql.major_version >= 120000: @@ -480,6 +528,10 @@ class ConfigHandler(object): elif not self._primary_conninfo: return False + if 'passfile' in self._primary_conninfo and 'password' in wanted_primary_conninfo and \ + 'password' not in self._primary_conninfo and not self._check_passfile(wanted_primary_conninfo): + return False + return all(self._primary_conninfo.get(p) == str(v) for p, v in wanted_primary_conninfo.items()) @staticmethod @@ -487,6 +539,28 @@ class ConfigHandler(object): if os.path.isfile(name) or os.path.islink(name): os.unlink(name) + @staticmethod + def _pgpass_line(record): + if 'password' in record: + def escape(value): + return re.sub(r'([:\\])', r'\\\1', str(value)) + + record = {n: escape(record.get(n, '*')) for n in ('host', 'port', 'user', 'password')} + return '{host}:{port}:*:{user}:{password}'.format(**record) + + def write_pgpass(self, record): + line = self._pgpass_line(record) + if not line: + return os.environ.copy() + + with open(self._pgpass, 'w') as f: + os.chmod(self._pgpass, stat.S_IWRITE | stat.S_IREAD) + f.write(line) + + env = os.environ.copy() + env['PGPASSFILE'] = self._pgpass + return env + def write_recovery_conf(self, recovery_params): if self._postgresql.major_version >= 120000: if parse_bool(recovery_params.pop('standby_mode', None)): @@ -496,10 +570,9 @@ class ConfigHandler(object): open(self._recovery_signal, 'w').close() self._recovery_params = recovery_params else: - with open(self._recovery_conf, 'w') as f: + with ConfigWriter(self._recovery_conf) as f: os.chmod(self._recovery_conf, stat.S_IWRITE | stat.S_IREAD) - for name, value in recovery_params.items(): - f.write("{0} = '{1}'\n".format(name, value)) + self._write_recovery_params(f, recovery_params) def remove_recovery_conf(self): for name in (self._recovery_conf, self._standby_signal, self._recovery_signal): @@ -678,7 +751,6 @@ class ConfigHandler(object): self._postgresql.set_pending_restart(pending_restart) self._server_parameters = server_parameters self._adjust_recovery_parameters() - self._connect_address = config.get('connect_address') self._krbsrvname = config.get('krbsrvname') # for not so obvious connection attempts that may happen outside of pyscopg2 diff --git a/patroni/postgresql/rewind.py b/patroni/postgresql/rewind.py index eab88b41..e0975ecf 100644 --- a/patroni/postgresql/rewind.py +++ b/patroni/postgresql/rewind.py @@ -145,17 +145,9 @@ class Rewind(object): def pg_rewind(self, r): # prepare pg_rewind connection - env = self._postgresql.write_pgpass(r) + env = self._postgresql.config.write_pgpass(r) env['PGOPTIONS'] = '-c statement_timeout=0' - dsn_attrs = [ - ('user', r.get('user')), - ('host', r.get('host')), - ('port', r.get('port')), - ('dbname', r.get('database') or self._postgresql.database), - ('sslmode', 'prefer'), - ('sslcompression', '1'), - ] - dsn = " ".join("{0}={1}".format(k, v) for k, v in dsn_attrs if v is not None) + dsn = self._postgresql.config.format_dsn(r, True) logger.info('running pg_rewind from %s', dsn) try: return self._postgresql.cancellable.call([self._postgresql.pgcommand('pg_rewind'), '-D', diff --git a/tests/__init__.py b/tests/__init__.py index a7e86bf6..58ec0505 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -5,7 +5,6 @@ import shutil import unittest from mock import Mock, patch -from tempfile import gettempdir import psycopg2 import requests @@ -183,7 +182,7 @@ class PostgresInit(unittest.TestCase): data_dir = 'data/test0' self.p = Postgresql({'name': 'postgresql0', 'scope': 'batman', 'data_dir': data_dir, 'config_dir': data_dir, 'retry_timeout': 10, - 'krbsrvname': 'postgres', 'pgpass': os.path.join(gettempdir(), 'pgpass0'), + 'krbsrvname': 'postgres', 'pgpass': os.path.join(data_dir, 'pgpass0'), 'listen': '127.0.0.2, 127.0.0.3:5432', 'connect_address': '127.0.0.2:5432', 'authentication': {'superuser': {'username': 'foo', 'password': 'test'}, 'replication': {'username': '', 'password': 'rep-pass'}}, diff --git a/tests/test_ha.py b/tests/test_ha.py index 367c491a..0ad0c417 100644 --- a/tests/test_ha.py +++ b/tests/test_ha.py @@ -152,7 +152,7 @@ def run_async(self, func, args=()): @patch.object(Postgresql, 'controldata', Mock(return_value={'Database system identifier': SYSID})) @patch.object(SlotsHandler, 'sync_replication_slots', Mock()) @patch.object(ConfigHandler, 'append_pg_hba', Mock()) -@patch.object(Postgresql, 'write_pgpass', Mock(return_value={})) +@patch.object(ConfigHandler, 'write_pgpass', Mock(return_value={})) @patch.object(ConfigHandler, 'write_recovery_conf', Mock()) @patch.object(Postgresql, 'query', Mock()) @patch.object(Postgresql, 'checkpoint', Mock()) diff --git a/tests/test_postgresql.py b/tests/test_postgresql.py index 878eeb68..343b2e10 100644 --- a/tests/test_postgresql.py +++ b/tests/test_postgresql.py @@ -186,10 +186,11 @@ class TestPostgresql(BaseTestPostgresql): self.assertFalse(self.p.restart()) self.assertEqual(self.p.state, 'restart failed (restarting)') + @patch('os.chmod', Mock()) @patch.object(builtins, 'open', MagicMock()) def test_write_pgpass(self): - self.p.write_pgpass({'host': 'localhost', 'port': '5432', 'user': 'foo'}) - self.p.write_pgpass({'host': 'localhost', 'port': '5432', 'user': 'foo', 'password': 'bar'}) + self.p.config.write_pgpass({'host': 'localhost', 'port': '5432', 'user': 'foo'}) + self.p.config.write_pgpass({'host': 'localhost', 'port': '5432', 'user': 'foo', 'password': 'bar'}) def test_checkpoint(self): with patch.object(MockCursor, 'fetchone', Mock(return_value=(True, ))): @@ -199,29 +200,31 @@ class TestPostgresql(BaseTestPostgresql): self.assertEqual(self.p.checkpoint(), 'not accessible or not healty') @patch('patroni.postgresql.config.mtime', mock_mtime) - @patch.object(MockCursor, 'fetchone', Mock(side_effect=[('foo=bar',), ('',), ('',), ('a=b',)])) - def test_check_recovery_conf(self): + @patch.object(MockCursor, 'fetchone') + def test_check_recovery_conf(self, mock_fetchone): + mock_fetchone.side_effect = [('foo=bar',), ('',), ('foo',), ('host=1 passfile=' + self.p.config._pgpass,)] + conninfo = {'host': '1', 'password': 'bar'} for version in (120000, 100000): with patch.object(Postgresql, 'major_version', PropertyMock(return_value=version)): - self.p.config.write_recovery_conf({'standby_mode': 'on', 'primary_conninfo': 'foo'}) + self.p.config.write_recovery_conf({'standby_mode': 'on', 'primary_conninfo': {'sslmode': 'prefer'}}) self.assertFalse(self.p.config.check_recovery_conf(None)) - self.p.config.write_recovery_conf({'primary_conninfo': 'foo'}) + self.p.config.write_recovery_conf({'primary_conninfo': {'sslmode': 'prefer'}}) self.p.config.write_postgresql_conf() self.assertFalse(self.p.config.check_recovery_conf(None)) self.p.config.write_recovery_conf({'standby_mode': 'on'}) self.assertTrue(self.p.config.check_recovery_conf(None)) with patch('patroni.postgresql.config.ConfigHandler.primary_conninfo_params', - Mock(return_value={'a': 'b'})): + Mock(return_value=conninfo.copy())): self.assertFalse(self.p.config.check_recovery_conf(None)) - self.p.config.write_recovery_conf({'standby_mode': 'on', 'primary_conninfo': 'a=b'}) - self.assertTrue(self.p.config.check_recovery_conf(None)) + self.p.config.write_recovery_conf({'standby_mode': 'on', 'primary_conninfo': conninfo.copy()}) + self.p.config.check_recovery_conf(None) @patch.object(Postgresql, 'major_version', PropertyMock(return_value=120000)) @patch.object(Postgresql, 'is_running', MockPostmaster) @patch.object(MockPostmaster, 'create_time', Mock(return_value=1234567), create=True) @patch.object(MockCursor, 'fetchone', Mock(return_value=('',))) def test__read_primary_conninfo(self): - self.p.config.write_recovery_conf({'standby_mode': 'on'}) + self.p.config.write_recovery_conf({'standby_mode': 'on', 'primary_conninfo': {'password': 'foo'}}) self.p.config.write_postgresql_conf() self.assertTrue(self.p.config.check_recovery_conf(None)) self.assertTrue(self.p.config.check_recovery_conf(None)) @@ -231,9 +234,9 @@ class TestPostgresql(BaseTestPostgresql): @patch.object(Postgresql, 'major_version', PropertyMock(return_value=100000)) def test__read_primary_conninfo_pre_v12(self): - self.p.config.write_recovery_conf({'standby_mode': 'on'}) - self.assertTrue(self.p.config.check_recovery_conf(None)) - self.assertTrue(self.p.config.check_recovery_conf(None)) + self.p.config.write_recovery_conf({'standby_mode': 'on', 'primary_conninfo': {'password': 'foo'}}) + self.assertFalse(self.p.config.check_recovery_conf(None)) + self.assertFalse(self.p.config.check_recovery_conf(None)) def test_write_postgresql_and_sanitize_auto_conf(self): read_data = 'primary_conninfo = foo\nfoo = bar\n'