Skip to content

Commit fe68409

Browse files
committed
Add srv record-based DNS discovery.
We use the same keys used by confd (https://github.com/kelseyhightower/confd) to allow service discovery via DNS.
1 parent 9fccae1 commit fe68409

6 files changed

Lines changed: 74 additions & 5 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/test_client.py

Lines changed: 25 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,21 @@ 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+
r.target = dns.name.from_unicode(u'etcd{}.example.com'.format(i))
115+
answers.append(r)
116+
dns.resolver.query = mock.create_autospec(dns.resolver.query, return_value=answers)
117+
self.machines = etcd.Client.machines
118+
etcd.Client.machines = mock.create_autospec(etcd.Client.machines, return_value=[u'https://etcd2.example.com:2379'])
119+
c = etcd.Client(srv_domain="example.com", allow_reconnect=True, protocol="https")
120+
etcd.Client.machines = self.machines
121+
self.assertEquals(c.host, u'etcd1.example.com')
122+
self.assertEquals(c.port, 2379)
123+
self.assertEquals(c._machines_cache,
124+
[u'https://etcd2.example.com:2379'])

0 commit comments

Comments
 (0)