Skip to content

Commit 14ef9f4

Browse files
authored
Merge pull request #469 from splunk/release/1.7.0
Release/1.7.0
2 parents e045c07 + 9d0b166 commit 14ef9f4

File tree

11 files changed

+147
-33
lines changed

11 files changed

+147
-33
lines changed

.env

+1-1
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

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

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Splunk Enterprise SDK for Python Changelog
22

3+
## Version 1.7.0
4+
5+
### New features and APIs
6+
* [#468](https://github.com/splunk/splunk-sdk-python/pull/468) SDK Support for splunkd search API changes
7+
8+
### Bug fixes
9+
* [#464](https://github.com/splunk/splunk-sdk-python/pull/464) updated checks for wildcards in StoragePasswords [[issue#458](https://github.com/splunk/splunk-sdk-python/issues/458)]
10+
11+
### Minor changes
12+
* [#463](https://github.com/splunk/splunk-sdk-python/pull/463) Preserve thirdparty cookies
13+
314
## Version 1.6.20
415

516
### New features and APIs

CONTRIBUTING.md

+1-1
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

+3-3
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.20
6+
#### Version 1.7.0
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/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,5 @@ def setup_logging(level, log_format=DEFAULT_LOG_FORMAT, date_format=DEFAULT_DATE
3131
format=log_format,
3232
datefmt=date_format)
3333

34-
__version_info__ = (1, 6, 20)
34+
__version_info__ = (1, 7, 0)
3535
__version__ = ".".join(map(str, __version_info__))

splunklib/binding.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -526,23 +526,27 @@ def _auth_headers(self):
526526
527527
:returns: A list of 2-tuples containing key and value
528528
"""
529+
header = []
529530
if self.has_cookies():
530531
return [("Cookie", _make_cookie_header(list(self.get_cookies().items())))]
531532
elif self.basic and (self.username and self.password):
532533
token = 'Basic %s' % b64encode(("%s:%s" % (self.username, self.password)).encode('utf-8')).decode('ascii')
533-
return [("Authorization", token)]
534534
elif self.bearerToken:
535535
token = 'Bearer %s' % self.bearerToken
536-
return [("Authorization", token)]
537536
elif self.token is _NoAuthenticationToken:
538-
return []
537+
token = []
539538
else:
540539
# Ensure the token is properly formatted
541540
if self.token.startswith('Splunk '):
542541
token = self.token
543542
else:
544543
token = 'Splunk %s' % self.token
545-
return [("Authorization", token)]
544+
if token:
545+
header.append(("Authorization", token))
546+
if self.get_cookies().__len__() > 0:
547+
header.append("Cookie", _make_cookie_header(self.get_cookies().items()))
548+
549+
return header
546550

547551
def connect(self):
548552
"""Returns an open connection (socket) to the Splunk instance.
@@ -1430,7 +1434,7 @@ def request(url, message, **kwargs):
14301434
head = {
14311435
"Content-Length": str(len(body)),
14321436
"Host": host,
1433-
"User-Agent": "splunk-sdk-python/1.6.20",
1437+
"User-Agent": "splunk-sdk-python/1.7.0",
14341438
"Accept": "*/*",
14351439
"Connection": "Close",
14361440
} # defaults

splunklib/client.py

+92-9
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
@@ -99,6 +100,7 @@
99100
PATH_INDEXES = "data/indexes/"
100101
PATH_INPUTS = "data/inputs/"
101102
PATH_JOBS = "search/jobs/"
103+
PATH_JOBS_V2 = "search/v2/jobs/"
102104
PATH_LOGGER = "/services/server/logger/"
103105
PATH_MESSAGES = "messages/"
104106
PATH_MODULAR_INPUTS = "data/modular-inputs"
@@ -570,6 +572,8 @@ def parse(self, query, **kwargs):
570572
:type kwargs: ``dict``
571573
:return: A semantic map of the parsed search query.
572574
"""
575+
if self.splunk_version >= (9,):
576+
return self.post("search/v2/parser", q=query, **kwargs)
573577
return self.get("search/parser", q=query, **kwargs)
574578

575579
def restart(self, timeout=None):
@@ -741,6 +745,25 @@ def __init__(self, service, path):
741745
self.service = service
742746
self.path = path
743747

748+
def get_api_version(self, path):
749+
"""Return the API version of the service used in the provided path.
750+
751+
Args:
752+
path (str): A fully-qualified endpoint path (for example, "/services/search/jobs").
753+
754+
Returns:
755+
int: Version of the API (for example, 1)
756+
"""
757+
# Default to v1 if undefined in the path
758+
# For example, "/services/search/jobs" is using API v1
759+
api_version = 1
760+
761+
versionSearch = re.search('(?:servicesNS\/[^/]+\/[^/]+|services)\/[^/]+\/v(\d+)\/', path)
762+
if versionSearch:
763+
api_version = int(versionSearch.group(1))
764+
765+
return api_version
766+
744767
def get(self, path_segment="", owner=None, app=None, sharing=None, **query):
745768
"""Performs a GET operation on the path segment relative to this endpoint.
746769
@@ -803,6 +826,22 @@ def get(self, path_segment="", owner=None, app=None, sharing=None, **query):
803826
app=app, sharing=sharing)
804827
# ^-- This was "%s%s" % (self.path, path_segment).
805828
# That doesn't work, because self.path may be UrlEncoded.
829+
830+
# Get the API version from the path
831+
api_version = self.get_api_version(path)
832+
833+
# Search API v2+ fallback to v1:
834+
# - In v2+, /results_preview, /events and /results do not support search params.
835+
# - Fallback from v2+ to v1 if Splunk Version is < 9.
836+
# if api_version >= 2 and ('search' in query and path.endswith(tuple(["results_preview", "events", "results"])) or self.service.splunk_version < (9,)):
837+
# path = path.replace(PATH_JOBS_V2, PATH_JOBS)
838+
839+
if api_version == 1:
840+
if isinstance(path, UrlEncoded):
841+
path = UrlEncoded(path.replace(PATH_JOBS_V2, PATH_JOBS), skip_encode=True)
842+
else:
843+
path = path.replace(PATH_JOBS_V2, PATH_JOBS)
844+
806845
return self.service.get(path,
807846
owner=owner, app=app, sharing=sharing,
808847
**query)
@@ -855,13 +894,29 @@ def post(self, path_segment="", owner=None, app=None, sharing=None, **query):
855894
apps.get('nonexistant/path') # raises HTTPError
856895
s.logout()
857896
apps.get() # raises AuthenticationError
858-
"""
897+
"""
859898
if path_segment.startswith('/'):
860899
path = path_segment
861900
else:
862901
if not self.path.endswith('/') and path_segment != "":
863902
self.path = self.path + '/'
864903
path = self.service._abspath(self.path + path_segment, owner=owner, app=app, sharing=sharing)
904+
905+
# Get the API version from the path
906+
api_version = self.get_api_version(path)
907+
908+
# Search API v2+ fallback to v1:
909+
# - In v2+, /results_preview, /events and /results do not support search params.
910+
# - Fallback from v2+ to v1 if Splunk Version is < 9.
911+
# if api_version >= 2 and ('search' in query and path.endswith(tuple(["results_preview", "events", "results"])) or self.service.splunk_version < (9,)):
912+
# path = path.replace(PATH_JOBS_V2, PATH_JOBS)
913+
914+
if api_version == 1:
915+
if isinstance(path, UrlEncoded):
916+
path = UrlEncoded(path.replace(PATH_JOBS_V2, PATH_JOBS), skip_encode=True)
917+
else:
918+
path = path.replace(PATH_JOBS_V2, PATH_JOBS)
919+
865920
return self.service.post(path, owner=owner, app=app, sharing=sharing, **query)
866921

867922

@@ -1846,8 +1901,6 @@ class StoragePasswords(Collection):
18461901
instance. Retrieve this collection using :meth:`Service.storage_passwords`.
18471902
"""
18481903
def __init__(self, service):
1849-
if service.namespace.owner == '-' or service.namespace.app == '-':
1850-
raise ValueError("StoragePasswords cannot have wildcards in namespace.")
18511904
super(StoragePasswords, self).__init__(service, PATH_STORAGE_PASSWORDS, item=StoragePassword)
18521905

18531906
def create(self, password, username, realm=None):
@@ -1865,6 +1918,9 @@ def create(self, password, username, realm=None):
18651918
18661919
:return: The :class:`StoragePassword` object created.
18671920
"""
1921+
if self.service.namespace.owner == '-' or self.service.namespace.app == '-':
1922+
raise ValueError("While creating StoragePasswords, namespace cannot have wildcards.")
1923+
18681924
if not isinstance(username, six.string_types):
18691925
raise ValueError("Invalid name: %s" % repr(username))
18701926

@@ -1896,6 +1952,9 @@ def delete(self, username, realm=None):
18961952
:return: The `StoragePassword` collection.
18971953
:rtype: ``self``
18981954
"""
1955+
if self.service.namespace.owner == '-' or self.service.namespace.app == '-':
1956+
raise ValueError("app context must be specified when removing a password.")
1957+
18991958
if realm is None:
19001959
# This case makes the username optional, so
19011960
# the full name can be passed in as realm.
@@ -2660,7 +2719,14 @@ def oneshot(self, path, **kwargs):
26602719
class Job(Entity):
26612720
"""This class represents a search job."""
26622721
def __init__(self, service, sid, **kwargs):
2663-
path = PATH_JOBS + sid
2722+
# Default to v2 in Splunk Version 9+
2723+
path = "{path}{sid}"
2724+
# Formatting path based on the Splunk Version
2725+
if service.splunk_version < (9,):
2726+
path = path.format(path=PATH_JOBS, sid=sid)
2727+
else:
2728+
path = path.format(path=PATH_JOBS_V2, sid=sid)
2729+
26642730
Entity.__init__(self, service, path, skip_refresh=True, **kwargs)
26652731
self.sid = sid
26662732

@@ -2714,7 +2780,11 @@ def events(self, **kwargs):
27142780
:return: The ``InputStream`` IO handle to this job's events.
27152781
"""
27162782
kwargs['segmentation'] = kwargs.get('segmentation', 'none')
2717-
return self.get("events", **kwargs).body
2783+
2784+
# Search API v1(GET) and v2(POST)
2785+
if self.service.splunk_version < (9,):
2786+
return self.get("events", **kwargs).body
2787+
return self.post("events", **kwargs).body
27182788

27192789
def finalize(self):
27202790
"""Stops the job and provides intermediate results for retrieval.
@@ -2802,7 +2872,11 @@ def results(self, **query_params):
28022872
:return: The ``InputStream`` IO handle to this job's results.
28032873
"""
28042874
query_params['segmentation'] = query_params.get('segmentation', 'none')
2805-
return self.get("results", **query_params).body
2875+
2876+
# Search API v1(GET) and v2(POST)
2877+
if self.service.splunk_version < (9,):
2878+
return self.get("results", **query_params).body
2879+
return self.post("results", **query_params).body
28062880

28072881
def preview(self, **query_params):
28082882
"""Returns a streaming handle to this job's preview search results.
@@ -2843,7 +2917,11 @@ def preview(self, **query_params):
28432917
:return: The ``InputStream`` IO handle to this job's preview results.
28442918
"""
28452919
query_params['segmentation'] = query_params.get('segmentation', 'none')
2846-
return self.get("results_preview", **query_params).body
2920+
2921+
# Search API v1(GET) and v2(POST)
2922+
if self.service.splunk_version < (9,):
2923+
return self.get("results_preview", **query_params).body
2924+
return self.post("results_preview", **query_params).body
28472925

28482926
def searchlog(self, **kwargs):
28492927
"""Returns a streaming handle to this job's search log.
@@ -2932,7 +3010,12 @@ class Jobs(Collection):
29323010
"""This class represents a collection of search jobs. Retrieve this
29333011
collection using :meth:`Service.jobs`."""
29343012
def __init__(self, service):
2935-
Collection.__init__(self, service, PATH_JOBS, item=Job)
3013+
# Splunk 9 introduces the v2 endpoint
3014+
if service.splunk_version >= (9,):
3015+
path = PATH_JOBS_V2
3016+
else:
3017+
path = PATH_JOBS
3018+
Collection.__init__(self, service, path, item=Job)
29363019
# The count value to say list all the contents of this
29373020
# Collection is 0, not -1 as it is on most.
29383021
self.null_count = 0
@@ -3770,4 +3853,4 @@ def batch_save(self, *documents):
37703853

37713854
data = json.dumps(documents)
37723855

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

splunkrc.spec

-12
This file was deleted.

tests/test_job.py

+23
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,29 @@ def test_search_invalid_query_as_json(self):
382382
except Exception as e:
383383
self.fail("Got some unexpected error. %s" % e.message)
384384

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

386409
class TestResultsReader(unittest.TestCase):
387410
def test_results_reader(self):

tests/test_service.py

+5
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):

0 commit comments

Comments
 (0)