Merge pull request #112 from zalando/feature/patronictl_password

Feature/patronictl password
This commit is contained in:
Feike Steenbergen
2016-02-03 17:03:49 +01:00
6 changed files with 70 additions and 28 deletions
+1
View File
@@ -20,6 +20,7 @@ RUN pip install python-etcd psycopg2
ENV PATH /usr/lib/postgresql/${PGVERSION}/bin:$PATH
ADD patroni.py /patroni.py
ADD patronictl.py /patronictl.py
ADD patroni/ /patroni
ENV ETCDVERSION 2.0.13
+8 -3
View File
@@ -79,7 +79,12 @@ then
ETCD_CLUSTER="127.0.0.1:4001"
fi
cat > /patroni/postgres.yml <<__EOF__
mkdir -p ~postgres/.config/patroni
cat > ~postgres/.config/patroni/patronictl.yaml <<__EOF__
{dcs_api: 'etcd://${ETCD_CLUSTER}', namespace: /service/}
__EOF__
cat > /patroni/postgres.yaml <<__EOF__
ttl: &ttl 30
loop_wait: &loop_wait 10
@@ -126,7 +131,7 @@ postgresql:
hot_standby: "on"
__EOF__
cat /patroni/postgres.yml
cat /patroni/postgres.yaml
if [ ! -z $CHEAT ]
then
@@ -135,5 +140,5 @@ then
sleep 60
done
else
exec python /patroni.py /patroni/postgres.yml
exec python /patroni.py /patroni/postgres.yaml
fi
+29 -5
View File
@@ -168,7 +168,9 @@ def watching(w, watch, max_count=None, clear=True):
yield 0
def build_connect_parameters(conn_url, connect_parameters={}):
def build_connect_parameters(conn_url, connect_parameters=None):
if connect_parameters is None:
connect_parameters = {}
params = connect_parameters.copy()
parsed = parseurl(conn_url)
params['host'] = parsed['host']
@@ -200,7 +202,9 @@ def get_any_member(cluster, role='master', member=None):
return None
def get_cursor(cluster, role='master', member=None, connect_parameters={}):
def get_cursor(cluster, role='master', member=None, connect_parameters=None):
if connect_parameters is None:
connect_parameters = {}
member = get_any_member(cluster=cluster, role=role, member=member)
if member is None:
return None
@@ -252,6 +256,8 @@ def dsn(cluster_name, config_file, dcs, role, member):
@option_format
@click.option('--format', help='Output format (pretty, json)', default='tsv')
@click.option('--file', '-f', help='Execute the SQL commands from this file', type=click.File('rb'))
@click.option('--password', help='force password prompt', is_flag=True)
@click.option('-U', '--username', help='database user name', type=str)
@option_dcs
@option_watch
@option_watchrefresh
@@ -260,6 +266,7 @@ def dsn(cluster_name, config_file, dcs, role, member):
@click.option('--member', '-m', help='Query a specific member', type=str)
@click.option('--delimiter', help='The column delimiter', default='\t')
@click.option('--command', '-c', help='The SQL commands to execute')
@click.option('-d', '--dbname', help='database name to connect to', type=str)
def query(
cluster_name,
config_file,
@@ -271,6 +278,9 @@ def query(
delimiter,
command,
file,
password,
username,
dbname,
format='tsv',
):
if role is not None and member is not None:
@@ -281,6 +291,17 @@ def query(
if file is not None and command is not None:
raise PatroniCtlException('--file and --command are mutually exclusive options')
if file is None and command is None:
raise PatroniCtlException('You need to specify either --command or --file')
connect_parameters = dict()
if username:
connect_parameters['user'] = username
if password:
connect_parameters['password'] = click.prompt('Password', hide_input=True, type=str)
if dbname:
connect_parameters['database'] = dbname
if file is not None:
command = file.read()
@@ -289,17 +310,20 @@ def query(
cursor = None
for _ in watching(w, watch, clear=False):
output, cursor = query_member(cluster=cluster, cursor=cursor, member=member, role=role, command=command)
output, cursor = query_member(cluster=cluster, cursor=cursor, member=member, role=role, command=command,
connect_parameters=connect_parameters)
print_output(None, output, format=format, delimiter=delimiter)
if cursor is None:
cluster = dcs.get_cluster()
def query_member(cluster, cursor, member, role, command):
def query_member(cluster, cursor, member, role, command, connect_parameters=None):
if connect_parameters is None:
connect_parameters = {}
try:
if cursor is None:
cursor = get_cursor(cluster, role=role, member=member)
cursor = get_cursor(cluster, role=role, member=member, connect_parameters=connect_parameters)
if cursor is None:
if role is None:
+4 -1
View File
@@ -1,3 +1,6 @@
from click import ClickException
class PatroniException(Exception):
"""Parent class for all kind of exceptions related to selected distributed configuration store"""
@@ -13,7 +16,7 @@ class PatroniException(Exception):
return repr(self.value)
class PatroniCtlException(Exception):
class PatroniCtlException(ClickException):
pass
+27 -18
View File
@@ -103,35 +103,35 @@ y''')
result = runner.invoke(ctl, ['failover', 'dummy', '--dcs', '8.8.8.8'], input='''leader
other
N''')
assert 'Aborting failover' in str(result.exception)
assert 'Aborting failover' in str(result.output)
result = runner.invoke(ctl, ['failover', 'dummy', '--dcs', '8.8.8.8'], input='''leader
leader
y''')
assert 'target and source are the same' in str(result.exception)
assert 'target and source are the same' in str(result.output)
result = runner.invoke(ctl, ['failover', 'dummy', '--dcs', '8.8.8.8'], input='''leader
Reality
y''')
assert 'Reality does not exist' in str(result.exception)
assert 'Reality does not exist' in str(result.output)
result = runner.invoke(ctl, ['failover', 'dummy', '--force'])
assert 'Failing over to new leader' in result.output
result = runner.invoke(ctl, ['failover', 'dummy', '--dcs', '8.8.8.8'], input='dummy')
assert 'is not the leader of cluster' in str(result.exception)
assert 'is not the leader of cluster' in str(result.output)
with patch('patroni.etcd.Etcd.get_cluster', Mock(return_value=get_cluster_initialized_with_only_leader())):
result = runner.invoke(ctl, ['failover', 'dummy', '--dcs', '8.8.8.8'], input='''leader
other
y''')
assert 'No candidates found to failover to' in str(result.exception)
assert 'No candidates found to failover to' in str(result.output)
with patch('patroni.etcd.Etcd.get_cluster', Mock(return_value=get_cluster_initialized_without_leader())):
result = runner.invoke(ctl, ['failover', 'dummy', '--dcs', '8.8.8.8'], input='''leader
other
y''')
assert 'This cluster has no master' in str(result.exception)
assert 'This cluster has no master' in str(result.output)
with patch('patroni.ctl.post_patroni', Mock(side_effect=Exception())):
result = runner.invoke(ctl, ['failover', 'dummy', '--dcs', '8.8.8.8'], input='''leader
@@ -150,13 +150,13 @@ y''')
# with patch('patroni.dcs.AbstractDCS.get_cluster', Mock(return_value=get_cluster_initialized_with_leader())):
# result = runner.invoke(ctl, ['failover', 'alpha', '--dcs', '8.8.8.8'], input='nonsense')
# assert 'is not the leader of cluster' in str(result.exception)
# assert 'is not the leader of cluster' in str(result.output)
# result = runner.invoke(ctl, ['failover', 'alpha', '--dcs', '8.8.8.8', '--master', 'nonsense'])
# assert 'is not the leader of cluster' in str(result.exception)
# assert 'is not the leader of cluster' in str(result.output)
# result = runner.invoke(ctl, ['failover', 'alpha', '--dcs', '8.8.8.8'], input='leader\nother\nn')
# assert 'Aborting failover' in str(result.exception)
# assert 'Aborting failover' in str(result.output)
# with patch('patroni.ctl.wait_for_leader', Mock(return_value = get_cluster_initialized_with_leader())):
# result = runner.invoke(ctl, ['failover', 'alpha', '--dcs', '8.8.8.8'], input='leader\nother\nY')
@@ -182,13 +182,19 @@ y''')
'--role',
'master',
])
assert 'mutually exclusive' in str(result.exception)
assert 'mutually exclusive' in str(result.output)
with runner.isolated_filesystem():
dummy_file = open('dummy', 'w')
dummy_file.write('SELECT 1')
dummy_file.close()
result = runner.invoke(ctl, [
'query',
'alpha'
])
assert 'You need to specify' in str(result.output)
result = runner.invoke(ctl, [
'query',
'alpha',
@@ -197,7 +203,7 @@ y''')
'--command',
'dummy',
])
assert 'mutually exclusive' in str(result.exception)
assert 'mutually exclusive' in str(result.output)
result = runner.invoke(ctl, ['query', 'alpha', '--file', 'dummy'])
@@ -206,6 +212,9 @@ y''')
result = runner.invoke(ctl, ['query', 'alpha', '--command', 'SELECT 1'])
assert 'mock column' in result.output
result = runner.invoke(ctl, ['query', 'alpha', '--command', 'SELECT 1', '--dbname', 'dummy', '--password', '--username', 'dummy'], input='password\n')
assert 'mock column' in result.output
@patch('patroni.ctl.get_cursor', Mock(return_value=MockConnect().cursor()))
def test_query_member(self):
rows = query_member(None, None, None, 'master', 'SELECT pg_is_in_recovery()')
@@ -243,10 +252,10 @@ y''')
'--member',
'dummy',
])
assert 'mutually exclusive' in str(result.exception)
assert 'mutually exclusive' in str(result.output)
result = runner.invoke(ctl, ['dsn', 'alpha', '--member', 'dummy'])
assert 'Can not find' in str(result.exception)
assert 'Can not find' in str(result.output)
# result = runner.invoke(ctl, ['dsn', 'alpha', '--dcs', '8.8.8.8', '--role', 'replica'])
# assert 'host=127.0.0.1 port=5436' in result.output
@@ -270,7 +279,7 @@ y''')
'dummy',
'--any',
], input='y')
assert 'not a member' in str(result.exception)
assert 'not a member' in str(result.output)
with patch('requests.post', Mock(return_value=MockResponse())):
result = runner.invoke(ctl, ['restart', 'alpha', '--dcs', '8.8.8.8'], input='y')
@@ -283,15 +292,15 @@ y''')
result = runner.invoke(ctl, ['remove', 'alpha', '--dcs', '8.8.8.8'], input='alpha\nslave')
assert 'Please confirm' in result.output
assert 'You are about to remove all' in result.output
assert 'You did not exactly type' in str(result.exception)
assert 'You did not exactly type' in str(result.output)
result = runner.invoke(ctl, ['remove', 'alpha', '--dcs', '8.8.8.8'], input='''alpha
Yes I am aware
slave''')
assert 'You did not specify the current master of the cluster' in str(result.exception)
assert 'You did not specify the current master of the cluster' in str(result.output)
result = runner.invoke(ctl, ['remove', 'alpha', '--dcs', '8.8.8.8'], input='beta\nleader')
assert 'Cluster names specified do not match' in str(result.exception)
assert 'Cluster names specified do not match' in str(result.output)
with patch('patroni.etcd.Etcd.get_cluster', get_cluster_initialized_with_leader):
result = runner.invoke(ctl, ['remove', 'alpha', '--dcs', '8.8.8.8'],
@@ -305,7 +314,7 @@ leader''')
input='''alpha
Yes I am aware
leader''')
assert 'We have not implemented this for DCS of type' in str(result.exception)
assert 'We have not implemented this for DCS of type' in str(result.output)
@patch('patroni.etcd.Etcd.watch', Mock(return_value=None))
@patch('patroni.etcd.Etcd.get_cluster', Mock(return_value=get_cluster_initialized_with_leader()))
+1 -1
View File
@@ -18,7 +18,7 @@ def false(*args, **kwargs):
def get_cluster(initialize, leader, members, failover):
return Cluster(initialize, leader, None, members, failover)
return Cluster(initialize, leader, 10, members, failover)
def get_cluster_not_initialized_without_leader():