Skip to content

Commit 42aa30c

Browse files
committed
Merge pull request jplana#136 from jplana/srv_record_corrected
Add SRV record
2 parents 9fccae1 + daf8309 commit 42aa30c

9 files changed

Lines changed: 87 additions & 16 deletions

File tree

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
language: python
22
python:
33
- "2.7"
4-
- "3.3"
4+
- "3.5"
55

66
before_install:
7-
- ./build_etcd.sh v2.0.10
7+
- ./build_etcd.sh v2.2.0
88
- pip install --upgrade setuptools
99

1010
# command to install dependencies

README.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ Create a client object
4141
client = etcd.Client(port=4002)
4242
client = etcd.Client(host='127.0.0.1', port=4003)
4343
client = etcd.Client(host='127.0.0.1', port=4003, allow_redirect=False) # wont let you run sensitive commands on non-leader machines, default is true
44+
# If you have defined a SRV record for _etcd._tcp.example.com pointing to the clients
45+
client = etcd.Client(srv_domain='example.com', protocol="https")
4446
# create a client against https://api.example.com:443/etcd
4547
client = etcd.Client(host='api.example.com', protocol='https', port=443, version_prefix='/etcd')
4648
Write a key

buildout.cfg

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ develop = .
66
eggs =
77
urllib3==1.7.1
88
pyOpenSSL==0.13.1
9+
${deps:extraeggs}
910

1011
[python]
1112
recipe = zc.recipe.egg
@@ -21,3 +22,12 @@ eggs = ${python:eggs}
2122
recipe = collective.recipe.sphinxbuilder
2223
source = ${buildout:directory}/docs-source
2324
build = ${buildout:directory}/docs
25+
26+
27+
[deps:python2]
28+
extraeggs =
29+
dnspython==1.12.0
30+
31+
[deps:python3]
32+
extraeggs =
33+
dnspython3==1.12.0

setup.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@
88

99
version = '0.4.2'
1010

11+
# Dnspython is two different packages depending on python version
12+
if sys.version_info.major == 2:
13+
dns = 'dnspython'
14+
else:
15+
dns = 'dnspython3'
16+
1117
install_requires = [
12-
'urllib3>=1.7.1'
18+
'urllib3>=1.7.1',
19+
dns
1320
]
1421

