Skip to content

Commit 91e8333

Browse files
author
Frederick Ross
committed
Resolved comments in code reviews by Itay and David.
1 parent e61f568 commit 91e8333

File tree

5 files changed

+175
-115
lines changed

5 files changed

+175
-115
lines changed

docs/tutorial.rst

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
Introduction
22
------------
33

4-
Here's a simple program using the Python SDK. Obviously you'll have to change the host, username, password, and any other data that you may have customized. And don't experiment on your production Splunk server! Install the free version of Splunk on your own machine to experiment.::
4+
Here's a simple program using the Python SDK. Obviously you'll have to
5+
change the host, username, password, and any other data that you may
6+
have customized. And don't experiment on your production Splunk
7+
server! Install the free version of Splunk on your own machine to
8+
experiment.::
59

610
import splunklib.client as client
711
c = client.connect(host="localhost",
@@ -14,31 +18,61 @@ Here's a simple program using the Python SDK. Obviously you'll have to change th
1418
assert "my_saved_search" in saved_searches
1519
saved_searches.delete("my_saved_search")
1620

17-
It's worth spending a few minute in ``ipython`` examining the objects produced in this example. ``c`` is a ``Service``[[TODO: link to reference docs]], which has [fields](link to list of fields in Service's docs) that provide access to most of Splunk's contents. ``saved_searches`` is a ``Collection``, and each entity in it is identified by a unique name (``"my_saved_search"`` in the example). All the names should be alphanumeric plus ``_`` and ``-``; no spaces are allowed[#]_.
18-
19-
.. [#] Splunk has two names for each entity: the pretty one meant to be displayed to users in the web browser, and the alphanumeric one that shows up in the URL of the REST call. It is the latter that is used in the SDK. Thus the Search app in Splunk is called "Search" in the web interface, but to fetch it via the SDK, you would write ``c.apps['search']``, not ``c.apps['Search']``. The "Getting Started" app is ``c.apps['gettingstarted']``, not ``c.apps['Getting Started']``.
20-
21-
A ``Collection`` acts like a dictionary. You can call ``keys``, ``iteritems``, and ``itervalues`` just like on a dictionary. However, you cannot assign to keys. ``saved_searches['some_name'] = ...`` is nonsense. Use the ``create`` method instead. Also, ``del saved_searches['some_name']`` does not currently work. Use the ``delete`` method instead.
21+
It's worth spending a few minute in ``ipython`` examining the objects
22+
produced in this example. ``c`` is a ``Service``[[TODO: link to
23+
reference docs]], which has [fields](link to list of fields in
24+
Service's docs) that provide access to most of Splunk's contents.
25+
``saved_searches`` is a ``Collection``, and each entity in it is
26+
identified by a unique name (``"my_saved_search"`` in the example).
27+
All the names should be alphanumeric plus ``_`` and ``-``; no spaces
28+
are allowed[#]_.
29+
30+
.. [#] Splunk has two names for each entity: the pretty one meant to
31+
be displayed to users in the web browser, and the alphanumeric one
32+
that shows up in the URL of the REST call. It is the latter that is
33+
used in the SDK. Thus the Search app in Splunk is called "Search"
34+
in the web interface, but to fetch it via the SDK, you would write
35+
``c.apps['search']``, not ``c.apps['Search']``. The "Getting
36+
Started" app is ``c.apps['gettingstarted']``, not ``c.apps['Getting Started']``.
37+
38+
A ``Collection`` acts like a dictionary. You can call ``keys``,
39+
``iteritems``, and ``itervalues`` just like on a dictionary. However,
40+
you cannot assign to keys. ``saved_searches['some_name'] = ...`` is
41+
nonsense. Use the ``create`` method instead. Also,
42+
``del saved_searches['some_name']`` does not currently work. Use the
43+
``delete`` method instead.
2244

2345
Note that in the example code we did not assert::
2446

2547
mss == saved_searches["my_saved_search"]
2648

27-
The Python objects you are manipulating represent snapshots of the server's state at some point in the past. There is no good way of defining equality on these that isn't misleading in many cases, so we have made ``==`` and ``!=`` raise exceptions for entities.
49+
The Python objects you are manipulating represent snapshots of the
50+
server's state at some point in the past. There is no good way of
51+
defining equality on these that isn't misleading in many cases, so we
52+
have made ``==`` and ``!=`` raise exceptions for entities.
2853

29-
Another side effect of using snapshots: after we delete the saved search in the example, ``mss`` is still bound to the same local object representing that search, even though it no longer exists on the server. If you need to update your snapshot, call the ``refresh`` method[#]_. For more on caching and snapshots, see [[TODO: link to section on roundtrips and caching]]
54+
Another side effect of using snapshots: after we delete the saved
55+
search in the example, ``mss`` is still bound to the same local object
56+
representing that search, even though it no longer exists on the
57+
server. If you need to update your snapshot, call the ``refresh``
58+
method[#]_. For more on caching and snapshots, see [[TODO: link to
59+
section on roundtrips and caching]]
3060

31-
.. [#] Calling ``refresh`` on an entity that has already been deleted raises an ``HTTPError``.
61+
.. [#] Calling ``refresh`` on an entity that has already been deleted
62+
raises an ``HTTPError``.
3263

33-
You can access the fields of an entity either as if they were keys in a dictionary, or fields of an object::
64+
You can access the fields of an entity either as if they were keys in
65+
a dictionary, or fields of an object::
3466

3567
mss['search'] == "search * | head 10"
3668
mss.search == "search * | head 10"
3769

3870
mss['action.email'] == '0'
3971
mss.action.email == '0'
4072

41-
A ``.`` isn't a valid character in identifiers in Python. The second form is actually a series of field lookups. As as side effect, you can get groups of fields that share prefixes.::
73+
A ``.`` isn't a valid character in identifiers in Python. The second
74+
form is actually a series of field lookups. As as side effect, you can
75+
get groups of fields that share prefixes.::
4276

4377
mss['action'] == {'email': '0',
4478
'populate_lookup': '0',
@@ -51,17 +85,27 @@ A ``.`` isn't a valid character in identifiers in Python. The second form is act
5185
'script': '0',
5286
'summary_index': '0'}
5387

54-
Those look like dictionaries, but they're actually a subclass called ``Record`` [[TODO: link to reference documentation]] that allows keys to be looked up as fields. [[TODO: Implement keys() on entities, and document it here]] In addition to fields, each kind of entity has a range of methods.::
88+
Those look like dictionaries, but they're actually a subclass called
89+
``Record`` [[TODO: link to reference documentation]] that allows keys
90+
to be looked up as fields. [[TODO: Implement keys() on entities, and
91+
document it here]] In addition to fields, each kind of entity has a
92+
range of methods.::
5593

5694
mss.dispatch() # Runs the saved search.
5795
mss.suppress(30) # Suppress all alerts from this saved search for 30 seconds
5896

59-
This should be enough information to understand the reference documentation and start using the SDK productively.
97+
This should be enough information to understand the reference
98+
documentation and start using the SDK productively.
6099

61100
Roundtrips and caching
62101
----------------------
63102

64-
The rate limiting step in most programs that call REST APIs is calls to the server. The SDK is designed to minimize and postpone these as much as possible. When you fetch an object from the SDK, you get a snapshot. If there are updates on the server after that snapshot, you won't know about them until you call ``refresh`` on your object. The object might even have been deleted.
103+
The rate limiting step in most programs that call REST APIs is calls
104+
to the server. The SDK is designed to minimize and postpone these as
105+
much as possible. When you fetch an object from the SDK, you get a
106+
snapshot. If there are updates on the server after that snapshot, you
107+
won't know about them until you call ``refresh`` on your object. The
108+
object might even have been deleted.
65109

66110

67111

setup.py

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

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

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

3032
name="splunk-sdk",

splunklib/binding.py

Lines changed: 94 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import socket
3131
import ssl
3232
import urllib
33+
import functools
3334

3435
from contextlib import contextmanager
3536

@@ -157,6 +158,68 @@ def _handle_auth_error(msg):
157158
else:
158159
raise
159160

161+
def _authentication(request_fun):
162+
"""Decorator to handle autologin and authentication errors.
163+
164+
*request_fun* is a function taking no arguments that needs to
165+
be run with this ``Context`` logged into Splunk.
166+
167+
``_authentication``'s behavior depends on whether the
168+
``autologin`` field of ``Context`` is set to ``True`` or
169+
``False``. If it's ``False``, then ``_authentication``
170+
aborts if the ``Context`` is not logged in, and raises an
171+
``AuthenticationError`` if an ``HTTPError`` of status 401 is
172+
raised in *request_fun*. If it's ``True``, then
173+
``_authentication`` will try at all sensible places to
174+
log in before issuing the request.
175+
176+
If ``autologin`` is ``False``, ``_authentication`` makes
177+
one roundtrip to the server if the ``Context`` is logged in,
178+
or zero if it is not. If ``autologin`` is ``True``, it's less
179+
deterministic, and may make at most three roundtrips (though
180+
that would be a truly pathological case).
181+
182+
:param request_fun: A function of no arguments encapsulating
183+
the request to make to the server.
184+
185+
**Example**::
186+
187+
import splunklib.binding as binding
188+
c = binding.connect(..., autologin=True)
189+
c.logout()
190+
def f():
191+
c.get("/services")
192+
return 42
193+
print _authentication(f)
194+
"""
195+
@functools.wraps(request_fun)
196+
def wrapper(self, *args, **kwargs):
197+
if self.token is None:
198+
# Not yet logged in.
199+
if self.autologin and self.username and self.password:
200+
# This will throw an uncaught
201+
# AuthenticationError if it fails.
202+
self.login()
203+
else:
204+
raise AuthenticationError("Request aborted: not logged in.")
205+
try:
206+
# Issue the request
207+
return request_fun(self, *args, **kwargs)
208+
except HTTPError as he:
209+
if he.status == 401 and self.autologin:
210+
# Authentication failed. Try logging in, and then
211+
# rerunning the request. If either step fails, throw
212+
# an AuthenticationError and give up.
213+
with _handle_auth_error("Autologin failed."):
214+
self.login()
215+
with _handle_auth_error("Autologin succeeded, but there was an auth error on next request. Something's very wrong."):
216+
return request_fun()
217+
elif he.status == 401 and not self.autologin:
218+
raise AuthenticationError("Request failed: Session is not logged in.")
219+
else:
220+
raise
221+
return wrapper
222+
160223
def _authority(scheme=DEFAULT_SCHEME, host=DEFAULT_HOST, port=DEFAULT_PORT):
161224
"""Construct a URL authority from the given *scheme*, *host*, and *port*.
162225
@@ -353,65 +416,7 @@ def connect(self):
353416
sock.connect((self.host, self.port))
354417
return sock
355418

356-
def _authentication(self, request_fun):
357-
"""Wrapper to handle autologin and authentication errors.
358-
359-
*request_fun* is a function taking no arguments that needs to
360-
be run with this ``Context`` logged into Splunk.
361-
362-
``_authentication``'s behavior depends on whether the
363-
``autologin`` field of ``Context`` is set to ``True`` or
364-
``False``. If it's ``False``, then ``_authentication``
365-
aborts if the ``Context`` is not logged in, and raises an
366-
``AuthenticationError`` if an ``HTTPError`` of status 401 is
367-
raised in *request_fun*. If it's ``True``, then
368-
``_authentication`` will try at all sensible places to
369-
log in before issuing the request.
370-
371-
If ``autologin`` is ``False``, ``_authentication`` makes
372-
one roundtrip to the server if the ``Context`` is logged in,
373-
or zero if it is not. If ``autologin`` is ``True``, it's less
374-
deterministic, and may make at most three roundtrips (though
375-
that would be a truly pathological case).
376-
377-
:param request_fun: A function of no arguments encapsulating
378-
the request to make to the server.
379-
380-
**Example**::
381-
382-
import splunklib.binding as binding
383-
c = binding.connect(..., autologin=True)
384-
c.logout()
385-
def f():
386-
c.get("/services")
387-
return 42
388-
print _authentication(f)
389-
"""
390-
if self.token is None:
391-
# Not yet logged in.
392-
if self.autologin:
393-
# This will throw an uncaught
394-
# AuthenticationError if it fails.
395-
self.login()
396-
else:
397-
raise AuthenticationError("Request aborted: not logged in.")
398-
try:
399-
# Issue the request
400-
return request_fun()
401-
except HTTPError as he:
402-
if he.status == 401 and self.autologin:
403-
# Authentication failed. Try logging in, and then
404-
# rerunning the request. If either step fails, throw
405-
# an AuthenticationError and give up.
406-
with _handle_auth_error("Autologin failed."):
407-
self.login()
408-
with _handle_auth_error("Autologin succeeded, but there was an auth error on next request. Something's very wrong."):
409-
return request_fun()
410-
elif he.status == 401 and not self.autologin:
411-
raise AuthenticationError("Request failed: Session is not logged in.")
412-
else:
413-
raise
414-
419+
@_authentication
415420
def delete(self, path_segment, owner=None, app=None, sharing=None, **query):
416421
"""DELETE at *path_segment* with the given namespace and query.
417422
@@ -454,12 +459,11 @@ def delete(self, path_segment, owner=None, app=None, sharing=None, **query):
454459
c.logout()
455460
c.delete('apps/local') # raises AuthenticationError
456461
"""
457-
def f():
458-
path = self.authority + self._abspath(path_segment, owner=owner,
459-
app=app, sharing=sharing)
460-
return self.http.delete(path, self._auth_headers, **query)
461-
return self._authentication(f)
462+
path = self.authority + self._abspath(path_segment, owner=owner,
463+
app=app, sharing=sharing)
464+
return self.http.delete(path, self._auth_headers, **query)
462465

466+
@_authentication
463467
def get(self, path_segment, owner=None, app=None, sharing=None, **query):
464468
"""GET from *path_segment* with the given namespace and query.
465469
@@ -502,12 +506,11 @@ def get(self, path_segment, owner=None, app=None, sharing=None, **query):
502506
c.logout()
503507
c.get('apps/local') # raises AuthenticationError
504508
"""
505-
def f():
506-
path = self.authority + self._abspath(path_segment, owner=owner,
507-
app=app, sharing=sharing)
508-
return self.http.get(path, self._auth_headers, **query)
509-
return self._authentication(f)
509+
path = self.authority + self._abspath(path_segment, owner=owner,
510+
app=app, sharing=sharing)
511+
return self.http.get(path, self._auth_headers, **query)
510512

513+
@_authentication
511514
def post(self, path_segment, owner=None, app=None, sharing=None, **query):
512515
"""POST to *path_segment* with the given namespace and query.
513516
@@ -553,12 +556,11 @@ def post(self, path_segment, owner=None, app=None, sharing=None, **query):
553556
c.post('saved/searches', name='boris',
554557
search='search * earliest=-1m | head 1')
555558
"""
556-
def f():
557-
path = self.authority + self._abspath(path_segment, owner=owner,
558-
app=app, sharing=sharing)
559-
return self.http.post(path, self._auth_headers, **query)
560-
return self._authentication(f)
559+
path = self.authority + self._abspath(path_segment, owner=owner,
560+
app=app, sharing=sharing)
561+
return self.http.post(path, self._auth_headers, **query)
561562

563+
@_authentication
562564
def request(self, path_segment, method="GET", headers=[], body="",
563565
owner=None, app=None, sharing=None):
564566
"""Issue an arbitrary HTTP request to *path_segment*.
@@ -605,24 +607,24 @@ def request(self, path_segment, method="GET", headers=[], body="",
605607
c.logout()
606608
c.get('apps/local') # raises AuthenticationError
607609
"""
608-
def f():
609-
path = self.authority \
610-
+ self._abspath(path_segment, owner=owner,
611-
app=app, sharing=sharing)
612-
# all_headers can't be named headers, due to a error in
613-
# Python's implementation of closures. In particular:
614-
# def f(x):
615-
# def g():
616-
# x = x + "a"
617-
# return x
618-
# return g()
619-
# throws UnboundLocalError, claiming that x is not bound.
620-
all_headers = headers + self._auth_headers
621-
return self.http.request(path,
622-
{'method': method,
623-
'headers': all_headers,
624-
'body': body})
625-
return self._authentication(f)
610+
path = self.authority \
611+
+ self._abspath(path_segment, owner=owner,
612+
app=app, sharing=sharing)
613+
# all_headers can't be named headers, due to how
614+
# Python implements closures. In particular:
615+
# def f(x):
616+
# def g():
617+
# x = x + "a"
618+
# return x
619+
# return g()
620+
# throws UnboundLocalError, since x must be either a member of
621+
# f's local namespace or g's, and cannot switch between them
622+
# during the run of the function.
623+
all_headers = headers + self._auth_headers
624+
return self.http.request(path,
625+
{'method': method,
626+
'headers': all_headers,
627+
'body': body})
626628

627629
def login(self):
628630
"""Log into the Splunk instance referred to by this ``Context``.

0 commit comments

Comments
 (0)