Skip to content

Commit 8881cd8

Browse files
authored
Merge pull request splunk#442 from splunk/optional-retry
Optional retries feature added
2 parents ce390c6 + 7acd49a commit 8881cd8

File tree

4 files changed

+64
-23
lines changed

4 files changed

+64
-23
lines changed

splunklib/binding.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import logging
3131
import socket
3232
import ssl
33+
import sys
34+
import time
3335
from base64 import b64encode
3436
from contextlib import contextmanager
3537
from datetime import datetime
@@ -452,6 +454,12 @@ class Context(object):
452454
:type splunkToken: ``string``
453455
:param headers: List of extra HTTP headers to send (optional).
454456
:type headers: ``list`` of 2-tuples.
457+
:param retires: Number of retries for each HTTP connection (optional, the default is 0).
458+
NOTE THAT THIS MAY INCREASE THE NUMBER OF ROUND TRIP CONNECTIONS TO THE SPLUNK SERVER AND BLOCK THE
459+
CURRENT THREAD WHILE RETRYING.
460+
:type retries: ``int``
461+
:param retryDelay: How long to wait between connection attempts if `retries` > 0 (optional, defaults to 10s).
462+
:type retryDelay: ``int`` (in seconds)
455463
:param handler: The HTTP request handler (optional).
456464
:returns: A ``Context`` instance.
457465
@@ -469,7 +477,8 @@ class Context(object):
469477
"""
470478
def __init__(self, handler=None, **kwargs):
471479
self.http = HttpLib(handler, kwargs.get("verify", False), key_file=kwargs.get("key_file"),
472-
cert_file=kwargs.get("cert_file"), context=kwargs.get("context")) # Default to False for backward compat
480+
cert_file=kwargs.get("cert_file"), context=kwargs.get("context"), # Default to False for backward compat
481+
retries=kwargs.get("retries", 0), retryDelay=kwargs.get("retryDelay", 10))
473482
self.token = kwargs.get("token", _NoAuthenticationToken)
474483
if self.token is None: # In case someone explicitly passes token=None
475484
self.token = _NoAuthenticationToken
@@ -1153,12 +1162,14 @@ class HttpLib(object):
11531162
11541163
If using the default handler, SSL verification can be disabled by passing verify=False.
11551164
"""
1156-
def __init__(self, custom_handler=None, verify=False, key_file=None, cert_file=None, context=None):
1165+
def __init__(self, custom_handler=None, verify=False, key_file=None, cert_file=None, context=None, retries=0, retryDelay=10):
11571166
if custom_handler is None:
11581167
self.handler = handler(verify=verify, key_file=key_file, cert_file=cert_file, context=context)
11591168
else:
11601169
self.handler = custom_handler
11611170
self._cookies = {}
1171+
self.retries = retries
1172+
self.retryDelay = retryDelay
11621173

11631174
def delete(self, url, headers=None, **kwargs):
11641175
"""Sends a DELETE request to a URL.
@@ -1272,7 +1283,16 @@ def request(self, url, message, **kwargs):
12721283
its structure).
12731284
:rtype: ``dict``
12741285
"""
1275-
response = self.handler(url, message, **kwargs)
1286+
while True:
1287+
try:
1288+
response = self.handler(url, message, **kwargs)
1289+
break
1290+
except Exception:
1291+
if self.retries <= 0:
1292+
raise
1293+
else:
1294+
time.sleep(self.retryDelay)
1295+
self.retries -= 1
12761296
response = record(response)
12771297
if 400 <= response.status:
12781298
raise HTTPError(response)

splunklib/client.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,11 @@ def connect(**kwargs):
323323
:type username: ``string``
324324
:param `password`: The password for the Splunk account.
325325
:type password: ``string``
326+
:param retires: Number of retries for each HTTP connection (optional, the default is 0).
327+
NOTE THAT THIS MAY INCREASE THE NUMBER OF ROUND TRIP CONNECTIONS TO THE SPLUNK SERVER.
328+
:type retries: ``int``
329+
:param retryDelay: How long to wait between connection attempts if `retries` > 0 (optional, defaults to 10s).
330+
:type retryDelay: ``int`` (in seconds)
326331
:param `context`: The SSLContext that can be used when setting verify=True (optional)
327332
:type context: ``SSLContext``
328333
:return: An initialized :class:`Service` connection.
@@ -391,6 +396,11 @@ class Service(_BaseService):
391396
:param `password`: The password, which is used to authenticate the Splunk
392397
instance.
393398
:type password: ``string``
399+
:param retires: Number of retries for each HTTP connection (optional, the default is 0).
400+
NOTE THAT THIS MAY INCREASE THE NUMBER OF ROUND TRIP CONNECTIONS TO THE SPLUNK SERVER.
401+
:type retries: ``int``
402+
:param retryDelay: How long to wait between connection attempts if `retries` > 0 (optional, defaults to 10s).
403+
:type retryDelay: ``int`` (in seconds)
394404
:return: A :class:`Service` instance.
395405
396406
**Example**::

