mirror of
https://github.com/outbackdingo/patroni.git
synced 2026-08-31 00:29:29 +00:00
Merge branch 'master' of github.com:zalando/patroni into feature/terminaltables
This commit is contained in:
+2
-8
@@ -1,4 +1,3 @@
|
||||
import datetime
|
||||
import os
|
||||
import shutil
|
||||
import unittest
|
||||
@@ -88,7 +87,7 @@ class MockCursor(object):
|
||||
elif sql.startswith('SELECT slot_name'):
|
||||
self.results = [('blabla', 'physical'), ('foobar', 'physical'), ('ls', 'logical', 'a', 'b')]
|
||||
elif sql.startswith('SELECT CASE WHEN pg_catalog.pg_is_in_recovery()'):
|
||||
self.results = [(1, 2, 1)]
|
||||
self.results = [(1, 2, 1, 0, False, 1, 1, None, None)]
|
||||
elif sql.startswith('SELECT pg_catalog.pg_is_in_recovery()'):
|
||||
self.results = [(False, 2)]
|
||||
elif sql.startswith('SELECT pg_catalog.to_char'):
|
||||
@@ -106,12 +105,7 @@ class MockCursor(object):
|
||||
('autovacuum', 'on', None, 'bool', 'sighup'),
|
||||
('unix_socket_directories', '/tmp', None, 'string', 'postmaster')]
|
||||
elif sql.startswith('IDENTIFY_SYSTEM'):
|
||||
self.results = [('1', 2, '0/402EEC0', '')]
|
||||
elif sql.startswith('SELECT isdir, modification'):
|
||||
self.results = [(False, datetime.datetime.now())]
|
||||
elif sql.startswith('SELECT pg_catalog.pg_read_file'):
|
||||
self.results = [('1\t0/40159C0\tno recovery target specified\n\n'
|
||||
'2\t1/40159C0\tno recovery target specified\n',)]
|
||||
self.results = [('1', 3, '0/402EEC0', '')]
|
||||
elif sql.startswith('TIMELINE_HISTORY '):
|
||||
self.results = [('', b'x\t0/40159C0\tno recovery target specified\n\n'
|
||||
b'1\t0/40159C0\tno recovery target specified\n\n'
|
||||
|
||||
+40
-13
@@ -30,6 +30,8 @@ class MockPostgresql(object):
|
||||
pending_restart = True
|
||||
wal_name = 'wal'
|
||||
lsn_name = 'lsn'
|
||||
POSTMASTER_START_TIME = 'pg_catalog.to_char(pg_catalog.pg_postmaster_start_time'
|
||||
TL_LSN = 'CASE WHEN pg_catalog.pg_is_in_recovery()'
|
||||
|
||||
@staticmethod
|
||||
def connection():
|
||||
@@ -177,6 +179,8 @@ class TestRestApiHandler(unittest.TestCase):
|
||||
MockRestApiServer(RestApiHandler, 'GET /asynchronous')
|
||||
MockPatroni.ha.is_leader = Mock(return_value=True)
|
||||
MockRestApiServer(RestApiHandler, 'GET /replica')
|
||||
with patch.object(MockHa, 'is_standby_cluster', Mock(return_value=True)):
|
||||
MockRestApiServer(RestApiHandler, 'GET /standby_leader')
|
||||
MockPatroni.dcs.cluster = None
|
||||
with patch.object(RestApiHandler, 'get_postgresql_status', Mock(return_value={'role': 'master'})):
|
||||
MockRestApiServer(RestApiHandler, 'GET /master')
|
||||
@@ -201,8 +205,8 @@ class TestRestApiHandler(unittest.TestCase):
|
||||
|
||||
@patch.object(MockPatroni, 'dcs')
|
||||
def test_do_GET_cluster(self, mock_dcs):
|
||||
mock_dcs.cluster = get_cluster_initialized_without_leader()
|
||||
mock_dcs.cluster.members[1].data['xlog_location'] = 11
|
||||
mock_dcs.get_cluster.return_value = get_cluster_initialized_without_leader()
|
||||
mock_dcs.get_cluster.return_value.members[1].data['xlog_location'] = 11
|
||||
self.assertIsNotNone(MockRestApiServer(RestApiHandler, 'GET /cluster'))
|
||||
|
||||
@patch.object(MockPatroni, 'dcs')
|
||||
@@ -307,6 +311,15 @@ class TestRestApiHandler(unittest.TestCase):
|
||||
request = 'DELETE /restart HTTP/1.0' + self._authorization
|
||||
self.assertIsNotNone(MockRestApiServer(RestApiHandler, request))
|
||||
|
||||
@patch.object(MockPatroni, 'dcs')
|
||||
def test_do_DELETE_switchover(self, mock_dcs):
|
||||
request = 'DELETE /switchover HTTP/1.0' + self._authorization
|
||||
self.assertIsNotNone(MockRestApiServer(RestApiHandler, request))
|
||||
mock_dcs.manual_failover.return_value = False
|
||||
self.assertIsNotNone(MockRestApiServer(RestApiHandler, request))
|
||||
mock_dcs.get_cluster.return_value.failover = None
|
||||
self.assertIsNotNone(MockRestApiServer(RestApiHandler, request))
|
||||
|
||||
@patch.object(MockPatroni, 'dcs')
|
||||
def test_do_POST_reinitialize(self, mock_dcs):
|
||||
cluster = mock_dcs.get_cluster.return_value
|
||||
@@ -413,25 +426,27 @@ class TestRestApiHandler(unittest.TestCase):
|
||||
MockRestApiServer(RestApiHandler, post + '37\n\n{"candidate":"2","scheduled_at": "1"}')
|
||||
|
||||
|
||||
@patch('ssl.SSLContext.load_cert_chain', Mock())
|
||||
@patch('ssl.SSLContext.wrap_socket', Mock(return_value=0))
|
||||
@patch.object(BaseHTTPServer.HTTPServer, '__init__', Mock())
|
||||
class TestRestApiServer(unittest.TestCase):
|
||||
|
||||
@patch('ssl.SSLContext.load_cert_chain', Mock())
|
||||
@patch('ssl.SSLContext.wrap_socket', Mock(return_value=0))
|
||||
@patch.object(BaseHTTPServer.HTTPServer, '__init__', Mock())
|
||||
def setUp(self):
|
||||
self.srv = MockRestApiServer(Mock(), '', {'listen': '*:8008', 'certfile': 'a', 'verify_client': 'required'})
|
||||
|
||||
@patch.object(BaseHTTPServer.HTTPServer, '__init__', Mock())
|
||||
def test_reload_config(self):
|
||||
bad_config = {'listen': 'foo'}
|
||||
self.assertRaises(ValueError, MockRestApiServer, None, '', bad_config)
|
||||
srv = MockRestApiServer(Mock(), '', {'listen': '*:8008', 'certfile': 'a', 'verify_client': 'required'})
|
||||
self.assertRaises(ValueError, srv.reload_config, bad_config)
|
||||
self.assertRaises(ValueError, srv.reload_config, {})
|
||||
self.assertRaises(ValueError, self.srv.reload_config, bad_config)
|
||||
self.assertRaises(ValueError, self.srv.reload_config, {})
|
||||
with patch.object(socket.socket, 'setsockopt', Mock(side_effect=socket.error)):
|
||||
srv.reload_config({'listen': ':8008'})
|
||||
self.srv.reload_config({'listen': ':8008'})
|
||||
|
||||
def test_check_auth(self):
|
||||
srv = MockRestApiServer(Mock(), '', {'listen': '*:8008', 'certfile': 'a', 'verify_client': 'required'})
|
||||
mock_rh = Mock()
|
||||
mock_rh.request.getpeercert.return_value = None
|
||||
self.assertIsNot(srv.check_auth(mock_rh), True)
|
||||
self.assertIsNot(self.srv.check_auth(mock_rh), True)
|
||||
|
||||
def test_handle_error(self):
|
||||
try:
|
||||
@@ -439,6 +454,18 @@ class TestRestApiServer(unittest.TestCase):
|
||||
except Exception:
|
||||
self.assertIsNone(MockRestApiServer.handle_error(None, ('127.0.0.1', 55555)))
|
||||
|
||||
@patch.object(BaseHTTPServer.HTTPServer, '__init__', Mock(side_effect=socket.error))
|
||||
def test_socket_error(self):
|
||||
with patch.object(BaseHTTPServer.HTTPServer, '__init__', Mock(side_effect=socket.error)):
|
||||
self.assertRaises(socket.error, MockRestApiServer, Mock(), '', {'listen': '*:8008'})
|
||||
self.assertRaises(socket.error, MockRestApiServer, Mock(), '', {'listen': '*:8008'})
|
||||
|
||||
@patch.object(MockRestApiServer, 'finish_request', Mock())
|
||||
def test_process_request_thread(self):
|
||||
mock_socket = Mock()
|
||||
self.srv.process_request_thread((mock_socket, 1), '2')
|
||||
mock_socket.context.wrap_socket.side_effect = socket.error
|
||||
self.srv.process_request_thread((mock_socket, 1), '2')
|
||||
|
||||
@patch.object(socket.socket, 'accept', Mock(return_value=(1, '2')))
|
||||
def test_get_request(self):
|
||||
self.srv.socket = Mock()
|
||||
self.assertEqual(self.srv.get_request(), ((self.srv.socket, 1), '2'))
|
||||
|
||||
@@ -13,7 +13,7 @@ class TestCancellableSubprocess(unittest.TestCase):
|
||||
|
||||
def test_call(self):
|
||||
self.c.cancel()
|
||||
self.assertRaises(PostgresException, self.c.call, communicate_input=None)
|
||||
self.assertRaises(PostgresException, self.c.call)
|
||||
|
||||
def test__kill_children(self):
|
||||
self.c._process_children = [Mock()]
|
||||
|
||||
@@ -122,6 +122,8 @@ class TestConsul(unittest.TestCase):
|
||||
self.c.refresh_session = Mock(return_value=True)
|
||||
for _ in range(0, 4):
|
||||
self.c.touch_member({'balbla': 'blabla'})
|
||||
self.c.refresh_session = Mock(side_effect=ConsulError('foo'))
|
||||
self.assertFalse(self.c.touch_member({'balbla': 'blabla'}))
|
||||
|
||||
@patch.object(consul.Consul.KV, 'put', Mock(side_effect=InvalidSession))
|
||||
def test_take_leader(self):
|
||||
|
||||
+55
-8
@@ -19,9 +19,7 @@ from .test_ha import get_cluster_initialized_without_leader, get_cluster_initial
|
||||
get_cluster_initialized_with_only_leader, get_cluster_not_initialized_without_leader, get_cluster, Member
|
||||
|
||||
|
||||
|
||||
def test_rw_config():
|
||||
global CONFIG_FILE_PATH
|
||||
runner = CliRunner()
|
||||
with runner.isolated_filesystem():
|
||||
load_config(CONFIG_FILE_PATH, None)
|
||||
@@ -32,10 +30,9 @@ def test_rw_config():
|
||||
os.rmdir(CONFIG_PATH)
|
||||
|
||||
|
||||
@patch('patroni.ctl.load_config',
|
||||
Mock(return_value={'scope': 'alpha', 'postgresql': {'data_dir': '.', 'pgpass': './pgpass',
|
||||
'parameters': {}, 'retry_timeout': 5},
|
||||
'restapi': {'listen': '::', 'certfile': 'a'}, 'etcd': {'host': 'localhost:2379'}}))
|
||||
@patch('patroni.ctl.load_config', Mock(return_value={
|
||||
'scope': 'alpha', 'restapi': {'listen': '::', 'certfile': 'a'}, 'etcd': {'host': 'localhost:2379'},
|
||||
'postgresql': {'data_dir': '.', 'pgpass': './pgpass', 'parameters': {}, 'retry_timeout': 5}}))
|
||||
class TestCtl(unittest.TestCase):
|
||||
|
||||
@patch('socket.getaddrinfo', socket_getaddrinfo)
|
||||
@@ -76,7 +73,7 @@ class TestCtl(unittest.TestCase):
|
||||
scheduled_at = datetime.now(tzutc) + timedelta(seconds=600)
|
||||
cluster = get_cluster_initialized_with_leader(Failover(1, 'foo', 'bar', scheduled_at))
|
||||
del cluster.members[1].data['conn_url']
|
||||
for fmt in ('pretty', 'json', 'yaml', 'tsv'):
|
||||
for fmt in ('pretty', 'json', 'yaml', 'tsv', 'topology'):
|
||||
self.assertIsNone(output_members(cluster, name='abc', fmt=fmt))
|
||||
|
||||
@patch('patroni.ctl.get_dcs')
|
||||
@@ -426,9 +423,39 @@ class TestCtl(unittest.TestCase):
|
||||
assert '2100' in result.output
|
||||
assert 'Scheduled restart' in result.output
|
||||
|
||||
@patch('patroni.ctl.get_dcs')
|
||||
def test_topology(self, mock_get_dcs):
|
||||
mock_get_dcs.return_value = self.e
|
||||
cluster = get_cluster_initialized_with_leader()
|
||||
cascade_member = Member(0, 'cascade', 28, {'conn_url': 'postgres://replicator:[email protected]:5437/postgres',
|
||||
'api_url': 'http://127.0.0.1:8012/patroni',
|
||||
'state': 'running',
|
||||
'tags': {'replicatefrom': 'other'},
|
||||
})
|
||||
cascade_member_wrong_tags = Member(0, 'wrong_cascade', 28,
|
||||
{'conn_url': 'postgres://replicator:[email protected]:5438/postgres',
|
||||
'api_url': 'http://127.0.0.1:8013/patroni',
|
||||
'state': 'running',
|
||||
'tags': {'replicatefrom': 'nonexistinghost'},
|
||||
})
|
||||
cluster.members.append(cascade_member)
|
||||
cluster.members.append(cascade_member_wrong_tags)
|
||||
mock_get_dcs.return_value.get_cluster = Mock(return_value=cluster)
|
||||
result = self.runner.invoke(ctl, ['topology', 'dummy'])
|
||||
assert '+\n| leader | 127.0.0.1:5435 | Leader |' in result.output
|
||||
assert '|\n| + other | 127.0.0.1:5436 | Replica |' in result.output
|
||||
assert '|\n| + cascade | 127.0.0.1:5437 | Replica |' in result.output
|
||||
assert '|\n| + wrong_cascade | 127.0.0.1:5438 | Replica |' in result.output
|
||||
|
||||
cluster = get_cluster_initialized_without_leader()
|
||||
mock_get_dcs.return_value.get_cluster = Mock(return_value=cluster)
|
||||
result = self.runner.invoke(ctl, ['topology', 'dummy'])
|
||||
assert '+\n| + leader | 127.0.0.1:5435 | Replica |' in result.output
|
||||
assert '|\n| + other | 127.0.0.1:5436 | Replica |' in result.output
|
||||
|
||||
@patch('patroni.ctl.get_dcs')
|
||||
@patch.object(PoolManager, 'request', Mock(return_value=MockResponse()))
|
||||
def test_flush(self, mock_get_dcs):
|
||||
def test_flush_restart(self, mock_get_dcs):
|
||||
mock_get_dcs.return_value = self.e
|
||||
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_leader
|
||||
|
||||
@@ -441,6 +468,26 @@ class TestCtl(unittest.TestCase):
|
||||
result = self.runner.invoke(ctl, ['flush', 'dummy', 'restart', '--force'])
|
||||
assert 'Failed: flush scheduled restart' in result.output
|
||||
|
||||
@patch('patroni.ctl.get_dcs')
|
||||
@patch.object(PoolManager, 'request', Mock(return_value=MockResponse()))
|
||||
def test_flush_switchover(self, mock_get_dcs):
|
||||
mock_get_dcs.return_value = self.e
|
||||
|
||||
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_leader
|
||||
result = self.runner.invoke(ctl, ['flush', 'dummy', 'switchover'])
|
||||
assert 'No pending scheduled switchover' in result.output
|
||||
|
||||
scheduled_at = datetime.now(tzutc) + timedelta(seconds=600)
|
||||
mock_get_dcs.return_value.get_cluster = Mock(
|
||||
return_value=get_cluster_initialized_with_leader(Failover(1, 'a', 'b', scheduled_at)))
|
||||
result = self.runner.invoke(ctl, ['flush', 'dummy', 'switchover'])
|
||||
assert result.output.startswith('Success: ')
|
||||
|
||||
mock_get_dcs.return_value.manual_failover = Mock()
|
||||
with patch.object(PoolManager, 'request', side_effect=[MockResponse(409), Exception]):
|
||||
result = self.runner.invoke(ctl, ['flush', 'dummy', 'switchover'])
|
||||
assert 'Could not find any accessible member of cluster' in result.output
|
||||
|
||||
@patch.object(PoolManager, 'request')
|
||||
@patch('patroni.ctl.get_dcs')
|
||||
@patch('patroni.ctl.polling_loop', Mock(return_value=[1]))
|
||||
|
||||
+21
-14
@@ -130,12 +130,11 @@ class TestClient(unittest.TestCase):
|
||||
self.client.http.request_encode_body = http_request
|
||||
|
||||
def test_machines(self):
|
||||
self.client._base_uri = 'http://localhost:4001'
|
||||
self.client._machines_cache = ['http://localhost:2379']
|
||||
self.client._base_uri = 'http://localhost:4002'
|
||||
self.client._machines_cache = ['http://localhost:4002', 'http://localhost:2379']
|
||||
self.assertIsNotNone(self.client.machines)
|
||||
self.client._base_uri = 'http://localhost:4001'
|
||||
self.client._machines_cache = []
|
||||
self.assertIsNotNone(self.client.machines)
|
||||
self.client._machines_cache = ['http://localhost:4001']
|
||||
self.client._update_machines_cache = True
|
||||
machines = None
|
||||
try:
|
||||
@@ -146,16 +145,13 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
@patch.object(Client, 'machines')
|
||||
def test_api_execute(self, mock_machines):
|
||||
mock_machines.__get__ = Mock(return_value=['http://localhost:2379'])
|
||||
mock_machines.__get__ = Mock(return_value=['http://localhost:4001', 'http://localhost:2379'])
|
||||
self.assertRaises(ValueError, self.client.api_execute, '', '')
|
||||
self.client._base_uri = 'http://localhost:4001'
|
||||
self.client._machines_cache = ['http://localhost:2379']
|
||||
self.assertRaises(etcd.EtcdException, self.client.api_execute, '/', 'POST', timeout=0)
|
||||
self.client._base_uri = 'http://localhost:4001'
|
||||
self.client._machines_cache = ['http://localhost:2379']
|
||||
rtry = Retry(deadline=10, max_delay=1, max_tries=-1, retry_exceptions=(etcd.EtcdLeaderElectionInProgress,))
|
||||
rtry(self.client.api_execute, '/', 'POST', timeout=0, params={'retry': rtry})
|
||||
mock_machines.__get__ = Mock(return_value=['http://localhost:2379'])
|
||||
self.client._machines_cache_updated = 0
|
||||
self.client.api_execute('/', 'POST', timeout=0)
|
||||
self.client._machines_cache = [self.client._base_uri]
|
||||
@@ -163,10 +159,17 @@ class TestClient(unittest.TestCase):
|
||||
self.assertRaises(etcd.EtcdWatchTimedOut, self.client.api_execute, '/timeout', 'POST', params={'wait': 'true'})
|
||||
self.assertRaises(etcd.EtcdException, self.client.api_execute, '/', '')
|
||||
|
||||
with patch.object(Client, '_do_http_request', Mock(side_effect=etcd.EtcdConnectionFailed)):
|
||||
with patch.object(Client, '_calculate_timeouts', Mock(side_effect=[(1, 1, 0), (1, 1, 0), (0, 1, 0)])):
|
||||
self.assertRaises(etcd.EtcdException, rtry, self.client.api_execute, '/', 'GET', params={'retry': rtry})
|
||||
self.client._read_timeout = 0
|
||||
with patch.object(Client, '_calculate_timeouts', Mock(side_effect=[(1, 1, 0), (1, 1, 0), (0, 1, 0)])),\
|
||||
patch.object(Client, '_load_machines_cache', Mock(side_effect=Exception)):
|
||||
self.client.http.request = Mock(side_effect=socket.error)
|
||||
self.assertRaises(etcd.EtcdException, rtry, self.client.api_execute, '/', 'GET', params={'retry': rtry})
|
||||
|
||||
with patch.object(Client, '_calculate_timeouts', Mock(side_effect=[(1, 1, 0), (1, 1, 0), (0, 1, 0)])),\
|
||||
patch.object(Client, '_load_machines_cache', Mock(return_value=True)):
|
||||
self.assertRaises(etcd.EtcdException, rtry, self.client.api_execute, '/', 'GET', params={'retry': rtry})
|
||||
|
||||
with patch.object(Client, '_do_http_request', Mock(side_effect=etcd.EtcdException)):
|
||||
self.client._read_timeout = 0.01
|
||||
self.assertRaises(etcd.EtcdException, self.client.api_execute, '/', 'GET')
|
||||
|
||||
def test_get_srv_record(self):
|
||||
@@ -182,8 +185,12 @@ class TestClient(unittest.TestCase):
|
||||
self.client._get_machines_cache_from_dns('error', 2379)
|
||||
|
||||
@patch.object(Client, 'machines')
|
||||
def test__load_machines_cache(self, mock_machines):
|
||||
mock_machines.__get__ = Mock(return_value=['http://localhost:2379'])
|
||||
def test__refresh_machines_cache(self, mock_machines):
|
||||
mock_machines.__get__ = Mock(side_effect=etcd.EtcdConnectionFailed)
|
||||
self.assertIsNone(self.client._refresh_machines_cache())
|
||||
self.assertRaises(etcd.EtcdException, self.client._refresh_machines_cache, True)
|
||||
|
||||
def test__load_machines_cache(self):
|
||||
self.client._config = {}
|
||||
self.assertRaises(Exception, self.client._load_machines_cache)
|
||||
self.client._config = {'srv': 'blabla'}
|
||||
|
||||
+26
-6
@@ -3,7 +3,7 @@ import etcd
|
||||
import os
|
||||
import sys
|
||||
|
||||
from mock import Mock, MagicMock, PropertyMock, patch
|
||||
from mock import Mock, MagicMock, PropertyMock, patch, mock_open
|
||||
from patroni.config import Config
|
||||
from patroni.dcs import Cluster, ClusterConfig, Failover, Leader, Member, get_dcs, SyncState, TimelineHistory
|
||||
from patroni.dcs.etcd import Client
|
||||
@@ -17,6 +17,7 @@ from patroni.postgresql.rewind import Rewind
|
||||
from patroni.postgresql.slots import SlotsHandler
|
||||
from patroni.utils import tzutc
|
||||
from patroni.watchdog import Watchdog
|
||||
from six.moves import builtins
|
||||
|
||||
from . import PostgresInit, MockPostmaster, psycopg2_connect, requests_get
|
||||
from .test_etcd import socket_getaddrinfo, etcd_read, etcd_write
|
||||
@@ -152,9 +153,11 @@ def run_async(self, func, args=()):
|
||||
@patch.object(Postgresql, 'is_leader', Mock(return_value=True))
|
||||
@patch.object(Postgresql, 'timeline_wal_position', Mock(return_value=(1, 10, 1)))
|
||||
@patch.object(Postgresql, '_cluster_info_state_get', Mock(return_value=3))
|
||||
@patch.object(Postgresql, 'call_nowait', Mock(return_value=True))
|
||||
@patch.object(Postgresql, 'data_directory_empty', Mock(return_value=False))
|
||||
@patch.object(Postgresql, 'controldata', Mock(return_value={'Database system identifier': SYSID}))
|
||||
@patch.object(Postgresql, 'controldata', Mock(return_value={
|
||||
'Database system identifier': SYSID,
|
||||
'Database cluster state': 'shut down',
|
||||
'Latest checkpoint location': '0/12345678'}))
|
||||
@patch.object(SlotsHandler, 'sync_replication_slots', Mock())
|
||||
@patch.object(ConfigHandler, 'append_pg_hba', Mock())
|
||||
@patch.object(ConfigHandler, 'write_pgpass', Mock(return_value={}))
|
||||
@@ -163,7 +166,7 @@ def run_async(self, func, args=()):
|
||||
@patch.object(Postgresql, 'query', Mock())
|
||||
@patch.object(Postgresql, 'checkpoint', Mock())
|
||||
@patch.object(CancellableSubprocess, 'call', Mock(return_value=0))
|
||||
@patch.object(Postgresql, 'get_local_timeline_lsn_from_replication_connection', Mock(return_value=[2, 10]))
|
||||
@patch.object(Postgresql, 'get_replica_timeline', Mock(return_value=2))
|
||||
@patch.object(Postgresql, 'get_master_timeline', Mock(return_value=2))
|
||||
@patch.object(ConfigHandler, 'restore_configuration_files', Mock())
|
||||
@patch.object(etcd.Client, 'write', etcd_write)
|
||||
@@ -172,6 +175,7 @@ def run_async(self, func, args=()):
|
||||
@patch('patroni.postgresql.polling_loop', Mock(return_value=range(1)))
|
||||
@patch('patroni.async_executor.AsyncExecutor.busy', PropertyMock(return_value=False))
|
||||
@patch('patroni.async_executor.AsyncExecutor.run_async', run_async)
|
||||
@patch('patroni.postgresql.rewind.Thread', Mock())
|
||||
@patch('subprocess.call', Mock(return_value=0))
|
||||
@patch('time.sleep', Mock())
|
||||
class TestHa(PostgresInit):
|
||||
@@ -198,6 +202,7 @@ class TestHa(PostgresInit):
|
||||
self.p.last_operation = Mock(side_effect=PostgresConnectionException(''))
|
||||
self.assertTrue(self.ha.update_lock(True))
|
||||
|
||||
@patch.object(Postgresql, 'received_timeline', Mock(return_value=None))
|
||||
def test_touch_member(self):
|
||||
self.p.timeline_wal_position = Mock(return_value=(0, 1, 0))
|
||||
self.p.replica_cached_timeline = Mock(side_effect=Exception)
|
||||
@@ -253,7 +258,7 @@ class TestHa(PostgresInit):
|
||||
self.ha.cluster = get_cluster_initialized_with_leader()
|
||||
self.assertEqual(self.ha.run_cycle(), 'starting as readonly because i had the session lock')
|
||||
|
||||
@patch.object(Postgresql, 'fix_cluster_state', Mock())
|
||||
@patch.object(Rewind, 'ensure_clean_shutdown', Mock())
|
||||
def test_crash_recovery(self):
|
||||
self.p.is_running = false
|
||||
self.p.controldata = lambda: {'Database cluster state': 'in production', 'Database system identifier': SYSID}
|
||||
@@ -689,13 +694,19 @@ class TestHa(PostgresInit):
|
||||
self.ha.cluster = get_cluster_initialized_with_leader(Failover(0, '', self.p.name, None))
|
||||
self.assertEqual(self.ha.run_cycle(), 'PAUSE: waiting to become master after promote...')
|
||||
|
||||
@patch('patroni.postgresql.mtime', Mock(return_value=1588316884))
|
||||
@patch.object(builtins, 'open', mock_open(read_data='1\t0/40159C0\tno recovery target specified\n'))
|
||||
def test_process_healthy_standby_cluster_as_standby_leader(self):
|
||||
self.p.is_leader = false
|
||||
self.p.name = 'leader'
|
||||
self.ha.cluster = get_standby_cluster_initialized_with_only_leader()
|
||||
self.p.config.check_recovery_conf = Mock(return_value=(False, False))
|
||||
self.ha._leader_timeline = 1
|
||||
self.assertEqual(self.ha.run_cycle(), 'promoted self to a standby leader because i had the session lock')
|
||||
self.assertEqual(self.ha.run_cycle(), 'no action. i am the standby leader with the lock')
|
||||
self.p.set_role('replica')
|
||||
self.p.config.check_recovery_conf = Mock(return_value=(True, False))
|
||||
self.assertEqual(self.ha.run_cycle(), 'promoted self to a standby leader because i had the session lock')
|
||||
|
||||
def test_process_healthy_standby_cluster_as_cascade_replica(self):
|
||||
self.p.is_leader = false
|
||||
@@ -1006,6 +1017,8 @@ class TestHa(PostgresInit):
|
||||
self.ha._disable_sync = False
|
||||
self.assertEqual(self.ha.get_effective_tags(), {'foo': 'bar'})
|
||||
|
||||
@patch('patroni.postgresql.mtime', Mock(return_value=1588316884))
|
||||
@patch.object(builtins, 'open', Mock(side_effect=Exception))
|
||||
def test_restore_cluster_config(self):
|
||||
self.ha.cluster.config.data.clear()
|
||||
self.ha.has_lock = true
|
||||
@@ -1037,7 +1050,9 @@ class TestHa(PostgresInit):
|
||||
# will not say bootstrap from leader as replica can't self elect
|
||||
self.assertEqual(self.ha.run_cycle(), "trying to bootstrap from replica 'other'")
|
||||
|
||||
@patch('psycopg2.connect', psycopg2_connect)
|
||||
@patch('patroni.postgresql.mtime', Mock(return_value=1588316884))
|
||||
@patch.object(builtins, 'open', mock_open(read_data=('1\t0/40159C0\tno recovery target specified\n\n'
|
||||
'2\t1/40159C0\tno recovery target specified\n')))
|
||||
def test_update_cluster_history(self):
|
||||
self.ha.has_lock = true
|
||||
self.ha.cluster.is_unlocked = false
|
||||
@@ -1068,3 +1083,8 @@ class TestHa(PostgresInit):
|
||||
self.ha.cluster = get_cluster_initialized_without_leader(leader=True, cluster_config=config)
|
||||
self.ha.has_lock = true
|
||||
self.assertEqual(self.ha.run_cycle(), 'no action. i am the leader with the lock')
|
||||
|
||||
@patch.object(Cluster, 'has_member', true)
|
||||
def test_run_cycle(self):
|
||||
self.ha.dcs.touch_member = Mock(side_effect=DCSError('foo'))
|
||||
self.assertEqual(self.ha.run_cycle(), 'Unexpected exception raised, please report it as a BUG')
|
||||
|
||||
+87
-67
@@ -8,7 +8,7 @@ from threading import Thread
|
||||
from . import SleepException
|
||||
|
||||
|
||||
def mock_list_namespaced_config_map(self, *args, **kwargs):
|
||||
def mock_list_namespaced_config_map(*args, **kwargs):
|
||||
metadata = {'resource_version': '1', 'labels': {'f': 'b'}, 'name': 'test-config',
|
||||
'annotations': {'initialize': '123', 'config': '{}'}}
|
||||
items = [k8s_client.V1ConfigMap(metadata=k8s_client.V1ObjectMeta(**metadata))]
|
||||
@@ -22,72 +22,72 @@ def mock_list_namespaced_config_map(self, *args, **kwargs):
|
||||
return k8s_client.V1ConfigMapList(metadata=metadata, items=items, kind='ConfigMapList')
|
||||
|
||||
|
||||
def mock_list_namespaced_pod(self, *args, **kwargs):
|
||||
metadata = k8s_client.V1ObjectMeta(resource_version='1', name='p-0', annotations={'status': '{}'})
|
||||
items = [k8s_client.V1Pod(metadata=metadata)]
|
||||
def mock_list_namespaced_endpoints(*args, **kwargs):
|
||||
target_ref = k8s_client.V1ObjectReference(kind='Pod', resource_version='10', name='p-0',
|
||||
namespace='default', uid='964dfeae-e79b-4476-8a5a-1920b5c2a69d')
|
||||
address0 = k8s_client.V1EndpointAddress(ip='10.0.0.0', target_ref=target_ref)
|
||||
address1 = k8s_client.V1EndpointAddress(ip='10.0.0.1')
|
||||
port = k8s_client.V1EndpointPort(port=5432, name='postgresql', protocol='TCP')
|
||||
subset = k8s_client.V1EndpointSubset(addresses=[address1, address0], ports=[port])
|
||||
metadata = k8s_client.V1ObjectMeta(resource_version='1', labels={'f': 'b'}, name='test',
|
||||
annotations={'optime': '1234', 'leader': 'p-0', 'ttl': '30s'})
|
||||
endpoint = k8s_client.V1Endpoints(subsets=[subset], metadata=metadata)
|
||||
metadata = k8s_client.V1ObjectMeta(resource_version='1')
|
||||
return k8s_client.V1EndpointsList(metadata=metadata, items=[endpoint], kind='V1EndpointsList')
|
||||
|
||||
|
||||
def mock_list_namespaced_pod(*args, **kwargs):
|
||||
metadata = k8s_client.V1ObjectMeta(resource_version='1', name='p-0', annotations={'status': '{}'},
|
||||
uid='964dfeae-e79b-4476-8a5a-1920b5c2a69d')
|
||||
status = k8s_client.V1PodStatus(pod_ip='10.0.0.0')
|
||||
spec = k8s_client.V1PodSpec(hostname='p-0', node_name='kind-control-plane', containers=[])
|
||||
items = [k8s_client.V1Pod(metadata=metadata, status=status, spec=spec)]
|
||||
return k8s_client.V1PodList(items=items, kind='PodList')
|
||||
|
||||
|
||||
def mock_config_map(*args, **kwargs):
|
||||
def mock_namespaced_kind(*args, **kwargs):
|
||||
mock = Mock()
|
||||
mock.metadata.resource_version = '2'
|
||||
return mock
|
||||
|
||||
@patch('socket.TCP_KEEPIDLE', 4, create=True)
|
||||
@patch('socket.TCP_KEEPINTVL', 5, create=True)
|
||||
@patch('socket.TCP_KEEPCNT', 6, create=True)
|
||||
@patch.object(k8s_client.CoreV1Api, 'patch_namespaced_config_map', mock_config_map)
|
||||
@patch.object(k8s_client.CoreV1Api, 'create_namespaced_config_map', mock_config_map)
|
||||
@patch('kubernetes.client.api_client.ThreadPool', Mock(), create=True)
|
||||
@patch.object(Thread, 'start', Mock())
|
||||
class TestKubernetes(unittest.TestCase):
|
||||
|
||||
class BaseTestKubernetes(unittest.TestCase):
|
||||
|
||||
@patch('socket.TCP_KEEPIDLE', 4, create=True)
|
||||
@patch('socket.TCP_KEEPINTVL', 5, create=True)
|
||||
@patch('socket.TCP_KEEPCNT', 6, create=True)
|
||||
@patch('kubernetes.config.load_kube_config', Mock())
|
||||
@patch.object(k8s_client.CoreV1Api, 'list_namespaced_config_map', mock_list_namespaced_config_map)
|
||||
@patch.object(k8s_client.CoreV1Api, 'list_namespaced_pod', mock_list_namespaced_pod)
|
||||
@patch('kubernetes.client.api_client.ThreadPool', Mock(), create=True)
|
||||
@patch.object(Thread, 'start', Mock())
|
||||
def setUp(self):
|
||||
self.k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0',
|
||||
'loop_wait': 10, 'retry_timeout': 10, 'labels': {'f': 'b'}})
|
||||
@patch.object(k8s_client.CoreV1Api, 'list_namespaced_pod', mock_list_namespaced_pod)
|
||||
@patch.object(k8s_client.CoreV1Api, 'list_namespaced_config_map', mock_list_namespaced_config_map)
|
||||
def setUp(self, config=None):
|
||||
config = config or {}
|
||||
config.update(ttl=30, scope='test', name='p-0', loop_wait=10, retry_timeout=10, labels={'f': 'b'})
|
||||
self.k = Kubernetes(config)
|
||||
self.assertRaises(AttributeError, self.k._pods._build_cache)
|
||||
self.k._pods._is_ready = True
|
||||
self.assertRaises(AttributeError, self.k._kinds._build_cache)
|
||||
self.k._kinds._is_ready = True
|
||||
self.k.get_cluster()
|
||||
|
||||
|
||||
@patch.object(k8s_client.CoreV1Api, 'patch_namespaced_config_map', mock_namespaced_kind)
|
||||
class TestKubernetesConfigMaps(BaseTestKubernetes):
|
||||
|
||||
@patch('time.time', Mock(side_effect=[1, 10.9, 100]))
|
||||
def test__wait_caches(self):
|
||||
self.k._pods._is_ready = False
|
||||
with self.k._condition:
|
||||
self.assertRaises(RetryFailedError, self.k._wait_caches)
|
||||
|
||||
@patch('time.time', Mock(return_value=time.time() + 100))
|
||||
def test_get_cluster(self):
|
||||
with patch.object(k8s_client.CoreV1Api, 'list_namespaced_config_map', mock_list_namespaced_config_map), \
|
||||
patch.object(k8s_client.CoreV1Api, 'list_namespaced_pod', mock_list_namespaced_pod), \
|
||||
patch('time.time', Mock(return_value=time.time() + 31)):
|
||||
self.k.get_cluster()
|
||||
self.k.get_cluster()
|
||||
|
||||
with patch.object(Kubernetes, '_wait_caches', Mock(side_effect=Exception)):
|
||||
self.assertRaises(KubernetesError, self.k.get_cluster)
|
||||
|
||||
@patch('kubernetes.config.load_kube_config', Mock())
|
||||
@patch.object(k8s_client.CoreV1Api, 'create_namespaced_endpoints', Mock())
|
||||
def test_update_leader(self):
|
||||
k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0', 'loop_wait': 10, 'retry_timeout': 10,
|
||||
'labels': {'f': 'b'}, 'use_endpoints': True, 'pod_ip': '10.0.0.0'})
|
||||
self.assertIsNotNone(k.update_leader('123'))
|
||||
|
||||
@patch('kubernetes.config.load_kube_config', Mock())
|
||||
@patch.object(k8s_client.CoreV1Api, 'create_namespaced_endpoints', Mock())
|
||||
def test_update_leader_with_restricted_access(self):
|
||||
k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0', 'loop_wait': 10, 'retry_timeout': 10,
|
||||
'labels': {'f': 'b'}, 'use_endpoints': True, 'pod_ip': '10.0.0.0'})
|
||||
self.assertIsNotNone(k.update_leader('123', True))
|
||||
|
||||
def test_take_leader(self):
|
||||
self.k.take_leader()
|
||||
self.k._leader_observed_record['leader'] = 'test'
|
||||
@@ -101,8 +101,9 @@ class TestKubernetes(unittest.TestCase):
|
||||
def test_set_config_value(self):
|
||||
self.k.set_config_value('{}')
|
||||
|
||||
@patch.object(k8s_client.CoreV1Api, 'patch_namespaced_pod', Mock(return_value=True))
|
||||
def test_touch_member(self):
|
||||
@patch.object(k8s_client.CoreV1Api, 'patch_namespaced_pod')
|
||||
def test_touch_member(self, mock_patch_namespaced_pod):
|
||||
mock_patch_namespaced_pod.return_value.metadata.resource_version = '10'
|
||||
self.k.touch_member({'role': 'replica'})
|
||||
self.k._name = 'p-1'
|
||||
self.k.touch_member({'state': 'running', 'role': 'replica'})
|
||||
@@ -112,7 +113,7 @@ class TestKubernetes(unittest.TestCase):
|
||||
self.k.initialize()
|
||||
|
||||
def test_delete_leader(self):
|
||||
self.k.delete_leader()
|
||||
self.k.delete_leader(1)
|
||||
|
||||
def test_cancel_initialization(self):
|
||||
self.k.cancel_initialization()
|
||||
@@ -122,14 +123,6 @@ class TestKubernetes(unittest.TestCase):
|
||||
def test_delete_cluster(self):
|
||||
self.k.delete_cluster()
|
||||
|
||||
@patch('kubernetes.config.load_kube_config', Mock())
|
||||
@patch.object(k8s_client.CoreV1Api, 'create_namespaced_endpoints',
|
||||
Mock(side_effect=[k8s_client.rest.ApiException(502, ''), k8s_client.rest.ApiException(500, '')]))
|
||||
def test_delete_sync_state(self):
|
||||
k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0', 'loop_wait': 10, 'retry_timeout': 10,
|
||||
'labels': {'f': 'b'}, 'use_endpoints': True, 'pod_ip': '10.0.0.0'})
|
||||
self.assertFalse(k.delete_sync_state())
|
||||
|
||||
def test_watch(self):
|
||||
self.k.set_ttl(10)
|
||||
self.k.watch(None, 0)
|
||||
@@ -138,31 +131,58 @@ class TestKubernetes(unittest.TestCase):
|
||||
def test_set_history_value(self):
|
||||
self.k.set_history_value('{}')
|
||||
|
||||
@patch('kubernetes.config.load_kube_config', Mock())
|
||||
@patch('patroni.dcs.kubernetes.ObjectCache', Mock())
|
||||
@patch.object(k8s_client.CoreV1Api, 'patch_namespaced_pod', Mock(return_value=True))
|
||||
@patch.object(k8s_client.CoreV1Api, 'create_namespaced_endpoints', Mock())
|
||||
|
||||
class TestKubernetesEndpoints(BaseTestKubernetes):
|
||||
|
||||
@patch.object(k8s_client.CoreV1Api, 'list_namespaced_endpoints', mock_list_namespaced_endpoints)
|
||||
def setUp(self, config=None):
|
||||
super(TestKubernetesEndpoints, self).setUp({'use_endpoints': True, 'pod_ip': '10.0.0.0'})
|
||||
|
||||
@patch.object(k8s_client.CoreV1Api, 'patch_namespaced_endpoints')
|
||||
def test_update_leader(self, mock_patch_namespaced_endpoints):
|
||||
self.assertIsNotNone(self.k.update_leader('123'))
|
||||
args = mock_patch_namespaced_endpoints.call_args[0]
|
||||
self.assertEqual(args[2].subsets[0].addresses[0].target_ref.resource_version, '10')
|
||||
self.k._kinds._object_cache['test'].subsets[:] = []
|
||||
self.assertIsNotNone(self.k.update_leader('123'))
|
||||
self.k._kinds._object_cache['test'].metadata.annotations['leader'] = 'p-1'
|
||||
self.assertFalse(self.k.update_leader('123'))
|
||||
|
||||
@patch.object(k8s_client.CoreV1Api, 'patch_namespaced_endpoints', mock_namespaced_kind)
|
||||
def test_update_leader_with_restricted_access(self):
|
||||
self.assertIsNotNone(self.k.update_leader('123', True))
|
||||
|
||||
@patch.object(k8s_client.CoreV1Api, 'patch_namespaced_endpoints')
|
||||
def test__update_leader_with_retry(self, mock_patch):
|
||||
mock_patch.side_effect = k8s_client.rest.ApiException(502, '')
|
||||
self.assertFalse(self.k.update_leader('123'))
|
||||
mock_patch.side_effect = RetryFailedError('')
|
||||
self.assertFalse(self.k.update_leader('123'))
|
||||
mock_patch.side_effect = k8s_client.rest.ApiException(409, '')
|
||||
with patch('time.time', Mock(side_effect=[0, 100, 200])):
|
||||
self.assertFalse(self.k.update_leader('123'))
|
||||
with patch('time.sleep', Mock()):
|
||||
self.assertFalse(self.k.update_leader('123'))
|
||||
mock_patch.side_effect = [k8s_client.rest.ApiException(409, ''), mock_namespaced_kind()]
|
||||
self.k._kinds._object_cache['test'].metadata.resource_version = '2'
|
||||
self.assertIsNotNone(self.k._update_leader_with_retry({}, '1', []))
|
||||
|
||||
@patch.object(k8s_client.CoreV1Api, 'create_namespaced_endpoints',
|
||||
Mock(side_effect=[k8s_client.rest.ApiException(500, ''), k8s_client.rest.ApiException(502, '')]))
|
||||
def test_delete_sync_state(self):
|
||||
self.assertFalse(self.k.delete_sync_state())
|
||||
|
||||
@patch.object(k8s_client.CoreV1Api, 'patch_namespaced_pod', mock_namespaced_kind)
|
||||
@patch.object(k8s_client.CoreV1Api, 'create_namespaced_endpoints', mock_namespaced_kind)
|
||||
@patch.object(k8s_client.CoreV1Api, 'create_namespaced_service',
|
||||
Mock(side_effect=[True, False, k8s_client.rest.ApiException(500, '')]))
|
||||
def test__create_config_service(self):
|
||||
k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0', 'loop_wait': 10, 'retry_timeout': 10,
|
||||
'labels': {'f': 'b'}, 'use_endpoints': True, 'pod_ip': '10.0.0.0'})
|
||||
self.assertIsNotNone(k.patch_or_create_config({'foo': 'bar'}))
|
||||
self.assertIsNotNone(k.patch_or_create_config({'foo': 'bar'}))
|
||||
k.touch_member({'state': 'running', 'role': 'replica'})
|
||||
self.assertIsNotNone(self.k.patch_or_create_config({'foo': 'bar'}))
|
||||
self.assertIsNotNone(self.k.patch_or_create_config({'foo': 'bar'}))
|
||||
self.k.touch_member({'state': 'running', 'role': 'replica'})
|
||||
|
||||
|
||||
class TestCacheBuilder(unittest.TestCase):
|
||||
|
||||
@patch('socket.TCP_KEEPIDLE', 4, create=True)
|
||||
@patch('socket.TCP_KEEPINTVL', 5, create=True)
|
||||
@patch('socket.TCP_KEEPCNT', 6, create=True)
|
||||
@patch('kubernetes.config.load_kube_config', Mock())
|
||||
@patch('kubernetes.client.api_client.ThreadPool', Mock(), create=True)
|
||||
@patch.object(Thread, 'start', Mock())
|
||||
def setUp(self):
|
||||
self.k = Kubernetes({'ttl': 30, 'scope': 'test', 'name': 'p-0',
|
||||
'loop_wait': 10, 'retry_timeout': 10, 'labels': {'f': 'b'}})
|
||||
class TestCacheBuilder(BaseTestKubernetes):
|
||||
|
||||
@patch.object(k8s_client.CoreV1Api, 'list_namespaced_config_map', mock_list_namespaced_config_map)
|
||||
@patch('patroni.dcs.kubernetes.ObjectCache._watch')
|
||||
|
||||
+22
-39
@@ -93,7 +93,7 @@ class TestPostgresql(BaseTestPostgresql):
|
||||
@patch('subprocess.call', Mock(return_value=0))
|
||||
@patch('os.rename', Mock())
|
||||
@patch('patroni.postgresql.CallbackExecutor', Mock())
|
||||
@patch.object(Postgresql, 'get_major_version', Mock(return_value=120000))
|
||||
@patch.object(Postgresql, 'get_major_version', Mock(return_value=130000))
|
||||
@patch.object(Postgresql, 'is_running', Mock(return_value=True))
|
||||
def setUp(self):
|
||||
super(TestPostgresql, self).setUp()
|
||||
@@ -250,6 +250,10 @@ class TestPostgresql(BaseTestPostgresql):
|
||||
self.p.config.write_recovery_conf({'standby_mode': 'on', 'primary_conninfo': conninfo.copy()})
|
||||
self.p.config.write_postgresql_conf()
|
||||
self.assertEqual(self.p.config.check_recovery_conf(None), (False, False))
|
||||
with patch.object(Postgresql, 'primary_conninfo', Mock(return_value='host=1')):
|
||||
mock_get_pg_settings.return_value['primary_slot_name'] = [
|
||||
'primary_slot_name', '', '', 'string', 'postmaster', self.p.config._postgresql_conf]
|
||||
self.assertEqual(self.p.config.check_recovery_conf(None), (True, True))
|
||||
|
||||
@patch.object(Postgresql, 'major_version', PropertyMock(return_value=120000))
|
||||
@patch.object(Postgresql, 'is_running', MockPostmaster)
|
||||
@@ -267,6 +271,7 @@ class TestPostgresql(BaseTestPostgresql):
|
||||
self.assertEqual(self.p.config.check_recovery_conf(None), (True, True))
|
||||
|
||||
@patch.object(Postgresql, 'major_version', PropertyMock(return_value=100000))
|
||||
@patch.object(Postgresql, 'primary_conninfo', Mock(return_value='host=1'))
|
||||
def test__read_recovery_params_pre_v12(self):
|
||||
self.p.config.write_recovery_conf({'standby_mode': 'on', 'primary_conninfo': {'password': 'foo'}})
|
||||
self.assertEqual(self.p.config.check_recovery_conf(None), (True, True))
|
||||
@@ -338,6 +343,11 @@ class TestPostgresql(BaseTestPostgresql):
|
||||
with patch.object(Postgresql, '_query', Mock(side_effect=RetryFailedError(''))):
|
||||
self.assertRaises(PostgresConnectionException, self.p.is_leader)
|
||||
|
||||
@patch.object(Postgresql, 'controldata',
|
||||
Mock(return_value={'Database cluster state': 'shut down', 'Latest checkpoint location': 'X/678'}))
|
||||
def test_latest_checkpoint_location(self):
|
||||
self.assertIsNone(self.p.latest_checkpoint_location())
|
||||
|
||||
def test_reload(self):
|
||||
self.assertTrue(self.p.reload())
|
||||
|
||||
@@ -412,6 +422,10 @@ class TestPostgresql(BaseTestPostgresql):
|
||||
pass
|
||||
os.makedirs(os.path.join(self.p.data_dir, 'foo'))
|
||||
_symlink('foo', os.path.join(self.p.data_dir, 'pg_wal'))
|
||||
os.makedirs(os.path.join(self.p.data_dir, 'foo_tsp'))
|
||||
pg_tblspc = os.path.join(self.p.data_dir, 'pg_tblspc')
|
||||
os.makedirs(pg_tblspc)
|
||||
_symlink('../foo_tsp', os.path.join(pg_tblspc, '12345'))
|
||||
self.p.remove_data_directory()
|
||||
open(self.p.data_dir, 'w').close()
|
||||
self.p.remove_data_directory()
|
||||
@@ -665,44 +679,8 @@ class TestPostgresql(BaseTestPostgresql):
|
||||
mock_postmaster.signal_stop.assert_called()
|
||||
mock_postmaster.wait.assert_called()
|
||||
|
||||
def test_read_postmaster_opts(self):
|
||||
m = mock_open(read_data='/usr/lib/postgres/9.6/bin/postgres "-D" "data/postgresql0" \
|
||||
"--listen_addresses=127.0.0.1" "--port=5432" "--hot_standby=on" "--wal_level=hot_standby" \
|
||||
"--wal_log_hints=on" "--max_wal_senders=5" "--max_replication_slots=5"\n')
|
||||
with patch.object(builtins, 'open', m):
|
||||
data = self.p.read_postmaster_opts()
|
||||
self.assertEqual(data['wal_level'], 'hot_standby')
|
||||
self.assertEqual(int(data['max_replication_slots']), 5)
|
||||
self.assertEqual(data.get('D'), None)
|
||||
|
||||
m.side_effect = IOError
|
||||
data = self.p.read_postmaster_opts()
|
||||
self.assertEqual(data, dict())
|
||||
|
||||
@patch('psutil.Popen')
|
||||
def test_single_user_mode(self, subprocess_popen_mock):
|
||||
subprocess_popen_mock.return_value.wait.return_value = 0
|
||||
self.assertEqual(self.p.single_user_mode('CHECKPOINT', {'archive_mode': 'on'}), 0)
|
||||
|
||||
@patch('os.listdir', Mock(side_effect=[OSError, ['a', 'b']]))
|
||||
@patch('os.unlink', Mock(side_effect=OSError))
|
||||
@patch('os.remove', Mock())
|
||||
@patch('os.path.islink', Mock(side_effect=[True, False]))
|
||||
@patch('os.path.isfile', Mock(return_value=True))
|
||||
def test_cleanup_archive_status(self):
|
||||
self.p.cleanup_archive_status()
|
||||
self.p.cleanup_archive_status()
|
||||
|
||||
@patch('os.unlink', Mock())
|
||||
@patch('os.listdir', Mock(return_value=[]))
|
||||
@patch('os.path.isfile', Mock(return_value=True))
|
||||
@patch.object(Postgresql, 'read_postmaster_opts', Mock(return_value={}))
|
||||
@patch.object(Postgresql, 'single_user_mode', Mock(return_value=0))
|
||||
def test_fix_cluster_state(self):
|
||||
self.assertTrue(self.p.fix_cluster_state())
|
||||
|
||||
def test_replica_cached_timeline(self):
|
||||
self.assertEqual(self.p.replica_cached_timeline(1), 2)
|
||||
self.assertEqual(self.p.replica_cached_timeline(2), 3)
|
||||
|
||||
def test_get_master_timeline(self):
|
||||
self.assertEqual(self.p.get_master_timeline(), 1)
|
||||
@@ -712,7 +690,6 @@ class TestPostgresql(BaseTestPostgresql):
|
||||
with patch.object(Postgresql, 'controldata',
|
||||
Mock(return_value={'max_connections setting': '200',
|
||||
'max_worker_processes setting': '20',
|
||||
'max_prepared_xacts setting': '100',
|
||||
'max_locks_per_xact setting': '100',
|
||||
'max_wal_senders setting': 10})):
|
||||
self.p.cancellable.cancel()
|
||||
@@ -723,3 +700,9 @@ class TestPostgresql(BaseTestPostgresql):
|
||||
@patch('os.path.isfile', Mock(return_value=False))
|
||||
def test_pgpass_is_dir(self):
|
||||
self.assertRaises(PatroniException, self.setUp)
|
||||
|
||||
@patch.object(Postgresql, '_query', Mock(side_effect=RetryFailedError('')))
|
||||
def test_received_timeline(self):
|
||||
self.p.set_role('standby_leader')
|
||||
self.p.reset_cluster_info_state()
|
||||
self.assertRaises(PostgresConnectionException, self.p.received_timeline)
|
||||
|
||||
+140
-17
@@ -1,12 +1,45 @@
|
||||
from mock import Mock, PropertyMock, patch
|
||||
from mock import Mock, PropertyMock, patch, mock_open
|
||||
|
||||
from patroni.postgresql import Postgresql
|
||||
from patroni.postgresql.cancellable import CancellableSubprocess
|
||||
from patroni.postgresql.rewind import Rewind
|
||||
from six.moves import builtins
|
||||
|
||||
from . import BaseTestPostgresql, MockCursor, psycopg2_connect
|
||||
|
||||
|
||||
class MockThread(object):
|
||||
|
||||
def __init__(self, target, args):
|
||||
self._target = target
|
||||
self._args = args
|
||||
|
||||
def start(self):
|
||||
self._target(*self._args)
|
||||
|
||||
|
||||
def mock_cancellable_call(*args, **kwargs):
|
||||
communicate = kwargs.pop('communicate', None)
|
||||
if isinstance(communicate, dict):
|
||||
communicate.update(stdout=b'', stderr=b'pg_rewind: error: could not open file ' +
|
||||
b'"data/postgresql0/pg_xlog/000000010000000000000003": No such file')
|
||||
return 1
|
||||
|
||||
|
||||
def mock_cancellable_call0(*args, **kwargs):
|
||||
communicate = kwargs.pop('communicate', None)
|
||||
if isinstance(communicate, dict):
|
||||
communicate.update(stdout=b'', stderr=b'')
|
||||
return 0
|
||||
|
||||
|
||||
def mock_cancellable_call1(*args, **kwargs):
|
||||
communicate = kwargs.pop('communicate', None)
|
||||
if isinstance(communicate, dict):
|
||||
communicate.update(stdout=b'', stderr=b'')
|
||||
return 1
|
||||
|
||||
|
||||
@patch('subprocess.call', Mock(return_value=0))
|
||||
@patch('psycopg2.connect', psycopg2_connect)
|
||||
class TestRewind(BaseTestPostgresql):
|
||||
@@ -25,13 +58,23 @@ class TestRewind(BaseTestPostgresql):
|
||||
self.p.config._config['use_pg_rewind'] = False
|
||||
self.assertFalse(self.r.can_rewind)
|
||||
|
||||
@patch.object(CancellableSubprocess, 'call')
|
||||
def test_pg_rewind(self, mock_cancellable_subprocess_call):
|
||||
def test_pg_rewind(self):
|
||||
r = {'user': '', 'host': '', 'port': '', 'database': '', 'password': ''}
|
||||
mock_cancellable_subprocess_call.return_value = 0
|
||||
self.assertTrue(self.r.pg_rewind(r))
|
||||
mock_cancellable_subprocess_call.side_effect = OSError
|
||||
self.assertFalse(self.r.pg_rewind(r))
|
||||
with patch.object(Postgresql, 'major_version', PropertyMock(return_value=130000)),\
|
||||
patch.object(CancellableSubprocess, 'call', Mock(return_value=None)):
|
||||
with patch('subprocess.check_output', Mock(return_value=b'boo')):
|
||||
self.assertFalse(self.r.pg_rewind(r))
|
||||
with patch('subprocess.check_output', Mock(side_effect=Exception)):
|
||||
self.assertFalse(self.r.pg_rewind(r))
|
||||
|
||||
with patch.object(Postgresql, 'major_version', PropertyMock(return_value=120000)),\
|
||||
patch('subprocess.check_output', Mock(return_value=b'foo %f %p %r %% % %')):
|
||||
with patch.object(CancellableSubprocess, 'call', mock_cancellable_call):
|
||||
self.assertFalse(self.r.pg_rewind(r))
|
||||
with patch.object(CancellableSubprocess, 'call', mock_cancellable_call0):
|
||||
self.assertTrue(self.r.pg_rewind(r))
|
||||
with patch.object(CancellableSubprocess, 'call', mock_cancellable_call1):
|
||||
self.assertFalse(self.r.pg_rewind(r))
|
||||
|
||||
@patch.object(Rewind, 'can_rewind', PropertyMock(return_value=True))
|
||||
def test__get_local_timeline_lsn(self):
|
||||
@@ -39,14 +82,15 @@ class TestRewind(BaseTestPostgresql):
|
||||
with patch.object(Postgresql, 'controldata',
|
||||
Mock(return_value={'Database cluster state': 'shut down in recovery',
|
||||
'Minimum recovery ending location': '0/0',
|
||||
"Min recovery ending loc's timeline": '0'})):
|
||||
"Min recovery ending loc's timeline": '0',
|
||||
'Latest checkpoint location': '0/'})):
|
||||
self.r.rewind_or_reinitialize_needed_and_possible(self.leader)
|
||||
|
||||
with patch.object(Postgresql, 'is_running', Mock(return_value=True)):
|
||||
with patch.object(MockCursor, 'fetchone', Mock(side_effect=[(False, ), Exception])):
|
||||
with patch.object(MockCursor, 'fetchone', Mock(side_effect=[(0, 0, 1, 1,), Exception])):
|
||||
self.r.rewind_or_reinitialize_needed_and_possible(self.leader)
|
||||
|
||||
@patch.object(CancellableSubprocess, 'call', Mock(return_value=0))
|
||||
@patch.object(CancellableSubprocess, 'call', mock_cancellable_call)
|
||||
@patch.object(Postgresql, 'checkpoint', side_effect=['', '1'],)
|
||||
@patch.object(Postgresql, 'stop', Mock(return_value=False))
|
||||
@patch.object(Postgresql, 'start', Mock())
|
||||
@@ -72,11 +116,19 @@ class TestRewind(BaseTestPostgresql):
|
||||
with patch.object(Postgresql, 'is_running', Mock(return_value=True)):
|
||||
self.r.execute(self.leader)
|
||||
|
||||
@patch('patroni.postgresql.rewind.logger.info')
|
||||
def test__log_master_history(self, mock_logger):
|
||||
history = [[n, n, ''] for n in range(1, 10)]
|
||||
self.r._log_master_history(history, 1)
|
||||
expected = '\n'.join(['{0}\t0/{0}\t'.format(n) for n in range(1, 4)] + ['...', '9\t0/9\t'])
|
||||
self.assertEqual(mock_logger.call_args[0][1], expected)
|
||||
|
||||
@patch.object(Postgresql, 'start', Mock())
|
||||
@patch.object(Rewind, 'can_rewind', PropertyMock(return_value=True))
|
||||
@patch.object(Rewind, '_get_local_timeline_lsn', Mock(return_value=(2, '40159C1')))
|
||||
@patch.object(Rewind, '_get_local_timeline_lsn')
|
||||
@patch.object(Rewind, 'check_leader_is_not_in_recovery')
|
||||
def test__check_timeline_and_lsn(self, mock_check_leader_is_not_in_recovery):
|
||||
def test__check_timeline_and_lsn(self, mock_check_leader_is_not_in_recovery, mock_get_local_timeline_lsn):
|
||||
mock_get_local_timeline_lsn.return_value = (True, 2, 67197377)
|
||||
mock_check_leader_is_not_in_recovery.return_value = False
|
||||
self.r.trigger_check_diverged_lsn()
|
||||
self.assertFalse(self.r.rewind_or_reinitialize_needed_and_possible(self.leader))
|
||||
@@ -88,20 +140,91 @@ class TestRewind(BaseTestPostgresql):
|
||||
with patch('psycopg2.connect', Mock(side_effect=Exception)):
|
||||
self.assertFalse(self.r.rewind_or_reinitialize_needed_and_possible(self.leader))
|
||||
self.r.trigger_check_diverged_lsn()
|
||||
with patch.object(MockCursor, 'fetchone', Mock(side_effect=[('', 2, '0/0'), ('', b'3\t0/40159C0\tn\n')])):
|
||||
with patch.object(MockCursor, 'fetchone', Mock(side_effect=[('', 3, '0/0'), ('', b'3\t0/40159C0\tn\n')])):
|
||||
self.assertFalse(self.r.rewind_or_reinitialize_needed_and_possible(self.leader))
|
||||
self.r.trigger_check_diverged_lsn()
|
||||
with patch.object(MockCursor, 'fetchone', Mock(return_value=('', 1, '0/0'))):
|
||||
with patch.object(Rewind, '_get_local_timeline_lsn', Mock(return_value=(1, '0/0'))):
|
||||
with patch.object(Rewind, '_get_local_timeline_lsn', Mock(return_value=(True, 1, '0/0'))):
|
||||
self.assertFalse(self.r.rewind_or_reinitialize_needed_and_possible(self.leader))
|
||||
self.r.trigger_check_diverged_lsn()
|
||||
self.assertTrue(self.r.rewind_or_reinitialize_needed_and_possible(self.leader))
|
||||
|
||||
self.r.reset_state()
|
||||
self.r.trigger_check_diverged_lsn()
|
||||
mock_get_local_timeline_lsn.return_value = (False, 2, 67296664)
|
||||
self.assertTrue(self.r.rewind_or_reinitialize_needed_and_possible(self.leader))
|
||||
|
||||
with patch('subprocess.Popen') as mock_popen:
|
||||
mock_popen.return_value.communicate.return_value = (
|
||||
b'0, lsn: 0/040159C1, prev 0/\n',
|
||||
b'pg_waldump: fatal: error in WAL record at 0/40159C1: invalid record length at /: wanted 24, got 0\n'
|
||||
)
|
||||
self.r.reset_state()
|
||||
self.r.trigger_check_diverged_lsn()
|
||||
mock_get_local_timeline_lsn.return_value = (False, 2, 67197377)
|
||||
self.assertTrue(self.r.rewind_or_reinitialize_needed_and_possible(self.leader))
|
||||
self.r.reset_state()
|
||||
self.r.trigger_check_diverged_lsn()
|
||||
mock_popen.side_effect = Exception
|
||||
self.assertTrue(self.r.rewind_or_reinitialize_needed_and_possible(self.leader))
|
||||
|
||||
@patch.object(MockCursor, 'fetchone', Mock(side_effect=[(True,), Exception]))
|
||||
def test_check_leader_is_not_in_recovery(self):
|
||||
self.r.check_leader_is_not_in_recovery()
|
||||
self.r.check_leader_is_not_in_recovery()
|
||||
|
||||
@patch.object(Postgresql, 'controldata', Mock(return_value={"Latest checkpoint's TimeLineID": 1}))
|
||||
def test_check_for_checkpoint_after_promote(self):
|
||||
self.r.check_for_checkpoint_after_promote()
|
||||
def test_read_postmaster_opts(self):
|
||||
m = mock_open(read_data='/usr/lib/postgres/9.6/bin/postgres "-D" "data/postgresql0" \
|
||||
"--listen_addresses=127.0.0.1" "--port=5432" "--hot_standby=on" "--wal_level=hot_standby" \
|
||||
"--wal_log_hints=on" "--max_wal_senders=5" "--max_replication_slots=5"\n')
|
||||
with patch.object(builtins, 'open', m):
|
||||
data = self.r.read_postmaster_opts()
|
||||
self.assertEqual(data['wal_level'], 'hot_standby')
|
||||
self.assertEqual(int(data['max_replication_slots']), 5)
|
||||
self.assertEqual(data.get('D'), None)
|
||||
|
||||
m.side_effect = IOError
|
||||
data = self.r.read_postmaster_opts()
|
||||
self.assertEqual(data, dict())
|
||||
|
||||
@patch('psutil.Popen')
|
||||
def test_single_user_mode(self, subprocess_popen_mock):
|
||||
subprocess_popen_mock.return_value.wait.return_value = 0
|
||||
subprocess_popen_mock.return_value.communicate.return_value = ('', '')
|
||||
self.assertEqual(self.r.single_user_mode({'input': 'CHECKPOINT'}, {'archive_mode': 'on'}), 0)
|
||||
|
||||
@patch('os.listdir', Mock(side_effect=[OSError, ['a', 'b']]))
|
||||
@patch('os.unlink', Mock(side_effect=OSError))
|
||||
@patch('os.remove', Mock())
|
||||
@patch('os.path.islink', Mock(side_effect=[True, False]))
|
||||
@patch('os.path.isfile', Mock(return_value=True))
|
||||
def test_cleanup_archive_status(self):
|
||||
self.r.cleanup_archive_status()
|
||||
self.r.cleanup_archive_status()
|
||||
|
||||
@patch('os.unlink', Mock())
|
||||
@patch('os.listdir', Mock(return_value=[]))
|
||||
@patch('os.path.isfile', Mock(return_value=True))
|
||||
@patch.object(Rewind, 'read_postmaster_opts', Mock(return_value={}))
|
||||
@patch.object(Rewind, 'single_user_mode', Mock(return_value=0))
|
||||
def test_ensure_clean_shutdown(self):
|
||||
self.assertTrue(self.r.ensure_clean_shutdown())
|
||||
|
||||
@patch('patroni.postgresql.rewind.Thread', MockThread)
|
||||
@patch.object(Postgresql, 'controldata')
|
||||
@patch.object(Postgresql, 'checkpoint')
|
||||
def test_ensure_checkpoint_after_promote(self, mock_checkpoint, mock_controldata):
|
||||
mock_checkpoint.return_value = None
|
||||
self.r.ensure_checkpoint_after_promote(Mock())
|
||||
self.r.ensure_checkpoint_after_promote(Mock())
|
||||
|
||||
self.r.reset_state()
|
||||
mock_controldata.return_value = {"Latest checkpoint's TimeLineID": 1}
|
||||
mock_checkpoint.side_effect = Exception
|
||||
self.r.ensure_checkpoint_after_promote(Mock())
|
||||
self.r.ensure_checkpoint_after_promote(Mock())
|
||||
|
||||
self.r.reset_state()
|
||||
mock_controldata.side_effect = TypeError
|
||||
self.r.ensure_checkpoint_after_promote(Mock())
|
||||
self.r.ensure_checkpoint_after_promote(Mock())
|
||||
|
||||
@@ -150,7 +150,7 @@ class TestZooKeeper(unittest.TestCase):
|
||||
|
||||
def test_get_cluster(self):
|
||||
self.assertRaises(ZooKeeperError, self.zk.get_cluster)
|
||||
cluster = self.zk.get_cluster()
|
||||
cluster = self.zk.get_cluster(True)
|
||||
self.assertIsInstance(cluster.leader, Leader)
|
||||
self.zk.touch_member({'foo': 'foo'})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user