Implement PUT /config and enhance some checks

This commit is contained in:
Alexander Kukushkin
2016-06-01 17:06:31 +02:00
parent c8b5003b86
commit 1c30948ef9
3 changed files with 83 additions and 34 deletions
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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))