Files
patroni/helpers/postgresql.py
T
Feike Steenbergen a56b346295 Hardened the health checks and implemented a real status page.
For the postgresql helper, hardened the code to get "the" cursor of the postgresql instance.
For the statuspage, a small status json is returned.
To find out what status a PostgreSQL cluster is in we use the cursor (instead of the provided query() function), as the query function does some retrying etc. For the healthcheck we want to simple provide an answer to a simple query, if we have to reconnect, we are not *that* healthy anyway.

Dropped catching exceptions in the do_GET block, as the HTTPServer will do that nicely for us anyway.
2015-05-12 11:58:47 +02:00

208 lines
7.8 KiB
Python

import os, psycopg2, re, time
import logging
from urlparse import urlparse
logger = logging.getLogger(__name__)
class Postgresql:
def __init__(self, config, aws_host_address=None):
self.name = config["name"]
self.host, self.port = config["listen"].split(":")
self.libpq_parameters = {
'host' : aws_host_address or self.host,
'port' : self.port,
'fallback_application_name' : 'Governor',
'connect_timeout' : 5,
'options' : '-c statement_timeout=2000'
}
self.data_dir = config["data_dir"]
self.replication = config["replication"]
self.superuser = config.get('superuser')
self.admin = config.get('admin')
self.config = config
self.cursor_holder = None
self.connection_string = "postgres://%s:%s@%s:%s/postgres" % (self.replication["username"], self.replication["password"], self.libpq_parameters['host'], self.port)
self.conn = None
def cursor(self):
if (self.cursor_holder is None) or self.cursor_holder.closed:
if (self.conn is None) or self.conn.closed:
self.conn = psycopg2.connect(**self.libpq_parameters)
self.conn.autocommit = True
self.cursor_holder = self.conn.cursor()
return self.cursor_holder
def disconnect(self):
try:
self.conn.close()
except Exception as e:
logger.error("Error disconnecting: %s" % e)
def query(self, sql):
max_attempts = 0
while True:
try:
self.cursor().execute(sql)
break
except psycopg2.OperationalError as e:
if self.conn:
self.disconnect()
self.cursor_holder = None
if max_attempts > 4:
raise e
max_attempts += 1
time.sleep(5)
return self.cursor()
def data_directory_empty(self):
return not os.path.exists(self.data_dir) or os.listdir(self.data_dir) == []
def initialize(self):
if os.system("initdb -D %s" % self.data_dir) == 0:
self.write_pg_hba()
return True
return False
def sync_from_leader(self, leader):
leader = urlparse(leader["address"])
f = open("./pgpass", "w")
f.write("%(hostname)s:%(port)s:*:%(username)s:%(password)s\n" %
{"hostname": leader.hostname, "port": leader.port, "username": leader.username, "password": leader.password})
f.close()
os.system("chmod 600 pgpass")
return os.system("PGPASSFILE=pgpass pg_basebackup -R -D %(data_dir)s --host=%(host)s --port=%(port)s -U %(username)s" %
{"data_dir": self.data_dir, "host": leader.hostname, "port": leader.port, "username": leader.username}) == 0
def is_leader(self):
return not self.query("SELECT pg_is_in_recovery();").fetchone()[0]
def is_running(self):
return os.system("pg_ctl status -D %s > /dev/null" % self.data_dir) == 0
def start(self):
if self.is_running():
logger.error("Cannot start PostgreSQL because one is already running.")
return False
pid_path = "%s/postmaster.pid" % self.data_dir
if os.path.exists(pid_path):
os.remove(pid_path)
logger.info("Removed %s" % pid_path)
command_code = os.system("postgres -D %s %s &" % (self.data_dir, self.server_options()))
time.sleep(5)
return command_code != 0
def stop(self):
return os.system("pg_ctl stop -w -D %s -m fast -w" % self.data_dir) != 0
def reload(self):
return os.system("pg_ctl reload -w -D %s" % self.data_dir) == 0
def restart(self):
return os.system("pg_ctl restart -w -D %s -m fast" % self.data_dir) == 0
def server_options(self):
options = "-c listen_addresses=%s -c port=%s" % (self.host, self.port)
for setting, value in self.config["parameters"].iteritems():
options += " -c \"%s=%s\"" % (setting, value)
return options
def is_healthy(self):
if not self.is_running():
logger.warning("Postgresql is not running.")
return False
return True
def is_healthiest_node(self, members):
for member in members:
if member["hostname"] == self.name:
continue
try:
member_conn = psycopg2.connect(member["address"])
member_conn.autocommit = True
member_cursor = member_conn.cursor()
member_cursor.execute("SELECT '%s'::pg_lsn - pg_last_xlog_replay_location() AS bytes;" % self.xlog_position())
xlog_diff = member_cursor.fetchone()[0]
logger.info([self.name, member["hostname"], xlog_diff])
if xlog_diff < 0:
member_cursor.close()
return False
member_cursor.close()
except psycopg2.OperationalError:
continue
return True
def replication_slot_name(self):
member = os.environ.get("MEMBER")
(member, _) = re.subn(r'[^a-z0-9]+', r'_', member)
return member
def write_pg_hba(self):
f = open("%s/pg_hba.conf" % self.data_dir, "a")
f.write("host replication %(username)s %(network)s md5" %
{"username": self.replication["username"], "network": self.replication["network"]})
# allow TCP connections from the host's own address
f.write("\nhost postgres postgres samehost trust\n")
# allow TCP connections from the rest of the world with a password
f.write("\nhost all all 0.0.0.0/0 md5\n")
f.close()
def write_recovery_conf(self, leader_hash):
leader = urlparse(leader_hash["address"])
f = open("%s/recovery.conf" % self.data_dir, "w")
f.write("""
standby_mode = 'on'
primary_slot_name = '%(recovery_slot)s'
primary_conninfo = 'user=%(user)s password=%(password)s host=%(hostname)s port=%(port)s sslmode=prefer sslcompression=1'
recovery_target_timeline = 'latest'
""" % {"recovery_slot": self.name, "user": leader.username, "password": leader.password, "hostname": leader.hostname, "port": leader.port})
if "recovery_conf" in self.config:
for name, value in self.config["recovery_conf"].iteritems():
f.write("%s = '%s'" % (name, value))
f.close()
def follow_the_leader(self, leader_hash):
leader = urlparse(leader_hash["address"])
if os.system("grep 'host=%(hostname)s port=%(port)s' %(data_dir)s/recovery.conf > /dev/null" % {"hostname": leader.hostname, "port": leader.port, "data_dir": self.data_dir}) != 0:
self.write_recovery_conf(leader_hash)
self.restart()
return True
def promote(self):
return os.system("pg_ctl promote -w -D %s" % self.data_dir) == 0
def demote(self, leader):
self.write_recovery_conf(leader)
self.restart()
def create_replication_user(self):
self.query("CREATE USER \"%s\" WITH REPLICATION ENCRYPTED PASSWORD '%s';" % (self.replication["username"], self.replication["password"]))
def create_connection_users(self):
if self.superuser:
if 'username' in self.superuser:
self.query("CREATE ROLE \"{0}\" LOGIN SUPERUSER PASSWORD '{1}';".format(self.superuser["username"], self.superuser["password"]))
else:
self.query("ALTER ROLE postgres PASSWORD '{0}';".format(self.superuser['password']))
if self.admin:
self.query("CREATE ROLE \"{0}\" LOGIN CREATEDB CREATEROLE PASSWORD '{1}';".format(self.admin["username"], self.admin["password"]))
def xlog_position(self):
return self.query("SELECT pg_last_xlog_replay_location();").fetchone()[0]