1522
test_requires = [

src/etcd/client.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import urllib3.util
1919
import json
2020
import ssl
21+
import dns.resolver
2122
import etcd
2223

2324
try:
@@ -46,6 +47,7 @@ def __init__(
4647
self,
4748
host='127.0.0.1',
4849
port=4001,
50+
srv_domain=None,
4951
version_prefix='/v2',
5052
read_timeout=60,
5153
allow_redirect=True,
@@ -67,6 +69,8 @@ def __init__(
6769
6870
port (int): Port used to connect to etcd.
6971
72+
srv_domain (str): Domain to search the SRV record for cluster autodiscovery.
73+
7074
version_prefix (str): Url or version prefix in etcd url (default=/v2).
7175
7276
read_timeout (int): max seconds to wait for a read.
@@ -98,8 +102,15 @@ def __init__(
98102
by host. By default this will use up to 10
99103
connections.
100104
"""
101-
_log.debug("New etcd client created for %s:%s%s",
102-
host, port, version_prefix)
105+
106+
# If a DNS record is provided, use it to get the hosts list
107+
if srv_domain is not None:
108+
try:
109+
host = self._discover(srv_domain)
110+
except Exception as e:
111+
_log.error("Could not discover the etcd hosts from %s: %s",
112+
srv_domain, e)
113+
103114
self._protocol = protocol
104115

105116
def uri(protocol, host, port):
@@ -153,6 +164,8 @@ def uri(protocol, host, port):
153164

154165
self.http = urllib3.PoolManager(num_pools=10, **kw)
155166

167+
_log.debug("New etcd client created for %s", self.base_uri)
168+
156169
if self._allow_reconnect:
157170
# we need the set of servers in the cluster in order to try
158171
# reconnecting upon error. The cluster members will be
@@ -174,6 +187,18 @@ def uri(protocol, host, port):
174187
_log.debug("Machines cache initialised to %s",
175188
self._machines_cache)
176189

190+
def _discover(self, domain):
191+
srv_name = "_etcd._tcp.{}".format(domain)
192+
answers = dns.resolver.query(srv_name, 'SRV')
193+
hosts = []
194+
for answer in answers:
195+
hosts.append(
196+
(answer.target.to_text(omit_final_dot=True), answer.port))
197+
_log.debug("Found %s", hosts)
198+
if not len(hosts):
199+
raise ValueError("The SRV record is present but no host were found")
200+
return tuple(hosts)
201+
177202
@property
178203
def base_uri(self):
179204
"""URI used by the client to connect to etcd."""

src/etcd/tests/unit/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ def _prepare_response(self, s, d, cluster_id=None):
2727

2828
def _mock_api(self, status, d, cluster_id=None):
2929
resp = self._prepare_response(status, d, cluster_id=cluster_id)
30-
self.client.api_execute = mock.create_autospec(
31-
self.client.api_execute, return_value=resp)
30+
self.client.api_execute = mock.MagicMock(return_value=resp)
3231

3332
def _mock_exception(self, exc, msg):
3433
self.client.api_execute = mock.Mock(side_effect=exc(msg))

src/etcd/tests/unit/test_client.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import unittest
22
import etcd
3+
import dns.name
4+
import dns.rdtypes.IN.SRV
5+
import dns.resolver
6+
try:
7+
import mock
8+
except ImportError:
9+
from unittest import mock
310

411

512
class TestClient(unittest.TestCase):
@@ -97,3 +104,25 @@ def test_allow_reconnect(self):
97104
allow_reconnect=True,
98105
use_proxies=True,
99106
)
107+
108+
def test_discover(self):
109+
"""Tests discovery."""
110+
answers = []
111+
for i in range(1,3):
112+
r = mock.create_autospec(dns.rdtypes.IN.SRV.SRV)
113+
r.port = 2379
114+
try:
115+
method = dns.name.from_unicode
116+
except AttributeError:
117+
method = dns.name.from_text
118+
r.target = method(u'etcd{}.example.com'.format(i))
119+
answers.append(r)
120+
dns.resolver.query = mock.create_autospec(dns.resolver.query, return_value=answers)
121+
self.machines = etcd.Client.machines
122+
etcd.Client.machines = mock.create_autospec(etcd.Client.machines, return_value=[u'https://etcd2.example.com:2379'])
123+
c = etcd.Client(srv_domain="example.com", allow_reconnect=True, protocol="https")
124+
etcd.Client.machines = self.machines
125+
self.assertEquals(c.host, u'etcd1.example.com')
126+
self.assertEquals(c.port, 2379)
127+
self.assertEquals(c._machines_cache,
128+
[u'https://etcd2.example.com:2379'])

src/etcd/tests/unit/test_lock.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import etcd
2-
import mock
2+
try:
3+
import mock
4+
except ImportError:
5+
from unittest import mock
36
from etcd.tests.unit import TestClientApiBase
47

58

@@ -92,8 +95,7 @@ def test_acquired(self):
9295
self.assertTrue(self.locker._acquired())
9396
self.assertTrue(self.locker.is_taken)
9497
retval = ('/_locks/test_lock/1', '/_locks/test_lock/4')
95-
self.locker._get_locker = mock.create_autospec(
96-
self.locker._get_locker, return_value=retval)
98+
self.locker._get_locker = mock.MagicMock(return_value=retval)
9799
self.assertFalse(self.locker._acquired(blocking=False))
98100
self.assertFalse(self.locker.is_taken)
99101
d = {

src/etcd/tests/unit/test_request.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -397,12 +397,9 @@ def setUp(self):
397397
def _mock_api(self, status, d, cluster_id=None):
398398
resp = self._prepare_response(status, d)
399399
resp.getheader.return_value = cluster_id or "abcdef1234"
400-
self.client.http.request_encode_body = mock.create_autospec(
401-
self.client.http.request_encode_body, return_value=resp
402-
)
403-
self.client.http.request = mock.create_autospec(
404-
self.client.http.request, return_value=resp
405-
)
400+
self.client.http.request_encode_body = mock.MagicMock(
401+
return_value=resp)
402+
self.client.http.request = mock.MagicMock(return_value=resp)
406403

407404
def _mock_error(self, error_code, msg, cause, method='PUT', fields=None,
408405
cluster_id=None):

0 commit comments

Comments
 (0)