Skip to content

Commit 944c87d

Browse files
author
Fred Ross
committed
Merge pull request #46 from splunk/feature/fross-working
Screwed up my branch management locally. Merging this, then fixes from another branch.
2 parents 4891f57 + e794d4d commit 944c87d

23 files changed

+1191
-665
lines changed

CHANGELOG.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
# Splunk Python SDK Changelog
22

3-
## 0.8.5
4-
5-
### Features
6-
3+
## 1.0
4+
5+
* Added User.role_entities to return a list of the actual entity objects for the
6+
roles of a user. User.roles still returns a list of the role names.
7+
* The first time .cancel() is called on job, it cancels it. Any calls to it thereafter on that
8+
job are a nop.
9+
* Service.restart now takes a timeout argument. If it is specified, the function blocks until
10+
splunkd has restarted or the timeout has passed; if it is not specified, then it returns
11+
immediately and you have to check whether splunkd has restarted yourself.
12+
* Added .alert_count and .fired_alerts properties to SavedSearch entity.
13+
* Added Index.attached_socket(), which provides the same functionality as Index.attach(), but as
14+
a with block.
15+
* Added Indexes.default() which returns the name of the default index that data will be submitted into.
16+
* Connecting with a preexisting token works whether the token begins with 'Splunk ' or not;
17+
the SDK will handle either case correctly.
18+
* Added .is_ready() and .is_done() methods to Job to make it easy to loop until either point as been reached.
719
* Expanded endpoint coverage. Now at parity with the Java SDK.
820
* Replaced ResultsReader with something shorter. Iteration now
921
results either Message objects or dicts, and moved preview from

setup.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@
2525

2626
description="The Splunk Software Development Kit for Python.",
2727

28-
install_requires=['ordereddict'],
29-
3028
license="http://www.apache.org/licenses/LICENSE-2.0",
3129

3230
name="splunk-sdk",

splunklib/binding.py

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,23 @@
5555
DEFAULT_PORT = "8089"
5656
DEFAULT_SCHEME = "https"
5757

58-
@contextmanager
59-
def log_duration():
60-
start_time = datetime.now()
61-
yield
62-
end_time = datetime.now()
63-
logging.debug("Operation took %s", end_time-start_time)
64-
58+
def log_duration(f):
59+
def new_f(*args, **kwargs):
60+
start_time = datetime.now()
61+
val = f(*args, **kwargs)
62+
end_time = datetime.now()
63+
logging.debug("Operation took %s", end_time-start_time)
64+
return val
65+
return new_f
66+
67+
# Custom exceptions
6568
class AuthenticationError(Exception):
6669
pass
6770

71+
# Singleton values to eschew None
72+
class NoAuthenticationToken(object):
73+
pass
74+
6875
class UrlEncoded(str):
6976
"""String subclass to represent URL encoded strings.
7077
@@ -205,7 +212,7 @@ def f():
205212
"""
206213
@functools.wraps(request_fun)
207214
def wrapper(self, *args, **kwargs):
208-
if self.token is None:
215+
if self.token is NoAuthenticationToken:
209216
# Not yet logged in.
210217
if self.autologin and self.username and self.password:
211218
# This will throw an uncaught
@@ -377,7 +384,9 @@ class Context(object):
377384
"""
378385
def __init__(self, handler=None, **kwargs):
379386
self.http = HttpLib(handler)
380-
self.token = kwargs.get("token", None)
387+
self.token = kwargs.get("token", NoAuthenticationToken)
388+
if self.token is None: # In case someone explicitly passes token=None
389+
self.token = NoAuthenticationToken
381390
self.scheme = kwargs.get("scheme", DEFAULT_SCHEME)
382391
self.host = kwargs.get("host", DEFAULT_HOST)
383392
self.port = int(kwargs.get("port", DEFAULT_PORT))
@@ -398,7 +407,12 @@ def _auth_headers(self):
398407
399408
:returns: A list of 2-tuples containing key and value
400409
"""
401-
return [("Authorization", self.token)]
410+
# Ensure the token is properly formatted
411+
if self.token.startswith('Splunk'):
412+
token = self.token
413+
else:
414+
token = 'Splunk %s' % self.token
415+
return [("Authorization", token)]
402416

403417
def connect(self):
404418
"""Returns an open connection (socket) to the Splunk instance.
@@ -428,6 +442,7 @@ def connect(self):
428442
return sock
429443

430444
@_authentication
445+
@log_duration
431446
def delete(self, path_segment, owner=None, app=None, sharing=None, **query):
432447
"""DELETE at *path_segment* with the given namespace and query.
433448
@@ -473,11 +488,11 @@ def delete(self, path_segment, owner=None, app=None, sharing=None, **query):
473488
path = self.authority + self._abspath(path_segment, owner=owner,
474489
app=app, sharing=sharing)
475490
logging.debug("DELETE request to %s (body: %s)", path, repr(query))
476-
with log_duration():
477-
response = self.http.delete(path, self._auth_headers, **query)
491+
response = self.http.delete(path, self._auth_headers, **query)
478492
return response
479493

