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 4e601d22..f1b01cf6 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)