mirror of
https://github.com/outbackdingo/patroni.git
synced 2026-08-26 15:40:21 +00:00
Create a superuser role with a given name and password (or change the postgres role if the name is not given). Also, create an admin role with the name and the password provided. The admin role has CREATEDB and CREATEROLE permisssions, effectively making it the most powerful role after the superuser.
201 lines
7.5 KiB
Python
201 lines
7.5 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.data_dir = config["data_dir"]
|
|
self.replication = config["replication"]
|
|
self.superuser = config.get('superuser')
|
|
self.admin_user = config.get('admin')
|
|
|
|
self.config = config
|
|
|
|
self.cursor_holder = None
|
|
connection_host = aws_host_address or self.host
|
|
self.connection_string = "postgres://%s:%s@%s:%s/postgres" % (self.replication["username"], self.replication["password"], connection_host, self.port)
|
|
|
|
self.conn = None
|
|
|
|
def cursor(self):
|
|
if not self.cursor_holder:
|
|
self.conn = psycopg2.connect("postgres://%s:%s/postgres" % (self.host, self.port))
|
|
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 postgres postgres 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 \"%s\" LOGIN SUPERUSER WITH PASSWORD '%s';".format(self.superuser["username"], self.superuser["password"]))
|
|
else:
|
|
self.query("ALTER ROLE postgres WITH PASSWORD '{0}'".format(self.superuser['password']))
|
|
if self.admin:
|
|
self.query("CREATE ROLE \"%s\" LOGIN CREATEDB CREATEROLE WITH PASSWORD '%s';".format(self.admin["username"], self.admin["password"]))
|
|
|
|
def xlog_position(self):
|
|
return self.query("SELECT pg_last_xlog_replay_location();").fetchone()[0]
|