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
This commit is contained in:
Alan Bishop 2020-02-13 13:45:15 -08:00
parent ba27954b06
commit a598cce62b
7 changed files with 168 additions and 7 deletions

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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))

View File

@ -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)

View File

@ -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)