diff --git a/patroni/api.py b/patroni/api.py index ba18b333..1186321f 100644 --- a/patroni/api.py +++ b/patroni/api.py @@ -174,38 +174,48 @@ class RestApiHandler(BaseHTTPRequestHandler): @check_auth def do_POST_failover(self): content_length = int(self.headers.get('content-length', 0)) - request = json.loads(self.rfile.read(content_length).decode('utf-8')) + try: + request = json.loads(self.rfile.read(content_length).decode('utf-8')) + except ValueError: + request = {} leader = request.get('leader') member = request.get('member') cluster = self.server.patroni.ha.dcs.get_cluster() status_code = 500 + logger.info("received failover request with leader {0} member {1} scheduled_at {2}". + format(leader, member, request.get("scheduled_at"))) + data = b'' - if request.get('scheduled_at'): - try: - scheduled_at = dateutil.parser.parse(request['scheduled_at']) - if scheduled_at.tzinfo is None: - data = b'Timezone information is mandatory for scheduled_at' - status_code = 400 - elif scheduled_at < datetime.datetime.now(pytz.utc): - data = b'Cannot schedule failover in the past' + if leader or member: + if request.get('scheduled_at'): + try: + scheduled_at = dateutil.parser.parse(request['scheduled_at']) + if scheduled_at.tzinfo is None: + data = b'Timezone information is mandatory for scheduled_at' + status_code = 400 + elif scheduled_at < datetime.datetime.now(pytz.utc): + data = b'Cannot schedule failover in the past' + status_code = 422 + elif self.server.patroni.dcs.manual_failover(leader, member, scheduled_at): + data = b'Failover scheduled' + status_code = 200 + except (ValueError, TypeError): + logger.exception('Invalid scheduled failover time: {}'.format(request['scheduled_at'])) + data = b'Unable to parse scheduled timestamp. It should be in an unambiguous format, e.g. ISO 8601' status_code = 422 - elif self.server.patroni.dcs.manual_failover(leader, member, scheduled_at): - data = b'Failover scheduled' - status_code = 200 - except (ValueError, TypeError): - logger.exception('Invalid scheduled failover time: {}'.format(request['scheduled_at'])) - data = b'Unable to parse scheduled timestamp. It should be in an unambiguous format, e.g. ISO 8601' - status_code = 422 + else: + data = self.is_failover_possible(cluster, leader, member) + if not data: + if not self.server.patroni.dcs.manual_failover(leader, member): + data = b'failed to write failover key into DCS' + status_code = 503 + else: + self.server.patroni.dcs.event.set() + status_code, data = self.poll_failover_result(cluster.leader and cluster.leader.name, member) else: - data = self.is_failover_possible(cluster, leader, member) - if not data: - if not self.server.patroni.dcs.manual_failover(leader, member): - data = b'failed to write failover key into DCS' - status_code = 503 - else: - self.server.patroni.dcs.event.set() - status_code, data = self.poll_failover_result(cluster.leader and cluster.leader.name, member) + status_code = 400 + data = b'No values given for required parameters leader and member' self.send_response(status_code) self.send_header('Content-Type', 'text/html') diff --git a/patroni/ha.py b/patroni/ha.py index 8f5d772d..98ba4398 100644 --- a/patroni/ha.py +++ b/patroni/ha.py @@ -287,10 +287,10 @@ class Ha(object): try: delta = (failover.scheduled_at - now).total_seconds() - if delta > 10: + if delta > self.patroni.nap_time: logging.info('Awaiting failover at %s (in %.0f seconds)', failover.scheduled_at.isoformat(), delta) return - elif delta < -15: + elif delta < - int(self.patroni.nap_time * 1.5): logger.warning('Found a stale failover value, cleaning up: %s', failover.scheduled_at) self.dcs.manual_failover('', '', self.cluster.failover.index) return diff --git a/tests/test_api.py b/tests/test_api.py index 1be0f96c..0d6dd151 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -153,6 +153,10 @@ class TestRestApiHandler(unittest.TestCase): @patch.object(MockHa, 'dcs') def test_do_POST_failover(self, dcs): cluster = dcs.get_cluster.return_value + + request = b'POST /failover HTTP/1.0\nAuthorization: Basic dGVzdDp0ZXN0\n' +\ + b'Content-Length: 0\n\n' + MockRestApiServer(RestApiHandler, request) request = b'POST /failover HTTP/1.0\nAuthorization: Basic dGVzdDp0ZXN0\n' +\ b'Content-Length: 25\n\n{"leader": "postgresql1"}' MockRestApiServer(RestApiHandler, request) diff --git a/tests/test_ha.py b/tests/test_ha.py index a65a444a..98b1ad1c 100644 --- a/tests/test_ha.py +++ b/tests/test_ha.py @@ -54,6 +54,7 @@ class MockPatroni(object): self.api = Mock() self.tags = {} self.nofailover = None + self.nap_time = 10 self.replicatefrom = None self.api.connection_string = 'http://127.0.0.1:8008'