tests/test_service.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,13 @@ def test_capabilities(self):
3636
capabilities = self.service.capabilities
3737
self.assertTrue(isinstance(capabilities, list))
3838
self.assertTrue(all([isinstance(c, str) for c in capabilities]))
39-
self.assertTrue('change_own_password' in capabilities) # This should always be there...
39+
self.assertTrue('change_own_password' in capabilities) # This should always be there...
4040

4141
def test_info(self):
4242
info = self.service.info
4343
keys = ["build", "cpu_arch", "guid", "isFree", "isTrial", "licenseKeys",
44-
"licenseSignature", "licenseState", "master_guid", "mode",
45-
"os_build", "os_name", "os_version", "serverName", "version"]
44+
"licenseSignature", "licenseState", "master_guid", "mode",
45+
"os_build", "os_name", "os_version", "serverName", "version"]
4646
for key in keys:
4747
self.assertTrue(key in list(info.keys()))
4848

@@ -74,25 +74,25 @@ def test_app_namespace(self):
7474

7575
def test_owner_wildcard(self):
7676
kwargs = self.opts.kwargs.copy()
77-
kwargs.update({ 'app': "search", 'owner': "-" })
77+
kwargs.update({'app': "search", 'owner': "-"})
7878
service_ns = client.connect(**kwargs)
7979
service_ns.apps.list()
8080

8181
def test_default_app(self):
8282
kwargs = self.opts.kwargs.copy()
83-
kwargs.update({ 'app': None, 'owner': "admin" })
83+
kwargs.update({'app': None, 'owner': "admin"})
8484
service_ns = client.connect(**kwargs)
8585
service_ns.apps.list()
8686

8787
def test_app_wildcard(self):
8888
kwargs = self.opts.kwargs.copy()
89-
kwargs.update({ 'app': "-", 'owner': "admin" })
89+
kwargs.update({'app': "-", 'owner': "admin"})
9090
service_ns = client.connect(**kwargs)
9191
service_ns.apps.list()
9292

9393
def test_user_namespace(self):
9494
kwargs = self.opts.kwargs.copy()
95-
kwargs.update({ 'app': "search", 'owner': "admin" })
95+
kwargs.update({'app': "search", 'owner': "admin"})
9696
service_ns = client.connect(**kwargs)
9797
service_ns.apps.list()
9898

@@ -114,7 +114,7 @@ def test_parse_fail(self):
114114
def test_restart(self):
115115
service = client.connect(**self.opts.kwargs)
116116
self.service.restart(timeout=300)
117-
service.login() # Make sure we are awake
117+
service.login() # Make sure we are awake
118118

119119
def test_read_outputs_with_type(self):
120120
name = testlib.tmpname()
@@ -138,7 +138,7 @@ def test_splunk_version(self):
138138
for p in v:
139139
self.assertTrue(isinstance(p, int) and p >= 0)
140140

141-
for version in [(4,3,3), (5,), (5,0,1)]:
141+
for version in [(4, 3, 3), (5,), (5, 0, 1)]:
142142
with self.fake_splunk_version(version):
143143
self.assertEqual(version, self.service.splunk_version)
144144

@@ -167,15 +167,15 @@ def _create_unauthenticated_service(self):
167167
'scheme': self.opts.kwargs['scheme']
168168
})
169169

170-
#To check the HEC event endpoint using Endpoint instance
170+
# To check the HEC event endpoint using Endpoint instance
171171
def test_hec_event(self):
172172
import json
173173
service_hec = client.connect(host='localhost', scheme='https', port=8088,
174174
token="11111111-1111-1111-1111-1111111111113")
175175
event_collector_endpoint = client.Endpoint(service_hec, "/services/collector/event")
176176
msg = {"index": "main", "event": "Hello World"}
177177
response = event_collector_endpoint.post("", body=json.dumps(msg))
178-
self.assertEqual(response.status,200)
178+
self.assertEqual(response.status, 200)
179179

180180

181181
class TestCookieAuthentication(unittest.TestCase):
@@ -287,6 +287,7 @@ def test_login_with_multiple_cookies(self):
287287
service2.login()
288288
self.assertEqual(service2.apps.get().status, 200)
289289

290+
290291
class TestSettings(testlib.SDKTestCase):
291292
def test_read_settings(self):
292293
settings = self.service.settings
@@ -316,6 +317,7 @@ def test_update_settings(self):
316317
self.assertEqual(updated, original)
317318
self.restartSplunk()
318319

320+
319321
class TestTrailing(unittest.TestCase):
320322
template = '/servicesNS/boris/search/another/path/segment/that runs on'
321323

@@ -329,19 +331,21 @@ def test_no_args_is_identity(self):
329331
self.assertEqual(self.template, client._trailing(self.template))
330332

331333
def test_trailing_with_one_arg_works(self):
332-
self.assertEqual('boris/search/another/path/segment/that runs on', client._trailing(self.template, 'ervicesNS/'))
334+
self.assertEqual('boris/search/another/path/segment/that runs on',
335+
client._trailing(self.template, 'ervicesNS/'))
333336

