mirror of
https://github.com/outbackdingo/patroni.git
synced 2026-01-27 18:20:05 +00:00
Implementation of "standby cluster" described in #657. Standby cluster consists of a "standby leader", that replicates from a "remote master" (which is not a part of current patroni cluster and can be anywhere), and cascade replicas, that replicate from the corresponding standby leader. "Standby leader" behaves pretty much like a regular leader, which means that it holds a leader lock in DSC, in case if disappears there will be an election of a new "standby leader". One can define such a cluster using the section "standby_cluster" in patroni config file. This section provides parameters for standby cluster, that will be applied only once during bootstrap and can be changed only through DSC.
577 lines
29 KiB
Python
577 lines
29 KiB
Python
import etcd
|
|
import os
|
|
import requests
|
|
import sys
|
|
import unittest
|
|
|
|
from click.testing import CliRunner
|
|
from datetime import datetime, timedelta
|
|
from mock import patch, Mock
|
|
from patroni.ctl import ctl, store_config, load_config, output_members, request_patroni, get_dcs, parse_dcs, \
|
|
get_all_members, get_any_member, get_cursor, query_member, configure, PatroniCtlException, apply_config_changes, \
|
|
format_config_for_editing, show_diff, invoke_editor, format_pg_version
|
|
from patroni.dcs.etcd import Client, Failover
|
|
from patroni.utils import tzutc
|
|
from psycopg2 import OperationalError
|
|
from test_etcd import etcd_read, requests_get, socket_getaddrinfo, MockResponse
|
|
from test_ha import get_cluster_initialized_without_leader, get_cluster_initialized_with_leader, \
|
|
get_cluster_initialized_with_only_leader, get_cluster_not_initialized_without_leader, get_cluster, Member
|
|
from test_postgresql import MockConnect, psycopg2_connect
|
|
|
|
CONFIG_FILE_PATH = './test-ctl.yaml'
|
|
|
|
|
|
def test_rw_config():
|
|
runner = CliRunner()
|
|
with runner.isolated_filesystem():
|
|
store_config({'etcd': {'host': 'localhost:2379'}}, CONFIG_FILE_PATH + '/dummy')
|
|
sys.argv = ['patronictl.py', '']
|
|
load_config(CONFIG_FILE_PATH + '/dummy', None)
|
|
load_config(CONFIG_FILE_PATH + '/dummy', '0.0.0.0')
|
|
os.remove(CONFIG_FILE_PATH + '/dummy')
|
|
os.rmdir(CONFIG_FILE_PATH)
|
|
|
|
|
|
@patch('patroni.ctl.load_config',
|
|
Mock(return_value={'scope': 'alpha', 'postgresql': {'data_dir': '.', 'parameters': {}, 'retry_timeout': 5},
|
|
'restapi': {'auth': 'u:p', 'listen': ''}, 'etcd': {'host': 'localhost:2379'}}))
|
|
class TestCtl(unittest.TestCase):
|
|
|
|
@patch('socket.getaddrinfo', socket_getaddrinfo)
|
|
def setUp(self):
|
|
with patch.object(Client, 'machines') as mock_machines:
|
|
mock_machines.__get__ = Mock(return_value=['http://remotehost:2379'])
|
|
self.runner = CliRunner()
|
|
self.e = get_dcs({'etcd': {'ttl': 30, 'host': 'ok:2379', 'retry_timeout': 10}}, 'foo')
|
|
|
|
@patch('psycopg2.connect', psycopg2_connect)
|
|
def test_get_cursor(self):
|
|
self.assertIsNone(get_cursor(get_cluster_initialized_without_leader(), {}, role='master'))
|
|
|
|
self.assertIsNotNone(get_cursor(get_cluster_initialized_with_leader(), {}, role='master'))
|
|
|
|
# MockCursor returns pg_is_in_recovery as false
|
|
self.assertIsNone(get_cursor(get_cluster_initialized_with_leader(), {}, role='replica'))
|
|
|
|
self.assertIsNotNone(get_cursor(get_cluster_initialized_with_leader(), {'database': 'foo'}, role='any'))
|
|
|
|
def test_parse_dcs(self):
|
|
assert parse_dcs(None) is None
|
|
assert parse_dcs('localhost') == {'etcd': {'host': 'localhost:2379'}}
|
|
assert parse_dcs('') == {'etcd': {'host': 'localhost:2379'}}
|
|
assert parse_dcs('localhost:8500') == {'consul': {'host': 'localhost:8500'}}
|
|
assert parse_dcs('zookeeper://localhost') == {'zookeeper': {'hosts': ['localhost:2181']}}
|
|
assert parse_dcs('exhibitor://dummy') == {'exhibitor': {'hosts': ['dummy'], 'port': 8181}}
|
|
assert parse_dcs('consul://localhost') == {'consul': {'host': 'localhost:8500'}}
|
|
self.assertRaises(PatroniCtlException, parse_dcs, 'invalid://test')
|
|
|
|
def test_output_members(self):
|
|
scheduled_at = datetime.now(tzutc) + timedelta(seconds=600)
|
|
cluster = get_cluster_initialized_with_leader(Failover(1, 'foo', 'bar', scheduled_at))
|
|
self.assertIsNone(output_members(cluster, name='abc', fmt='pretty'))
|
|
self.assertIsNone(output_members(cluster, name='abc', fmt='json'))
|
|
self.assertIsNone(output_members(cluster, name='abc', fmt='yaml'))
|
|
self.assertIsNone(output_members(cluster, name='abc', fmt='tsv'))
|
|
|
|
@patch('patroni.ctl.get_dcs')
|
|
@patch('patroni.ctl.request_patroni', Mock(return_value=MockResponse()))
|
|
def test_switchover(self, mock_get_dcs):
|
|
mock_get_dcs.return_value = self.e
|
|
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_leader
|
|
mock_get_dcs.return_value.set_failover_value = Mock()
|
|
result = self.runner.invoke(ctl, ['switchover', 'dummy'], input='leader\nother\n\ny')
|
|
assert 'leader' in result.output
|
|
|
|
result = self.runner.invoke(ctl, ['switchover', 'dummy'], input='leader\nother\n2300-01-01T12:23:00\ny')
|
|
assert result.exit_code == 0
|
|
|
|
with patch('patroni.dcs.Cluster.is_paused', Mock(return_value=True)):
|
|
result = self.runner.invoke(ctl, ['switchover', 'dummy', '--force', '--scheduled', '2015-01-01T12:00:00'])
|
|
assert result.exit_code == 1
|
|
|
|
# Aborting switchover, as we anser NO to the confirmation
|
|
result = self.runner.invoke(ctl, ['switchover', 'dummy'], input='leader\nother\n\nN')
|
|
assert result.exit_code == 1
|
|
|
|
# Target and source are equal
|
|
result = self.runner.invoke(ctl, ['switchover', 'dummy'], input='leader\nleader\n\ny')
|
|
assert result.exit_code == 1
|
|
|
|
# Reality is not part of this cluster
|
|
result = self.runner.invoke(ctl, ['switchover', 'dummy'], input='leader\nReality\n\ny')
|
|
assert result.exit_code == 1
|
|
|
|
result = self.runner.invoke(ctl, ['switchover', 'dummy', '--force'])
|
|
assert 'Member' in result.output
|
|
|
|
result = self.runner.invoke(ctl, ['switchover', 'dummy', '--force', '--scheduled', '2015-01-01T12:00:00+01:00'])
|
|
assert result.exit_code == 0
|
|
|
|
# Invalid timestamp
|
|
result = self.runner.invoke(ctl, ['switchover', 'dummy', '--force', '--scheduled', 'invalid'])
|
|
assert result.exit_code != 0
|
|
|
|
# Invalid timestamp
|
|
result = self.runner.invoke(ctl, ['switchover', 'dummy', '--force', '--scheduled', '2115-02-30T12:00:00+01:00'])
|
|
assert result.exit_code != 0
|
|
|
|
# Specifying wrong leader
|
|
result = self.runner.invoke(ctl, ['switchover', 'dummy'], input='dummy')
|
|
assert result.exit_code == 1
|
|
|
|
with patch('patroni.ctl.request_patroni', Mock(side_effect=Exception)):
|
|
# Non-responding patroni
|
|
result = self.runner.invoke(ctl, ['switchover', 'dummy'], input='leader\nother\n2300-01-01T12:23:00\ny')
|
|
assert 'falling back to DCS' in result.output
|
|
|
|
with patch('patroni.ctl.request_patroni') as mocked:
|
|
mocked.return_value.status_code = 500
|
|
result = self.runner.invoke(ctl, ['switchover', 'dummy'], input='leader\nother\n\ny')
|
|
assert 'Switchover failed' in result.output
|
|
|
|
mocked.return_value.status_code = 501
|
|
mocked.return_value.text = 'Server does not support this operation'
|
|
result = self.runner.invoke(ctl, ['switchover', 'dummy'], input='leader\nother\n\ny')
|
|
assert 'Switchover failed' in result.output
|
|
|
|
# No members available
|
|
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_only_leader
|
|
result = self.runner.invoke(ctl, ['switchover', 'dummy'], input='leader\nother\n\ny')
|
|
assert result.exit_code == 1
|
|
|
|
# No master available
|
|
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_without_leader
|
|
result = self.runner.invoke(ctl, ['switchover', 'dummy'], input='leader\nother\n\ny')
|
|
assert result.exit_code == 1
|
|
|
|
@patch('patroni.ctl.get_dcs')
|
|
@patch('patroni.ctl.request_patroni', Mock(return_value=MockResponse()))
|
|
def test_failover(self, mock_get_dcs):
|
|
mock_get_dcs.return_value = self.e
|
|
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_leader
|
|
mock_get_dcs.return_value.set_failover_value = Mock()
|
|
result = self.runner.invoke(ctl, ['failover', 'dummy'], input='\n')
|
|
assert 'Failover could be performed only to a specific candidate' in result.output
|
|
|
|
def test_get_dcs(self):
|
|
self.assertRaises(PatroniCtlException, get_dcs, {'dummy': {}}, 'dummy')
|
|
|
|
@patch('psycopg2.connect', psycopg2_connect)
|
|
@patch('patroni.ctl.query_member', Mock(return_value=([['mock column']], None)))
|
|
@patch('patroni.ctl.get_dcs')
|
|
@patch.object(etcd.Client, 'read', etcd_read)
|
|
def test_query(self, mock_get_dcs):
|
|
mock_get_dcs.return_value = self.e
|
|
# Mutually exclusive
|
|
result = self.runner.invoke(ctl, ['query', 'alpha', '--member', 'abc', '--role', 'master'])
|
|
assert result.exit_code == 1
|
|
|
|
with self.runner.isolated_filesystem():
|
|
with open('dummy', 'w') as dummy_file:
|
|
dummy_file.write('SELECT 1')
|
|
|
|
# Mutually exclusive
|
|
result = self.runner.invoke(ctl, ['query', 'alpha', '--file', 'dummy', '--command', 'dummy'])
|
|
assert result.exit_code == 1
|
|
|
|
result = self.runner.invoke(ctl, ['query', 'alpha', '--file', 'dummy'])
|
|
assert result.exit_code == 0
|
|
|
|
os.remove('dummy')
|
|
|
|
result = self.runner.invoke(ctl, ['query', 'alpha', '--command', 'SELECT 1'])
|
|
assert 'mock column' in result.output
|
|
|
|
# --command or --file is mandatory
|
|
result = self.runner.invoke(ctl, ['query', 'alpha'])
|
|
assert result.exit_code == 1
|
|
|
|
result = self.runner.invoke(ctl, ['query', 'alpha', '--command', 'SELECT 1', '--username', 'root',
|
|
'--password', '--dbname', 'postgres'], input='ab\nab')
|
|
assert 'mock column' in result.output
|
|
|
|
def test_query_member(self):
|
|
with patch('patroni.ctl.get_cursor', Mock(return_value=MockConnect().cursor())):
|
|
rows = query_member(None, None, None, 'master', 'SELECT pg_is_in_recovery()', {})
|
|
self.assertTrue('False' in str(rows))
|
|
|
|
rows = query_member(None, None, None, 'replica', 'SELECT pg_is_in_recovery()', {})
|
|
self.assertEquals(rows, (None, None))
|
|
|
|
with patch('test_postgresql.MockCursor.execute', Mock(side_effect=OperationalError('bla'))):
|
|
rows = query_member(None, None, None, 'replica', 'SELECT pg_is_in_recovery()', {})
|
|
|
|
with patch('patroni.ctl.get_cursor', Mock(return_value=None)):
|
|
rows = query_member(None, None, None, None, 'SELECT pg_is_in_recovery()', {})
|
|
self.assertTrue('No connection to' in str(rows))
|
|
|
|
rows = query_member(None, None, None, 'replica', 'SELECT pg_is_in_recovery()', {})
|
|
self.assertTrue('No connection to' in str(rows))
|
|
|
|
with patch('patroni.ctl.get_cursor', Mock(side_effect=OperationalError('bla'))):
|
|
rows = query_member(None, None, None, 'replica', 'SELECT pg_is_in_recovery()', {})
|
|
|
|
@patch('patroni.ctl.get_dcs')
|
|
def test_dsn(self, mock_get_dcs):
|
|
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_leader
|
|
result = self.runner.invoke(ctl, ['dsn', 'alpha'])
|
|
assert 'host=127.0.0.1 port=5435' in result.output
|
|
|
|
# Mutually exclusive options
|
|
result = self.runner.invoke(ctl, ['dsn', 'alpha', '--role', 'master', '--member', 'dummy'])
|
|
assert result.exit_code == 1
|
|
|
|
# Non-existing member
|
|
result = self.runner.invoke(ctl, ['dsn', 'alpha', '--member', 'dummy'])
|
|
assert result.exit_code == 1
|
|
|
|
@patch('requests.post')
|
|
@patch('patroni.ctl.get_dcs')
|
|
def test_reload(self, mock_get_dcs, mock_post):
|
|
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_leader
|
|
|
|
result = self.runner.invoke(ctl, ['reload', 'alpha'], input='y')
|
|
assert 'Failed: reload for member' in result.output
|
|
|
|
mock_post.return_value.status_code = 200
|
|
result = self.runner.invoke(ctl, ['reload', 'alpha'], input='y')
|
|
assert 'No changes to apply on member' in result.output
|
|
|
|
mock_post.return_value.status_code = 202
|
|
result = self.runner.invoke(ctl, ['reload', 'alpha'], input='y')
|
|
assert 'Reload request received for member' in result.output
|
|
|
|
@patch('requests.post', requests_get)
|
|
@patch('patroni.ctl.get_dcs')
|
|
def test_restart_reinit(self, mock_get_dcs):
|
|
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_leader
|
|
result = self.runner.invoke(ctl, ['restart', 'alpha'], input='y\n\nnow')
|
|
assert 'Failed: restart for' in result.output
|
|
assert result.exit_code == 0
|
|
|
|
result = self.runner.invoke(ctl, ['reinit', 'alpha'], input='y')
|
|
assert result.exit_code == 1
|
|
|
|
# successful reinit
|
|
result = self.runner.invoke(ctl, ['reinit', 'alpha', 'other'], input='y\ny')
|
|
assert result.exit_code == 0
|
|
|
|
# Aborted restart
|
|
result = self.runner.invoke(ctl, ['restart', 'alpha'], input='N')
|
|
assert result.exit_code == 1
|
|
|
|
result = self.runner.invoke(ctl, ['restart', 'alpha', '--pending', '--force'])
|
|
assert result.exit_code == 0
|
|
|
|
# Not a member
|
|
result = self.runner.invoke(ctl, ['restart', 'alpha', 'dummy', '--any'], input='y')
|
|
assert result.exit_code == 1
|
|
|
|
# Wrong pg version
|
|
result = self.runner.invoke(ctl, ['restart', 'alpha', '--any', '--pg-version', '9.1'], input='y')
|
|
assert 'Error: Invalid PostgreSQL version format' in result.output
|
|
assert result.exit_code == 1
|
|
|
|
result = self.runner.invoke(ctl, ['restart', 'alpha', '--pending', '--force', '--timeout', '10min'])
|
|
assert result.exit_code == 0
|
|
|
|
with patch('requests.delete', Mock(return_value=MockResponse(500))):
|
|
# normal restart, the schedule is actually parsed, but not validated in patronictl
|
|
result = self.runner.invoke(ctl, ['restart', 'alpha', 'other', '--force',
|
|
'--scheduled', '2300-10-01T14:30'])
|
|
assert 'Failed: flush scheduled restart' in result.output
|
|
|
|
with patch('patroni.dcs.Cluster.is_paused', Mock(return_value=True)):
|
|
result = self.runner.invoke(ctl,
|
|
['restart', 'alpha', 'other', '--force', '--scheduled', '2300-10-01T14:30'])
|
|
assert result.exit_code == 1
|
|
|
|
with patch('requests.post', Mock(return_value=MockResponse())):
|
|
# normal restart, the schedule is actually parsed, but not validated in patronictl
|
|
result = self.runner.invoke(ctl, ['restart', 'alpha', '--pg-version', '42.0.0',
|
|
'--scheduled', '2300-10-01T14:30'], input='y')
|
|
assert result.exit_code == 0
|
|
|
|
with patch('requests.post', Mock(return_value=MockResponse(204))):
|
|
# get restart with the non-200 return code
|
|
# normal restart, the schedule is actually parsed, but not validated in patronictl
|
|
result = self.runner.invoke(ctl, ['restart', 'alpha', '--pg-version', '42.0',
|
|
'--scheduled', '2300-10-01T14:30'], input='y')
|
|
assert result.exit_code == 0
|
|
|
|
# force restart with restart already present
|
|
with patch('patroni.ctl.request_patroni', Mock(return_value=MockResponse(204))):
|
|
result = self.runner.invoke(ctl, ['restart', 'alpha', 'other', '--force',
|
|
'--scheduled', '2300-10-01T14:30'])
|
|
assert result.exit_code == 0
|
|
|
|
with patch('requests.post', Mock(return_value=MockResponse(202))):
|
|
# get restart with the non-200 return code
|
|
# normal restart, the schedule is actually parsed, but not validated in patronictl
|
|
result = self.runner.invoke(
|
|
ctl, ['restart', 'alpha', '--pg-version', '99.0.0', '--scheduled', '2300-10-01T14:30'], input='y'
|
|
)
|
|
assert 'Success: restart scheduled' in result.output
|
|
assert result.exit_code == 0
|
|
|
|
with patch('requests.post', Mock(return_value=MockResponse(409))):
|
|
# get restart with the non-200 return code
|
|
# normal restart, the schedule is actually parsed, but not validated in patronictl
|
|
result = self.runner.invoke(
|
|
ctl, ['restart', 'alpha', '--pg-version', '99.0.0', '--scheduled', '2300-10-01T14:30'], input='y'
|
|
)
|
|
assert 'Failed: another restart is already' in result.output
|
|
assert result.exit_code == 0
|
|
|
|
@patch('patroni.ctl.get_dcs')
|
|
def test_remove(self, mock_get_dcs):
|
|
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_leader
|
|
result = self.runner.invoke(ctl, ['remove', 'alpha'], input='alpha\nslave')
|
|
assert 'Please confirm' in result.output
|
|
assert 'You are about to remove all' in result.output
|
|
# Not typing an exact confirmation
|
|
assert result.exit_code == 1
|
|
|
|
# master specified does not match master of cluster
|
|
result = self.runner.invoke(ctl, ['remove', 'alpha'], input='alpha\nYes I am aware\nslave')
|
|
assert result.exit_code == 1
|
|
|
|
# cluster specified on cmdline does not match verification prompt
|
|
result = self.runner.invoke(ctl, ['remove', 'alpha'], input='beta\nleader')
|
|
assert result.exit_code == 1
|
|
|
|
result = self.runner.invoke(ctl, ['remove', 'alpha'], input='alpha\nYes I am aware\nleader')
|
|
assert result.exit_code == 0
|
|
|
|
@patch('requests.post', Mock(side_effect=requests.exceptions.ConnectionError('foo')))
|
|
@patch('click.get_current_context')
|
|
def test_request_patroni(self, mock_context):
|
|
member = get_cluster_initialized_with_leader().leader.member
|
|
|
|
mock_context.return_value.obj = {'ctl': {'cacert': 'cert.pem'}}
|
|
self.assertRaises(requests.exceptions.ConnectionError, request_patroni, member, 'post', 'dummy', {})
|
|
|
|
mock_context.return_value.obj = {'ctl': {'insecure': True}}
|
|
self.assertRaises(requests.exceptions.ConnectionError, request_patroni, member, 'post', 'dummy', {})
|
|
|
|
def test_ctl(self):
|
|
self.runner.invoke(ctl, ['list'])
|
|
|
|
result = self.runner.invoke(ctl, ['--help'])
|
|
assert 'Usage:' in result.output
|
|
|
|
def test_get_any_member(self):
|
|
self.assertIsNone(get_any_member(get_cluster_initialized_without_leader(), role='master'))
|
|
|
|
m = get_any_member(get_cluster_initialized_with_leader(), role='master')
|
|
self.assertEquals(m.name, 'leader')
|
|
|
|
def test_get_all_members(self):
|
|
self.assertEquals(list(get_all_members(get_cluster_initialized_without_leader(), role='master')), [])
|
|
|
|
r = list(get_all_members(get_cluster_initialized_with_leader(), role='master'))
|
|
self.assertEquals(len(r), 1)
|
|
self.assertEquals(r[0].name, 'leader')
|
|
|
|
r = list(get_all_members(get_cluster_initialized_with_leader(), role='replica'))
|
|
self.assertEquals(len(r), 1)
|
|
self.assertEquals(r[0].name, 'other')
|
|
|
|
self.assertEquals(len(list(get_all_members(get_cluster_initialized_without_leader(), role='replica'))), 2)
|
|
|
|
@patch('patroni.ctl.get_dcs')
|
|
def test_members(self, mock_get_dcs):
|
|
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_leader
|
|
result = self.runner.invoke(ctl, ['list'])
|
|
assert '127.0.0.1' in result.output
|
|
assert result.exit_code == 0
|
|
with patch('patroni.ctl.load_config', Mock(return_value={})):
|
|
self.runner.invoke(ctl, ['list'])
|
|
|
|
def test_configure(self):
|
|
result = self.runner.invoke(configure, ['--dcs', 'abc', '-c', 'dummy', '-n', 'bla'])
|
|
assert result.exit_code == 0
|
|
|
|
@patch('patroni.ctl.get_dcs')
|
|
def test_scaffold(self, mock_get_dcs):
|
|
mock_get_dcs.return_value = self.e
|
|
mock_get_dcs.return_value.get_cluster = get_cluster_not_initialized_without_leader
|
|
mock_get_dcs.return_value.initialize = Mock(return_value=True)
|
|
mock_get_dcs.return_value.touch_member = Mock(return_value=True)
|
|
mock_get_dcs.return_value.attempt_to_acquire_leader = Mock(return_value=True)
|
|
mock_get_dcs.return_value.delete_cluster = Mock()
|
|
|
|
with patch.object(self.e, 'initialize', return_value=False):
|
|
result = self.runner.invoke(ctl, ['scaffold', 'alpha'])
|
|
assert result.exception
|
|
|
|
with patch.object(mock_get_dcs.return_value, 'touch_member', Mock(return_value=False)):
|
|
result = self.runner.invoke(ctl, ['scaffold', 'alpha'])
|
|
assert result.exception
|
|
|
|
result = self.runner.invoke(ctl, ['scaffold', 'alpha'])
|
|
assert result.exit_code == 0
|
|
|
|
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_leader
|
|
result = self.runner.invoke(ctl, ['scaffold', 'alpha'])
|
|
assert result.exception
|
|
|
|
@patch('patroni.ctl.get_dcs')
|
|
def test_list_extended(self, mock_get_dcs):
|
|
mock_get_dcs.return_value = self.e
|
|
cluster = get_cluster_initialized_with_leader(sync=('leader', 'other'))
|
|
mock_get_dcs.return_value.get_cluster = Mock(return_value=cluster)
|
|
|
|
result = self.runner.invoke(ctl, ['list', 'dummy', '--extended', '--timestamp'])
|
|
assert '2100' in result.output
|
|
assert 'Scheduled restart' in result.output
|
|
|
|
@patch('patroni.ctl.get_dcs')
|
|
@patch('requests.delete', Mock(return_value=MockResponse()))
|
|
def test_flush(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', 'restart', '-r', 'master'], input='y')
|
|
assert 'No scheduled restart' in result.output
|
|
|
|
result = self.runner.invoke(ctl, ['flush', 'dummy', 'restart', '--force'])
|
|
assert 'Success: flush scheduled restart' in result.output
|
|
with patch.object(requests, 'delete', return_value=MockResponse(404)):
|
|
result = self.runner.invoke(ctl, ['flush', 'dummy', 'restart', '--force'])
|
|
assert 'Failed: flush scheduled restart' in result.output
|
|
|
|
@patch('patroni.ctl.get_dcs')
|
|
@patch('patroni.ctl.polling_loop', Mock(return_value=[1]))
|
|
def test_pause_cluster(self, mock_get_dcs):
|
|
mock_get_dcs.return_value = self.e
|
|
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_leader
|
|
|
|
with patch('requests.patch', Mock(return_value=MockResponse(500))):
|
|
result = self.runner.invoke(ctl, ['pause', 'dummy'])
|
|
assert 'Failed' in result.output
|
|
|
|
with patch('requests.patch', Mock(return_value=MockResponse(200))),\
|
|
patch('patroni.dcs.Cluster.is_paused', Mock(return_value=True)):
|
|
result = self.runner.invoke(ctl, ['pause', 'dummy'])
|
|
assert 'Cluster is already paused' in result.output
|
|
|
|
with patch('requests.patch', Mock(return_value=MockResponse(200))):
|
|
result = self.runner.invoke(ctl, ['pause', 'dummy', '--wait'])
|
|
assert "'pause' request sent" in result.output
|
|
mock_get_dcs.return_value.get_cluster = Mock(side_effect=[get_cluster_initialized_with_leader(),
|
|
get_cluster(None, None, [], None, None)])
|
|
self.runner.invoke(ctl, ['pause', 'dummy', '--wait'])
|
|
member = Member(1, 'other', 28, {})
|
|
mock_get_dcs.return_value.get_cluster = Mock(side_effect=[get_cluster_initialized_with_leader(),
|
|
get_cluster(None, None, [member], None, None)])
|
|
self.runner.invoke(ctl, ['pause', 'dummy', '--wait'])
|
|
|
|
@patch('patroni.ctl.get_dcs')
|
|
def test_resume_cluster(self, mock_get_dcs):
|
|
mock_get_dcs.return_value = self.e
|
|
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_leader
|
|
|
|
with patch('patroni.dcs.Cluster.is_paused', Mock(return_value=True)):
|
|
with patch('requests.patch', Mock(return_value=MockResponse(200))):
|
|
result = self.runner.invoke(ctl, ['resume', 'dummy'])
|
|
assert 'Success' in result.output
|
|
|
|
with patch('requests.patch', Mock(return_value=MockResponse(500))):
|
|
result = self.runner.invoke(ctl, ['resume', 'dummy'])
|
|
assert 'Failed' in result.output
|
|
|
|
with patch('requests.patch', Mock(return_value=MockResponse(200))),\
|
|
patch('patroni.dcs.Cluster.is_paused', Mock(return_value=False)):
|
|
result = self.runner.invoke(ctl, ['resume', 'dummy'])
|
|
assert 'Cluster is not paused' in result.output
|
|
|
|
with patch('requests.patch', Mock(side_effect=Exception)):
|
|
result = self.runner.invoke(ctl, ['resume', 'dummy'])
|
|
assert 'Can not find accessible cluster member' in result.output
|
|
|
|
def test_apply_config_changes(self):
|
|
config = {"postgresql": {"parameters": {"work_mem": "4MB"}, "use_pg_rewind": True}, "ttl": 30}
|
|
|
|
before_editing = format_config_for_editing(config)
|
|
|
|
# Spaces are allowed and stripped, numbers and booleans are interpreted
|
|
after_editing, changed_config = apply_config_changes(before_editing, config,
|
|
["postgresql.parameters.work_mem = 5MB",
|
|
"ttl=15", "postgresql.use_pg_rewind=off", 'a.b=c'])
|
|
self.assertEquals(changed_config, {"a": {"b": "c"}, "postgresql": {"parameters": {"work_mem": "5MB"},
|
|
"use_pg_rewind": False}, "ttl": 15})
|
|
|
|
# postgresql.parameters namespace is flattened
|
|
after_editing, changed_config = apply_config_changes(before_editing, config,
|
|
["postgresql.parameters.work_mem.sub = x"])
|
|
self.assertEquals(changed_config, {"postgresql": {"parameters": {"work_mem": "4MB", "work_mem.sub": "x"},
|
|
"use_pg_rewind": True}, "ttl": 30})
|
|
|
|
# Setting to null deletes
|
|
after_editing, changed_config = apply_config_changes(before_editing, config,
|
|
["postgresql.parameters.work_mem=null"])
|
|
self.assertEquals(changed_config, {"postgresql": {"use_pg_rewind": True}, "ttl": 30})
|
|
after_editing, changed_config = apply_config_changes(before_editing, config,
|
|
["postgresql.use_pg_rewind=null",
|
|
"postgresql.parameters.work_mem=null"])
|
|
self.assertEquals(changed_config, {"ttl": 30})
|
|
|
|
self.assertRaises(PatroniCtlException, apply_config_changes, before_editing, config, ['a'])
|
|
|
|
@patch('sys.stdout.isatty', return_value=False)
|
|
@patch('cdiff.markup_to_pager')
|
|
def test_show_diff(self, mock_markup_to_pager, mock_isatty):
|
|
show_diff("foo:\n bar: 1\n", "foo:\n bar: 2\n")
|
|
mock_markup_to_pager.assert_not_called()
|
|
|
|
mock_isatty.return_value = True
|
|
show_diff("foo:\n bar: 1\n", "foo:\n bar: 2\n")
|
|
mock_markup_to_pager.assert_called_once()
|
|
|
|
# Test that unicode handling doesn't fail with an exception
|
|
show_diff(b"foo:\n bar: \xc3\xb6\xc3\xb6\n".decode('utf-8'),
|
|
b"foo:\n bar: \xc3\xbc\xc3\xbc\n".decode('utf-8'))
|
|
|
|
def test_invoke_editor(self):
|
|
for e in ('', 'false'):
|
|
os.environ['EDITOR'] = e
|
|
self.assertRaises(PatroniCtlException, invoke_editor, 'foo: bar\n', 'test')
|
|
|
|
@patch('patroni.ctl.get_dcs')
|
|
def test_show_config(self, mock_get_dcs):
|
|
mock_get_dcs.return_value = self.e
|
|
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_leader
|
|
self.runner.invoke(ctl, ['show-config', 'dummy'])
|
|
|
|
@patch('patroni.ctl.get_dcs')
|
|
def test_edit_config(self, mock_get_dcs):
|
|
mock_get_dcs.return_value = self.e
|
|
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_leader
|
|
os.environ['EDITOR'] = 'true'
|
|
self.runner.invoke(ctl, ['edit-config', 'dummy'])
|
|
self.runner.invoke(ctl, ['edit-config', 'dummy', '-s', 'foo=bar'])
|
|
self.runner.invoke(ctl, ['edit-config', 'dummy', '--replace', 'postgres0.yml'])
|
|
self.runner.invoke(ctl, ['edit-config', 'dummy', '--apply', '-'], input='foo: bar')
|
|
self.runner.invoke(ctl, ['edit-config', 'dummy', '--force', '--apply', '-'], input='foo: bar')
|
|
mock_get_dcs.return_value.set_config_value = Mock(return_value=True)
|
|
self.runner.invoke(ctl, ['edit-config', 'dummy', '--force', '--apply', '-'], input='foo: bar')
|
|
|
|
@patch('patroni.ctl.get_dcs')
|
|
def test_version(self, mock_get_dcs):
|
|
mock_get_dcs.return_value = self.e
|
|
mock_get_dcs.return_value.get_cluster = get_cluster_initialized_with_leader
|
|
with patch('patroni.ctl.request_patroni') as mocked:
|
|
result = self.runner.invoke(ctl, ['version'])
|
|
assert 'patronictl version' in result.output
|
|
mocked.return_value.json = lambda: {'patroni': {'version': '1.2.3'}, 'server_version': 100001}
|
|
result = self.runner.invoke(ctl, ['version', 'dummy'])
|
|
assert '1.2.3' in result.output
|
|
with patch('requests.get', Mock(side_effect=Exception)):
|
|
result = self.runner.invoke(ctl, ['version', 'dummy'])
|
|
assert 'failed to get version' in result.output
|
|
|
|
def test_format_pg_version(self):
|
|
self.assertEquals(format_pg_version(100001), '10.1')
|
|
self.assertEquals(format_pg_version(90605), '9.6.5')
|