Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release/1.7.0 #469

Merged
merged 23 commits into from Jun 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2f974a9
[DVPL-10898] Convert to POST - search/parser
Mar 31, 2022
4de603b
[DVPL-10898] Remove 'search' request parameter - search/jobs/{search_…
Mar 31, 2022
d6d0f81
[DVPL-10898] Use the v2 endpoint of search where applicable
Apr 1, 2022
e8c5841
[DVPL-10898] Remove unnecessary else
Apr 1, 2022
ebff1ba
[DVPL-10898] Update all jobs endpoints to use v2 paths
Apr 4, 2022
ce8d09c
[DVPL-10898] Update all jobs endpoints to use v2 paths
Apr 4, 2022
7070ca0
[DVPL-10898] Update get/post & Jobs
Apr 19, 2022
f8639a3
[DVPL-10898] Update get/post for v2-to-v1 fallback
Apr 19, 2022
c7ab101
[DVPL-10898] Cosmetic changes and revert some values
Apr 28, 2022
4109c78
Update binding.py
ashah-splunk Jun 6, 2022
f11fe6a
Merge pull request #462 from splunk/master
ashah-splunk Jun 7, 2022
9dbc86b
Update binding.py
ashah-splunk Jun 7, 2022
55da0de
Search API jobs v2 (/results_preview, /results, /events) allows searc…
Jun 7, 2022
ceb6f62
updated checks for wildcards in StoragePasswords
akaila-splunk Jun 8, 2022
31cf507
Merge pull request #464 from splunk/DVPL-11235
akaila-splunk Jun 9, 2022
27f8fbf
Merge pull request #463 from splunk/preserve_thirdparty_cookies
fantavlik Jun 13, 2022
bbc74a3
Merge commit '27f8fbff99966acaddbb2c30ad2b09c29c359905' into DVPL-10898
fantavlik Jun 13, 2022
964fd30
Take changes from develop
fantavlik Jun 13, 2022
cd54110
code refactoring
akaila-splunk Jun 16, 2022
bccd01a
Reformatted the code logic to set Path based on Splunk Version
ashah-splunk Jun 16, 2022
2b2dee7
Update test matrix to test 8.2 and latest (currently 9.0) and update …
fantavlik Jun 16, 2022
d764e75
Merge pull request #468 from splunk/DVPL-10898-develop
fantavlik Jun 16, 2022
9d0b166
release 1.7.0 version changes
akaila-splunk Jun 17, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env
Expand Up @@ -9,7 +9,7 @@ password=changed!
# Access scheme (default: https)
scheme=https
# Your version of Splunk (default: 6.2)
version=8.0
version=9.0
# Bearer token for authentication
#bearerToken="<Bearer-token>"
# Session key for authentication
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Expand Up @@ -13,7 +13,7 @@ jobs:
- ubuntu-latest
python: [ 2.7, 3.7 ]
splunk-version:
- "8.0"
- "8.2"
- "latest"
fail-fast: false

Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,16 @@
# Splunk Enterprise SDK for Python Changelog

## Version 1.7.0

### New features and APIs
* [#468](https://github.com/splunk/splunk-sdk-python/pull/468) SDK Support for splunkd search API changes

### Bug fixes
* [#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)]

### Minor changes
* [#463](https://github.com/splunk/splunk-sdk-python/pull/463) Preserve thirdparty cookies

## Version 1.6.20

### New features and APIs
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Expand Up @@ -11,7 +11,7 @@ If you're seeing some unexpected behavior with this project, please create an [i
1. Version of this project you're using (ex: 1.5.0)
2. Platform version (ex: Windows Server 2012 R2)
3. Framework version (ex: Python 3.7)
4. Splunk Enterprise version (ex: 8.0)
4. Splunk Enterprise version (ex: 9.0)
5. Other relevant information (ex: local/remote environment, Splunk network configuration, standalone or distributed deployment, are load balancers used)

Alternatively, if you have a Splunk question please ask on [Splunk Answers](https://community.splunk.com/t5/Splunk-Development/ct-p/developer-tools).
Expand Down
6 changes: 3 additions & 3 deletions README.md
Expand Up @@ -3,7 +3,7 @@

# The Splunk Enterprise Software Development Kit for Python

#### Version 1.6.20
#### Version 1.7.0

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

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

```
make up SPLUNK_VERSION=8.0
make up SPLUNK_VERSION=9.0
make wait_up
make test
make down
Expand Down Expand Up @@ -107,7 +107,7 @@ here is an example of .env file:
# Access scheme (default: https)
scheme=https
# Your version of Splunk Enterprise
version=8.0
version=9.0
# Bearer token for authentication
#bearerToken=<Bearer-token>
# Session key for authentication
Expand Down
2 changes: 1 addition & 1 deletion splunklib/__init__.py
Expand Up @@ -31,5 +31,5 @@ def setup_logging(level, log_format=DEFAULT_LOG_FORMAT, date_format=DEFAULT_DATE
format=log_format,
datefmt=date_format)

__version_info__ = (1, 6, 20)
__version_info__ = (1, 7, 0)
__version__ = ".".join(map(str, __version_info__))
14 changes: 9 additions & 5 deletions splunklib/binding.py
Expand Up @@ -526,23 +526,27 @@ def _auth_headers(self):

:returns: A list of 2-tuples containing key and value
"""
header = []
if self.has_cookies():
return [("Cookie", _make_cookie_header(list(self.get_cookies().items())))]
elif self.basic and (self.username and self.password):
token = 'Basic %s' % b64encode(("%s:%s" % (self.username, self.password)).encode('utf-8')).decode('ascii')
return [("Authorization", token)]
elif self.bearerToken:
token = 'Bearer %s' % self.bearerToken
return [("Authorization", token)]
elif self.token is _NoAuthenticationToken:
return []
token = []
else:
# Ensure the token is properly formatted
if self.token.startswith('Splunk '):
token = self.token
else:
token = 'Splunk %s' % self.token
return [("Authorization", token)]
if token:
header.append(("Authorization", token))
if self.get_cookies().__len__() > 0:
header.append("Cookie", _make_cookie_header(self.get_cookies().items()))

return header

def connect(self):
"""Returns an open connection (socket) to the Splunk instance.
Expand Down Expand Up @@ -1430,7 +1434,7 @@ def request(url, message, **kwargs):
head = {
"Content-Length": str(len(body)),
"Host": host,
"User-Agent": "splunk-sdk-python/1.6.20",
"User-Agent": "splunk-sdk-python/1.7.0",
"Accept": "*/*",
"Connection": "Close",
} # defaults
Expand Down
101 changes: 92 additions & 9 deletions splunklib/client.py
Expand Up @@ -62,6 +62,7 @@
import datetime
import json
import logging
import re
import socket
from datetime import datetime, timedelta
from time import sleep
Expand Down Expand Up @@ -99,6 +100,7 @@
PATH_INDEXES = "data/indexes/"
PATH_INPUTS = "data/inputs/"
PATH_JOBS = "search/jobs/"
PATH_JOBS_V2 = "search/v2/jobs/"
PATH_LOGGER = "/services/server/logger/"
PATH_MESSAGES = "messages/"
PATH_MODULAR_INPUTS = "data/modular-inputs"
Expand Down Expand Up @@ -570,6 +572,8 @@ def parse(self, query, **kwargs):
:type kwargs: ``dict``
:return: A semantic map of the parsed search query.
"""
if self.splunk_version >= (9,):
return self.post("search/v2/parser", q=query, **kwargs)
return self.get("search/parser", q=query, **kwargs)

def restart(self, timeout=None):
Expand Down Expand Up @@ -741,6 +745,25 @@ def __init__(self, service, path):
self.service = service
self.path = path

def get_api_version(self, path):
"""Return the API version of the service used in the provided path.

Args:
path (str): A fully-qualified endpoint path (for example, "/services/search/jobs").

Returns:
int: Version of the API (for example, 1)
"""
# Default to v1 if undefined in the path
# For example, "/services/search/jobs" is using API v1
api_version = 1

versionSearch = re.search('(?:servicesNS\/[^/]+\/[^/]+|services)\/[^/]+\/v(\d+)\/', path)
if versionSearch:
api_version = int(versionSearch.group(1))

return api_version

def get(self, path_segment="", owner=None, app=None, sharing=None, **query):
"""Performs a GET operation on the path segment relative to this endpoint.

Expand Down Expand Up @@ -803,6 +826,22 @@ def get(self, path_segment="", owner=None, app=None, sharing=None, **query):
app=app, sharing=sharing)
# ^-- This was "%s%s" % (self.path, path_segment).
# That doesn't work, because self.path may be UrlEncoded.

# Get the API version from the path
api_version = self.get_api_version(path)

# Search API v2+ fallback to v1:
# - In v2+, /results_preview, /events and /results do not support search params.
# - Fallback from v2+ to v1 if Splunk Version is < 9.
# if api_version >= 2 and ('search' in query and path.endswith(tuple(["results_preview", "events", "results"])) or self.service.splunk_version < (9,)):
# path = path.replace(PATH_JOBS_V2, PATH_JOBS)

if api_version == 1:
if isinstance(path, UrlEncoded):
path = UrlEncoded(path.replace(PATH_JOBS_V2, PATH_JOBS), skip_encode=True)
else:
path = path.replace(PATH_JOBS_V2, PATH_JOBS)

return self.service.get(path,
owner=owner, app=app, sharing=sharing,
**query)
Expand Down Expand Up @@ -855,13 +894,29 @@ def post(self, path_segment="", owner=None, app=None, sharing=None, **query):
apps.get('nonexistant/path') # raises HTTPError
s.logout()
apps.get() # raises AuthenticationError
"""
"""
if path_segment.startswith('/'):
path = path_segment
else:
if not self.path.endswith('/') and path_segment != "":
self.path = self.path + '/'
path = self.service._abspath(self.path + path_segment, owner=owner, app=app, sharing=sharing)

# Get the API version from the path
api_version = self.get_api_version(path)

# Search API v2+ fallback to v1:
# - In v2+, /results_preview, /events and /results do not support search params.
# - Fallback from v2+ to v1 if Splunk Version is < 9.
# if api_version >= 2 and ('search' in query and path.endswith(tuple(["results_preview", "events", "results"])) or self.service.splunk_version < (9,)):
# path = path.replace(PATH_JOBS_V2, PATH_JOBS)

if api_version == 1:
if isinstance(path, UrlEncoded):
path = UrlEncoded(path.replace(PATH_JOBS_V2, PATH_JOBS), skip_encode=True)
else:
path = path.replace(PATH_JOBS_V2, PATH_JOBS)

return self.service.post(path, owner=owner, app=app, sharing=sharing, **query)


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

def create(self, password, username, realm=None):
Expand All @@ -1865,6 +1918,9 @@ def create(self, password, username, realm=None):

:return: The :class:`StoragePassword` object created.
"""
if self.service.namespace.owner == '-' or self.service.namespace.app == '-':
raise ValueError("While creating StoragePasswords, namespace cannot have wildcards.")

if not isinstance(username, six.string_types):
raise ValueError("Invalid name: %s" % repr(username))

Expand Down Expand Up @@ -1896,6 +1952,9 @@ def delete(self, username, realm=None):
:return: The `StoragePassword` collection.
:rtype: ``self``
"""
if self.service.namespace.owner == '-' or self.service.namespace.app == '-':
raise ValueError("app context must be specified when removing a password.")

if realm is None:
# This case makes the username optional, so
# the full name can be passed in as realm.
Expand Down Expand Up @@ -2660,7 +2719,14 @@ def oneshot(self, path, **kwargs):
class Job(Entity):
"""This class represents a search job."""
def __init__(self, service, sid, **kwargs):
path = PATH_JOBS + sid
# Default to v2 in Splunk Version 9+
path = "{path}{sid}"
# Formatting path based on the Splunk Version
if service.splunk_version < (9,):
path = path.format(path=PATH_JOBS, sid=sid)
else:
path = path.format(path=PATH_JOBS_V2, sid=sid)

Entity.__init__(self, service, path, skip_refresh=True, **kwargs)
self.sid = sid

Expand Down Expand Up @@ -2714,7 +2780,11 @@ def events(self, **kwargs):
:return: The ``InputStream`` IO handle to this job's events.
"""
kwargs['segmentation'] = kwargs.get('segmentation', 'none')
return self.get("events", **kwargs).body

# Search API v1(GET) and v2(POST)
if self.service.splunk_version < (9,):
return self.get("events", **kwargs).body
return self.post("events", **kwargs).body

def finalize(self):
"""Stops the job and provides intermediate results for retrieval.
Expand Down Expand Up @@ -2802,7 +2872,11 @@ def results(self, **query_params):
:return: The ``InputStream`` IO handle to this job's results.
"""
query_params['segmentation'] = query_params.get('segmentation', 'none')
return self.get("results", **query_params).body

# Search API v1(GET) and v2(POST)
if self.service.splunk_version < (9,):
return self.get("results", **query_params).body
return self.post("results", **query_params).body

def preview(self, **query_params):
"""Returns a streaming handle to this job's preview search results.
Expand Down Expand Up @@ -2843,7 +2917,11 @@ def preview(self, **query_params):
:return: The ``InputStream`` IO handle to this job's preview results.
"""
query_params['segmentation'] = query_params.get('segmentation', 'none')
return self.get("results_preview", **query_params).body

# Search API v1(GET) and v2(POST)
if self.service.splunk_version < (9,):
return self.get("results_preview", **query_params).body
return self.post("results_preview", **query_params).body

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

data = json.dumps(documents)

return json.loads(self._post('batch_save', headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read().decode('utf-8'))
return json.loads(self._post('batch_save', headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read().decode('utf-8'))
12 changes: 0 additions & 12 deletions splunkrc.spec

This file was deleted.

23 changes: 23 additions & 0 deletions tests/test_job.py
Expand Up @@ -382,6 +382,29 @@ def test_search_invalid_query_as_json(self):
except Exception as e:
self.fail("Got some unexpected error. %s" % e.message)

def test_v1_job_fallback(self):
self.assertEventuallyTrue(self.job.is_done)
self.assertLessEqual(int(self.job['eventCount']), 3)

preview_stream = self.job.preview(output_mode='json', search='| head 1')
preview_r = results.JSONResultsReader(preview_stream)
self.assertFalse(preview_r.is_preview)

events_stream = self.job.events(output_mode='json', search='| head 1')
events_r = results.JSONResultsReader(events_stream)

results_stream = self.job.results(output_mode='json', search='| head 1')
results_r = results.JSONResultsReader(results_stream)

n_events = len([x for x in events_r if isinstance(x, dict)])
n_preview = len([x for x in preview_r if isinstance(x, dict)])
n_results = len([x for x in results_r if isinstance(x, dict)])

# Fallback test for Splunk Version 9+
if self.service.splunk_version[0] >= 9:
self.assertGreaterEqual(9, self.service.splunk_version[0])
self.assertEqual(n_events, n_preview, n_results)


class TestResultsReader(unittest.TestCase):
def test_results_reader(self):
Expand Down
5 changes: 5 additions & 0 deletions tests/test_service.py
Expand Up @@ -102,6 +102,11 @@ def test_parse(self):
# objectified form of the results, but for now there's
# nothing to test but a good response code.
response = self.service.parse('search * abc="def" | dedup abc')

# Splunk Version 9+ using API v2: search/v2/parser
if self.service.splunk_version[0] >= 9:
self.assertGreaterEqual(9, self.service.splunk_version[0])

self.assertEqual(response.status, 200)

def test_parse_fail(self):
Expand Down