Merge branch 'master' of https://github.com/zalando/patroni into feature/nofailover

This commit is contained in:
Oleksii Kliukin
2015-10-26 10:41:51 +01:00
15 changed files with 219 additions and 60 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash
DOCKER_IMAGE="os-registry.stups.zalan.do/acid/patroni:1.0-SNAPSHOT"
DOCKER_IMAGE="registry.opensource.zalan.do/acid/patroni:1.0-SNAPSHOT"
MEMBERS=3
+9 -8
View File
@@ -3,25 +3,25 @@
function usage()
{
cat <<__EOF__
Usage: $0
Usage: $0
Options:
--etcd ETCD Provide an external etcd to connect to
--name NAME Give the cluster a specific name
--name NAME Give the cluster a specific name
--etcd-only Do not run Patroni, run a standalone etcd
Examples:
$0 --etcd=127.17.0.84:4001
$0 --etcd-only
$0
$0
$0 --name=true_scotsman
__EOF__
}
DOCKER_IP=$(hostname --ip-address)
PATRONI_SCOPE=batman
PATRONI_SCOPE=${PATRONI_SCOPE:-batman}
optspec=":vh-:"
while getopts "$optspec" optchar; do
@@ -32,7 +32,7 @@ while getopts "$optspec" optchar; do
exec etcd --data-dir /tmp/etcd.data \
-advertise-client-urls=http://${DOCKER_IP}:4001 \
-listen-client-urls=http://0.0.0.0:4001 \
-listen-peer-urls=http://0.0.0.0:2380
-listen-peer-urls=http://0.0.0.0:2380
exit 0
;;
cheat)
@@ -83,10 +83,11 @@ cat > /patroni/postgres.yml <<__EOF__
ttl: &ttl 30
loop_wait: &loop_wait 10
scope: &scope ${PATRONI_SCOPE}
scope: &scope '${PATRONI_SCOPE}'
namespace: 'patroni'
restapi:
listen: 127.0.0.1:8008
connect_address: 127.0.0.1:8008
listen: 0.0.0.0:8008
connect_address: ${DOCKER_IP}:8008
etcd:
scope: *scope
ttl: *ttl
+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
@@ -123,6 +124,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
+6 -3
View File
@@ -126,8 +126,8 @@ class AbstractDCS:
i.e.: `zookeeper` for zookeeper, `etcd` for etcd, etc...
"""
self._name = name
self._scope = config['scope']
self._base_path = '/service/' + self._scope
self._namespace = '/{}'.format(config.get('namespace', '/service/').strip('/'))
self._base_path = '/'.join([self._namespace, config['scope']])
self._cluster = None
self._cluster_thread_lock = Lock()
@@ -244,8 +244,11 @@ class AbstractDCS:
overwriting the key if necessary."""
@abc.abstractmethod
def initialize(self):
def initialize(self, create_new=True, sysid=""):
"""Race for cluster initialization.
:param create_new: False if the key should already exist (in the case we are setting the system_id)
:param sysid: PostgreSQL cluster system identifier, if specified, is written to the key
:returns: `!True` if key has been created successfully.
this method should create atomically initialize key and return `!True`
+10 -5
View File
@@ -52,7 +52,11 @@ class Client(etcd.Client):
def api_execute(self, path, method, **kwargs):
# Update machines_cache if previous attempt of update has failed
self._update_machines_cache and self._load_machines_cache()
return super(Client, self).api_execute(path, method, **kwargs)
try:
return super(Client, self).api_execute(path, method, **kwargs)
except etcd.EtcdConnectionFailed:
self._update_machines_cache = True
raise
@staticmethod
def get_srv_record(host):
@@ -177,7 +181,8 @@ class Etcd(AbstractDCS):
nodes = {os.path.relpath(node.key, result.key): node for node in result.leaves}
# get initialize flag
initialize = bool(nodes.get(self._INITIALIZE, False))
initialize = nodes.get(self._INITIALIZE, None)
initialize = initialize and initialize.value
# get last leader operation
last_leader_operation = nodes.get(self._LEADER_OPTIME, None)
@@ -235,8 +240,8 @@ class Etcd(AbstractDCS):
return self.retry(self.client.test_and_set, self.leader_path, self._name, self._name, self.ttl)
@catch_etcd_errors
def initialize(self):
return self.retry(self.client.write, self.initialize_path, self._name, prevExist=False)
def initialize(self, create_new=True, sysid=""):
return self.retry(self.client.write, self.initialize_path, sysid, prevExist=(not create_new))
@catch_etcd_errors
def delete_leader(self):
@@ -244,7 +249,7 @@ class Etcd(AbstractDCS):
@catch_etcd_errors
def cancel_initialization(self):
return self.retry(self.client.delete, self.initialize_path, prevValue=self._name)
return self.retry(self.client.delete, self.initialize_path)
def watch(self, timeout):
cluster = self.cluster
+18 -5
View File
@@ -2,6 +2,7 @@ import json
import logging
import psycopg2
import requests
import sys
from patroni.async_executor import AsyncExecutor
from patroni.exceptions import DCSError, PostgresConnectionException
@@ -74,9 +75,10 @@ class Ha:
self._async_executor.run_async(self.copy_backup_from_leader, args=(self.cluster.leader, ))
return 'trying to bootstrap from leader'
elif not self.cluster.initialize: # no initialize key
if self.dcs.initialize(): # race for initialization
if self.dcs.initialize(create_new=True): # race for initialization
try:
self.state_handler.bootstrap()
self.dcs.initialize(create_new=False, sysid=self.state_handler.sysid)
except: # initdb or start failed
# remove initialization key and give a chance to other members
logger.info("removing initialize key after failed attempt to initialize the cluster")
@@ -365,6 +367,11 @@ class Ha:
else:
return self._async_executor.scheduled_action + ' in progress'
def sysid_valid(self, sysid):
# sysid does tv_sec << 32, where tv_sec is the number of seconds sine 1970,
# so even 1 << 32 would have 10 digits.
return str(sysid) and len(str(sysid)) >= 10 and str(sysid).isdigit()
def _run_cycle(self):
try:
self.load_cluster_from_dcs()
@@ -372,8 +379,8 @@ class Ha:
self.touch_member()
# cluster has leader key but not initialize key
if not self.cluster.is_unlocked() and not self.cluster.initialize:
self.dcs.initialize() # fix it
if not self.cluster.is_unlocked() and not self.sysid_valid(self.cluster.initialize) and self.has_lock():
self.dcs.initialize(create_new=(self.cluster.initialize is None), sysid=self.state_handler.sysid)
if self._async_executor.busy:
return self.handle_long_action_in_progress()
@@ -387,8 +394,14 @@ class Ha:
if self.state_handler.data_directory_empty():
return self.bootstrap() # new node
# "bootstrap", but data directory is not empty
elif not self.cluster.initialize and self.cluster.is_unlocked():
self.dcs.initialize()
elif not self.sysid_valid(self.cluster.initialize) and self.cluster.is_unlocked():
self.dcs.initialize(create_new=(self.cluster.initialize is None), sysid=self.state_handler.sysid)
else:
# check if we are allowed to join
if self.sysid_valid(self.cluster.initialize) and self.cluster.initialize != self.state_handler.sysid:
logger.fatal("system ID mismatch, node {0} belongs to a different cluster".
format(self.state_handler.name))
sys.exit(1)
# try to start dead postgres
if not self.state_handler.is_healthy():
+25 -14
View File
@@ -48,6 +48,7 @@ class Postgresql:
self.replication = config['replication']
self.superuser = config['superuser']
self.admin = config['admin']
self.pgpass = config.get('pgpass', None) or os.path.join(os.path.expanduser('~'), 'pgpass')
self.pg_rewind = config.get('pg_rewind', {})
self.callback = config.get('callbacks', {})
self.use_slots = config.get('use_slots', True)
@@ -69,6 +70,7 @@ class Postgresql:
self._connection = None
self._cursor_holder = None
self._need_rewind = False
self._sysid = None
self.replication_slots = [] # list of already existing replication slots
self.retry = Retry(max_tries=-1, deadline=5, max_delay=1, retry_exceptions=PostgresConnectionException)
@@ -100,10 +102,14 @@ class Postgresql:
return False
# check if the cluster's configuration permits pg_rewind
data = self.controldata()
if data:
return data.get('wal_log_hints setting', 'off') == 'on' or\
data.get('Data page checksum version', '0') != '0'
return False
return data.get('wal_log_hints setting', 'off') == 'on' or data.get('Data page checksum version', '0') != '0'
@property
def sysid(self):
if not self._sysid:
data = self.controldata()
self._sysid = data.get('Database system identifier', "")
return self._sysid
def require_rewind(self):
self._need_rewind = True
@@ -171,12 +177,12 @@ class Postgresql:
os.path.exists(self.trigger_file) and os.unlink(self.trigger_file)
def write_pgpass(self, record):
pgpass = 'pgpass'
with open(pgpass, 'w') as f:
with open(self.pgpass, 'w') as f:
os.fchmod(f.fileno(), 0o600)
f.write('{host}:{port}:*:{user}:{password}\n'.format(**record))
env = os.environ.copy()
env['PGPASSFILE'] = pgpass
env['PGPASSFILE'] = self.pgpass
return env
def sync_from_leader(self, leader):
@@ -391,7 +397,7 @@ recovery_target_timeline = 'latest'
try:
data = subprocess.check_output(['pg_controldata', self.data_dir])
if data:
data = data.splitlines()
data = data.decode().splitlines()
result = {l.split(':')[0].replace('Current ', '', 1): l.split(':')[1].strip() for l in data if l}
except subprocess.CalledProcessError:
logger.exception("Error when calling pg_controldata")
@@ -485,19 +491,23 @@ recovery_target_timeline = 'latest'
def save_configuration_files(self):
"""
copy postgresql.conf to postgresql.conf.backup to preserve it in the WAL-e backup.
see http://comments.gmane.org/gmane.comp.db.postgresql.wal-e/239
copy postgresql.conf to postgresql.conf.backup to be able to retrive configuration files
- originally stored as symlinks, those are normally skipped by pg_basebackup
- in case of WAL-E basebackup (see http://comments.gmane.org/gmane.comp.db.postgresql.wal-e/239)
"""
for f in self.configuration_to_save:
shutil.copy(f, f + '.backup')
try:
for f in self.configuration_to_save:
os.path.isfile(f) and shutil.copy(f, f + '.backup')
except:
logger.exception('unable to create backup copies of configuration files')
def restore_configuration_files(self):
""" restore a previously saved postgresql.conf """
try:
for f in self.configuration_to_save:
shutil.copy(f + '.backup', f)
not os.path.isfile(f) and os.path.isfile(f+'.backup') and shutil.copy(f + '.backup', f)
except:
logger.exception('unable to restore configuration from WAL-E backup')
logger.exception('unable to restore configuration files from backup')
def promote(self):
if self.role == 'master':
@@ -585,6 +595,7 @@ recovery_target_timeline = 'latest'
raise PostgresException("Could not bootstrap master PostgreSQL")
else:
if self.sync_from_leader(current_leader):
self.restore_configuration_files()
self.write_recovery_conf(current_leader)
ret = self.start()
return ret
+6 -5
View File
@@ -139,7 +139,7 @@ class ZooKeeper(AbstractDCS):
self.fetch_cluster = True
# get initialize flag
initialize = self._INITIALIZE in nodes
initialize = (self.get_node(self.initialize_path) or [None])[0] if self._INITIALIZE in nodes else None
# get list of members
members = self.load_members() if self._MEMBERS[:-1] in nodes else []
@@ -198,13 +198,14 @@ 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
def initialize(self):
return self._create(self.initialize_path, self._name, makepath=True)
def initialize(self, create_new=True, sysid=""):
return self._create(self.initialize_path, sysid, makepath=True) if create_new \
else self.client.retry(self.client.set, self.initialize_path, sysid.encode("utf-8"))
def touch_member(self, data, ttl=None):
cluster = self.cluster
@@ -270,7 +271,7 @@ class ZooKeeper(AbstractDCS):
def _cancel_initialization(self):
node = self.get_node(self.initialize_path)
if node and node[0] == self._name:
if node:
self.client.delete(self.initialize_path, version=node[1].version)
def cancel_initialization(self):
+1
View File
@@ -34,6 +34,7 @@ postgresql:
data_dir: data/postgresql0
maximum_lag_on_failover: 1048576 # 1 megabyte in bytes
use_slots: True
pgpass: /tmp/pgpass0
pg_rewind:
username: postgres
password: zalando
+1
View File
@@ -34,6 +34,7 @@ postgresql:
data_dir: data/postgresql1
maximum_lag_on_failover: 1048576 # 1 megabyte in bytes
use_slots: True
pgpass: /tmp/pgpass1
pg_rewind:
username: postgres
password: zalando
+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:
@@ -118,3 +122,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)
+14 -6
View File
@@ -80,7 +80,7 @@ def etcd_watch(key, index=None, timeout=None, recursive=None):
def etcd_write(key, value, **kwargs):
if key == '/service/exists/leader':
raise etcd.EtcdAlreadyExist
if key == '/service/test/leader':
if key == '/service/test/leader' or key == '/patroni/test/leader':
if kwargs.get('prevValue', None) == 'foo' or not kwargs.get('prevExist', True):
return True
raise etcd.EtcdException
@@ -106,13 +106,13 @@ def etcd_read(key, **kwargs):
"modifiedIndex": 20437, "createdIndex": 20437},
{"key": "/service/batman5/members", "dir": True, "nodes": [
{"key": "/service/batman5/members/postgresql1",
"value": "postgres://replicator:[email protected]:5434/postgres"
+ "?application_name=http://127.0.0.1:8009/patroni",
"value": "postgres://replicator:[email protected]:5434/postgres" +
"?application_name=http://127.0.0.1:8009/patroni",
"expiration": "2015-05-15T09:10:59.949384522Z", "ttl": 21,
"modifiedIndex": 20727, "createdIndex": 20727},
{"key": "/service/batman5/members/postgresql0",
"value": "postgres://replicator:[email protected]:5433/postgres"
+ "?application_name=http://127.0.0.1:8008/patroni",
"value": "postgres://replicator:[email protected]:5433/postgres" +
"?application_name=http://127.0.0.1:8008/patroni",
"expiration": "2015-05-15T09:11:09.611860899Z", "ttl": 30,
"modifiedIndex": 20730, "createdIndex": 20730}],
"modifiedIndex": 1581, "createdIndex": 1581}], "modifiedIndex": 1581, "createdIndex": 1581}}
@@ -165,6 +165,11 @@ class TestClient(unittest.TestCase):
self.client._base_uri = 'http://localhost:4001'
self.client._machines_cache = ['http://localhost:2379']
self.client.api_execute('/', 'GET')
self.client._update_machines_cache = False
self.client._base_uri = 'http://localhost:4001'
self.client._machines_cache = []
self.assertRaises(etcd.EtcdConnectionFailed, self.client.api_execute, '/', 'GET')
self.assertTrue(self.client._update_machines_cache)
def test_get_srv_record(self):
self.assertEquals(self.client.get_srv_record('blabla'), [])
@@ -199,11 +204,14 @@ class TestEtcd(unittest.TestCase):
def setUp(self):
with patch.object(Client, 'machines') as mock_machines:
mock_machines.__get__ = Mock(return_value=['http://localhost:2379', 'http://localhost:4001'])
self.etcd = Etcd('foo', {'ttl': 30, 'host': 'localhost:2379', 'scope': 'test'})
self.etcd = Etcd('foo', {'namespace': '/patroni/', 'ttl': 30, 'host': 'localhost:2379', 'scope': 'test'})
self.etcd.client.write = etcd_write
self.etcd.client.read = etcd_read
self.etcd.client.delete = Mock(side_effect=etcd.EtcdException())
def test_base_path(self):
self.assertEquals(self.etcd._base_path, '/patroni/test')
@patch('dns.resolver.query', dns_query)
def test_get_etcd_client(self):
with patch.object(etcd.Client, 'machines') as mock_machines:
+7 -1
View File
@@ -1,7 +1,7 @@
import etcd
import unittest
from mock import Mock, patch
from mock import Mock, MagicMock, patch
from patroni.dcs import Cluster, Failover, Leader, Member
from patroni.etcd import Client, Etcd
from patroni.exceptions import DCSError, PostgresException
@@ -132,6 +132,12 @@ class TestHa(unittest.TestCase):
self.ha.has_lock = true
self.assertEquals(self.ha.run_cycle(), 'removed leader key after trying and failing to start postgres')
@patch('sys.exit', return_value=1)
@patch('patroni.ha.Ha.sysid_valid', MagicMock(return_value=True))
def test_sysid_no_match(self, exit_mock):
self.ha.run_cycle()
exit_mock.assert_called_once_with(1)
@patch.object(Cluster, 'is_unlocked', Mock(return_value=False))
def test_start_as_readonly(self):
self.p.is_leader = self.p.is_healthy = false
+35 -11
View File
@@ -4,12 +4,7 @@ import psycopg2
import shutil
import unittest
from sys import version_info
if version_info.major == 2:
import __builtin__ as builtins
else:
import builtins
from six.moves import builtins
from mock import Mock, MagicMock, PropertyMock, patch, mock_open
from patroni.dcs import Cluster, Leader, Member
from patroni.exceptions import PostgresException, PostgresConnectionException
@@ -19,6 +14,11 @@ from test_ha import false
import subprocess
def is_file_raise_on_backup(*args, **kwargs):
if args[0].endswith('.backup'):
raise Exception("foo")
class MockCursor:
def __init__(self, connection):
@@ -86,7 +86,7 @@ class MockConnect(Mock):
def pg_controldata_string(*args, **kwargs):
return """
return b"""
pg_control version number: 942
Catalog version number: 201509161
Database system identifier: 6200971513092291716
@@ -141,10 +141,10 @@ Data page checksum version: 0
def postmaster_opts_string(*args, **kwargs):
return '/usr/local/pgsql/bin/postgres "-D" "data/postgresql0" "--listen_addresses=127.0.0.1" "--port=5432"'\
' "--hot_standby=on" "--wal_keep_segments=8" "--wal_level=hot_standby" "--archive_command=mkdir -p ../wal_archive \n'\
'&& cp %p ../wal_archive/%f" "--wal_log_hints=on" "--max_wal_senders=5" "--archive_timeout=1800s" "--archive_mode=on"'\
' "--max_replication_slots=5"\n'
return '/usr/local/pgsql/bin/postgres "-D" "data/postgresql0" "--listen_addresses=127.0.0.1" \
"--port=5432" "--hot_standby=on" "--wal_keep_segments=8" "--wal_level=hot_standby" \
"--archive_command=mkdir -p ../wal_archive && cp %p ../wal_archive/%f" "--wal_log_hints=on" \
"--max_wal_senders=5" "--archive_timeout=1800s" "--archive_mode=on" "--max_replication_slots=5"\n'
def psycopg2_connect(*args, **kwargs):
@@ -210,10 +210,16 @@ class TestPostgresql(unittest.TestCase):
self.assertFalse(self.p.restart())
self.assertEquals(self.p.state, 'restart failed (restarting)')
@patch.object(builtins, 'open', MagicMock())
def test_write_pgpass(self):
self.p.write_pgpass({'host': 'localhost', 'port': '5432', 'user': 'foo', 'password': 'bar'})
@patch('patroni.postgresql.Postgresql.write_pgpass', MagicMock(return_value=dict()))
def test_sync_from_leader(self):
self.assertTrue(self.p.sync_from_leader(self.leader))
@patch('subprocess.call', side_effect=Exception("Test"))
@patch('patroni.postgresql.Postgresql.write_pgpass', MagicMock(return_value=dict()))
def test_pg_rewind(self, mock_call):
self.assertTrue(self.p.rewind(self.leader))
subprocess.call = mock_call
@@ -222,6 +228,7 @@ class TestPostgresql(unittest.TestCase):
@patch('patroni.postgresql.Postgresql.rewind', return_value=False)
@patch('patroni.postgresql.Postgresql.remove_data_directory', MagicMock(return_value=True))
@patch('patroni.postgresql.Postgresql.single_user_mode', MagicMock(return_value=1))
@patch('patroni.postgresql.Postgresql.write_pgpass', MagicMock(return_value=dict()))
def test_follow_the_leader(self, mock_pg_rewind):
self.p.demote()
self.p.follow_the_leader(None)
@@ -327,6 +334,7 @@ class TestPostgresql(unittest.TestCase):
with patch('os.rename', Mock(side_effect=OSError())):
self.p.move_data_directory()
@patch('patroni.postgresql.Postgresql.write_pgpass', MagicMock(return_value=dict()))
def test_bootstrap(self):
with patch('subprocess.call', Mock(return_value=1)):
self.assertRaises(PostgresException, self.p.bootstrap)
@@ -429,3 +437,19 @@ class TestPostgresql(unittest.TestCase):
self.p.cleanup_archive_status()
mock_unlink.assert_not_called()
mock_remove.assert_not_called()
@patch('subprocess.check_output', MagicMock(return_value=0, side_effect=pg_controldata_string))
def test_sysid(self):
self.assertEqual(self.p.sysid, "6200971513092291716")
@patch('os.path.isfile', MagicMock(return_value=True))
@patch('shutil.copy', side_effect=Exception)
def test_save_configuration_files(self, mock_copy):
shutil.copy = mock_copy
self.p.save_configuration_files()
@patch('os.path.isfile', MagicMock(side_effect=is_file_raise_on_backup))
@patch('shutil.copy', side_effect=Exception)
def test_restore_configuration_files(self, mock_copy):
shutil.copy = mock_copy
self.p.restore_configuration_files()
+3 -1
View File
@@ -67,7 +67,9 @@ class TestRetrySleeper(unittest.TestCase):
self.assertRaises(RetryFailedError, retry, self._fail(times=100))
def test_copy(self):
_sleep = lambda t: None
def _sleep(t):
None
retry = self._makeOne(sleep_func=_sleep)
rcopy = retry.copy()
self.assertTrue(rcopy.sleep_func is _sleep)