Manage pg_hba.conf via patroni config or dynamic_configuration (#458)

So far Patroni was populating pg_hba.conf only when running bootstrap code and after that it was not very handy to manage it's content, because it was necessary to login to every node, change pg_hba.conf manually and run pg_ctl reload.

This commit intends to fix it and give Patroni control over pg_hba.conf. It is possible to define pg_hba.conf content via `postgresql.pg_hba` in the patroni configuration file or in the `DCS/config` (dynamic configuration).

If the `hba_file` is defined in the `postgresql.parameters`, Patroni will ignore `postgresql.pg_hba`.
This commit is contained in:
Alexander Kukushkin
2017-06-23 12:38:25 +02:00
committed by GitHub
parent 681b6b507b
commit b576e69362
3 changed files with 55 additions and 17 deletions
+4 -1
View File
@@ -87,8 +87,11 @@ PostgreSQL
- **use\_unix\_socket**: specifies that Patroni should prefer to use unix sockets to connect to the cluster. Default value is ``false``. If ``unix_socket_directories`` is definded, Patroni will use first suitable value from it to connect to the cluster and fallback to tcp if nothing is suitable. If ``unix_socket_directories`` is not specified in ``postgresql.parameters``, Patroni will assume that default value should be used and omit ``host`` from connection parameters.
- **pgpass**: path to the `.pgpass <https://www.postgresql.org/docs/current/static/libpq-pgpass.html>`__ password file. Patroni creates this file before executing pg\_basebackup, the post_init script and under some other circumstances. The location must be writable by Patroni.
- **recovery\_conf**: additional configuration settings written to recovery.conf when configuring follower.
- **custom_conf** : path to an optional custom ``postgresql.conf`` file, that will be used in place of ``postgresql.base.conf``. The file must exist on all cluster nodes, be readable by PostgreSQL and will be included from its location on the real ``postgresql.conf``. Note that Patroni will not monitor this file for changes, nor backup it. However, its settings can still be overriden by Patroni's own configuration facilities - see `dynamic configuration <https://github.com/zalando/patroni/blob/master/docs/dynamic_configuration.rst>`__ for details.
- **custom\_conf** : path to an optional custom ``postgresql.conf`` file, that will be used in place of ``postgresql.base.conf``. The file must exist on all cluster nodes, be readable by PostgreSQL and will be included from its location on the real ``postgresql.conf``. Note that Patroni will not monitor this file for changes, nor backup it. However, its settings can still be overriden by Patroni's own configuration facilities - see `dynamic configuration <https://github.com/zalando/patroni/blob/master/docs/dynamic_configuration.rst>`__ for details.
- **parameters**: list of configuration settings for Postgres. Many of these are required for replication to work.
- **pg\_hba**: list of lines that Patroni will use to generate ``pg_hba.conf``. This parameter has higher priority than ``bootstrap.pg_hba``. Together with `dynamic configuration <https://github.com/zalando/patroni/blob/master/docs/dynamic_configuration.rst>`__ it simplifies management of ``pg_hba.conf``.
- **- host all all 0.0.0.0/0 md5**.
- **- host replication replicator 127.0.0.1/32 md5**: A line like this is required for replication.
- **pg\_ctl\_timeout**: How long should pg_ctl wait when doing ``start``, ``stop`` or ``restart``. Default value is 60 seconds.
- **use\_pg\_rewind**: try to use pg\_rewind on the former leader when it joins cluster as a replica.
- **remove\_data\_directory\_on\_rewind\_failure**: If this option is enabled, Patroni will remove postgres data directory and recreate replica. Otherwise it will try to follow the new leader. Default value is **false**.
+35 -8
View File
@@ -157,6 +157,8 @@ class Postgresql(object):
self.set_state('running')
self.set_role('master' if self.is_leader() else 'replica')
self._write_postgresql_conf() # we are "joining" already running postgres
if self._replace_pg_hba():
self.reload()
@property
def _configuration_to_save(self):
@@ -280,7 +282,7 @@ class Postgresql(object):
self._superuser = config['authentication'].get('superuser', {})
server_parameters = self.get_server_parameters(config)
local_connection_address_changed = pending_reload = pending_restart = False
conf_changed = hba_changed = local_connection_address_changed = pending_restart = False
if self.state == 'running':
changes = {p: v for p, v in server_parameters.items() if '.' not in p}
changes.update({p: None for p, v in self._server_parameters.items() if not ('.' in p or p in changes)})
@@ -308,24 +310,27 @@ class Postgresql(object):
or r[0] in ('listen_addresses', 'port'):
local_connection_address_changed = True
else:
pending_reload = True
conf_changed = True
for param in changes:
if param in server_parameters:
logger.warning('Removing invalid parameter `%s` from postgresql.parameters', param)
server_parameters.pop(param)
# Check that user-defined-paramters have changed (parameters with period in name)
if not pending_reload:
if not conf_changed:
for p, v in server_parameters.items():
if '.' in p and (p not in self._server_parameters or str(v) != str(self._server_parameters[p])):
pending_reload = True
conf_changed = True
break
if not pending_reload:
if not conf_changed:
for p, v in self._server_parameters.items():
if '.' in p and (p not in server_parameters or str(v) != str(server_parameters[p])):
pending_reload = True
conf_changed = True
break
if not config['parameters'].get('hba_file') and config.get('pg_hba'):
hba_changed = self.config.get('pg_hba', []) != config['pg_hba']
self.config = config
self._pending_restart = pending_restart
self._server_parameters = server_parameters
@@ -334,9 +339,15 @@ class Postgresql(object):
if not local_connection_address_changed:
self.resolve_connection_addresses()
if pending_reload:
if conf_changed:
self._write_postgresql_conf()
if hba_changed:
self._replace_pg_hba()
if conf_changed or hba_changed:
self.reload()
self._is_leader_retry.deadline = self.retry.deadline = config['retry_timeout']/2.0
@property
@@ -501,7 +512,8 @@ class Postgresql(object):
if pwfile:
os.remove(pwfile)
if ret:
self.write_pg_hba(config.get('pg_hba', []))
if not self.config['parameters'].get('hba_file') and not self.config.get('pg_hba'):
self.write_pg_hba(config.get('pg_hba', []))
self._major_version = self.get_major_version()
self._server_parameters = self.get_server_parameters(self.config)
else:
@@ -789,6 +801,7 @@ class Postgresql(object):
self._pending_restart = False
self._write_postgresql_conf()
self._replace_pg_hba()
self.resolve_connection_addresses()
opts = {p: self._server_parameters[p] for p in self.CMDLINE_OPTIONS if p in self._server_parameters}
@@ -1060,6 +1073,20 @@ class Postgresql(object):
with open(os.path.join(self._data_dir, 'pg_hba.conf'), 'a') as f:
f.write('\n{}\n'.format('\n'.join(config)))
def _replace_pg_hba(self):
"""
Replace pg_hba.conf content in the PGDATA if hba_file is not defined in the
`postgresql.parameters` and pg_hba is defined in `postgresql` configuration section.
:returns: True if pg_hba.conf was rewritten.
"""
if not self.config['parameters'].get('hba_file') and self.config.get('pg_hba'):
with open(os.path.join(self._data_dir, 'pg_hba.conf'), 'w') as f:
f.write('# Do not edit this file manually!\n# It will be overwritten by Patroni!\n')
for line in self.config['pg_hba']:
f.write('{0}\n'.format(line))
return True
def primary_conninfo(self, member):
if not (member and member.conn_url) or member.name == self.name:
return None
+16 -8
View File
@@ -178,6 +178,7 @@ class TestPostgresql(unittest.TestCase):
'use_pg_rewind': True, 'pg_ctl_timeout': 'bla',
'parameters': self._PARAMETERS,
'recovery_conf': {'foo': 'bar'},
'pg_hba': ['host all all 0.0.0.0/0 md5'],
'callbacks': {'on_start': 'true', 'on_stop': 'true',
'on_restart': 'true', 'on_role_change': 'true',
'on_reload': 'true'
@@ -498,15 +499,22 @@ class TestPostgresql(unittest.TestCase):
with patch.object(Postgresql, 'run_bootstrap_post_init', Mock(return_value=False)):
self.assertRaises(PostgresException, self.p.bootstrap, {})
self.p.bootstrap({'users': {'replicator': {'password': 'rep-pass', 'options': ['replication']}},
'pg_hba': ['host replication replicator 127.0.0.1/32 md5',
'hostssl all all 0.0.0.0/0 md5',
'host all all 0.0.0.0/0 md5'],
'post_init': '/bin/false'})
config = {'users': {'replicator': {'password': 'rep-pass', 'options': ['replication']}}}
self.p.bootstrap(config)
with open(os.path.join(self.data_dir, 'pg_hba.conf')) as f:
lines = f.readlines()
assert 'host replication replicator 127.0.0.1/32 md5\n' in lines
assert 'host all all 0.0.0.0/0 md5\n' in lines
self.assertTrue('host all all 0.0.0.0/0 md5\n' in lines)
self.p.config.pop('pg_hba')
config.update({'post_init': '/bin/false',
'pg_hba': ['host replication replicator 127.0.0.1/32 md5',
'hostssl all all 0.0.0.0/0 md5',
'host all all 0.0.0.0/0 md5']})
self.p.bootstrap(config)
with open(os.path.join(self.data_dir, 'pg_hba.conf')) as f:
lines = f.readlines()
self.assertTrue('host replication replicator 127.0.0.1/32 md5\n' in lines)
def test_run_bootstrap_post_init(self):
with patch('subprocess.call', Mock(return_value=1)):
@@ -593,7 +601,7 @@ class TestPostgresql(unittest.TestCase):
def test_reload_config(self):
parameters = self._PARAMETERS.copy()
parameters.pop('f.oo')
config = {'use_unix_socket': True, 'authentication': {},
config = {'pg_hba': [''], 'use_unix_socket': True, 'authentication': {},
'retry_timeout': 10, 'listen': '*', 'parameters': parameters}
self.p.reload_config(config)
parameters['b.ar'] = 'bar'