Skip to content

Commit a7d7e0d

Browse files
committed
Add more agent endpoints, break out tests
- Add common validation of check options - Fix pep8 violations - Changed consulate.Consul.agent.checks to return a dict instead of list - Changed consulate.Consul.agent.services to return a dict instead of list - Renamed the check argument to consulate.Consul.agent.service.register to script - Renamed the httpcheck argument to consulate.Consul.agent.service.register to http - Break out tests and add full coverage
1 parent fd22137 commit a7d7e0d

4 files changed

Lines changed: 412 additions & 62 deletions

File tree

consulate/api/agent.py

Lines changed: 154 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,42 @@
44
"""
55
from consulate.api import base
66

7+
_TOKENS = [
8+
'acl_token',
9+
'acl_agent_token',
10+
'acl_agent_master_token',
11+
'acl_replication_token'
12+
]
13+
14+
15+
def _validate_check(script, http, interval, ttl):
16+
"""Validate the check arguments passed into check or service creation.
17+
18+
:param script: The optional script to run in the check
19+
:type script: str or None
20+
:param http: The optional HTTP endpoint to use in the check
21+
:type http: str or None
22+
:param interval: The optional check interval to specify
23+
:type interval: int or None
24+
:param ttl: The optional TTL interval for the check
25+
:type ttl: int or None
26+
:raises: ValueError
27+
28+
"""
29+
if script is not None and http is not None:
30+
raise ValueError('Can not specify script and http in the same check')
31+
if (script is not None or http is not None) and ttl is not None:
32+
raise ValueError('Can not specify a script or http check and ttl')
33+
elif (script or http) and interval is None:
34+
raise ValueError(
35+
'An interval is required for check scripts and '
36+
'http checks.')
37+
elif interval is not None and \
38+
(not isinstance(interval, int) or interval < 1):
39+
raise ValueError('interval must be a positive integer')
40+
elif ttl is not None and (not isinstance(ttl, int) or ttl < 1):
41+
raise ValueError('ttl must be a positive integer')
42+
743

844
class Agent(base.Endpoint):
945
"""The Consul agent is the core process of Consul. The agent maintains
@@ -24,7 +60,8 @@ def __init__(self, uri, adapter, datacenter=None, token=None):
2460
"""
2561
super(Agent, self).__init__(uri, adapter, datacenter, token)
2662
self.check = Agent.Check(self._base_uri, adapter, datacenter, token)
27-
self.service = Agent.Service(self._base_uri, adapter, datacenter, token)
63+
self.service = Agent.Service(
64+
self._base_uri, adapter, datacenter, token)
2865

2966
class Check(base.Endpoint):
3067
"""One of the primary roles of the agent is the management of system
@@ -94,21 +131,7 @@ def register(self, name,
94131
:raises: ValueError
95132
96133
"""
97-
# Validate the parameters
98-
if script and not interval:
99-
raise ValueError('Must specify interval when using script')
100-
elif script and ttl:
101-
raise ValueError('Can not specify script and ttl together')
102-
103-
if http and not interval:
104-
raise ValueError('Must specify interval when using http')
105-
elif http and ttl:
106-
raise ValueError('Can not specify http and ttl together')
107-
108-
if http and script:
109-
raise ValueError('Can not specify script and http together')
110-
111-
# Register the check
134+
_validate_check(script, http, interval, ttl)
112135
return self._put_no_response_body(['register'], None, {
113136
'ID': check_id,
114137
'Name': name,
@@ -124,39 +147,49 @@ def deregister(self, check_id):
124147
of deregistering the check with the Catalog.
125148
126149
:param str check_id: The check id
150+
:rtype: bool
127151
128152
"""
129153
return self._put_no_response_body(['deregister', check_id])
130154

131-
def ttl_pass(self, check_id):
155+
def ttl_pass(self, check_id, note=None):
132156
"""This endpoint is used with a check that is of the TTL type.
133157
When this endpoint is accessed, the status of the check is set to
134158
"passing", and the TTL clock is reset.
135159
136160
:param str check_id: The check id
161+
:param str note: Note to include with the check pass
162+
:rtype: bool
137163
138164
"""
139-
return self._put_no_response_body(['pass', check_id])
165+
return self._put_no_response_body(
166+
['pass', check_id], {'note': note} if note else None)
140167

141-
def ttl_warn(self, check_id):
168+
def ttl_warn(self, check_id, note=None):
142169
"""This endpoint is used with a check that is of the TTL type.
143170
When this endpoint is accessed, the status of the check is set
144171
to "warning", and the TTL clock is reset.
145172
146173
:param str check_id: The check id
174+
:param str note: Note to include with the check warning
175+
:rtype: bool
147176
148177
"""
149-
return self._put_no_response_body(['warn', check_id])
178+
return self._put_no_response_body(
179+
['warn', check_id], {'note': note} if note else None)
150180

151-
def ttl_fail(self, check_id):
181+
def ttl_fail(self, check_id, note=None):
152182
"""This endpoint is used with a check that is of the TTL type.
153183
When this endpoint is accessed, the status of the check is set
154184
to "critical", and the TTL clock is reset.
155185
156186
:param str check_id: The check id
187+
:param str note: Note to include with the check failure
188+
:rtype: bool
157189
158190
"""
159-
return self._put_no_response_body(['fail', check_id])
191+
return self._put_no_response_body(
192+
['fail', check_id], {'note': note} if note else None)
160193

161194
class Service(base.Endpoint):
162195
"""One of the main goals of service discovery is to provide a catalog
@@ -168,51 +201,46 @@ class Service(base.Endpoint):
168201
the HTTP interface.
169202
170203
"""
171-
CHECK_EXCEPTION = 'check must be a tuple of script, interval, and ttl'
172-
173204
def register(self, name,
174205
service_id=None,
175206
address=None,
176207
port=None,
177208
tags=None,
178-
check=None,
209+
script=None,
179210
interval=None,
180211
ttl=None,
181-
httpcheck=None):
212+
http=None,
213+
enable_tag_override=None):
182214
"""Add a new service to the local agent.
183215
184216
:param str name: The name of the service
185217
:param str service_id: The id for the service (optional)
186218
:param str address: The service IP address
187219
:param int port: The service port
188220
:param list tags: A list of tags for the service
189-
:param str check: The path to the check script to run
190-
:param str interval: The check execution interval
191-
:param str ttl: The TTL for external script check pings
192-
:param str httpcheck: An URL to check every interval
221+
:param str script: Optional script to execute to check service
222+
:param int interval: The check execution interval
223+
:param int ttl: The TTL for external script check pings
224+
:param str http: An URL to check every interval
225+
:param bool enable_tag_override: Toggle the tag override feature
193226
:rtype: bool
194227
:raises: ValueError
195228
196229
"""
197230
# Validate the parameters
198-
if port and not isinstance(port, int):
231+
if port is not None and not isinstance(port, int):
199232
raise ValueError('port must be an integer')
200-
elif tags and not isinstance(tags, list):
233+
elif tags is not None and not isinstance(tags, list):
201234
raise ValueError('tags must be a list of strings')
202-
elif (check or httpcheck) and ttl:
203-
raise ValueError('Can not specify both a check and ttl')
204235

