From a598cce62b7764e4ba31f47bde2b745633f33688 Mon Sep 17 00:00:00 2001 From: Alan Bishop Date: Thu, 13 Feb 2020 13:45:15 -0800 Subject: [PATCH] Add TLS support in etcd3 and etcd3gw drivers The etcd3 and etcd3gw drivers parse CA, key and cert options from the coordination URL, and pass them on to the backend clients. The etcd3gw driver implements the "etcd3+https" scheme. Change-Id: I78d8ca0583f883f7f746791f82fbcc116458ce2c --- ...-etcd3gw-tls-support-618ab207706e67af.yaml | 6 ++ setup.cfg | 3 +- test-requirements.txt | 1 + tooz/drivers/etcd3.py | 14 +++- tooz/drivers/etcd3gw.py | 21 ++++-- tooz/tests/drivers/test_etcd3.py | 63 +++++++++++++++++ tooz/tests/drivers/test_etcd3gw.py | 67 +++++++++++++++++++ 7 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/etcd3-etcd3gw-tls-support-618ab207706e67af.yaml create mode 100644 tooz/tests/drivers/test_etcd3.py create mode 100644 tooz/tests/drivers/test_etcd3gw.py diff --git a/releasenotes/notes/etcd3-etcd3gw-tls-support-618ab207706e67af.yaml b/releasenotes/notes/etcd3-etcd3gw-tls-support-618ab207706e67af.yaml new file mode 100644 index 00000000..99a30160 --- /dev/null +++ b/releasenotes/notes/etcd3-etcd3gw-tls-support-618ab207706e67af.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The etcd3 and etcd3gw drivers now support TLS, by adding the ability to + specify ca_cert, cert_key and cert_cert files. For the etcd3gw driver, + this is controlled by specifying "etcd3+https" in the coordination URL. diff --git a/setup.cfg b/setup.cfg index 1d0e0592..b2ddce42 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ tooz.backends = etcd = tooz.drivers.etcd:EtcdDriver etcd3 = tooz.drivers.etcd3:Etcd3Driver etcd3+http = tooz.drivers.etcd3gw:Etcd3Driver + etcd3+https = tooz.drivers.etcd3gw:Etcd3Driver kazoo = tooz.drivers.zookeeper:KazooDriver zake = tooz.drivers.zake:ZakeDriver memcached = tooz.drivers.memcached:MemcachedDriver @@ -47,7 +48,7 @@ consul = etcd = requests>=2.10.0 # Apache-2.0 etcd3 = - etcd3>=0.6.2 # Apache-2.0 + etcd3>=0.12.0 # Apache-2.0 grpcio>=1.18.0 etcd3gw = etcd3gw>=0.1.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 7f183d29..edcf3b9d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,3 +9,4 @@ coverage>=3.6 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD pifpaf>=0.10.0 # Apache-2.0 stestr>=2.0.0 +ddt>=1.2.1 # MIT diff --git a/tooz/drivers/etcd3.py b/tooz/drivers/etcd3.py index be3015ad..d8e7aa5c 100644 --- a/tooz/drivers/etcd3.py +++ b/tooz/drivers/etcd3.py @@ -126,7 +126,9 @@ class Etcd3Driver(coordination.CoordinationDriverCachedRunWatchers, ================== ======= Name Default ================== ======= - protocol http + ca_cert None + cert_key None + cert_cert None timeout 30 lock_timeout 30 membership_timeout 30 @@ -147,8 +149,16 @@ class Etcd3Driver(coordination.CoordinationDriverCachedRunWatchers, host = parsed_url.hostname or self.DEFAULT_HOST port = parsed_url.port or self.DEFAULT_PORT options = utils.collapse(options) + ca_cert = options.get('ca_cert') + cert_key = options.get('cert_key') + cert_cert = options.get('cert_cert') timeout = int(options.get('timeout', self.DEFAULT_TIMEOUT)) - self.client = etcd3.client(host=host, port=port, timeout=timeout) + self.client = etcd3.client(host=host, + port=port, + ca_cert=ca_cert, + cert_key=cert_key, + cert_cert=cert_cert, + timeout=timeout) self.lock_timeout = int(options.get('lock_timeout', timeout)) self.membership_timeout = int(options.get( 'membership_timeout', timeout)) diff --git a/tooz/drivers/etcd3gw.py b/tooz/drivers/etcd3gw.py index 0c64e706..e64301ef 100644 --- a/tooz/drivers/etcd3gw.py +++ b/tooz/drivers/etcd3gw.py @@ -169,15 +169,18 @@ class Etcd3Driver(coordination.CoordinationDriverWithExecutor): The Etcd driver connection URI should look like:: - etcd3+http://[HOST[:PORT]][?OPTION1=VALUE1[&OPTION2=VALUE2[&...]]] + etcd3+PROTOCOL://[HOST[:PORT]][?OPTION1=VALUE1[&OPTION2=VALUE2[&...]]] - If not specified, HOST defaults to localhost and PORT defaults to 2379. + The PROTOCOL can be http or https. If not specified, HOST defaults to + localhost and PORT defaults to 2379. Available options are: ================== ======= Name Default ================== ======= - protocol http + ca_cert None + cert_key None + cert_cert None timeout 30 lock_timeout 30 membership_timeout 30 @@ -197,11 +200,21 @@ class Etcd3Driver(coordination.CoordinationDriverWithExecutor): def __init__(self, member_id, parsed_url, options): super(Etcd3Driver, self).__init__(member_id, parsed_url, options) + protocol = 'https' if parsed_url.scheme.endswith('https') else 'http' host = parsed_url.hostname or self.DEFAULT_HOST port = parsed_url.port or self.DEFAULT_PORT options = utils.collapse(options) + ca_cert = options.get('ca_cert') + cert_key = options.get('cert_key') + cert_cert = options.get('cert_cert') timeout = int(options.get('timeout', self.DEFAULT_TIMEOUT)) - self.client = etcd3gw.client(host=host, port=port, timeout=timeout) + self.client = etcd3gw.client(host=host, + port=port, + protocol=protocol, + ca_cert=ca_cert, + cert_key=cert_key, + cert_cert=cert_cert, + timeout=timeout) self.lock_timeout = int(options.get('lock_timeout', timeout)) self.membership_timeout = int(options.get( 'membership_timeout', timeout)) diff --git a/tooz/tests/drivers/test_etcd3.py b/tooz/tests/drivers/test_etcd3.py new file mode 100644 index 00000000..7bd59b3a --- /dev/null +++ b/tooz/tests/drivers/test_etcd3.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2020 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ddt +from testtools import testcase +from unittest import mock + +import tooz.coordination +import tooz.drivers.etcd3 as etcd3_driver +import tooz.tests + + +@ddt.ddt +class TestEtcd3(testcase.TestCase): + FAKE_MEMBER_ID = tooz.tests.get_random_uuid() + + @ddt.data({'coord_url': 'etcd3://', + 'host': etcd3_driver.Etcd3Driver.DEFAULT_HOST, + 'port': etcd3_driver.Etcd3Driver.DEFAULT_PORT, + 'ca_cert': None, + 'cert_key': None, + 'cert_cert': None, + 'timeout': etcd3_driver.Etcd3Driver.DEFAULT_TIMEOUT}, + {'coord_url': ('etcd3://my_host:666?ca_cert=/my/ca_cert&' + 'cert_key=/my/cert_key&cert_cert=/my/cert_cert&' + 'timeout=42'), + 'host': 'my_host', + 'port': 666, + 'ca_cert': '/my/ca_cert', + 'cert_key': '/my/cert_key', + 'cert_cert': '/my/cert_cert', + 'timeout': 42}) + @ddt.unpack + @mock.patch('etcd3.client') + def test_etcd3_client_init(self, + mock_etcd3_client, + coord_url, + host, + port, + ca_cert, + cert_key, + cert_cert, + timeout): + tooz.coordination.get_coordinator(coord_url, self.FAKE_MEMBER_ID) + mock_etcd3_client.assert_called_with(host=host, + port=port, + ca_cert=ca_cert, + cert_key=cert_key, + cert_cert=cert_cert, + timeout=timeout) diff --git a/tooz/tests/drivers/test_etcd3gw.py b/tooz/tests/drivers/test_etcd3gw.py new file mode 100644 index 00000000..a182b777 --- /dev/null +++ b/tooz/tests/drivers/test_etcd3gw.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2020 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ddt +from testtools import testcase +from unittest import mock + +import tooz.coordination +import tooz.drivers.etcd3gw as etcd3gw_driver +import tooz.tests + + +@ddt.ddt +class TestEtcd3Gw(testcase.TestCase): + FAKE_MEMBER_ID = tooz.tests.get_random_uuid() + + @ddt.data({'coord_url': 'etcd3+http://', + 'protocol': 'http', + 'host': etcd3gw_driver.Etcd3Driver.DEFAULT_HOST, + 'port': etcd3gw_driver.Etcd3Driver.DEFAULT_PORT, + 'ca_cert': None, + 'cert_key': None, + 'cert_cert': None, + 'timeout': etcd3gw_driver.Etcd3Driver.DEFAULT_TIMEOUT}, + {'coord_url': ('etcd3+https://my_host:666?ca_cert=/my/ca_cert&' + 'cert_key=/my/cert_key&cert_cert=/my/cert_cert&' + 'timeout=42'), + 'protocol': 'https', + 'host': 'my_host', + 'port': 666, + 'ca_cert': '/my/ca_cert', + 'cert_key': '/my/cert_key', + 'cert_cert': '/my/cert_cert', + 'timeout': 42}) + @ddt.unpack + @mock.patch('etcd3gw.client') + def test_etcd3gw_client_init(self, + mock_etcd3gw_client, + coord_url, + protocol, + host, + port, + ca_cert, + cert_key, + cert_cert, + timeout): + tooz.coordination.get_coordinator(coord_url, self.FAKE_MEMBER_ID) + mock_etcd3gw_client.assert_called_with(host=host, + port=port, + protocol=protocol, + ca_cert=ca_cert, + cert_key=cert_key, + cert_cert=cert_cert, + timeout=timeout)