Add patronictl -k/--insecure flag and suport for restapi cert (#790)

Fixes https://github.com/zalando/patroni/issues/785
This commit is contained in:
wilfriedroset
2018-08-29 16:08:13 +02:00
committed by Alexander Kukushkin
parent 0e13677880
commit 0136f252ab
6 changed files with 50 additions and 5 deletions
+7
View File
@@ -146,6 +146,13 @@ REST API
- **certfile**: Specifies the file with the certificate in the PEM format. If the certfile is not specified or is left empty, the API server will work without SSL. - **certfile**: Specifies the file with the certificate in the PEM format. If the certfile is not specified or is left empty, the API server will work without SSL.
- **keyfile**: Specifies the file with the secret key in the PEM format. - **keyfile**: Specifies the file with the secret key in the PEM format.
CTL
---
- **Optional**:
- **insecure**: Allow connections to REST API without verifying SSL certs.
- **cacert**: Specifices the file with the CA_BUNDLE file or directory with certificates of trusted CAs to use while verifying REST API SSL certs.
- **certfile**: Specifies the file with the certificate in the PEM format to use while verifying REST API SSL certs. If not provided patronictl will use the value provided for REST API "certfile" parameter.
ZooKeeper ZooKeeper
---------- ----------
- **hosts**: list of ZooKeeper cluster members in format: ['host1:port1', 'host2:port2', 'etc...']. - **hosts**: list of ZooKeeper cluster members in format: ['host1:port1', 'host2:port2', 'etc...'].
+23 -3
View File
@@ -103,15 +103,20 @@ option_watch = click.option('-W', is_flag=True, help='Auto update the screen eve
option_force = click.option('--force', is_flag=True, help='Do not ask for confirmation at any point') option_force = click.option('--force', is_flag=True, help='Do not ask for confirmation at any point')
arg_cluster_name = click.argument('cluster_name', required=False, arg_cluster_name = click.argument('cluster_name', required=False,
default=lambda: click.get_current_context().obj.get('scope')) default=lambda: click.get_current_context().obj.get('scope'))
option_insecure = click.option('-k', '--insecure', is_flag=True, help='Allow connections to SSL sites without certs')
@click.group() @click.group()
@click.option('--config-file', '-c', help='Configuration file', default=CONFIG_FILE_PATH) @click.option('--config-file', '-c', help='Configuration file', default=CONFIG_FILE_PATH)
@click.option('--dcs', '-d', help='Use this DCS', envvar='DCS') @click.option('--dcs', '-d', help='Use this DCS', envvar='DCS')
@option_insecure
@click.pass_context @click.pass_context
def ctl(ctx, config_file, dcs): def ctl(ctx, config_file, dcs, insecure):
logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=os.environ.get('LOGLEVEL', 'WARNING')) logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=os.environ.get('LOGLEVEL', 'WARNING'))
logging.captureWarnings(True) # Capture eventual SSL warning
ctx.obj = load_config(config_file, dcs) ctx.obj = load_config(config_file, dcs)
# backward compatibility for configuration file where ctl section is not define
ctx.obj.setdefault('ctl', {})['insecure'] = ctx.obj.get('ctl', {}).get('insecure') or insecure
def get_dcs(config, scope): def get_dcs(config, scope):
@@ -129,6 +134,7 @@ def auth_header(config):
def request_patroni(member, request_type, endpoint, content=None, headers=None): def request_patroni(member, request_type, endpoint, content=None, headers=None):
ctx = click.get_current_context() # the current click context
headers = headers or {} headers = headers or {}
url_parts = urlparse(member.api_url) url_parts = urlparse(member.api_url)
logging.debug(url_parts) logging.debug(url_parts)
@@ -137,8 +143,21 @@ def request_patroni(member, request_type, endpoint, content=None, headers=None):
url = '{0}://{1}/{2}'.format(url_parts.scheme, url_parts.netloc, endpoint) url = '{0}://{1}/{2}'.format(url_parts.scheme, url_parts.netloc, endpoint)
insecure = ctx.obj.get('ctl', {}).get('insecure', False)
# Get certfile if any from several configuration namespace
cert = ctx.obj.get('ctl', {}).get('cacert') or \
ctx.obj.get('restapi', {}).get('cacert') or \
ctx.obj.get('restapi', {}).get('certfile')
# In the case we specificaly disable SSL cert verification we don't want to have the warning
if insecure:
verify = False
elif cert:
verify = cert
else:
verify = True
return getattr(requests, request_type)(url, headers=headers, return getattr(requests, request_type)(url, headers=headers,
data=json.dumps(content) if content else None, timeout=60) data=json.dumps(content) if content else None, timeout=60,
verify=verify)
def print_output(columns, rows=None, alignment=None, fmt='pretty', header=True, delimiter='\t'): def print_output(columns, rows=None, alignment=None, fmt='pretty', header=True, delimiter='\t'):
@@ -903,7 +922,8 @@ def toggle_pause(config, cluster_name, paused, wait):
for member in members: for member in members:
try: try:
r = request_patroni(member, 'patch', 'config', {'pause': paused or None}, auth_header(config)) r = request_patroni(member, 'patch', 'config', {'pause': paused or None}, auth_header(config))
except Exception: except Exception as err:
logging.warning(str(err))
logging.warning('Member %s is not accessible', member.name) logging.warning('Member %s is not accessible', member.name)
continue continue
+5
View File
@@ -11,6 +11,11 @@ restapi:
# username: username # username: username
# password: password # password: password
# ctl:
# insecure: false # Allow connections to SSL sites without certs
# certfile: /etc/ssl/certs/ssl-cert-snakeoil.pem
# cacert: /etc/ssl/certs/ssl-cacert-snakeoil.pem
etcd: etcd:
host: 127.0.0.1:2379 host: 127.0.0.1:2379
+5
View File
@@ -11,6 +11,11 @@ restapi:
# username: username # username: username
# password: password # password: password
# ctl:
# insecure: false # Allow connections to SSL sites without certs
# certfile: /etc/ssl/certs/ssl-cert-snakeoil.pem
# cacert: /etc/ssl/certs/ssl-cacert-snakeoil.pem
etcd: etcd:
host: 127.0.0.1:2379 host: 127.0.0.1:2379
+5
View File
@@ -11,6 +11,11 @@ restapi:
username: username username: username
password: password password: password
# ctl:
# insecure: false # Allow connections to SSL sites without certs
# certfile: /etc/ssl/certs/ssl-cert-snakeoil.pem
# cacert: /etc/ssl/certs/ssl-cacert-snakeoil.pem
etcd: etcd:
host: 127.0.0.1:2379 host: 127.0.0.1:2379
+5 -2
View File
@@ -345,8 +345,11 @@ class TestCtl(unittest.TestCase):
@patch('requests.post', Mock(side_effect=requests.exceptions.ConnectionError('foo'))) @patch('requests.post', Mock(side_effect=requests.exceptions.ConnectionError('foo')))
def test_request_patroni(self): def test_request_patroni(self):
member = get_cluster_initialized_with_leader().leader.member context = {'restapi': {'keyfile': '/etc/patroni/key.pem', 'certfile': 'cert.pem'}}
self.assertRaises(requests.exceptions.ConnectionError, request_patroni, member, 'post', 'dummy', {}) with patch('click.get_current_context') as mock_context:
mock_context.return_value.obj = context
member = get_cluster_initialized_with_leader().leader.member
self.assertRaises(requests.exceptions.ConnectionError, request_patroni, member, 'post', 'dummy', {})
def test_ctl(self): def test_ctl(self):
self.runner.invoke(ctl, ['list']) self.runner.invoke(ctl, ['list'])