205-
if (check or httpcheck) and not interval:
206-
raise ValueError('An interval is required for check scripts and http checks.')
236+
_validate_check(script, http, interval, ttl)
207237

208238
check_spec = None
209-
if check:
210-
check_spec = {'script': check,
211-
'interval': interval}
212-
elif httpcheck:
213-
check_spec = {'HTTP': httpcheck,
214-
'interval': interval}
215-
elif ttl:
239+
if script is not None:
240+
check_spec = {'script': script, 'interval': interval}
241+
elif http is not None:
242+
check_spec = {'HTTP': http, 'interval': interval}
243+
elif ttl is not None:
216244
check_spec = {'TTL': ttl}
217245

218246
# Build the payload to send to consul
@@ -221,7 +249,8 @@ def register(self, name,
221249
'name': name,
222250
'port': port,
223251
'address': address,
224-
'tags': tags
252+
'tags': tags,
253+
'EnableTagOverride': enable_tag_override
225254
}
226255

227256
if check_spec:
@@ -246,18 +275,18 @@ def deregister(self, service_id):
246275
return self._put_no_response_body(['deregister', service_id])
247276

248277
def checks(self):
249-
"""return the all the checks that are registered with the local agent.
278+
"""Return the all the checks that are registered with the local agent.
250279
These checks were either provided through configuration files, or
251280
added dynamically using the HTTP API. It is important to note that
252281
the checks known by the agent may be different than those reported
253282
by the Catalog. This is usually due to changes being made while there
254283
is no leader elected. The agent performs active anti-entropy, so in
255284
most situations everything will be in sync within a few seconds.
256285
257-
:rtype: list
286+
:rtype: dict
258287
259288
"""
260-
return self._get_list(['checks'])
289+
return self._get(['checks'])
261290

262291
def force_leave(self, node):
263292
"""Instructs the agent to force a node into the left state. If a node
@@ -283,6 +312,21 @@ def join(self, address, wan=False):
283312
query_params = {'wan': 1} if wan else None
284313
return self._put_no_response_body(['join', address], query_params)
285314

