From 84fcc7712b37a2ca53520c30610d6badcf38bbfc Mon Sep 17 00:00:00 2001 From: Alexander Kukushkin Date: Thu, 21 Apr 2016 18:01:47 +0200 Subject: [PATCH] Refactor api.py All responses to the client sent by single method `_write_response` encode('utf-8') is done only inside this method. It make easier to support existing code because eliminates need to put b'' everywhere --- patroni/api.py | 115 ++++++++++++++++++++++--------------------------- 1 file changed, 52 insertions(+), 63 deletions(-) diff --git a/patroni/api.py b/patroni/api.py index f5bf9ce1..84be0e43 100644 --- a/patroni/api.py +++ b/patroni/api.py @@ -34,12 +34,19 @@ def check_auth(func): class RestApiHandler(BaseHTTPRequestHandler): + def _write_response(self, status_code, body, headers=None): + self.send_response(status_code) + if body is not None: + headers = headers or {} + if 'Content-Type' not in headers: + headers['Content-Type'] = 'text/html' + for name, value in (headers or {}).items(): + self.send_header(name, value) + self.end_headers() + self.wfile.write(body.encode('utf-8')) + def send_auth_request(self, body): - self.send_response(401) - self.send_header('WWW-Authenticate', 'Basic realm=\"Patroni\"') - self.send_header('Content-type', 'text/html') - self.end_headers() - self.wfile.write(body.encode('utf-8')) + self._write_response(401, body, {'WWW-Authenticate': 'Basic realm=\"Patroni\"'}) def finish(self, *args, **kwargs): try: @@ -55,15 +62,23 @@ class RestApiHandler(BaseHTTPRequestHandler): status = self.server.check_auth_header(auth_header) return not status or self.send_auth_request(status) - def do_OPTIONS(self): - self.do_GET(options=True) + def _write_status_response(self, status_code, response, options=False): + if options: + body = None + else: + patroni = self.server.patroni + response.update({'tags': patroni.tags} if patroni.tags else {}) + if patroni.postgresql.sysid: + response['database_system_identifier'] = patroni.postgresql.sysid + response['patroni'] = {'version': patroni.version, 'scope': patroni.postgresql.scope} + body = json.dumps(response) + self._write_response(status_code, body, {'Content-Type': 'application/json'}) def do_GET(self, options=False): """Default method for processing all GET requests which can not be routed to other methods""" path = '/master' if self.path == '/' else self.path response = self.get_postgresql_status() - response.update(self.get_tags()) patroni = self.server.patroni cluster = patroni.dcs.cluster @@ -85,38 +100,25 @@ class RestApiHandler(BaseHTTPRequestHandler): status_code = 200 else: status_code = 503 + self._write_status_response(status_code, response, options) - self.send_response(status_code) - if not options: - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps(response).encode('utf-8')) + def do_OPTIONS(self): + self.do_GET(options=True) def do_GET_patroni(self): response = self.get_postgresql_status(True) - response.update(self.get_tags()) - response['patroni'] = {'version': self.server.patroni.version, 'scope': self.server.patroni.postgresql.scope} - - self.send_response(200) - self.send_header('Content-Type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps(response).encode('utf-8')) + self._write_status_response(200, response) @check_auth def do_POST_restart(self): status_code = 500 - data = b'restart failed' + data = 'restart failed' try: - status, msg = self.server.patroni.ha.restart() + status, data = self.server.patroni.ha.restart() status_code = 200 if status else 503 - data = msg.encode('utf-8') except Exception: logger.exception('Exception during restart') - - self.send_response(status_code) - self.send_header('Content-Type', 'text/html') - self.end_headers() - self.wfile.write(data) + self._write_response(status_code, data) @check_auth def do_POST_reinitialize(self): @@ -124,23 +126,19 @@ class RestApiHandler(BaseHTTPRequestHandler): cluster = ha.dcs.get_cluster() if cluster.is_unlocked(): status_code = 503 - data = b'Cluster has no leader, can not reinitialize' + data = 'Cluster has no leader, can not reinitialize' elif cluster.leader.name == ha.state_handler.name: status_code = 503 - data = b'I am the leader, can not reinitialize' + data = 'I am the leader, can not reinitialize' else: action = ha.schedule_reinitialize() if action is not None: status_code = 503 - data = (action + ' already in progress').encode('utf-8') + data = action + ' already in progress' else: status_code = 200 - data = b'reinitialize scheduled' - - self.send_response(status_code) - self.send_header('Content-Type', 'text/html') - self.end_headers() - self.wfile.write(data) + data = 'reinitialize scheduled' + self._write_response(status_code, data) def poll_failover_result(self, leader, candidate): for _ in range(0, 15): @@ -149,31 +147,30 @@ class RestApiHandler(BaseHTTPRequestHandler): cluster = self.server.patroni.dcs.get_cluster() if cluster.leader and cluster.leader.name != leader: if not candidate or candidate == cluster.leader.name: - return 200, ('Successfully failed over to ' + cluster.leader.name).encode('utf-8') + return 200, 'Successfully failed over to "{0}"'.format(cluster.leader.name) else: - return 200, 'Failed over to "{0}" instead of "{1}"'.format(cluster.leader.name, - candidate).encode('utf-8') + return 200, 'Failed over to "{0}" instead of "{1}"'.format(cluster.leader.name, candidate) if not cluster.failover: - return 503, b'Failover failed' + return 503, 'Failover failed' except Exception as e: logger.debug('Exception occured during polling failover result: %s', e) - return 503, b'Failover status unknown' + return 503, 'Failover status unknown' def is_failover_possible(self, cluster, leader, candidate): if leader and not cluster.leader or cluster.leader.name != leader: - return b'leader name does not match' + return 'leader name does not match' if candidate: members = [m for m in cluster.members if m.name == candidate] if not members: - return b'candidate does not exists' + return 'candidate 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' + return 'failover is not possible: cluster does not have members except leader' for _, reachable, _, _, tags in self.server.patroni.ha.fetch_nodes_statuses(members): if reachable and not tags.get('nofailover', False): return None - return b'failover is not possible: no good candidates have been found' + return 'failover is not possible: no good candidates have been found' @check_auth def do_POST_failover(self): @@ -191,27 +188,27 @@ class RestApiHandler(BaseHTTPRequestHandler): logger.info("received failover request with leader=%s candidate=%s scheduled_at=%s", leader, candidate, scheduled_at) - data = b'' + data = '' if leader or candidate: if scheduled_at: try: scheduled_at = dateutil.parser.parse(scheduled_at) if scheduled_at.tzinfo is None: - data = b'Timezone information is mandatory for scheduled_at' + data = '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' + data = 'Cannot schedule failover in the past' status_code = 422 elif self.server.patroni.dcs.manual_failover(leader, candidate, scheduled_at=scheduled_at): self.server.patroni.dcs.event.set() - data = b'Failover scheduled' + data = 'Failover scheduled' status_code = 200 else: - data = b'failed to write failover key into DCS' + data = 'failed to write failover key into DCS' status_code = 503 except (ValueError, TypeError): logger.exception('Invalid scheduled failover time: %s', request['scheduled_at']) - data = b'Unable to parse scheduled timestamp. It should be in an unambiguous format, e.g. ISO 8601' + data = '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, candidate) @@ -220,16 +217,12 @@ class RestApiHandler(BaseHTTPRequestHandler): self.server.patroni.dcs.event.set() status_code, data = self.poll_failover_result(cluster.leader and cluster.leader.name, candidate) else: - data = b'failed to write failover key into DCS' + data = 'failed to write failover key into DCS' status_code = 503 else: status_code = 400 - data = b'No values given for required parameters leader and candidate' - - self.send_response(status_code) - self.send_header('Content-Type', 'text/html') - self.end_headers() - self.wfile.write(data) + data = 'No values given for required parameters leader and candidate' + self._write_response(status_code, data) def parse_request(self): """Override parse_request method to enrich basic functionality of `BaseHTTPRequestHandler` class @@ -278,7 +271,6 @@ class RestApiHandler(BaseHTTPRequestHandler): 'postmaster_start_time': row[0], 'role': 'replica' if row[1] else 'master', 'server_version': self.server.patroni.postgresql.server_version, - 'database_system_identifier': self.server.patroni.postgresql.sysid, 'xlog': ({ 'received_location': row[3], 'replayed_location': row[4], @@ -294,9 +286,6 @@ class RestApiHandler(BaseHTTPRequestHandler): state = 'unknown' return {'state': state} - def get_tags(self): - return {'tags': self.server.patroni.tags} if self.server.patroni.tags else {} - def log_message(self, fmt, *args): logger.debug("API thread: %s - - [%s] %s", self.client_address[0], self.log_date_time_string(), fmt % args)