Files
patroni/tests/test_config.py
Polina Bungina 3e9bceac11 Don't filter out contradictory nofailover tag (#2992)
* Ensure that nofailover will always be used if both nofailover and
failover_priority tags are provided
* Call _validate_failover_tags from reload_local_configuration() as well
* Properly check values in the _validate_failover_tags(): nofailover value should be casted to boolean like it is done when accessed in other places
2024-01-05 10:16:52 +01:00

244 lines
12 KiB
Python

import os
import sys
import unittest
import io
from copy import deepcopy
from mock import MagicMock, Mock, patch
from patroni.config import Config, ConfigParseError, GlobalConfig
class TestConfig(unittest.TestCase):
@patch('os.path.isfile', Mock(return_value=True))
@patch('json.load', Mock(side_effect=Exception))
@patch('builtins.open', MagicMock())
def setUp(self):
sys.argv = ['patroni.py']
os.environ[Config.PATRONI_CONFIG_VARIABLE] = 'restapi: {}\npostgresql: {data_dir: foo}'
self.config = Config(None)
def test_set_dynamic_configuration(self):
with patch.object(Config, '_build_effective_configuration', Mock(side_effect=Exception)):
self.assertFalse(self.config.set_dynamic_configuration({'foo': 'bar'}))
self.assertTrue(self.config.set_dynamic_configuration({'standby_cluster': {}, 'postgresql': {
'parameters': {'cluster_name': 1, 'hot_standby': 1, 'wal_keep_size': 1,
'track_commit_timestamp': 1, 'wal_level': 1, 'max_connections': '100'}}}))
def test_reload_local_configuration(self):
os.environ.update({
'PATRONI_NAME': 'postgres0',
'PATRONI_NAMESPACE': '/patroni/',
'PATRONI_SCOPE': 'batman2',
'PATRONI_LOGLEVEL': 'ERROR',
'PATRONI_LOG_LOGGERS': 'patroni.postmaster: WARNING, urllib3: DEBUG',
'PATRONI_LOG_FILE_NUM': '5',
'PATRONI_CITUS_DATABASE': 'citus',
'PATRONI_CITUS_GROUP': '0',
'PATRONI_CITUS_HOST': '0',
'PATRONI_RESTAPI_USERNAME': 'username',
'PATRONI_RESTAPI_PASSWORD': 'password',
'PATRONI_RESTAPI_LISTEN': '0.0.0.0:8008',
'PATRONI_RESTAPI_CONNECT_ADDRESS': '127.0.0.1:8008',
'PATRONI_RESTAPI_CERTFILE': '/certfile',
'PATRONI_RESTAPI_KEYFILE': '/keyfile',
'PATRONI_RESTAPI_ALLOWLIST_INCLUDE_MEMBERS': 'on',
'PATRONI_POSTGRESQL_LISTEN': '0.0.0.0:5432',
'PATRONI_POSTGRESQL_CONNECT_ADDRESS': '127.0.0.1:5432',
'PATRONI_POSTGRESQL_PROXY_ADDRESS': '127.0.0.1:5433',
'PATRONI_POSTGRESQL_DATA_DIR': 'data/postgres0',
'PATRONI_POSTGRESQL_CONFIG_DIR': 'data/postgres0',
'PATRONI_POSTGRESQL_PGPASS': '/tmp/pgpass0',
'PATRONI_ETCD_HOST': '127.0.0.1:2379',
'PATRONI_ETCD_URL': 'https://127.0.0.1:2379',
'PATRONI_ETCD_PROXY': 'http://127.0.0.1:2379',
'PATRONI_ETCD_SRV': 'test',
'PATRONI_ETCD_CACERT': '/cacert',
'PATRONI_ETCD_CERT': '/cert',
'PATRONI_ETCD_KEY': '/key',
'PATRONI_CONSUL_HOST': '127.0.0.1:8500',
'PATRONI_CONSUL_REGISTER_SERVICE': 'on',
'PATRONI_KUBERNETES_LABELS': 'a: b: c',
'PATRONI_KUBERNETES_SCOPE_LABEL': 'a',
'PATRONI_KUBERNETES_PORTS': '[{"name": "postgresql"}]',
'PATRONI_KUBERNETES_RETRIABLE_HTTP_CODES': '401',
'PATRONI_ZOOKEEPER_HOSTS': "'host1:2181','host2:2181'",
'PATRONI_EXHIBITOR_HOSTS': 'host1,host2',
'PATRONI_EXHIBITOR_PORT': '8181',
'PATRONI_RAFT_PARTNER_ADDRS': "'host1:1234','host2:1234'",
'PATRONI_foo_HOSTS': '[host1,host2', # Exception in parse_list
'PATRONI_SUPERUSER_USERNAME': 'postgres',
'PATRONI_SUPERUSER_PASSWORD': 'zalando',
'PATRONI_REPLICATION_USERNAME': 'replicator',
'PATRONI_REPLICATION_PASSWORD': 'rep-pass',
'PATRONI_admin_PASSWORD': 'admin',
'PATRONI_admin_OPTIONS': 'createrole,createdb',
'PATRONI_POSTGRESQL_BIN_POSTGRES': 'sergtsop'
})
config = Config('postgres0.yml')
with patch.object(Config, '_load_config_file', Mock(return_value={'restapi': {}})):
with patch.object(Config, '_build_effective_configuration', Mock(side_effect=Exception)):
config.reload_local_configuration()
self.assertTrue(config.reload_local_configuration())
self.assertIsNone(config.reload_local_configuration())
@patch('tempfile.mkstemp', Mock(return_value=[3000, 'blabla']))
@patch('os.path.exists', Mock(return_value=True))
@patch('os.remove', Mock(side_effect=IOError))
@patch('os.close', Mock(side_effect=IOError))
@patch('os.chmod', Mock())
@patch('shutil.move', Mock(return_value=None))
@patch('json.dump', Mock())
def test_save_cache(self):
self.config.set_dynamic_configuration({'ttl': 30, 'postgresql': {'foo': 'bar'}})
with patch('os.fdopen', Mock(side_effect=IOError)):
self.config.save_cache()
with patch('os.fdopen', MagicMock()):
self.config.save_cache()
def test_standby_cluster_parameters(self):
dynamic_configuration = {
'standby_cluster': {
'create_replica_methods': ['wal_e', 'basebackup'],
'host': 'localhost',
'port': 5432
}
}
self.config.set_dynamic_configuration(dynamic_configuration)
for name, value in dynamic_configuration['standby_cluster'].items():
self.assertEqual(self.config['standby_cluster'][name], value)
@patch('os.path.exists', Mock(return_value=True))
@patch('os.path.isfile', Mock(side_effect=lambda fname: fname != 'postgres0'))
@patch('os.path.isdir', Mock(return_value=True))
@patch('os.listdir', Mock(return_value=['01-specific.yml', '00-base.yml']))
def test_configuration_directory(self):
def open_mock(fname, *args, **kwargs):
if fname.endswith('00-base.yml'):
return io.StringIO(
u'''
test: True
test2:
child-1: somestring
child-2: 5
child-3: False
test3: True
test4:
- abc: 3
- abc: 4
''')
elif fname.endswith('01-specific.yml'):
return io.StringIO(
u'''
test: False
test2:
child-2: 10
child-3: !!null
test4:
- ab: 5
new-attr: True
''')
with patch('builtins.open', MagicMock(side_effect=open_mock)):
config = Config('postgres0')
self.assertEqual(config._local_configuration,
{'test': False, 'test2': {'child-1': 'somestring', 'child-2': 10},
'test3': True, 'test4': [{'ab': 5}], 'new-attr': True})
@patch('os.path.exists', Mock(return_value=True))
@patch('os.path.isfile', Mock(return_value=False))
@patch('os.path.isdir', Mock(return_value=False))
def test_invalid_path(self):
self.assertRaises(ConfigParseError, Config, 'postgres0')
@patch.object(Config, 'get')
@patch('patroni.config.logger')
def test__validate_failover_tags(self, mock_logger, mock_get):
"""Ensures that only one of `nofailover` or `failover_priority` can be provided"""
config = Config("postgres0.yml")
# Providing one of `nofailover` or `failover_priority` is fine
for single_param in ({"nofailover": True}, {"failover_priority": 1}, {"failover_priority": 0}):
mock_get.side_effect = [single_param] * 2
self.assertIsNone(config._validate_failover_tags())
mock_logger.warning.assert_not_called()
# Providing both `nofailover` and `failover_priority` is fine if consistent
for consistent_state in (
{"nofailover": False, "failover_priority": 1},
{"nofailover": True, "failover_priority": 0},
{"nofailover": "False", "failover_priority": 0}
):
mock_get.side_effect = [consistent_state] * 2
self.assertIsNone(config._validate_failover_tags())
mock_logger.warning.assert_not_called()
# Providing both inconsistently should log a warning
for inconsistent_state in (
{"nofailover": False, "failover_priority": 0},
{"nofailover": True, "failover_priority": 1},
{"nofailover": "False", "failover_priority": 1},
{"nofailover": "", "failover_priority": 0}
):
mock_get.side_effect = [inconsistent_state] * 2
self.assertIsNone(config._validate_failover_tags())
mock_logger.warning.assert_called_once_with(
'Conflicting configuration between nofailover: %s and failover_priority: %s.'
+ ' Defaulting to nofailover: %s',
inconsistent_state['nofailover'],
inconsistent_state['failover_priority'],
inconsistent_state['nofailover'])
mock_logger.warning.reset_mock()
def test__process_postgresql_parameters(self):
expected_params = {
'f.oo': 'bar', # not in ConfigHandler.CMDLINE_OPTIONS
'max_connections': 100, # IntValidator
'wal_keep_size': '128MB', # IntValidator
'wal_level': 'hot_standby', # EnumValidator
}
input_params = deepcopy(expected_params)
input_params['max_connections'] = '100'
self.assertEqual(self.config._process_postgresql_parameters(input_params), expected_params)
expected_params['f.oo'] = input_params['f.oo'] = '100'
self.assertEqual(self.config._process_postgresql_parameters(input_params), expected_params)
input_params['wal_level'] = 'cold_standby'
expected_params.pop('wal_level')
self.assertEqual(self.config._process_postgresql_parameters(input_params), expected_params)
input_params['max_connections'] = 10
expected_params.pop('max_connections')
self.assertEqual(self.config._process_postgresql_parameters(input_params), expected_params)
def test__validate_and_adjust_timeouts(self):
with patch('patroni.config.logger.warning') as mock_logger:
self.config._validate_and_adjust_timeouts({'ttl': 15})
self.assertEqual(mock_logger.call_args_list[0][0],
("%s=%d can't be smaller than %d, adjusting...", 'ttl', 15, 20))
with patch('patroni.config.logger.warning') as mock_logger:
self.config._validate_and_adjust_timeouts({'loop_wait': 0})
self.assertEqual(mock_logger.call_args_list[0][0],
("%s=%d can't be smaller than %d, adjusting...", 'loop_wait', 0, 1))
with patch('patroni.config.logger.warning') as mock_logger:
self.config._validate_and_adjust_timeouts({'retry_timeout': 1})
self.assertEqual(mock_logger.call_args_list[0][0],
("%s=%d can't be smaller than %d, adjusting...", 'retry_timeout', 1, 3))
with patch('patroni.config.logger.warning') as mock_logger:
self.config._validate_and_adjust_timeouts({'ttl': 20, 'loop_wait': 11, 'retry_timeout': 5})
self.assertEqual(mock_logger.call_args_list[0][0],
('Violated the rule "loop_wait + 2*retry_timeout <= ttl", where ttl=%d '
'and retry_timeout=%d. Adjusting loop_wait from %d to %d', 20, 5, 11, 10))
with patch('patroni.config.logger.warning') as mock_logger:
self.config._validate_and_adjust_timeouts({'ttl': 20, 'loop_wait': 10, 'retry_timeout': 10})
self.assertEqual(mock_logger.call_args_list[0][0],
('Violated the rule "loop_wait + 2*retry_timeout <= ttl", where ttl=%d. Adjusting'
' loop_wait from %d to %d and retry_timeout from %d to %d', 20, 10, 1, 10, 9))
def test_global_config_is_synchronous_mode(self):
# we should ignore synchronous_mode setting in a standby cluster
config = {'standby_cluster': {'host': 'some_host'}, 'synchronous_mode': True}
self.assertFalse(GlobalConfig(config).is_synchronous_mode)