315+
def maintenance(self, enable=True, reason=None):
316+
"""Places the agent into or removes the agent from "maintenance mode".
317+
318+
.. versionadded:: 1.0.0
319+
320+
:param bool enable: Enable or disable maintenance. Default: `True`
321+
:param str reason: The reason for the maintenance
322+
:rtype: bool
323+
324+
"""
325+
query_params = {'enable': enable}
326+
if reason:
327+
query_params['reason'] = reason
328+
return self._put_no_response_body(['maintenance'], query_params)
329+
286330
def members(self):
287331
"""Returns the members the agent sees in the cluster gossip pool.
288332
Due to the nature of gossip, this is eventually consistent and the
@@ -294,6 +338,38 @@ def members(self):
294338
"""
295339
return self._get_list(['members'])
296340

341+
def metrics(self):
342+
"""Returns agent's metrics for the most recent finished interval
343+
344+
.. versionadded:: 1.0.0
345+
346+
:rtype: dict
347+
348+
"""
349+
return self._get(['metrics'])
350+
351+
def monitor(self):
352+
"""Iterator over logs from the local agent.
353+
354+
.. versionadded:: 1.0.0
355+
356+
:rtype: iterator
357+
358+
"""
359+
for line in self._get_stream(['monitor']):
360+
yield line
361+
362+
def reload(self):
363+
"""This endpoint instructs the agent to reload its configuration.
364+
Any errors encountered during this process are returned.
365+
366+
.. versionadded:: 1.0.0
367+
368+
:rtype: list
369+
370+
"""
371+
return self._put_response_body(['reload']) or None
372+
297373
def services(self):
298374
"""return the all the services that are registered with the local
299375
agent. These services were either provided through configuration
@@ -304,16 +380,43 @@ def services(self):
304380
anti-entropy, so in most situations everything will be in sync
305381
within a few seconds.
306382
307-
:rtype: list
383+
:rtype: dict
308384
309385
"""
310-
return self._get_list(['services'])
386+
return self._get(['services'])
311387

312388
def self(self):
313389
""" This endpoint is used to return the configuration and member
314390
information of the local agent under the Config key.
315391
316-
:rtype: list
392+
:rtype: dict
393+
394+
"""
395+
return self._get(['self'])
396+
397+
def token(self, name, value):
398+
"""Update the ACL tokens currently in use by the agent. It can be used
399+
to introduce ACL tokens to the agent for the first time, or to update
400+
tokens that were initially loaded from the agent's configuration.
401+
Tokens are not persisted, so will need to be updated again if the agent
402+
is restarted.
403+
404+
Valid names:
405+
406+
- ``acl_token``
407+
- ``acl_agent_token``
408+
- ``acl_agent_master_token``
409+
- ``acl_replication_token``
410+
411+
.. versionadded:: 1.0.0
412+
413+
:param str name: One of the valid token names.
414+
:param str value: The new token value
415+
:rtype: bool
416+
:raises: ValueError
317417
318418
"""
319-
return self._get_list(['self'])
419+
if name not in _TOKENS:
420+
raise ValueError('Invalid token name: {}'.format(name))
421+
return self._put_no_response_body(
422+
['token', name], {}, {'Token': value})

