Skip to content

Commit 78d9610

Browse files
committed
Merge branch 'develop' into py3-code-migration
2 parents c772272 + d764e75 commit 78d9610

File tree

10 files changed

+141
-27
lines changed

10 files changed

+141
-27
lines changed

.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ password=changed!
99
# Access scheme (default: https)
1010
scheme=https
1111
# Your version of Splunk (default: 6.2)
12-
version=8.0
12+
version=9.0
1313
# Bearer token for authentication
1414
#bearerToken="<Bearer-token>"
1515
# Session key for authentication

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
- ubuntu-latest
1414
python: [ 3.7, 3.9]
1515
splunk-version:
16-
- "8.0"
16+
- "8.2"
1717
- "latest"
1818
fail-fast: false
1919

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
# Splunk Enterprise SDK for Python Changelog
22

3+
## Version 1.6.20
4+
5+
### New features and APIs
6+
* [#442](https://github.com/splunk/splunk-sdk-python/pull/442) Optional retries feature added
7+
* [#447](https://github.com/splunk/splunk-sdk-python/pull/447) Create job support for "output_mode:json" [[issue#285](https://github.com/splunk/splunk-sdk-python/issues/285)]
8+
9+
### Bug fixes
10+
* [#449](https://github.com/splunk/splunk-sdk-python/pull/449) Set cookie [[issue#438](https://github.com/splunk/splunk-sdk-python/issues/438)]
11+
* [#460](https://github.com/splunk/splunk-sdk-python/pull/460) Remove restart from client.Entity.disable
12+
13+
### Minor changes
14+
* [#444](https://github.com/splunk/splunk-sdk-python/pull/444) Update tox.ini
15+
* [#446](https://github.com/splunk/splunk-sdk-python/pull/446) Release workflow refactor
16+
* [#448](https://github.com/splunk/splunk-sdk-python/pull/448) Documentation changes
17+
* [#450](https://github.com/splunk/splunk-sdk-python/pull/450) Removed examples and it's references from the SDK
18+
19+
320
## Version 1.6.19
421

522
### New features and APIs

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ If you're seeing some unexpected behavior with this project, please create an [i
1111
1. Version of this project you're using (ex: 1.5.0)
1212
2. Platform version (ex: Windows Server 2012 R2)
1313
3. Framework version (ex: Python 3.7)
14-
4. Splunk Enterprise version (ex: 8.0)
14+
4. Splunk Enterprise version (ex: 9.0)
1515
5. Other relevant information (ex: local/remote environment, Splunk network configuration, standalone or distributed deployment, are load balancers used)
1616

1717
Alternatively, if you have a Splunk question please ask on [Splunk Answers](https://community.splunk.com/t5/Splunk-Development/ct-p/developer-tools).

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
# The Splunk Enterprise Software Development Kit for Python
55

6-
#### Version 1.6.19
6+
#### Version 1.6.20
77

88
The Splunk Enterprise Software Development Kit (SDK) for Python contains library code designed to enable developers to build applications using the Splunk platform.
99

@@ -58,7 +58,7 @@ Install the sources you cloned from GitHub:
5858
You'll need `docker` and `docker-compose` to get up and running using this method.
5959

6060
```
61-
make up SPLUNK_VERSION=8.0
61+
make up SPLUNK_VERSION=9.0
6262
make wait_up
6363
make test
6464
make down
@@ -107,7 +107,7 @@ here is an example of .env file:
107107
# Access scheme (default: https)
108108
scheme=https
109109
# Your version of Splunk Enterprise
110-
version=8.0
110+
version=9.0
111111
# Bearer token for authentication
112112
#bearerToken=<Bearer-token>
113113
# Session key for authentication

splunklib/binding.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,7 @@ def _auth_headers(self):
529529
530530
:returns: A list of 2-tuples containing key and value
531531
"""
532+
header = []
532533
if self.has_cookies():
533534
return [("Cookie", _make_cookie_header(list(self.get_cookies().items())))]
534535
if self.basic and (self.username and self.password):
@@ -1440,7 +1441,7 @@ def request(url, message, **kwargs):
14401441
head = {
14411442
"Content-Length": str(len(body)),
14421443
"Host": host,
1443-
"User-Agent": "splunk-sdk-python/1.6.19",
1444+
"User-Agent": "splunk-sdk-python/1.6.20",
14441445
"Accept": "*/*",
14451446
"Connection": "Close",
14461447
} # defaults

splunklib/client.py

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
import datetime
6363
import json
6464
import logging
65+
import re
6566
import socket
6667
from datetime import datetime, timedelta
6768
from time import sleep
@@ -98,6 +99,7 @@
9899
PATH_INDEXES = "data/indexes/"
99100
PATH_INPUTS = "data/inputs/"
100101
PATH_JOBS = "search/jobs/"
102+
PATH_JOBS_V2 = "search/v2/jobs/"
101103
PATH_LOGGER = "/services/server/logger/"
102104
PATH_MESSAGES = "messages/"
103105
PATH_MODULAR_INPUTS = "data/modular-inputs"
@@ -563,6 +565,8 @@ def parse(self, query, **kwargs):
563565
:type kwargs: ``dict``
564566
:return: A semantic map of the parsed search query.
565567
"""
568+
if self.splunk_version >= (9,):
569+
return self.post("search/v2/parser", q=query, **kwargs)
566570
return self.get("search/parser", q=query, **kwargs)
567571

568572
def restart(self, timeout=None):
@@ -735,6 +739,25 @@ def __init__(self, service, path):
735739
self.service = service
736740
self.path = path
737741

742+
def get_api_version(self, path):
743+
"""Return the API version of the service used in the provided path.
744+
745+
Args:
746+
path (str): A fully-qualified endpoint path (for example, "/services/search/jobs").
747+
748+
Returns:
749+
int: Version of the API (for example, 1)
750+
"""
751+
# Default to v1 if undefined in the path
752+
# For example, "/services/search/jobs" is using API v1
753+
api_version = 1
754+
755+
versionSearch = re.search('(?:servicesNS\/[^/]+\/[^/]+|services)\/[^/]+\/v(\d+)\/', path)
756+
if versionSearch:
757+
api_version = int(versionSearch.group(1))
758+
759+
return api_version
760+
738761
def get(self, path_segment="", owner=None, app=None, sharing=None, **query):
739762
"""Performs a GET operation on the path segment relative to this endpoint.
740763
@@ -797,6 +820,22 @@ def get(self, path_segment="", owner=None, app=None, sharing=None, **query):
797820
app=app, sharing=sharing)
798821
# ^-- This was "%s%s" % (self.path, path_segment).
799822
# That doesn't work, because self.path may be UrlEncoded.
823+
824+
# Get the API version from the path
825+
api_version = self.get_api_version(path)
826+
827+
# Search API v2+ fallback to v1:
828+
# - In v2+, /results_preview, /events and /results do not support search params.
829+
# - Fallback from v2+ to v1 if Splunk Version is < 9.
830+
# if api_version >= 2 and ('search' in query and path.endswith(tuple(["results_preview", "events", "results"])) or self.service.splunk_version < (9,)):
831+
# path = path.replace(PATH_JOBS_V2, PATH_JOBS)
832+
833+
if api_version == 1:
834+
if isinstance(path, UrlEncoded):
835+
path = UrlEncoded(path.replace(PATH_JOBS_V2, PATH_JOBS), skip_encode=True)
836+
else:
837+
path = path.replace(PATH_JOBS_V2, PATH_JOBS)
838+
800839
return self.service.get(path,
801840
owner=owner, app=app, sharing=sharing,
802841
**query)
@@ -856,6 +895,22 @@ def post(self, path_segment="", owner=None, app=None, sharing=None, **query):
856895
if not self.path.endswith('/') and path_segment != "":
857896
self.path = self.path + '/'
858897
path = self.service._abspath(self.path + path_segment, owner=owner, app=app, sharing=sharing)
898+
899+
# Get the API version from the path
900+
api_version = self.get_api_version(path)
901+
902+
# Search API v2+ fallback to v1:
903+
# - In v2+, /results_preview, /events and /results do not support search params.
904+
# - Fallback from v2+ to v1 if Splunk Version is < 9.
905+
# if api_version >= 2 and ('search' in query and path.endswith(tuple(["results_preview", "events", "results"])) or self.service.splunk_version < (9,)):
906+
# path = path.replace(PATH_JOBS_V2, PATH_JOBS)
907+
908+
if api_version == 1:
909+
if isinstance(path, UrlEncoded):
910+
path = UrlEncoded(path.replace(PATH_JOBS_V2, PATH_JOBS), skip_encode=True)
911+
else:
912+
path = path.replace(PATH_JOBS_V2, PATH_JOBS)
913+
859914
return self.service.post(path, owner=owner, app=app, sharing=sharing, **query)
860915

861916

@@ -1082,8 +1137,6 @@ def content(self):
10821137
def disable(self):
10831138
"""Disables the entity at this endpoint."""
10841139
self.post("disable")
1085-
if self.service.restart_required:
1086-
self.service.restart(120)
10871140
return self
10881141

10891142
def enable(self):
@@ -1882,6 +1935,9 @@ def delete(self, username, realm=None):
18821935
:return: The `StoragePassword` collection.
18831936
:rtype: ``self``
18841937
"""
1938+
if self.service.namespace.owner == '-' or self.service.namespace.app == '-':
1939+
raise ValueError("app context must be specified when removing a password.")
1940+
18851941
if realm is None:
18861942
# This case makes the username optional, so
18871943
# the full name can be passed in as realm.
@@ -2645,7 +2701,14 @@ class Job(Entity):
26452701
"""This class represents a search job."""
26462702

26472703
def __init__(self, service, sid, **kwargs):
2648-
path = PATH_JOBS + sid
2704+
# Default to v2 in Splunk Version 9+
2705+
path = "{path}{sid}"
2706+
# Formatting path based on the Splunk Version
2707+
if service.splunk_version < (9,):
2708+
path = path.format(path=PATH_JOBS, sid=sid)
2709+
else:
2710+
path = path.format(path=PATH_JOBS_V2, sid=sid)
2711+
26492712
Entity.__init__(self, service, path, skip_refresh=True, **kwargs)
26502713
self.sid = sid
26512714

@@ -2699,7 +2762,11 @@ def events(self, **kwargs):
26992762
:return: The ``InputStream`` IO handle to this job's events.
27002763
"""
27012764
kwargs['segmentation'] = kwargs.get('segmentation', 'none')
2702-
return self.get("events", **kwargs).body
2765+
2766+
# Search API v1(GET) and v2(POST)
2767+
if self.service.splunk_version < (9,):
2768+
return self.get("events", **kwargs).body
2769+
return self.post("events", **kwargs).body
27032770

27042771
def finalize(self):
27052772
"""Stops the job and provides intermediate results for retrieval.
@@ -2787,7 +2854,11 @@ def results(self, **query_params):
27872854
:return: The ``InputStream`` IO handle to this job's results.
27882855
"""
27892856
query_params['segmentation'] = query_params.get('segmentation', 'none')
2790-
return self.get("results", **query_params).body
2857+
2858+
# Search API v1(GET) and v2(POST)
2859+
if self.service.splunk_version < (9,):
2860+
return self.get("results", **query_params).body
2861+
return self.post("results", **query_params).body
27912862

27922863
def preview(self, **query_params):
27932864
"""Returns a streaming handle to this job's preview search results.
@@ -2828,7 +2899,11 @@ def preview(self, **query_params):
28282899
:return: The ``InputStream`` IO handle to this job's preview results.
28292900
"""
28302901
query_params['segmentation'] = query_params.get('segmentation', 'none')
2831-
return self.get("results_preview", **query_params).body
2902+
2903+
# Search API v1(GET) and v2(POST)
2904+
if self.service.splunk_version < (9,):
2905+
return self.get("results_preview", **query_params).body
2906+
return self.post("results_preview", **query_params).body
28322907

28332908
def searchlog(self, **kwargs):
28342909
"""Returns a streaming handle to this job's search log.
@@ -2918,7 +2993,12 @@ class Jobs(Collection):
29182993
collection using :meth:`Service.jobs`."""
29192994

29202995
def __init__(self, service):
2921-
Collection.__init__(self, service, PATH_JOBS, item=Job)
2996+
# Splunk 9 introduces the v2 endpoint
2997+
if service.splunk_version >= (9,):
2998+
path = PATH_JOBS_V2
2999+
else:
3000+
path = PATH_JOBS
3001+
Collection.__init__(self, service, path, item=Job)
29223002
# The count value to say list all the contents of this
29233003
# Collection is 0, not -1 as it is on most.
29243004
self.null_count = 0
@@ -3770,4 +3850,4 @@ def batch_save(self, *documents):
37703850
data = json.dumps(documents)
37713851

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

splunkrc.spec

Lines changed: 0 additions & 12 deletions
This file was deleted.

tests/test_job.py

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

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

377400
class TestResultsReader(unittest.TestCase):
378401
def test_results_reader(self):

tests/test_service.py

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

106111
def test_parse_fail(self):

0 commit comments

Comments
 (0)