Skip to content

Commit 7acec26

Browse files
Split client class
* Turn Etcd3Client in MultiEndpointEtcd3Client, and move existing single-endpoint interface into a subclass retaining the old name. * Revert changes to client() * Autodoc MultiEndpointEtcd3Client
1 parent 18bb097 commit 7acec26

4 files changed

Lines changed: 77 additions & 72 deletions

File tree

docs/usage.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ You can check this has been stored correctly by testing with etcdctl:
4949
API
5050
===
5151

52+
.. autoclass:: etcd3.MultiEndpointEtcd3Client
53+
:members:
54+
5255
.. autoclass:: etcd3.Etcd3Client
5356
:members:
5457

etcd3/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import etcd3.etcdrpc as etcdrpc
44
from etcd3.client import Endpoint
55
from etcd3.client import Etcd3Client
6+
from etcd3.client import MultiEndpointEtcd3Client
67
from etcd3.client import Transactions
78
from etcd3.client import client
89
from etcd3.exceptions import Etcd3Exception
@@ -24,4 +25,5 @@
2425
'Lease',
2526
'Lock',
2627
'Member',
28+
'MultiEndpointEtcd3Client'
2729
)

etcd3/client.py

Lines changed: 68 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -142,99 +142,43 @@ def __call__(self, context, callback):
142142
callback(metadata, None)
143143

144144

145-
class Etcd3Client(object):
145+
class MultiEndpointEtcd3Client(object):
146146
"""
147-
etcd v3 API client.
148-
149-
If ``endpoints`` is specified, ``host``, ``port``, ``ca_cert``,
150-
``cert_key``, ``cert_cert``, and ``grpc_options`` must not be.
147+
etcd v3 API client with multiple endpoints.
151148
152149
When failover is enabled, requests still will not be auto-retried.
153150
Instead, the application may retry the request, and the ``Etcd3Client``
154151
will then attempt to send it to a different endpoint that has not recently
155152
failed. If all configured endpoints have failed and are not ready to be
156153
retried, an ``exceptions.NoServerAvailableError`` will be raised.
157154
158-
:param host: Host to connect to, 'localhost' if not specified
159-
:type host: str, optional
160-
:param port: Port to connect to on host, 2379 if not specified
161-
:type port: int, optional
162155
:param endpoints: Endpoints to use in lieu of host and port
163156
:type endpoints: Iterable(Endpoint), optional
164-
:param ca_cert: Filesystem path of etcd CA certificate
165-
:type ca_cert: str or os.PathLike, optional
166-
:param cert_key: Filesystem path of client key
167-
:type cert_key: str or os.PathLike, optional
168-
:param cert_cert: Filesystem path of client certificate
169-
:type cert_cert: str or os.PathLike, optional
170157
:param timeout: Timeout for all RPC in seconds
171158
:type timeout: int or float, optional
172159
:param user: Username for authentication
173160
:type user: str, optional
174161
:param password: Password for authentication
175162
:type password: str, optional
176-
:param dict grpc_options: Additional gRPC options
177-
:type grpc_options: dict, optional
178163
:param bool failover: Failover between endpoints, default False
179164
"""
180165

181-
def __init__(self, host=None, port=None, endpoints=None,
182-
ca_cert=None, cert_key=None, cert_cert=None, timeout=None,
183-
user=None, password=None, grpc_options=None, failover=False):
184-
185-
if endpoints and any(
186-
(host, port, ca_cert, cert_key, cert_cert, grpc_options)
187-
):
188-
raise ValueError(
189-
"endpoints may not be specified with host, port, ca_cert, "
190-
"cert_key, cert_cert, or grpc_options"
191-
)
192-
host = host or "localhost"
193-
port = port or 2379
166+
def __init__(self, endpoints=None, timeout=None, user=None, password=None,
167+
failover=False):
194168

195169
self.metadata = None
196170
self.failover = failover
197171

198172
# Cache GRPC stubs here
199173
self._stubs = {}
200174

201-
# Step 1: verify credentials
202-
cert_params = [c is not None for c in (cert_cert, cert_key)]
203-
if ca_cert is not None:
204-
if all(cert_params):
205-
credentials = self.get_secure_creds(
206-
ca_cert,
207-
cert_key,
208-
cert_cert
209-
)
210-
self.uses_secure_channel = True
211-
elif any(cert_params):
212-
# some of the cert parameters are set
213-
raise ValueError(
214-
'to use a secure channel ca_cert is required by itself, '
215-
'or cert_cert and cert_key must both be specified.')
216-
else:
217-
credentials = self.get_secure_creds(ca_cert, None, None)
218-
self.uses_secure_channel = True
219-
else:
220-
self.uses_secure_channel = False
221-
credentials = None
222-
223-
# Step 2: if more than one endpoint is available, add all of them, else
224-
# use the host/port combination
225-
if endpoints is None:
226-
ep = Endpoint(host, port, secure=self.uses_secure_channel,
227-
creds=credentials, opts=grpc_options)
228-
self.endpoints = {ep.netloc: ep}
229-
else:
230-
# If the endpoints are passed externally, just use those.
231-
self.endpoints = {ep.netloc: ep for ep in endpoints}
232-
175+
# Step 1: setup endpoints
176+
self.endpoints = {ep.netloc: ep for ep in endpoints}
233177
self._current_endpoint_label = random.choice(
234178
list(self.endpoints.keys())
235179
)
236180

