Merge pull request #196 from zalando/feature/write-options-to-postgresql.conf

Write postgres options to postgresql.conf (except listen_addresses and port)
This commit is contained in:
Alexander Kukushkin
2016-05-13 13:29:23 +02:00
3 changed files with 117 additions and 145 deletions
+83 -93
View File
@@ -44,10 +44,11 @@ class Postgresql(object):
def __init__(self, config):
self.config = config
self.name = config['name']
self.server_parameters = config.get('parameters') or {}
self._server_parameters = self.get_server_parameters(config)
self._listen_addresses, self._port = (config['listen'] + ':5432').split(':')[:2]
self.scope = config['scope']
self.listen_addresses, self.port = config['listen'].split(':')
self.data_dir = config['data_dir']
self._data_dir = config['data_dir']
self.replication = config['replication']
self.superuser = config.get('superuser') or {}
self.admin = config.get('admin') or {}
@@ -57,15 +58,18 @@ class Postgresql(object):
self.pg_rewind = config.get('pg_rewind') or {}
self.callback = config.get('callbacks') or {}
self.use_slots = config.get('use_slots', True)
self.schedule_load_slots = self.use_slots
self.recovery_conf = os.path.join(self.data_dir, 'recovery.conf')
self.configuration_to_save = (os.path.join(self.data_dir, 'pg_hba.conf'),
os.path.join(self.data_dir, 'postgresql.conf'))
self.postmaster_pid = os.path.join(self.data_dir, 'postmaster.pid')
self.trigger_file = config.get('recovery_conf', {}).get('trigger_file') or 'promote'
self.trigger_file = os.path.abspath(os.path.join(self.data_dir, self.trigger_file))
self._schedule_load_slots = self.use_slots
self._postgresql_conf = os.path.join(self._data_dir, 'postgresql.conf')
self._postgresql_base_conf_name = 'postgresql.base.conf'
self._postgresql_base_conf = os.path.join(self._data_dir, self._postgresql_base_conf_name)
self._recovery_conf = os.path.join(self._data_dir, 'recovery.conf')
self._configuration_to_save = (self._postgresql_conf, self._postgresql_base_conf,
os.path.join(self._data_dir, 'pg_hba.conf'))
self._postmaster_pid = os.path.join(self._data_dir, 'postmaster.pid')
self._trigger_file = config.get('recovery_conf', {}).get('trigger_file') or 'promote'
self._trigger_file = os.path.abspath(os.path.join(self._data_dir, self._trigger_file))
self._pg_ctl = ['pg_ctl', '-w', '-D', self.data_dir]
self._pg_ctl = ['pg_ctl', '-w', '-D', self._data_dir]
self.local_address = self.get_local_address()
connect_address = config.get('connect_address') or self.local_address
@@ -75,17 +79,22 @@ class Postgresql(object):
self._connection = None
self._cursor_holder = None
self._sysid = None
self.replication_slots = [] # list of already existing replication slots
self._replication_slots = [] # list of already existing replication slots
self.retry = Retry(max_tries=-1, deadline=5, max_delay=1, retry_exceptions=PostgresConnectionException)
self._state = 'stopped'
self._state_lock = Lock()
self._role = self.get_postgres_role_from_data_directory()
self.set_state('stopped')
self._role_lock = Lock()
self.set_role(self.get_postgres_role_from_data_directory())
if self.is_running():
self._state = 'running'
self._role = 'master' if self.is_leader() else 'replica'
self.set_state('running')
self.set_role('master' if self.is_leader() else 'replica')
self._write_postgresql_conf() # we are "joining" already running postgres
@staticmethod
def get_server_parameters(config):
return {p: v for p, v in (config.get('parameters') or {}).items() if p not in ('listen_addresses', 'port')}
@property
def can_rewind(self):
@@ -116,19 +125,19 @@ class Postgresql(object):
return self._sysid
def get_local_address(self):
listen_addresses = self.listen_addresses.split(',')
listen_addresses = self._listen_addresses.split(',')
local_address = listen_addresses[0].strip() # take first address from listen_addresses
for la in listen_addresses:
if la.strip() in ['*', '0.0.0.0']: # we are listening on *
local_address = 'localhost' # connection via localhost is preferred
break
return local_address + ':' + self.port
return local_address + ':' + self._port
def get_postgres_role_from_data_directory(self):
if self.data_directory_empty():
return 'uninitialized'
elif os.path.exists(self.recovery_conf):
elif os.path.exists(self._recovery_conf):
return 'replica'
else:
return 'master'
@@ -180,7 +189,7 @@ class Postgresql(object):
raise PostgresConnectionException(str(e))
def data_directory_empty(self):
return not os.path.exists(self.data_dir) or os.listdir(self.data_dir) == []
return not os.path.exists(self._data_dir) or os.listdir(self._data_dir) == []
@staticmethod
def initdb_allowed_option(name):
@@ -226,8 +235,8 @@ class Postgresql(object):
return ret
def delete_trigger_file(self):
if os.path.exists(self.trigger_file):
os.unlink(self.trigger_file)
if os.path.exists(self._trigger_file):
os.unlink(self._trigger_file)
def write_pgpass(self, record):
with open(self.pgpass, 'w') as f:
@@ -238,14 +247,6 @@ class Postgresql(object):
env['PGPASSFILE'] = self.pgpass
return env
@staticmethod
def build_connstring(conn):
"""
>>> Postgresql.build_connstring({'host': '127.0.0.1', 'port': '5432'}) == 'host=127.0.0.1 port=5432'
True
"""
return ' '.join('{0}={1}'.format(param, val) for param, val in sorted(conn.items()))
def replica_method_can_work_without_replication_connection(self, method):
return method != 'basebackup' and self.config and self.config.get(method, {}).get('no_master')
@@ -305,7 +306,7 @@ class Postgresql(object):
try:
method_config.update({"scope": self.scope,
"role": "replica",
"datadir": self.data_dir,
"datadir": self._data_dir,
"connstring": connstring})
params = ["--{0}={1}".format(arg, val) for arg, val in method_config.items()]
# call script with the full set of parameters
@@ -363,9 +364,9 @@ class Postgresql(object):
return True
self.set_role(self.get_postgres_role_from_data_directory())
if os.path.exists(self.postmaster_pid):
os.remove(self.postmaster_pid)
logger.info('Removed %s', self.postmaster_pid)
if os.path.exists(self._postmaster_pid):
os.remove(self._postmaster_pid)
logger.info('Removed %s', self._postmaster_pid)
if not block_callbacks:
self.set_state('starting')
@@ -374,11 +375,13 @@ class Postgresql(object):
# pg_ctl will write a FATAL if the username is incorrect. exporting PGUSER if necessary
if 'username' in self.superuser and self.superuser['username'] != os.environ.get('USER'):
env['PGUSER'] = self.superuser['username']
ret = subprocess.call(self._pg_ctl + ['start', '-o', self.server_options()], env=env, preexec_fn=os.setsid) == 0
self._write_postgresql_conf()
server_arguments = ['-o', "--listen_addresses='{0}' --port={1}".format(self._listen_addresses, self._port)]
ret = subprocess.call(self._pg_ctl + ['start'] + server_arguments, env=env, preexec_fn=os.setsid) == 0
self.set_state('running' if ret else 'start failed')
self.schedule_load_slots = ret and self.use_slots
self._schedule_load_slots = ret and self.use_slots
self.save_configuration_files()
# block_callbacks is used during restart to avoid
# running start/stop callbacks in addition to restart ones
@@ -442,11 +445,16 @@ class Postgresql(object):
self.set_state('restart failed ({0})'.format(self.state))
return ret
def server_options(self):
options = "--listen_addresses='{0}' --port={1}".format(self.listen_addresses, self.port)
for setting, value in self.server_parameters.items():
options += " --{0}='{1}'".format(setting, value)
return options
def _write_postgresql_conf(self):
# rename the original configuration if it is necessary
if not os.path.exists(self._postgresql_base_conf):
os.rename(self._postgresql_conf, self._postgresql_base_conf)
with open(self._postgresql_conf, 'w') as f:
f.write('# Do not edit this file manually!\n# It will be overwritten by Patroni!\n')
f.write("include '{0}'\n\n".format(self._postgresql_base_conf_name))
for setting, value in sorted(self._server_parameters.items()):
f.write("{0} = '{1}'\n".format(setting, value))
def is_healthy(self):
if not self.is_running():
@@ -458,7 +466,7 @@ class Postgresql(object):
return (last_leader_operation or 0) - self.xlog_position() <= self.config.get('maximum_lag_on_failover', 0)
def write_pg_hba(self):
with open(os.path.join(self.data_dir, 'pg_hba.conf'), 'a') as f:
with open(os.path.join(self._data_dir, 'pg_hba.conf'), 'a') as f:
f.write('\n{}\n'.format('\n'.join(self.config.get('pg_hba', []))))
def primary_conninfo(self, leader_url):
@@ -468,19 +476,19 @@ class Postgresql(object):
return ' '.join('{0}={{{0}}}'.format(kw) for kw in keywords).format(**r)
def check_recovery_conf(self, leader):
if not os.path.isfile(self.recovery_conf):
if not os.path.isfile(self._recovery_conf):
return False
pattern = leader and leader.conn_url and self.primary_conninfo(leader.conn_url)
with open(self.recovery_conf, 'r') as f:
with open(self._recovery_conf, 'r') as f:
for line in f:
if line.startswith('primary_conninfo'):
return pattern and (pattern in line)
return not pattern
def write_recovery_conf(self, leader):
with open(self.recovery_conf, 'w') as f:
with open(self._recovery_conf, 'w') as f:
f.write("standby_mode = 'on'\nrecovery_target_timeline = 'latest'\n")
if leader and leader.conn_url:
f.write("primary_conninfo = '{0}'\n".format(self.primary_conninfo(leader.conn_url)))
@@ -501,7 +509,7 @@ class Postgresql(object):
# to make it store the new timeline ([email protected])
self.checkpoint(r)
logger.info("running pg_rewind from %s", pc)
pg_rewind = ['pg_rewind', '-D', self.data_dir, '--source-server', pc]
pg_rewind = ['pg_rewind', '-D', self._data_dir, '--source-server', pc]
try:
ret = subprocess.call(pg_rewind, env=env) == 0
except OSError:
@@ -515,7 +523,7 @@ class Postgresql(object):
result = {}
if self.state != 'creating replica': # Don't try to call pg_controldata during backup restore
try:
data = subprocess.check_output(['pg_controldata', self.data_dir])
data = subprocess.check_output(['pg_controldata', self._data_dir])
if data:
data = data.decode('utf-8').splitlines()
result = {l.split(':')[0].replace('Current ', '', 1): l.split(':')[1].strip() for l in data if l}
@@ -523,25 +531,9 @@ class Postgresql(object):
logger.exception("Error when calling pg_controldata")
return result
def read_postmaster_opts(self):
""" returns the list of option names/values from postgres.opts, Empty dict if read failed or no file """
result = {}
try:
with open(os.path.join(self.data_dir, "postmaster.opts")) as f:
data = f.read()
opts = [opt.strip('"\n') for opt in data.split(' "')]
for opt in opts:
if '=' in opt and opt.startswith('--'):
name, val = opt.split('=', 1)
name = name.strip('-')
result[name] = val
except IOError:
logger.exception('Error when reading postmaster.opts')
return result
def single_user_mode(self, command=None, options=None):
""" run a given command in a single-user mode. If the command is empty - then just start and stop """
cmd = ['postgres', '--single', '-D', self.data_dir]
cmd = ['postgres', '--single', '-D', self._data_dir]
for opt, val in sorted((options or {}).items()):
cmd.extend(['-c', '{0}={1}'.format(opt, val)])
# need a database name to connect
@@ -555,7 +547,7 @@ class Postgresql(object):
return 1
def cleanup_archive_status(self):
status_dir = os.path.join(self.data_dir, 'pg_xlog', 'archive_status')
status_dir = os.path.join(self._data_dir, 'pg_xlog', 'archive_status')
try:
for f in os.listdir(status_dir):
path = os.path.join(status_dir, f)
@@ -584,17 +576,15 @@ class Postgresql(object):
# and not shutdown in recovery. We have to remove the recovery.conf if present
# and start/shutdown in a single user mode to emulate this.
# XXX: if recovery.conf is linked, it will be written anew as a normal file.
if os.path.islink(self.recovery_conf):
os.unlink(self.recovery_conf)
if os.path.islink(self._recovery_conf):
os.unlink(self._recovery_conf)
else:
os.remove(self.recovery_conf)
os.remove(self._recovery_conf)
# Archived segments might be useful to pg_rewind,
# clean the flags that tell we should remove them.
self.cleanup_archive_status()
# Start in a single user mode and stop to produce a clean shutdown
opts = self.read_postmaster_opts()
opts.update({'archive_mode': 'on', 'archive_command': 'false'})
self.single_user_mode(options=opts)
self.single_user_mode(options={'archive_mode': 'on', 'archive_command': 'false'})
if self.rewind(leader):
ret = self.start()
else:
@@ -614,7 +604,7 @@ class Postgresql(object):
- in case of WAL-E basebackup (see http://comments.gmane.org/gmane.comp.db.postgresql.wal-e/239)
"""
try:
for f in self.configuration_to_save:
for f in self._configuration_to_save:
if os.path.isfile(f):
shutil.copy(f, f + '.backup')
except IOError:
@@ -623,7 +613,7 @@ class Postgresql(object):
def restore_configuration_files(self):
""" restore a previously saved postgresql.conf """
try:
for f in self.configuration_to_save:
for f in self._configuration_to_save:
if not os.path.isfile(f) and os.path.isfile(f + '.backup'):
shutil.copy(f + '.backup', f)
except IOError:
@@ -666,10 +656,10 @@ $$""".format(name, options), name, password, password)
END, '0/0')::bigint""").fetchone()[0]
def load_replication_slots(self):
if self.use_slots and self.schedule_load_slots:
if self.use_slots and self._schedule_load_slots:
cursor = self.query("SELECT slot_name FROM pg_replication_slots WHERE slot_type='physical'")
self.replication_slots = [r[0] for r in cursor]
self.schedule_load_slots = False
self._replication_slots = [r[0] for r in cursor]
self._schedule_load_slots = False
def sync_replication_slots(self, cluster):
if self.use_slots:
@@ -688,18 +678,18 @@ $$""".format(name, options), name, password, password)
slots = [m.name for m in cluster.members if m.replicatefrom == self.name and
m.name != cluster.leader.name]
# drop unused slots
for slot in set(self.replication_slots) - set(slots):
for slot in set(self._replication_slots) - set(slots):
self.query("""SELECT pg_drop_replication_slot(%s)
WHERE EXISTS(SELECT 1 FROM pg_replication_slots
WHERE slot_name = %s)""", slot, slot)
# create new slots
for slot in set(slots) - set(self.replication_slots):
for slot in set(slots) - set(self._replication_slots):
self.query("""SELECT pg_create_physical_replication_slot(%s)
WHERE NOT EXISTS (SELECT 1 FROM pg_replication_slots
WHERE slot_name = %s)""", slot, slot)
self.replication_slots = slots
self._replication_slots = slots
except psycopg2.Error:
logger.exception('Exception when changing replication slots')
@@ -729,27 +719,27 @@ $$""".format(name, options), name, password, password)
raise PostgresException("Could not bootstrap master PostgreSQL")
def move_data_directory(self):
if os.path.isdir(self.data_dir) and not self.is_running():
if os.path.isdir(self._data_dir) and not self.is_running():
try:
new_name = '{0}_{1}'.format(self.data_dir, time.strftime('%Y-%m-%d-%H-%M-%S'))
new_name = '{0}_{1}'.format(self._data_dir, time.strftime('%Y-%m-%d-%H-%M-%S'))
logger.info('renaming data directory to %s', new_name)
os.rename(self.data_dir, new_name)
os.rename(self._data_dir, new_name)
except OSError:
logger.exception("Could not rename data directory %s", self.data_dir)
logger.exception("Could not rename data directory %s", self._data_dir)
def remove_data_directory(self):
logger.info('Removing data directory: %s', self.data_dir)
logger.info('Removing data directory: %s', self._data_dir)
try:
if os.path.islink(self.data_dir):
os.unlink(self.data_dir)
elif not os.path.exists(self.data_dir):
if os.path.islink(self._data_dir):
os.unlink(self._data_dir)
elif not os.path.exists(self._data_dir):
return
elif os.path.isfile(self.data_dir):
os.remove(self.data_dir)
elif os.path.isdir(self.data_dir):
shutil.rmtree(self.data_dir)
elif os.path.isfile(self._data_dir):
os.remove(self._data_dir)
elif os.path.isdir(self._data_dir):
shutil.rmtree(self._data_dir)
except (IOError, OSError):
logger.exception('Could not remove data directory %s', self.data_dir)
logger.exception('Could not remove data directory %s', self._data_dir)
self.move_data_directory()
def basebackup(self, conn_url, env):
@@ -761,7 +751,7 @@ $$""".format(name, options), name, password, password)
ret = 1
for bbfailures in range(0, maxfailures):
try:
ret = subprocess.call(['pg_basebackup', '--pgdata=' + self.data_dir,
ret = subprocess.call(['pg_basebackup', '--pgdata=' + self._data_dir,
'--xlog-method=stream', "--dbname=" + conn_url], env=env)
if ret == 0:
break
+16 -15
View File
@@ -52,22 +52,23 @@ class TestPatroni(unittest.TestCase):
@patch.object(Etcd, 'delete_leader', Mock())
@patch.object(Client, 'machines')
def test_patroni_main(self, mock_machines):
_main()
sys.argv = ['patroni.py', 'postgres0.yml']
mock_machines.__get__ = Mock(return_value=['http://remotehost:2379'])
with patch.object(Patroni, 'run', Mock(side_effect=SleepException)):
self.assertRaises(SleepException, _main)
with patch.object(Patroni, 'run', Mock(side_effect=KeyboardInterrupt())):
with patch('subprocess.call', Mock(return_value=1)):
_main()
sys.argv = ['patroni.py']
# read the content of the yaml configuration file into the environment variable
# in order to test how does patroni handle the configuration passed from the environment.
with open('postgres0.yml', 'r') as f:
os.environ[Patroni.PATRONI_CONFIG_VARIABLE] = f.read()
with patch.object(Patroni, 'run', Mock(side_effect=SleepException())):
self.assertRaises(SleepException, _main)
del os.environ[Patroni.PATRONI_CONFIG_VARIABLE]
sys.argv = ['patroni.py', 'postgres0.yml']
mock_machines.__get__ = Mock(return_value=['http://remotehost:2379'])
with patch.object(Patroni, 'run', Mock(side_effect=SleepException)):
self.assertRaises(SleepException, _main)
with patch.object(Patroni, 'run', Mock(side_effect=KeyboardInterrupt())):
_main()
sys.argv = ['patroni.py']
# read the content of the yaml configuration file into the environment variable
# in order to test how does patroni handle the configuration passed from the environment.
with open('postgres0.yml', 'r') as f:
os.environ[Patroni.PATRONI_CONFIG_VARIABLE] = f.read()
with patch.object(Patroni, 'run', Mock(side_effect=SleepException())):
self.assertRaises(SleepException, _main)
del os.environ[Patroni.PATRONI_CONFIG_VARIABLE]
def test_run(self):
self.p.ha.dcs.watch = Mock(side_effect=SleepException)
+18 -37
View File
@@ -5,7 +5,7 @@ import shutil
import subprocess
import unittest
from mock import Mock, MagicMock, PropertyMock, patch, mock_open
from mock import Mock, MagicMock, PropertyMock, patch
from patroni.dcs import Cluster, Leader, Member
from patroni.exceptions import PostgresException, PostgresConnectionException
from patroni.postgresql import Postgresql
@@ -140,13 +140,6 @@ 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 && 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):
return MockConnect()
@@ -161,8 +154,12 @@ class TestPostgresql(unittest.TestCase):
@patch('subprocess.call', Mock(return_value=0))
@patch('psycopg2.connect', psycopg2_connect)
@patch('os.rename', Mock())
def setUp(self):
self.p = Postgresql({'name': 'test0', 'scope': 'batman', 'data_dir': 'data/test0',
self.data_dir = 'data/test0'
if not os.path.exists(self.data_dir):
os.makedirs(self.data_dir)
self.p = Postgresql({'name': 'test0', 'scope': 'batman', 'data_dir': self.data_dir,
'listen': '127.0.0.1, *:5432', 'connect_address': '127.0.0.2:5432',
'pg_hba': ['host replication replicator 127.0.0.1/32 md5',
'hostssl all all 0.0.0.0/0 md5',
@@ -178,8 +175,6 @@ class TestPostgresql(unittest.TestCase):
'on_reload': 'true'
},
'restore': 'true'})
if not os.path.exists(self.p.data_dir):
os.makedirs(self.p.data_dir)
self.leadermem = Member(0, 'leader', 28, {'conn_url': 'postgres://replicator:[email protected]:5435/postgres'})
self.leader = Leader(-1, 28, self.leadermem)
self.other = Member(0, 'test1', 28, {'conn_url': 'postgres://replicator:[email protected]:5433/postgres',
@@ -189,9 +184,6 @@ class TestPostgresql(unittest.TestCase):
def tearDown(self):
shutil.rmtree('data')
def test_data_directory_empty(self):
self.assertTrue(self.p.data_directory_empty())
def test_get_initdb_options(self):
self.p.initdb_options = [{'encoding': 'UTF8'}, 'data-checksums']
self.assertEquals(self.p.get_initdb_options(), ['--encoding=UTF8', '--data-checksums'])
@@ -204,9 +196,8 @@ class TestPostgresql(unittest.TestCase):
def test_initialize(self):
self.assertTrue(self.p.initialize())
self.assertTrue(os.path.exists(os.path.join(self.p.data_dir, 'pg_hba.conf')))
with open(os.path.join(self.p.data_dir, 'pg_hba.conf')) as f:
with open(os.path.join(self.data_dir, 'pg_hba.conf')) as f:
lines = f.readlines()
assert 'host replication replicator 127.0.0.1/32 md5\n' in lines
assert 'host all all 0.0.0.0/0 md5\n' in lines
@@ -219,8 +210,13 @@ class TestPostgresql(unittest.TestCase):
def test_start(self):
self.assertTrue(self.p.start())
self.p.is_running = false
open(os.path.join(self.p.data_dir, 'postmaster.pid'), 'w').close()
open(os.path.join(self.data_dir, 'postmaster.pid'), 'w').close()
pg_conf = os.path.join(self.data_dir, 'postgresql.conf')
open(pg_conf, 'w').close()
self.assertTrue(self.p.start())
with open(pg_conf) as f:
lines = f.readlines()
self.assertTrue("foo = 'bar'\n" in lines)
def test_stop(self):
self.assertTrue(self.p.stop())
@@ -381,13 +377,10 @@ class TestPostgresql(unittest.TestCase):
self.assertEquals(self.p.get_postgres_role_from_data_directory(), 'replica')
def test_remove_data_directory(self):
self.p.data_dir = 'data_dir'
self.p.remove_data_directory()
os.mkdir(self.p.data_dir)
open(self.data_dir, 'w').close()
self.p.remove_data_directory()
open(self.p.data_dir, 'w').close()
self.p.remove_data_directory()
os.symlink('unexisting', self.p.data_dir)
os.symlink('unexisting', self.data_dir)
with patch('os.unlink', Mock(side_effect=OSError)):
self.p.remove_data_directory()
self.p.remove_data_directory()
@@ -403,31 +396,19 @@ class TestPostgresql(unittest.TestCase):
with patch('subprocess.check_output', Mock(side_effect=subprocess.CalledProcessError(1, ''))):
self.assertEquals(self.p.controldata(), {})
def test_read_postmaster_opts(self):
m = mock_open(read_data=postmaster_opts_string())
with patch.object(builtins, 'open', m):
data = self.p.read_postmaster_opts()
self.assertEquals(data['wal_level'], 'hot_standby')
self.assertEquals(int(data['max_replication_slots']), 5)
self.assertEqual(data.get('D'), None)
m.side_effect = IOError
data = self.p.read_postmaster_opts()
self.assertEqual(data, dict())
@patch('subprocess.Popen')
@patch.object(builtins, 'open', MagicMock(return_value=42))
def test_single_user_mode(self, subprocess_popen_mock):
subprocess_popen_mock.return_value.wait.return_value = 0
self.assertEquals(self.p.single_user_mode(options=dict(archive_mode='on', archive_command='false')), 0)
subprocess_popen_mock.assert_called_once_with(['postgres', '--single', '-D', self.p.data_dir,
subprocess_popen_mock.assert_called_once_with(['postgres', '--single', '-D', self.data_dir,
'-c', 'archive_command=false', '-c', 'archive_mode=on',
'postgres'], stdin=subprocess.PIPE,
stdout=42,
stderr=subprocess.STDOUT)
subprocess_popen_mock.reset_mock()
self.assertEquals(self.p.single_user_mode(command="CHECKPOINT"), 0)
subprocess_popen_mock.assert_called_once_with(['postgres', '--single', '-D', self.p.data_dir,
subprocess_popen_mock.assert_called_once_with(['postgres', '--single', '-D', self.data_dir,
'postgres'], stdin=subprocess.PIPE,
stdout=42,
stderr=subprocess.STDOUT)
@@ -440,7 +421,7 @@ class TestPostgresql(unittest.TestCase):
@patch('os.path.islink', return_value=False)
@patch('os.path.isfile', return_value=True)
def test_cleanup_archive_status(self, mock_file, mock_link, mock_remove, mock_unlink):
ap = os.path.join(self.p.data_dir, 'pg_xlog', 'archive_status/')
ap = os.path.join(self.data_dir, 'pg_xlog', 'archive_status/')
self.p.cleanup_archive_status()
mock_remove.assert_has_calls([mock.call(ap+'a'), mock.call(ap+'b'), mock.call(ap+'c')])
mock_unlink.assert_not_called()