Skip to content

Commit 7070ca0

Browse files
author
Philippe Tang
committed
[DVPL-10898] Update get/post & Jobs
1 parent ce8d09c commit 7070ca0

File tree

4 files changed

+76
-46
lines changed

4 files changed

+76
-46
lines changed

splunklib/client.py

Lines changed: 45 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -559,8 +559,8 @@ def parse(self, query, **kwargs):
559559
:return: A semantic map of the parsed search query.
560560
"""
561561
if self.splunk_version >= (9,):
562-
return self.get("search/v2/parser", q=query, **kwargs)
563-
return self.post("search/parser", q=query, **kwargs)
562+
return self.post("search/v2/parser", q=query, **kwargs)
563+
return self.get("search/parser", q=query, **kwargs)
564564

565565
def restart(self, timeout=None):
566566
"""Restarts this Splunk instance.
@@ -793,6 +793,13 @@ def get(self, path_segment="", owner=None, app=None, sharing=None, **query):
793793
app=app, sharing=sharing)
794794
# ^-- This was "%s%s" % (self.path, path_segment).
795795
# That doesn't work, because self.path may be UrlEncoded.
796+
797+
# Search API v2 fallback to v1:
798+
# - In v2, /results_preview, /events and /results do not support search params.
799+
# - Fallback from v2 to v1 if Splunk Version is < 9.
800+
if (PATH_JOBS_V2 in path and 'search' in query and path.endswith(tuple(["results_preview", "events", "results"]))) or self.service.splunk_version < (9,):
801+
path = path.replace(PATH_JOBS_V2, PATH_JOBS)
802+
796803
return self.service.get(path,
797804
owner=owner, app=app, sharing=sharing,
798805
**query)
@@ -845,13 +852,20 @@ def post(self, path_segment="", owner=None, app=None, sharing=None, **query):
845852
apps.get('nonexistant/path') # raises HTTPError
846853
s.logout()
847854
apps.get() # raises AuthenticationError
848-
"""
855+
"""
849856
if path_segment.startswith('/'):
850857
path = path_segment
851858
else:
852859
if not self.path.endswith('/') and path_segment != "":
853860
self.path = self.path + '/'
854861
path = self.service._abspath(self.path + path_segment, owner=owner, app=app, sharing=sharing)
862+
863+
# Search API v2 fallback to v1:
864+
# - In v2, /results_preview, /events and /results do not support search params.
865+
# - Fallback from v2 to v1 if Splunk Version is < 9.
866+
if (PATH_JOBS_V2 in path and 'search' in query and path.endswith(tuple(["results_preview", "events", "results"]))) or self.service.splunk_version < (9,):
867+
path = path.replace(PATH_JOBS_V2, PATH_JOBS)
868+
855869
return self.service.post(path, owner=owner, app=app, sharing=sharing, **query)
856870

857871

@@ -2661,14 +2675,17 @@ def oneshot(self, path, **kwargs):
26612675

26622676

26632677
class Job(Entity):
2664-
26652678
"""This class represents a search job."""
2666-
def __init__(self, service, sid, defaultPath, **kwargs):
2667-
2668-
# Don't provide a path, allow it to be dynamically generated
2669-
Entity.__init__(self, service, '', skip_refresh=True, **kwargs)
2679+
def __init__(self, service, sid, **kwargs):
2680+
# Default to v2 in Splunk Version 9+
2681+
path = "{path}{sid}"
2682+
path = path.format(path=PATH_JOBS_V2, sid=sid)
2683+
# Fallback to v1 if Splunk Version < 9
2684+
if service.splunk_version < (9,):
2685+
path = path.format(path=PATH_JOBS, sid=sid)
2686+
2687+
Entity.__init__(self, service, path, skip_refresh=True, **kwargs)
26702688
self.sid = sid
2671-
self.defaultPath = defaultPath + sid + '/'
26722689

26732690
# The Job entry record is returned at the root of the response
26742691
def _load_atom_entry(self, response):
@@ -2680,7 +2697,7 @@ def cancel(self):
26802697
:return: The :class:`Job`.
26812698
"""
26822699
try:
2683-
self.post(self.defaultPath + "control", action="cancel")
2700+
self.post("control", action="cancel")
26842701
except HTTPError as he:
26852702
if he.status == 404:
26862703
# The job has already been cancelled, so
@@ -2695,7 +2712,7 @@ def disable_preview(self):
26952712
26962713
:return: The :class:`Job`.
26972714
"""
2698-
self.post(self.defaultPath + "control", action="disablepreview")
2715+
self.post("control", action="disablepreview")
26992716
return self
27002717