237-
# Step 3: if auth is enabled, call the auth endpoint
181+
# Step 2: if auth is enabled, call the auth endpoint
238182
self.timeout = timeout
239183
self.call_credentials = None
240184
cred_params = [c is not None for c in (user, password)]
@@ -1393,18 +1337,74 @@ def snapshot(self, file_obj):
13931337
file_obj.write(response.blob)
13941338

13951339

1396-
def client(host=None, port=None, endpoints=None,
1340+
class Etcd3Client(MultiEndpointEtcd3Client):
1341+
"""
1342+
etcd v3 API client.
1343+
1344+
:param host: Host to connect to, 'localhost' if not specified
1345+
:type host: str, optional
1346+
:param port: Port to connect to on host, 2379 if not specified
1347+
:type port: int, optional
1348+
:param ca_cert: Filesystem path of etcd CA certificate
1349+
:type ca_cert: str or os.PathLike, optional
1350+
:param cert_key: Filesystem path of client key
1351+
:type cert_key: str or os.PathLike, optional
1352+
:param cert_cert: Filesystem path of client certificate
1353+
:type cert_cert: str or os.PathLike, optional
1354+
:param timeout: Timeout for all RPC in seconds
1355+
:type timeout: int or float, optional
1356+
:param user: Username for authentication
1357+
:type user: str, optional
1358+
:param password: Password for authentication
1359+
:type password: str, optional
1360+
:param dict grpc_options: Additional gRPC options
1361+
:type grpc_options: dict, optional
1362+
"""
1363+
1364+
def __init__(self, host='localhost', port=2379, ca_cert=None,
1365+
cert_key=None, cert_cert=None, timeout=None, user=None,
1366+
password=None, grpc_options=None):
1367+
1368+
# Step 1: verify credentials
1369+
cert_params = [c is not None for c in (cert_cert, cert_key)]
1370+
if ca_cert is not None:
1371+
if all(cert_params):
1372+
credentials = self.get_secure_creds(
1373+
ca_cert,
1374+
cert_key,
1375+
cert_cert
1376+
)
1377+
self.uses_secure_channel = True
1378+
elif any(cert_params):
1379+
# some of the cert parameters are set
1380+
raise ValueError(
1381+
'to use a secure channel ca_cert is required by itself, '
1382+
'or cert_cert and cert_key must both be specified.')
1383+
else:
1384+
credentials = self.get_secure_creds(ca_cert, None, None)
1385+
self.uses_secure_channel = True
1386+
else:
1387+
self.uses_secure_channel = False
1388+
credentials = None
1389+
1390+
# Step 2: create Endpoint
1391+
ep = Endpoint(host, port, secure=self.uses_secure_channel,
1392+
creds=credentials, opts=grpc_options)
1393+
1394+
super(Etcd3Client, self).__init__(endpoints=[ep], timeout=timeout,
1395+
user=user, password=password)
1396+
1397+
1398+
def client(host='localhost', port=2379,
13971399
ca_cert=None, cert_key=None, cert_cert=None, timeout=None,
1398-
user=None, password=None, grpc_options=None, failover=False):
1400+
user=None, password=None, grpc_options=None):
13991401
"""Return an instance of an Etcd3Client."""
14001402
return Etcd3Client(host=host,
14011403
port=port,
1402-
endpoints=endpoints,
14031404
ca_cert=ca_cert,
14041405
cert_key=cert_key,
14051406
cert_cert=cert_cert,
14061407
timeout=timeout,
14071408
user=user,
14081409
password=password,
1409-
grpc_options=grpc_options,
1410-
failover=failover)
1410+
grpc_options=grpc_options)

tests/test_etcd3.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,9 +1326,9 @@ def etcd(self):
13261326
endpoints.append(etcd3.Endpoint(host=url.hostname,
13271327
port=url.port,
13281328
secure=False))
1329-
with etcd3.client(endpoints=endpoints,
1330-
timeout=timeout,
1331-
failover=True) as client:
1329+
with etcd3.MultiEndpointEtcd3Client(endpoints=endpoints,
1330+
timeout=timeout,
1331+
failover=True) as client:
13321332
yield client
13331333

13341334
@retry(wait=wait_fixed(2), stop=stop_after_attempt(3))
@@ -1346,7 +1346,7 @@ def test_endpoint_offline(self, etcd):
13461346
exception = TestEtcd3.MockedException(grpc.StatusCode.UNAVAILABLE)
13471347
kv_mock = mock.PropertyMock()
13481348
kv_mock.Range.side_effect = exception
1349-
with mock.patch('etcd3.Etcd3Client.kvstub',
1349+
with mock.patch('etcd3.MultiEndpointEtcd3Client.kvstub',
13501350
new_callable=mock.PropertyMock) as property_mock:
13511351
property_mock.return_value = kv_mock
13521352
with pytest.raises(etcd3.exceptions.ConnectionFailedError):

0 commit comments

Comments
 (0)