diff --git a/docs/SETTINGS.rst b/docs/SETTINGS.rst index 542d8f1d..a7c68604 100644 --- a/docs/SETTINGS.rst +++ b/docs/SETTINGS.rst @@ -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. - **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 ---------- - **hosts**: list of ZooKeeper cluster members in format: ['host1:port1', 'host2:port2', 'etc...']. diff --git a/patroni/ctl.py b/patroni/ctl.py index 5be1fea9..58d75d1e 100644 --- a/patroni/ctl.py +++ b/patroni/ctl.py @@ -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') arg_cluster_name = click.argument('cluster_name', required=False, 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.option('--config-file', '-c', help='Configuration file', default=CONFIG_FILE_PATH) @click.option('--dcs', '-d', help='Use this DCS', envvar='DCS') +@option_insecure @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.captureWarnings(True) # Capture eventual SSL warning 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): @@ -129,6 +134,7 @@ def auth_header(config): def request_patroni(member, request_type, endpoint, content=None, headers=None): + ctx = click.get_current_context() # the current click context headers = headers or {} url_parts = urlparse(member.api_url) 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) + 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, - 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'): @@ -903,7 +922,8 @@ def toggle_pause(config, cluster_name, paused, wait): for member in members: try: 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) continue diff --git a/postgres0.yml b/postgres0.yml index 8d7ef60a..0f3a1935 100644 --- a/postgres0.yml +++ b/postgres0.yml @@ -11,6 +11,11 @@ restapi: # username: username # 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: host: 127.0.0.1:2379 diff --git a/postgres1.yml b/postgres1.yml index 9178b088..1ba9bd54 100644 --- a/postgres1.yml +++ b/postgres1.yml @@ -11,6 +11,11 @@ restapi: # username: username # 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: host: 127.0.0.1:2379 diff --git a/postgres2.yml b/postgres2.yml index dcefe6f4..684ec066 100644 --- a/postgres2.yml +++ b/postgres2.yml @@ -11,6 +11,11 @@ restapi: username: username 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: host: 127.0.0.1:2379 diff --git a/tests/test_ctl.py b/tests/test_ctl.py index 12b22770..e388d910 100644 --- a/tests/test_ctl.py +++ b/tests/test_ctl.py @@ -345,8 +345,11 @@ class TestCtl(unittest.TestCase): @patch('requests.post', Mock(side_effect=requests.exceptions.ConnectionError('foo'))) def test_request_patroni(self): - member = get_cluster_initialized_with_leader().leader.member - self.assertRaises(requests.exceptions.ConnectionError, request_patroni, member, 'post', 'dummy', {}) + context = {'restapi': {'keyfile': '/etc/patroni/key.pem', 'certfile': 'cert.pem'}} + 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): self.runner.invoke(ctl, ['list'])