334337
def test_trailing_with_n_args_works(self):
335338
self.assertEqual(
336339
'another/path/segment/that runs on',
337340
client._trailing(self.template, 'servicesNS/', '/', '/')
338341
)
339342

343+
340344
class TestEntityNamespacing(testlib.SDKTestCase):
341345
def test_proper_namespace_with_arguments(self):
342346
entity = self.service.apps['search']
343-
self.assertEqual((None,None,"global"), entity._proper_namespace(sharing="global"))
344-
self.assertEqual((None,"search","app"), entity._proper_namespace(sharing="app", app="search"))
347+
self.assertEqual((None, None, "global"), entity._proper_namespace(sharing="global"))
348+
self.assertEqual((None, "search", "app"), entity._proper_namespace(sharing="app", app="search"))
345349
self.assertEqual(
346350
("admin", "search", "user"),
347351
entity._proper_namespace(sharing="user", app="search", owner="admin")
@@ -360,6 +364,7 @@ def test_proper_namespace_with_service_namespace(self):
360364
self.service.namespace.sharing)
361365
self.assertEqual(namespace, entity._proper_namespace())
362366

367+
363368
if __name__ == "__main__":
364369
try:
365370
import unittest2 as unittest

tests/testlib.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121

2222
import sys
2323
from splunklib import six
24+
2425
# Run the test suite on the SDK without installing it.
2526
sys.path.insert(0, '../')
2627
sys.path.insert(0, '../examples')
2728

2829
import splunklib.client as client
2930
from time import sleep
3031
from datetime import datetime, timedelta
32+
3133
try:
3234
import unittest2 as unittest
3335
except ImportError:
@@ -43,17 +45,21 @@
4345
import time
4446

4547
import logging
48+
4649
logging.basicConfig(
4750
filename='test.log',
4851
level=logging.DEBUG,
4952
format="%(asctime)s:%(levelname)s:%(message)s")
5053

54+
5155
class NoRestartRequiredError(Exception):
5256
pass
5357

58+
5459
class WaitTimedOutError(Exception):
5560
pass
5661

62+
5763
def to_bool(x):
5864
if x == '1':
5965
return True
@@ -64,7 +70,7 @@ def to_bool(x):
6470

6571

6672
def tmpname():
67-
name = 'delete-me-' + str(os.getpid()) + str(time.time()).replace('.','-')
73+
name = 'delete-me-' + str(os.getpid()) + str(time.time()).replace('.', '-')
6874
return name
6975

7076

@@ -77,7 +83,7 @@ def wait(predicate, timeout=60, pause_time=0.5):
7783
logging.debug("wait timed out after %d seconds", timeout)
7884
raise WaitTimedOutError
7985
sleep(pause_time)
80-
logging.debug("wait finished after %s seconds", datetime.now()-start)
86+
logging.debug("wait finished after %s seconds", datetime.now() - start)
8187

8288

8389
class SDKTestCase(unittest.TestCase):
@@ -94,7 +100,7 @@ def assertEventuallyTrue(self, predicate, timeout=30, pause_time=0.5,
94100
logging.debug("wait timed out after %d seconds", timeout)
95101
self.fail(timeout_message)
96102
sleep(pause_time)
97-
logging.debug("wait finished after %s seconds", datetime.now()-start)
103+
logging.debug("wait finished after %s seconds", datetime.now() - start)
98104

99105
def check_content(self, entity, **kwargs):
100106
for k, v in six.iteritems(kwargs):
@@ -163,12 +169,11 @@ def fake_splunk_version(self, version):
163169
finally:
164170
self.service._splunk_version = original_version
165171

166-
167172
def install_app_from_collection(self, name):
168173
collectionName = 'sdkappcollection'
169174
if collectionName not in self.service.apps:
170175
raise ValueError("sdk-test-application not installed in splunkd")
171-
appPath = self.pathInApp(collectionName, ["build", name+".tar"])
176+
appPath = self.pathInApp(collectionName, ["build", name + ".tar"])
172177
kwargs = {"update": True, "name": appPath, "filename": True}
173178

174179
try:
@@ -233,14 +238,15 @@ def restartSplunk(self, timeout=240):
233238
@classmethod
234239
def setUpClass(cls):
235240
cls.opts = parse([], {}, ".env")
236-
241+
cls.opts.kwargs.update({'retries': 3})
237242
# Before we start, make sure splunk doesn't need a restart.
238243
service = client.connect(**cls.opts.kwargs)
239244
if service.restart_required:
240245
service.restart(timeout=120)
241246

242247
def setUp(self):
243248
unittest.TestCase.setUp(self)
249+
self.opts.kwargs.update({'retries': 3})
244250
self.service = client.connect(**self.opts.kwargs)
245251
# If Splunk is in a state requiring restart, go ahead
246252
# and restart. That way we'll be sane for the rest of

0 commit comments

Comments
 (0)