27012718
def enable_preview(self):
@@ -2705,7 +2722,7 @@ def enable_preview(self):
27052722
27062723
:return: The :class:`Job`.
27072724
"""
2708-
self.post(self.defaultPath + "control", action="enablepreview")
2725+
self.post("control", action="enablepreview")
27092726
return self
27102727

27112728
def events(self, **kwargs):
@@ -2720,19 +2737,14 @@ def events(self, **kwargs):
27202737
:return: The ``InputStream`` IO handle to this job's events.
27212738
"""
27222739
kwargs['segmentation'] = kwargs.get('segmentation', 'none')
2723-
path = "{path}{sid}/events"
2724-
2725-
# Splunk version doesn't support v2 (pre-9.0) or the 'search' arg is included (which is v1 specific)
2726-
if self.splunk_version < (9,) or 'search' in kwargs:
2727-
return self.get(path.format(PATH_JOBS, self.sid), **kwargs).body
2728-
return self.get(path.format(PATH_JOBS_V2, self.sid), **kwargs).body
2740+
return self.get("events", **kwargs).body
27292741

27302742
def finalize(self):
27312743
"""Stops the job and provides intermediate results for retrieval.
27322744
27332745
:return: The :class:`Job`.
27342746
"""
2735-
self.post(self.defaultPath + "control", action="finalize")
2747+
self.post("control", action="finalize")
27362748
return self
27372749

27382750
def is_done(self):
@@ -2753,7 +2765,7 @@ def is_ready(self):
27532765
:rtype: ``boolean``
27542766
27552767
"""
2756-
response = self.get(self.defaultPath)
2768+
response = self.get()
27572769
if response.status == 204:
27582770
return False
27592771
self._state = self.read(response)
@@ -2774,7 +2786,7 @@ def pause(self):
27742786
27752787
:return: The :class:`Job`.
27762788
"""
2777-
self.post(self.defaultPath + "control", action="pause")
2789+
self.post("control", action="pause")
27782790
return self
27792791

27802792
def results(self, **query_params):
@@ -2813,12 +2825,7 @@ def results(self, **query_params):
28132825
:return: The ``InputStream`` IO handle to this job's results.
28142826
"""
28152827
query_params['segmentation'] = query_params.get('segmentation', 'none')
2816-
path = "{path}{sid}/results"
2817-
2818-
# Splunk version doesn't support v2 (pre-9.0) or the 'search' arg is included (which is v1 specific)
2819-
if self.splunk_version < (9,) or 'search' in query_params:
2820-
return self.get(path.format(PATH_JOBS, self.sid), **query_params).body
2821-
return self.get(path.format(PATH_JOBS_V2, self.sid), **query_params).body
2828+
return self.get("results", **query_params).body
28222829

28232830
def preview(self, **query_params):
28242831
"""Returns a streaming handle to this job's preview search results.
@@ -2859,12 +2866,7 @@ def preview(self, **query_params):
28592866
:return: The ``InputStream`` IO handle to this job's preview results.
28602867
"""
28612868
query_params['segmentation'] = query_params.get('segmentation', 'none')
2862-
path = "{path}{sid}/results_preview"
2863-
2864-
# Splunk version doesn't support v2 (pre-9.0) or the 'search' arg is included (which is v1 specific)
2865-
if self.splunk_version < (9,) or 'search' in query_params:
2866-
return self.get(path.format(PATH_JOBS, self.sid), **query_params).body
2867-
return self.get(path.format(PATH_JOBS_V2, self.sid), **query_params).body
2869+
return self.get("results_preview", **query_params).body
28682870

28692871
def searchlog(self, **kwargs):
28702872
"""Returns a streaming handle to this job's search log.
@@ -2877,7 +2879,7 @@ def searchlog(self, **kwargs):
28772879
28782880
:return: The ``InputStream`` IO handle to this job's search log.
28792881
"""
2880-
return self.get(self.defaultPath + "search.log", **kwargs).body
2882+
return self.get("search.log", **kwargs).body
28812883

28822884
def set_priority(self, value):
28832885
"""Sets this job's search priority in the range of 0-10.
@@ -2890,7 +2892,7 @@ def set_priority(self, value):
28902892
28912893
:return: The :class:`Job`.
28922894
"""
2893-
self.post(self.defaultPath + 'control', action="setpriority", priority=value)
2895+
self.post('control', action="setpriority", priority=value)
28942896
return self
28952897

28962898
def summary(self, **kwargs):
@@ -2904,7 +2906,7 @@ def summary(self, **kwargs):
29042906
29052907
:return: The ``InputStream`` IO handle to this job's summary.
29062908
"""
2907-
return self.get(self.defaultPath + "summary", **kwargs).body
2909+
return self.get("summary", **kwargs).body
29082910

29092911
def timeline(self, **kwargs):
29102912
"""Returns a streaming handle to this job's timeline results.
@@ -2917,15 +2919,15 @@ def timeline(self, **kwargs):
29172919
29182920
:return: The ``InputStream`` IO handle to this job's timeline.
29192921
"""
2920-
return self.get(self.defaultPath + "timeline", **kwargs).body
2922+
return self.get("timeline", **kwargs).body
29212923

