Skip to content

Commit 7be4448

Browse files
committed
Add support for Network Tomography
This commit adds support for Consul's network tomography. The tomography is a collection of nodes' coordinates and RTT between the nodes. See also: [Network Coordinates](https://www.consul.io/docs/internals/coordinates.html)
1 parent 312f362 commit 7be4448

File tree

6 files changed

+120
-1
lines changed

6 files changed

+120
-1
lines changed

consulate/api/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from consulate.api.catalog import Catalog
88
from consulate.api.event import Event
99
from consulate.api.health import Health
10+
from consulate.api.coordinate import Coordinate
1011
from consulate.api.kv import KV
1112
from consulate.api.lock import Lock
1213
from consulate.api.session import Session

consulate/api/coordinate.py

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""
2+
Consul Coordinate Endpoint Access
3+
4+
"""
5+
from consulate.api import base
6+
from math import sqrt
7+
8+
class Coordinate(base.Endpoint):
9+
"""Used to query node coordinates.
10+
"""
11+
12+
def node(self, node_id):
13+
"""Return coordinates for the given node.
14+
15+
:param str node_id: The node ID
16+
:rtype: dict
17+
18+
"""
19+
return self._get(['node', node_id])
20+
21+
def nodes(self):
22+
"""Return coordinates for the current datacenter.
23+
24+
:rtype: list
25+
26+
"""
27+
return self._get_list(['nodes'])
28+
29+
def rtt(self, src, dst):
30+
"""Calculated RTT between two node coordinates.
31+
32+
:param dict src
33+
:param dict dst
34+
:rtype float
35+
36+
"""
37+
38+
if not isinstance(src, (dict)):
39+
raise ValueError('coordinate object must be a dictionary')
40+
if not isinstance(dst, (dict)):
41+
raise ValueError('coordinate object must be a dictionary')
42+
if 'Coord' not in src:
43+
raise ValueError('coordinate object has no Coord key')
44+
if 'Coord' not in dst:
45+
raise ValueError('coordinate object has no Coord key')
46+
47+
src_coord = src['Coord']
48+
dst_coord = dst['Coord']
49+
50+
if len(src_coord.get('Vec')) != len(dst_coord.get('Vec')):
51+
raise ValueError('coordinate objects are not compatible due to different length')
52+
53+
sumsq = 0.0
54+
for i in xrange(len(src_coord.get('Vec'))):
55+
diff = src_coord.get('Vec')[i] - dst_coord.get('Vec')[i]
56+
sumsq += diff * diff
57+
58+
rtt = sqrt(sumsq) + src_coord.get('Height') + dst_coord.get('Height')
59+
adjusted = rtt + src_coord.get('Adjustment') + dst_coord.get('Adjustment')
60+
if adjusted > 0.0:
61+
rtt = adjusted
62+
63+
return rtt * 1000

consulate/client.py

+11
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def __init__(self,
5454
self._catalog = api.Catalog(base_uri, self._adapter, datacenter, token)
5555
self._event = api.Event(base_uri, self._adapter, datacenter, token)
5656
self._health = api.Health(base_uri, self._adapter, datacenter, token)
57+
self._coordinate = api.Coordinate(base_uri, self._adapter, datacenter, token)
5758
self._kv = api.KV(base_uri, self._adapter, datacenter, token)
5859
self._session = api.Session(base_uri, self._adapter, datacenter, token)
5960
self._status = api.Status(base_uri, self._adapter, datacenter, token)
@@ -110,6 +111,16 @@ def health(self):
110111
"""
111112
return self._health
112113

114+
@property
115+
def coordinate(self):
116+
"""Access the Consul
117+
`Coordinate <https://www.consul.io/api/coordinate.html#read-lan-coordinates-for-a-node>`_ API
118+
119+
:rtype: :py:class:`consulate.api.coordinate.Coordinate`
120+
121+
"""
122+
return self._coordinate
123+
113124
@property
114125
def kv(self):
115126
"""Access the Consul

docs/coordinate.rst

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Coordinate
2+
==========
3+
4+
The :py:class:`Coordinate <consulate.api.coordinate.Coordinate>` class provides
5+
access to Consul's Network Tomography.
6+
7+
.. autoclass:: consulate.api.coordinate.Coordinate
8+
:members:
9+
:special-members:
10+
11+
Usage
12+
-----
13+
14+
This code fetches the coordinates for the nodes in ``ny1`` cluster, and then
15+
calculates the RTT between two random nodes.
16+
17+
.. code:: python
18+
19+
import consulate
20+
21+
# Create a new instance of a consulate session
22+
session = consulate.Consul()
23+
24+
# Get coordinates for all notes in ny1 cluster
25+
coordinates = session.coordinate.nodes('ny1')
26+
27+
# Calculate RTT between two nodes
28+
session.coordinate.rtt(coordinates[0], coordinates[1])

tests/api_tests.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ class ConsulTests(unittest.TestCase):
2525
@mock.patch('consulate.api.Catalog')
2626
@mock.patch('consulate.api.KV')
2727
@mock.patch('consulate.api.Health')
28+
@mock.patch('consulate.api.Coordinate')
2829
@mock.patch('consulate.api.ACL')
2930
@mock.patch('consulate.api.Event')
3031
@mock.patch('consulate.api.Session')
3132
@mock.patch('consulate.api.Status')
32-
def setUp(self, status, session, event, acl, health, kv, catalog, agent,
33+
def setUp(self, status, session, event, acl, health, coordinate, kv, catalog, agent,
3334
adapter):
3435
self.host = '127.0.0.1'
3536
self.port = 8500
@@ -43,6 +44,7 @@ def setUp(self, status, session, event, acl, health, kv, catalog, agent,
4344
self.event = event
4445
self.kv = kv
4546
self.health = health
47+
self.coordinate = coordinate
4648
self.session = session
4749
self.status = status
4850

@@ -93,6 +95,11 @@ def test_health_initialization(self):
9395
self.health.called_once_with(self.base_uri, self.adapter, self.dc,
9496
self.token))
9597

98+
def test_coordinate_initialization(self):
99+
self.assertTrue(
100+
self.coordinate.called_once_with(self.base_uri, self.adapter, self.dc,
101+
self.token))
102+
96103
def test_session_initialization(self):
97104
self.assertTrue(
98105
self.session.called_once_with(self.base_uri, self.adapter, self.dc,
@@ -118,6 +125,9 @@ def test_event_property(self):
118125
def test_health_property(self):
119126
self.assertEqual(self.consul.health, self.consul._health)
120127

128+
def test_coordinate_property(self):
129+
self.assertEqual(self.consul.coordinate, self.consul._coordinate)
130+
121131
def test_kv_property(self):
122132
self.assertEqual(self.consul.kv, self.consul._kv)
123133

tests/coordinate_tests.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from . import base
2+
3+
class TestCoordinate(base.TestCase):
4+
def test_coordinate(self):
5+
coordinates = self.consul.coordinate.nodes()
6+
self.assertIsInstance(coordinates, list)

0 commit comments

Comments
 (0)