mirror of
https://github.com/outbackdingo/patroni.git
synced 2026-08-25 14:53:37 +00:00
Implement PUT /config and enhance some checks
This commit is contained in:
@@ -18,6 +18,8 @@ Scenario: check API requests on a stand-alone server
|
||||
And I receive a response text failover is not possible: cluster does not have members except leader
|
||||
When I issue an empty POST request to http://127.0.0.1:8008/failover
|
||||
Then I receive a response code 400
|
||||
When I issue a POST request to http://127.0.0.1:8008/failover with {"foo": "bar"}
|
||||
Then I receive a response code 400
|
||||
And I receive a response text "No values given for required parameters leader and candidate"
|
||||
|
||||
Scenario: check local configuration reload
|
||||
|
||||
+45
-16
@@ -9,7 +9,7 @@ import datetime
|
||||
import pytz
|
||||
|
||||
from patroni.exceptions import PostgresConnectionException
|
||||
from patroni.utils import Retry, RetryFailedError
|
||||
from patroni.utils import deep_compare, Retry, RetryFailedError
|
||||
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
from six.moves.socketserver import ThreadingMixIn
|
||||
from threading import Thread
|
||||
@@ -127,17 +127,46 @@ class RestApiHandler(BaseHTTPRequestHandler):
|
||||
is_changed = True
|
||||
return is_changed
|
||||
|
||||
def _read_json_content(self):
|
||||
if 'content-length' not in self.headers:
|
||||
return self.send_error(411)
|
||||
try:
|
||||
content_length = int(self.headers.get('content-length'))
|
||||
request = json.loads(self.rfile.read(content_length).decode('utf-8'))
|
||||
if isinstance(request, dict) and request:
|
||||
return request
|
||||
except Exception:
|
||||
logger.exception('Bad request')
|
||||
self.send_error(400)
|
||||
|
||||
@check_auth
|
||||
def do_PATCH_config(self):
|
||||
content_length = int(self.headers.get('content-length', 0))
|
||||
request = json.loads(self.rfile.read(content_length).decode('utf-8'))
|
||||
cluster = self.server.patroni.ha.dcs.get_cluster()
|
||||
data = cluster.config.data.copy()
|
||||
if RestApiHandler._patch_config(data, request):
|
||||
self.server.patroni.ha.dcs.set_config_value(json.dumps(data, separators=(',', ':')), cluster.config.index)
|
||||
self._write_json_response(200, data)
|
||||
else:
|
||||
self._write_response(304, '', '')
|
||||
request = self._read_json_content()
|
||||
if request:
|
||||
cluster = self.server.patroni.ha.dcs.get_cluster()
|
||||
data = cluster.config.data.copy()
|
||||
if RestApiHandler._patch_config(data, request):
|
||||
value = json.dumps(data, separators=(',', ':'))
|
||||
if self.server.patroni.ha.dcs.set_config_value(value, cluster.config.index):
|
||||
self._write_json_response(200, data)
|
||||
else:
|
||||
self.send_error(409)
|
||||
else:
|
||||
self.send_error(304)
|
||||
|
||||
@check_auth
|
||||
def do_PUT_config(self):
|
||||
request = self._read_json_content()
|
||||
if request:
|
||||
cluster = self.server.patroni.ha.dcs.get_cluster()
|
||||
if deep_compare(request, cluster.config.data):
|
||||
self.send_error(304)
|
||||
else:
|
||||
value = json.dumps(request, separators=(',', ':'))
|
||||
if self.server.patroni.ha.dcs.set_config_value(value):
|
||||
self._write_json_response(200, request)
|
||||
else:
|
||||
self.send_error(502)
|
||||
|
||||
@check_auth
|
||||
def do_POST_reload(self):
|
||||
@@ -184,7 +213,8 @@ class RestApiHandler(BaseHTTPRequestHandler):
|
||||
self._write_response(status_code, data)
|
||||
|
||||
def poll_failover_result(self, leader, candidate):
|
||||
for _ in range(0, 15):
|
||||
timeout = 10 if self.server.patroni.nap_time < 10 else self.server.patroni.nap_time
|
||||
for _ in range(0, timeout*2):
|
||||
time.sleep(1)
|
||||
try:
|
||||
cluster = self.server.patroni.dcs.get_cluster()
|
||||
@@ -217,11 +247,10 @@ class RestApiHandler(BaseHTTPRequestHandler):
|
||||
|
||||
@check_auth
|
||||
def do_POST_failover(self):
|
||||
content_length = int(self.headers.get('content-length', 0))
|
||||
try:
|
||||
request = json.loads(self.rfile.read(content_length).decode('utf-8'))
|
||||
except ValueError:
|
||||
request = {}
|
||||
request = self._read_json_content()
|
||||
if not request:
|
||||
return
|
||||
|
||||
leader = request.get('leader')
|
||||
candidate = request.get('candidate') or request.get('member')
|
||||
scheduled_at = request.get('scheduled_at')
|
||||
|
||||
+36
-18
@@ -49,6 +49,7 @@ class MockHa(object):
|
||||
|
||||
class MockPatroni(object):
|
||||
|
||||
nap_time = 10
|
||||
config = Mock()
|
||||
postgresql = MockPostgresql()
|
||||
ha = MockHa()
|
||||
@@ -126,12 +127,30 @@ class TestRestApiHandler(unittest.TestCase):
|
||||
def test_do_PATCH_config(self, mock_dcs):
|
||||
config = {'postgresql': {'use_slots': False, 'use_pg_rewind': True, 'parameters': {'wal_level': 'logical'}}}
|
||||
mock_dcs.get_cluster.return_value.config = ClusterConfig.from_node(1, json.dumps(config))
|
||||
request = 'PATCH /config HTTP/1.0' + self._authorization + '\nContent-Length: '
|
||||
self.assertIsNotNone(MockRestApiServer(RestApiHandler, request + '2\n\n{}'))
|
||||
request = 'PATCH /config HTTP/1.0' + self._authorization
|
||||
self.assertIsNotNone(MockRestApiServer(RestApiHandler, request))
|
||||
request += '\nContent-Length: '
|
||||
self.assertIsNotNone(MockRestApiServer(RestApiHandler, request + '34\n\n{"postgresql":{"use_slots":false}}'))
|
||||
config['ttl'] = 5
|
||||
config['postgresql'].update({'use_slots': True, "parameters": None})
|
||||
config = json.dumps(config)
|
||||
MockRestApiServer(RestApiHandler, request + str(len(config)) + '\n\n' + config)
|
||||
request += str(len(config)) + '\n\n' + config
|
||||
MockRestApiServer(RestApiHandler, request)
|
||||
mock_dcs.set_config_value.return_value = False
|
||||
MockRestApiServer(RestApiHandler, request)
|
||||
|
||||
@patch.object(MockHa, 'dcs')
|
||||
def test_do_PUT_config(self, mock_dcs):
|
||||
mock_dcs.get_cluster.return_value.config = ClusterConfig.from_node(1, '{}')
|
||||
request = 'PUT /config HTTP/1.0' + self._authorization + '\nContent-Length: '
|
||||
self.assertIsNotNone(MockRestApiServer(RestApiHandler, request + '2\n\n{}'))
|
||||
config = '{"foo": "bar"}'
|
||||
request += str(len(config)) + '\n\n' + config
|
||||
MockRestApiServer(RestApiHandler, request)
|
||||
mock_dcs.set_config_value.return_value = False
|
||||
MockRestApiServer(RestApiHandler, request)
|
||||
mock_dcs.get_cluster.return_value.config = ClusterConfig.from_node(1, config)
|
||||
MockRestApiServer(RestApiHandler, request)
|
||||
|
||||
@patch.object(MockPatroni, 'sighup_handler', Mock(side_effect=Exception))
|
||||
def test_do_POST_reload(self):
|
||||
@@ -170,18 +189,20 @@ class TestRestApiHandler(unittest.TestCase):
|
||||
def test_do_POST_failover(self, dcs):
|
||||
cluster = dcs.get_cluster.return_value
|
||||
|
||||
request = 'POST /failover HTTP/1.0' + self._authorization + '\nContent-Length: 0\n\n'
|
||||
post = 'POST /failover HTTP/1.0' + self._authorization + '\nContent-Length: '
|
||||
|
||||
MockRestApiServer(RestApiHandler, post + '7\n\n{"1":2}')
|
||||
|
||||
request = post + '0\n\n'
|
||||
MockRestApiServer(RestApiHandler, request)
|
||||
|
||||
cluster.leader.name = 'postgresql1'
|
||||
MockRestApiServer(RestApiHandler, request)
|
||||
|
||||
request = 'POST /failover HTTP/1.0' + self._authorization + '\nContent-Length: 25\n\n{"leader": "postgresql1"}'
|
||||
MockRestApiServer(RestApiHandler, request)
|
||||
MockRestApiServer(RestApiHandler, post + '25\n\n{"leader": "postgresql1"}')
|
||||
|
||||
cluster.leader.name = 'postgresql2'
|
||||
request = 'POST /failover HTTP/1.0' + self._authorization +\
|
||||
'\nContent-Length: 53\n\n{"leader": "postgresql1", "candidate": "postgresql2"}'
|
||||
request = post + '53\n\n{"leader": "postgresql1", "candidate": "postgresql2"}'
|
||||
MockRestApiServer(RestApiHandler, request)
|
||||
|
||||
cluster.leader.name = 'postgresql1'
|
||||
@@ -207,24 +228,21 @@ class TestRestApiHandler(unittest.TestCase):
|
||||
MockRestApiServer(RestApiHandler, request)
|
||||
|
||||
# Valid future date
|
||||
request = 'POST /failover HTTP/1.0' + self._authorization + '\nContent-Length: 103\n\n{"leader": ' +\
|
||||
'"postgresql1", "member": "postgresql2", "scheduled_at": "6016-02-15T18:13:30.568224+01:00"}'
|
||||
request = post + '103\n\n{"leader": "postgresql1", "member": "postgresql2",' +\
|
||||
' "scheduled_at": "6016-02-15T18:13:30.568224+01:00"}'
|
||||
MockRestApiServer(RestApiHandler, request)
|
||||
with patch.object(MockPatroni, 'dcs') as d:
|
||||
d.manual_failover.return_value = False
|
||||
MockRestApiServer(RestApiHandler, request)
|
||||
|
||||
# Exception: No timezone specified
|
||||
request = 'POST /failover HTTP/1.0' + self._authorization + '\nContent-Length: 97\n\n{"leader": ' +\
|
||||
'"postgresql1", "member": "postgresql2", "scheduled_at": "6016-02-15T18:13:30.568224"}'
|
||||
request = post + '97\n\n{"leader": "postgresql1", "member": "postgresql2",' +\
|
||||
' "scheduled_at": "6016-02-15T18:13:30.568224"}'
|
||||
MockRestApiServer(RestApiHandler, request)
|
||||
|
||||
# Exception: Scheduled in the past
|
||||
request = 'POST /failover HTTP/1.0' + self._authorization + '\nContent-Length: 103\n\n{"leader": ' +\
|
||||
'"postgresql1", "member": "postgresql2", "scheduled_at": "1016-02-15T18:13:30.568224+01:00"}'
|
||||
MockRestApiServer(RestApiHandler, request)
|
||||
request = post + '103\n\n{"leader": "postgresql1", "member": "postgresql2", "scheduled_at": "'
|
||||
MockRestApiServer(RestApiHandler, request + '1016-02-15T18:13:30.568224+01:00"}')
|
||||
|
||||
# Invalid date
|
||||
request = 'POST /failover HTTP/1.0' + self._authorization + '\nContent-Length: 103\n\n{"leader": ' +\
|
||||
'"postgresql1", "member": "postgresql2", "scheduled_at": "2010-02-29T18:13:30.568224+01:00"}'
|
||||
self.assertIsNotNone(MockRestApiServer(RestApiHandler, request))
|
||||
self.assertIsNotNone(MockRestApiServer(RestApiHandler, request + '2010-02-29T18:13:30.568224+01:00"}'))
|
||||
|
||||
Reference in New Issue
Block a user