29222924
def touch(self):
29232925
"""Extends the expiration time of the search to the current time (now) plus
29242926
the time-to-live (ttl) value.
29252927
29262928
:return: The :class:`Job`.
29272929
"""
2928-
self.post(self.defaultPath + "control", action="touch")
2930+
self.post("control", action="touch")
29292931
return self
29302932

29312933
def set_ttl(self, value):
@@ -2937,15 +2939,15 @@ def set_ttl(self, value):
29372939
29382940
:return: The :class:`Job`.
29392941
"""
2940-
self.post(self.defaultPath + "control", action="setttl", ttl=value)
2942+
self.post("control", action="setttl", ttl=value)
29412943
return self
29422944

29432945
def unpause(self):
29442946
"""Resumes the current search, if paused.
29452947
29462948
:return: The :class:`Job`.
29472949
"""
2948-
self.post(self.defaultPath + "control", action="unpause")
2950+
self.post("control", action="unpause")
29492951
return self
29502952

29512953

@@ -2954,7 +2956,7 @@ class Jobs(Collection):
29542956
collection using :meth:`Service.jobs`."""
29552957
def __init__(self, service):
29562958
# Splunk 9 introduces the v2 endpoint
2957-
if self.splunk_version >= (9,):
2959+
if service.splunk_version >= (9,):
29582960
path = PATH_JOBS_V2
29592961
else:
29602962
path = PATH_JOBS
@@ -2995,7 +2997,7 @@ def create(self, query, **kwargs):
29952997
raise TypeError("Cannot specify exec_mode=oneshot; use the oneshot method instead.")
29962998
response = self.post(search=query, **kwargs)
29972999
sid = _load_sid(response)
2998-
return Job(self.service, sid, self.path)
3000+
return Job(self.service, sid)
29993001

30003002
def export(self, query, **params):
30013003
"""Runs a search and immediately starts streaming preview events. This method returns a streaming handle to
@@ -3796,4 +3798,4 @@ def batch_save(self, *documents):
37963798

37973799
data = json.dumps(documents)
37983800

3799-
return json.loads(self._post('batch_save', headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read().decode('utf-8'))
3801+
return json.loads(self._post('batch_save', headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read().decode('utf-8'))

tests/test_job.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,29 @@ def test_search_invalid_query_as_json(self):
377377
except Exception as e:
378378
self.fail("Got some unexpected error. %s" % e.message)
379379

380+
def test_v1_job_fallback(self):
381+
self.assertEventuallyTrue(self.job.is_done)
382+
self.assertLessEqual(int(self.job['eventCount']), 3)
383+
384+
preview_stream = self.job.preview(output_mode='json', search='| head 1')
385+
preview_r = results.JSONResultsReader(preview_stream)
386+
self.assertFalse(preview_r.is_preview)
387+
388+
events_stream = self.job.events(output_mode='json', search='| head 1')
389+
events_r = results.JSONResultsReader(events_stream)
390+
391+
results_stream = self.job.results(output_mode='json', search='| head 1')
392+
results_r = results.JSONResultsReader(results_stream)
393+
394+
n_events = len([x for x in events_r if isinstance(x, dict)])
395+
n_preview = len([x for x in preview_r if isinstance(x, dict)])
396+
n_results = len([x for x in results_r if isinstance(x, dict)])
397+
398+
# Fallback test for Splunk Version 9+
399+
if self.service.splunk_version[0] >= 9:
400+
self.assertGreaterEqual(9, self.service.splunk_version[0])
401+
self.assertEqual(n_events, n_preview, n_results)
402+
380403

381404
class TestResultsReader(unittest.TestCase):
382405
def test_results_reader(self):

tests/test_service.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ def test_parse(self):
102102
# objectified form of the results, but for now there's
103103
# nothing to test but a good response code.
104104
response = self.service.parse('search * abc="def" | dedup abc')
105+
106+
# Splunk Version 9+ using API v2: search/v2/parser
107+
if self.service.splunk_version[0] >= 9:
108+
self.assertGreaterEqual(9, self.service.splunk_version[0])
109+
105110
self.assertEqual(response.status, 200)
106111

107112
def test_parse_fail(self):

tests/testlib.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,9 @@ def fake_splunk_version(self, version):
165165

166166

167167
def install_app_from_collection(self, name):
168-
collectionName = 'sdkappcollection'
168+
collectionName = 'sdk-app-collection'
169169
if collectionName not in self.service.apps:
170-
raise ValueError("sdk-test-application not installed in splunkd")
170+
raise ValueError("sdk-app-collection not installed in splunkd")
171171
appPath = self.pathInApp(collectionName, ["build", name+".tar"])
172172
kwargs = {"update": True, "name": appPath, "filename": True}
173173

@@ -181,7 +181,7 @@ def install_app_from_collection(self, name):
181181
self.installedApps.append(name)
182182

183183
def app_collection_installed(self):
184-
collectionName = 'sdkappcollection'
184+
collectionName = 'sdk-app-collection'
185185
return collectionName in self.service.apps
186186

187187
def pathInApp(self, appName, pathComponents):

0 commit comments

Comments
 (0)