480494
@_authentication
495+
@log_duration
481496
def get(self, path_segment, owner=None, app=None, sharing=None, **query):
482497
"""GET from *path_segment* with the given namespace and query.
483498
@@ -523,12 +538,12 @@ def get(self, path_segment, owner=None, app=None, sharing=None, **query):
523538
path = self.authority + self._abspath(path_segment, owner=owner,
524539
app=app, sharing=sharing)
525540
logging.debug("GET request to %s (body: %s)", path, repr(query))
526-
with log_duration():
527-
response = self.http.get(path, self._auth_headers, **query)
541+
response = self.http.get(path, self._auth_headers, **query)
528542
return response
529543

530544
@_authentication
531-
def post(self, path_segment, owner=None, app=None, sharing=None, **query):
545+
@log_duration
546+
def post(self, path_segment, owner=None, app=None, sharing=None, headers=[], **query):
532547
"""POST to *path_segment* with the given namespace and query.
533548
534549
Named to match the HTTP method. This function makes at least
@@ -540,12 +555,21 @@ def post(self, path_segment, owner=None, app=None, sharing=None, **query):
540555
``Context``'s default namespace. All other keyword arguments
541556
are included in the URL as query parameters.
542557
558+
Some of Splunk's endpoints, such as receivers/simple and
559+
receivers/stream, require unstructured data in the POST body
560+
and all metadata passed as GET style arguments. If you provide
561+
a ``body`` argument to ``post``, it will be used as the POST
562+
body, and all other keyword arguments will be passed as
563+
GET-style arguments in the URL.
564+
543565
:raises AuthenticationError: when a the ``Context`` is not logged in.
544566
:raises HTTPError: when there was an error in POSTing to *path_segment*.
545567
:param path_segment: A path_segment to POST to.
546568
:type path_segment: string
547569
:param owner, app, sharing: Namespace parameters (optional).
548570
:type owner, app, sharing: string
571+
:param headers: A dict or list of (key,value) pairs to use as headers for
572+
this request.
549573
:param query: All other keyword arguments, used as query parameters.
550574
:type query: values should be strings
551575
:return: The server's response.
@@ -576,11 +600,18 @@ def post(self, path_segment, owner=None, app=None, sharing=None, **query):
576600
path = self.authority + self._abspath(path_segment, owner=owner,
577601
app=app, sharing=sharing)
578602
logging.debug("POST request to %s (body: %s)", path, repr(query))
579-
with log_duration():
580-
response = self.http.post(path, self._auth_headers, **query)
603+
if isinstance(headers, dict):
604+
all_headers = [(k,v) for k,v in headers.iteritems()]
605+
elif isinstance(headers, list):
606+
all_headers = headers
607+
else:
608+
raise ValueError("headers must be a list or dict (found: %s)" % headers)
609+
all_headers += self._auth_headers
610+
response = self.http.post(path, all_headers, **query)
581611
return response
582612

583613
@_authentication
614+
@log_duration
584615
def request(self, path_segment, method="GET", headers=[], body="",
585616
owner=None, app=None, sharing=None):
586617
"""Issue an arbitrary HTTP request to *path_segment*.
@@ -643,11 +674,10 @@ def request(self, path_segment, method="GET", headers=[], body="",
643674
all_headers = headers + self._auth_headers
644675
logging.debug("%s request to %s (headers: %s, body: %s)",
645676
method, path, str(all_headers), repr(body))
646-
with log_duration():
647-
response = self.http.request(path,
648-
{'method': method,
649-
'headers': all_headers,
650-
'body': body})
677+
response = self.http.request(path,
678+
{'method': method,
679+
'headers': all_headers,
680+
'body': body})
651681
return response
652682

653683
def login(self):
@@ -669,6 +699,12 @@ def login(self):
669699
c = binding.Context(...).login()
670700
# Then issue requests...
671701
"""
702+
if self.token is not NoAuthenticationToken and \
703+
(self.username and self.password):
704+
# If we were passed a session token, but no username or
705+
# password, then login is a nop, since we're automatically
706+
# logged in.
707+
return
672708
try:
673709
response = self.http.post(
674710
self.authority + self._abspath("/services/auth/login"),
@@ -686,7 +722,7 @@ def login(self):
686722

687723
def logout(self):
688724
"""Forget the current session token."""
689-
self.token = None
725+
self.token = NoAuthenticationToken
690726
return self
691727

692728
def _abspath(self, path_segment,
@@ -744,8 +780,8 @@ def _abspath(self, path_segment,
744780
if ns.app is None and ns.owner is None:
745781
return UrlEncoded("/services/%s" % path_segment, skip_encode=skip_encode)
746782

747-
oname = "-" if ns.owner is None else ns.owner
748-
aname = "-" if ns.app is None else ns.app
783+
oname = "nobody" if ns.owner is None else ns.owner
784+
aname = "system" if ns.app is None else ns.app
749785
path = UrlEncoded("/servicesNS/%s/%s/%s" % (oname, aname, path_segment),
750786
skip_encode=skip_encode)
751787
return path
@@ -788,7 +824,9 @@ def connect(**kwargs):
788824
c = binding.connect(...)
789825
response = c.get("apps/local")
790826
"""
791-
return Context(**kwargs).login()
827+
c = Context(**kwargs)
828+
c.login()
829+
return c
792830

793831
# Note: the error response schema supports multiple messages but we only
794832
# return the first, although we do return the body so that an exception
@@ -883,10 +921,18 @@ def get(self, url, headers=None, **kwargs):
883921
def post(self, url, headers=None, **kwargs):
884922
if headers is None: headers = []
885923
headers.append(("Content-Type", "application/x-www-form-urlencoded")),
924+
# We handle GET-style arguments and an unstructured body. This is here
925+
# to support the receivers/stream endpoint.
926+
if 'body' in kwargs:
927+
body = kwargs.pop('body')
928+
if len(kwargs) > 0:
929+
url = url + UrlEncoded('?' + encode(**kwargs), skip_encode=True)
930+
else:
931+
body = encode(**kwargs)
886932
message = {
887933
'method': "POST",
888934
'headers': headers,
889-
'body': encode(**kwargs)
935+
'body': body
890936
}
891937
return self.request(url, message)
892938

0 commit comments

Comments
 (0)