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