Merge pull request #67 from zalando/feature/api

Manual failover via REST API
This commit is contained in:
Alexander Kukushkin
2015-10-21 10:55:28 +02:00
3 changed files with 84 additions and 1 deletions
+51
View File
@@ -3,6 +3,7 @@ import fcntl
import json
import logging
import psycopg2
import time
from patroni.exceptions import PostgresConnectionException
from patroni.utils import Retry, RetryFailedError
@@ -121,6 +122,56 @@ class RestApiHandler(BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(data)
def poll_failover_result(self, leader, member):
for a in range(0, 15):
time.sleep(1)
try:
cluster = self.server.patroni.dcs.get_cluster()
if cluster.leader and cluster.leader.name != leader:
return 200, ('Successfully failed over to ' + cluster.leader.name).encode('utf-8')
if not cluster.failover:
return 503, b'Failover failed'
except:
pass
return 503, b'Failover status unknown'
def is_failover_possible(self, cluster, leader, member):
if leader and not cluster.leader or cluster.leader.name != leader:
return b'leader name does not match'
if member:
members = [m for m in cluster.members if m.name == member]
if not members:
return b'member does not exists'
else:
members = [m for m in cluster.members if m.name != cluster.leader.name and m.api_url]
if not members:
return b'failover is not possible: cluster does not have members except leader'
for member, reachable, in_recovery, xlog_location in self.server.patroni.ha.fetch_nodes_statuses(members):
if reachable:
return None
return b'failover is not possible: no good candidates have been found'
@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'))
leader = request.get('leader', None)
member = request.get('member', None)
cluster = self.server.patroni.ha.dcs.get_cluster()
status_code = 503
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'
else:
self.server.patroni.dcs.event.set()
status_code, data = self.poll_failover_result(cluster.leader and cluster.leader.name, member)
self.send_response(status_code)
self.send_header('Content-Type', 'text/html')
self.end_headers()
self.wfile.write(data)
def parse_request(self):
"""Override parse_request method to enrich basic functionality of `BaseHTTPRequestHandler` class
+1 -1
View File
@@ -198,7 +198,7 @@ class ZooKeeper(AbstractDCS):
self.client.retry(self.client.set, self.failover_path, value.encode('utf-8'), version=index or -1)
return True
except NoNodeError:
return value == '' or (not index and self._create(self.failover_path, value.encode('utf-8')))
return value == '' or (not index and self._create(self.failover_path, value))
except:
logging.exception('set_failover_value')
return False
+32
View File
@@ -3,6 +3,7 @@ import unittest
from mock import Mock, patch
from patroni.api import RestApiHandler, RestApiServer
from patroni.dcs import Member
from six import BytesIO as IO
from six.moves import BaseHTTPServer
from test_postgresql import psycopg2_connect, MockCursor
@@ -38,6 +39,9 @@ class MockHa(Mock):
def restart_scheduled(self):
return False
def fetch_nodes_statuses(self, members):
return [[None, True, None, None]]
class MockPatroni:
@@ -117,3 +121,31 @@ class TestRestApiHandler(unittest.TestCase):
MockRestApiServer(RestApiHandler, b'GET /patroni')
with patch.object(MockPostgresql, 'connection', Mock(side_effect=psycopg2.OperationalError)):
MockRestApiServer(RestApiHandler, b'GET /patroni')
@patch('time.sleep', Mock())
@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: 25\n\n{"leader": "postgresql1"}'
MockRestApiServer(RestApiHandler, request)
cluster.leader.name = 'postgresql1'
MockRestApiServer(RestApiHandler, request)
cluster.members = [Member(0, 'postgresql0', 30, {'api_url': 'http'})]
MockRestApiServer(RestApiHandler, request)
with patch.object(MockPatroni, 'dcs') as d:
cluster = d.get_cluster.return_value
cluster.leader.name = 'postgresql0'
MockRestApiServer(RestApiHandler, request)
cluster.leader.name = 'postgresql1'
cluster.failover = None
MockRestApiServer(RestApiHandler, request)
d.get_cluster = Mock(side_effect=Exception())
MockRestApiServer(RestApiHandler, request)
d.manual_failover.return_value = False
MockRestApiServer(RestApiHandler, request)
with patch.object(MockHa, 'fetch_nodes_statuses', Mock(return_value=[])):
MockRestApiServer(RestApiHandler, request)
request = b'POST /failover HTTP/1.0\nAuthorization: Basic dGVzdDp0ZXN0\n' +\
b'Content-Length: 50\n\n{"leader": "postgresql1", "member": "postgresql2"}'
MockRestApiServer(RestApiHandler, request)