consulate/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def response_ok(response, raise_on_404=True):
4444
if response.status_code == 200:
4545
return True
4646
elif response.status_code == 400:
47-
raise exceptions.RequestError(
47+
raise exceptions.ClientError(
4848
response.body.decode('utf-8')
4949
if hasattr(response, 'body') else str(response.status_code))
5050
elif response.status_code == 401:

docs/history.rst

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
Version History
22
===============
33
- 1.0.0
4-
- Removed the deprecated (since 0.3) `consulate.Session` handle
5-
- Fixed run_once wrong args + subprocess parsing (#65) - Anthony Scalisi
6-
- Fixed :meth:`~consulate.Consul.catalog.register` and :meth:`~consulate.Consul.catalog.deregister` (#59)
7-
- Add support for ``flags``, ``cas``, and ``value`` in :meth:`Consulate.kv.acquire_lock` (#63)
8-
- Add ``--pretty`` option to kv backup (#69) - Brian Clark
9-
- Don't try to b64decode null values on kv restore (#68, #70) - Brian Clark
10-
- Raise server-error exception when setting a key fails due to a server error (#67) - Fredric Newberg
11-
- Address Python 2.6 incompatibility with the consulate cli and null data (#62, #61) - Wayne Walker
12-
- New :class:`~consulate.api.lock.Lock` class for easier lock acquisition
13-
- New CLI feature to backup and restore ACLs (#71)
4+
- Breaking Changes
5+
- Removed support for Python 2.6 which has been EOLed since 2013
6+
- Removed the deprecated (since 0.3) `consulate.Session` handle
7+
- Changed :meth:`~consulate.Consul.agent.checks` to return a :data:`dict` instead of a :data:`list`.
8+
- Changed :meth:`~consulate.Consul.agent.services` to return a :data:`dict` instead of a :data:`list`.
9+
- Renamed the ``check`` argument to :meth:`~consulate.Consul.agent.service.register` to ``script``.
10+
- Renamed the ``httpcheck`` argument to :meth:`~consulate.Consul.agent.service.register` to ``http``.
11+
- Other Changes:
12+
- Added :meth:`~consulate.Consul.agent.maintenance`, :meth:`~consulate.Consul.agent.metrics`,
13+
:meth:`~consulate.Consul.agent.monitor`, :meth:`~consulate.Consul.agent.reload`,
14+
:meth:`~consulate.Consul.agent.self`, and :meth:`~consulate.Consul.agent.token`
15+
- Added :meth:`~consulate.Consul.acl.bootstrap` and :meth:`~consulate.Consul.acl.replication`
16+
- Fixed run_once wrong args + subprocess parsing (#65) - Anthony Scalisi
17+
- Fixed :meth:`~consulate.Consul.catalog.register` and :meth:`~consulate.Consul.catalog.deregister` (#59)
18+
- Add support for ``flags``, ``cas``, and ``value`` in :meth:`Consulate.kv.acquire_lock` (#63)
19+
- Add ``--pretty`` option to kv backup (#69) - Brian Clark
20+
- Don't try to b64decode null values on kv restore (#68, #70) - Brian Clark
21+
- Raise server-error exception when setting a key fails due to a server error (#67) - Fredric Newberg
22+
- Address Python 2.6 incompatibility with the consulate cli and null data (#62, #61) - Wayne Walker
23+
- Added :class:`~consulate.api.lock.Lock` class for easier lock acquisition
24+
- New CLI feature to backup and restore ACLs (#71)
1425

1526
- 0.6.0 - released *2015-07-22*
1627
- Added --recurse and --trim to cli kv_get (#58) - Matt Walker

0 commit comments

Comments
 (0)