Skip to content

Commit 43156b1

Browse files
author
Fred Ross
committed
Merge pull request #31 from splunk/fross-feature/naming
Lots of new functionality
2 parents c546e18 + 91e8333 commit 43156b1

32 files changed

+3118
-994
lines changed

CHANGELOG.md

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

3+
## 0.8.5
4+
5+
### Features
6+
7+
* Expanded endpoint coverage. Now at parity with the Java SDK.
8+
* Replaced ResultsReader with something shorter. Iteration now
9+
results either Message objects or dicts, and moved preview from
10+
iteration to a field.
11+
* Entities can be fetched from collections by name plus namespace
12+
combinations (which are unique, unlike names alone). Fetching
13+
entries by name alone properly throws errors on name conflicts.
14+
* Added a distinct AuthenticationError and optional autologin/autorelogin.
15+
* Reduced roundtrips and listings with specific lookups in __getitem__
16+
and similar methods.
17+
* Put in types and operations to make URL encoding of strings consistent.
18+
* Pagination is implemented to stream search results a hunk at a time.
19+
* Lots of docstrings expanded.
20+
* Lots of small bugs fixed.
21+
322
## 0.8.0 (beta)
423

524
### Features

docs/tutorial.rst

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
Introduction
2+
------------
3+
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.::
9+
10+
import splunklib.client as client
11+
c = client.connect(host="localhost",
12+
port=8089,
13+
scheme="https",
14+
username="admin",
15+
password="changeme")
16+
saved_searches = c.saved_searches
17+
mss = saved_searches.create("my_saved_search", "search * | head 10")
18+
assert "my_saved_search" in saved_searches
19+
saved_searches.delete("my_saved_search")
20+
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.
44+
45+
Note that in the example code we did not assert::
46+
47+
mss == saved_searches["my_saved_search"]
48+
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.
53+
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]]
60+
61+
.. [#] Calling ``refresh`` on an entity that has already been deleted
62+
raises an ``HTTPError``.
63+
64+
You can access the fields of an entity either as if they were keys in
65+
a dictionary, or fields of an object::
66+
67+
mss['search'] == "search * | head 10"
68+
mss.search == "search * | head 10"
69+
70+
mss['action.email'] == '0'
71+
mss.action.email == '0'
72+
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.::
76+
77+
mss['action'] == {'email': '0',
78+
'populate_lookup': '0',
79+
'rss': '0',
80+
'script': '0',
81+
'summary_index': '0'}
82+
mss.action == {'email': '0',
83+
'populate_lookup': '0',
84+
'rss': '0',
85+
'script': '0',
86+
'summary_index': '0'}
87+
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.::
93+
94+
mss.dispatch() # Runs the saved search.
95+
mss.suppress(30) # Suppress all alerts from this saved search for 30 seconds
96+
97+
This should be enough information to understand the reference
98+
documentation and start using the SDK productively.
99+
100+
Roundtrips and caching
101+
----------------------
102+
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.
109+
110+
111+
112+
113+

examples/analytics/input.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,17 @@ def __init__(self, application_name, splunk_info, index = ANALYTICS_INDEX_NAME):
3737
self.splunk = client.connect(**splunk_info)
3838
self.index = index
3939

40-
if not self.splunk.indexes.contains(self.index):
40+
if not self.index in self.splunk.indexes:
4141
self.splunk.indexes.create(self.index)
42-
assert(self.splunk.indexes.contains(self.index))
42+
assert(self.index in self.splunk.indexes)
4343

44-
if not self.splunk.confs['props'].contains(ANALYTICS_SOURCETYPE):
44+
if ANALYTICS_SOURCETYPE not in self.splunk.confs['props']:
4545
self.splunk.confs["props"].create(ANALYTICS_SOURCETYPE)
4646
stanza = self.splunk.confs["props"][ANALYTICS_SOURCETYPE]
4747
stanza.submit("LINE_BREAKER = (%s)" % EVENT_TERMINATOR)
4848
stanza.submit("CHARSET = UTF-8")
4949
stanza.submit("SHOULD_LINEMERGE = false")
50-
assert(self.splunk.confs['props'].contains(ANALYTICS_SOURCETYPE))
50+
assert(ANALYTICS_SOURCETYPE in self.splunk.confs['props'])
5151

5252
@staticmethod
5353
def encode(props):

examples/analytics/output.py

Lines changed: 28 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ class TimeRange:
3737
WEEK="1w"
3838
MONTH="1mon"
3939

40+
def counts(job, result_key):
41+
applications = []
42+
reader = results.ResultsReader(job.results())
43+
for result in reader:
44+
if isinstance(result, dict):
45+
applications.append({
46+
"name": result[result_key],
47+
"count": int(result["count"] or 0)
48+
})
49+
return applications
50+
51+
4052
class AnalyticsRetriever:
4153
def __init__(self, application_name, splunk_info, index = ANALYTICS_INDEX_NAME):
4254
self.application_name = application_name
@@ -46,32 +58,12 @@ def __init__(self, application_name, splunk_info, index = ANALYTICS_INDEX_NAME):
4658
def applications(self):
4759
query = "search index=%s | stats count by application" % (self.index)
4860
job = self.splunk.jobs.create(query, exec_mode="blocking")
49-
50-
applications = []
51-
reader = results.ResultsReader(job.results())
52-
for kind,result in reader:
53-
if kind == results.RESULT:
54-
applications.append({
55-
"name": result["application"],
56-
"count": int(result["count"] or 0)
57-
})
58-
59-
return applications
61+
return counts(job, "application")
6062

6163
def events(self):
6264
query = "search index=%s application=%s | stats count by event" % (self.index, self.application_name)
6365
job = self.splunk.jobs.create(query, exec_mode="blocking")
64-
65-
events = []
66-
reader = results.ResultsReader(job.results())
67-
for kind,result in reader:
68-
if kind == results.RESULT:
69-
events.append({
70-
"name": result["event"],
71-
"count": int(result["count"] or 0)
72-
})
73-
74-
return events
66+
return counts(job, "event")
7567

7668
def properties(self, event_name):
7769
query = 'search index=%s application=%s event="%s" | stats dc(%s*) as *' % (
@@ -81,17 +73,18 @@ def properties(self, event_name):
8173

8274
properties = []
8375
reader = results.ResultsReader(job.results())
84-
for kind,result in reader:
85-
if kind == results.RESULT:
86-
for field, count in result.iteritems():
87-
# Ignore internal ResultsReader properties
88-
if field.startswith("$"):
89-
continue
90-
91-
properties.append({
76+
for result in reader:
77+
if not isinstance(result, dict):
78+
continue
79+
for field, count in result.iteritems():
80+
# Ignore internal ResultsReader properties
81+
if field.startswith("$"):
82+
continue
83+
84+
properties.append({
9285
"name": field,
9386
"count": int(count or 0)
94-
})
87+
})
9588

9689
return properties
9790

@@ -105,8 +98,8 @@ def property_values(self, event_name, property):
10598

10699
values = []
107100
reader = results.ResultsReader(job.results())
108-
for kind,result in reader:
109-
if kind == results.RESULT:
101+
for result in reader:
102+
if isinstance(result, dict):
110103
if result[property]:
111104
values.append({
112105
"name": result[property],
@@ -125,8 +118,8 @@ def events_over_time(self, event_name = "", time_range = TimeRange.MONTH, proper
125118

126119
over_time = {}
127120
reader = results.ResultsReader(job.results())
128-
for kind,result in reader:
129-
if kind == results.RESULT:
121+
for result in reader:
122+
if isinstance(result, dict):
130123
# Get the time for this entry
131124
time = result["_time"]
132125
del result["_time"]

examples/job.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ def sid(self, spec):
183183
"""Convert the given search specifier into a search-id (sid)."""
184184
if spec.startswith('@'):
185185
index = int(spec[1:])
186-
jobs = self.service.jobs()
186+
jobs = self.service.jobs.list()
187187
if index < len(jobs):
188188
return jobs[index].sid
189189
return spec # Assume it was already a valid sid

examples/oneshot.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,9 @@
2828

2929
def pretty(response):
3030
reader = results.ResultsReader(response)
31-
while True:
32-
kind = reader.read()
33-
if kind == None: break
34-
if kind == results.RESULT:
35-
event = reader.value
36-
pprint(event)
31+
for result in reader:
32+
if isinstance(result, dict):
33+
pprint(result)
3734

3835
def main():
3936
usage = "usage: oneshot.py <search>"
@@ -44,7 +41,7 @@ def main():
4441
search = opts.args[0]
4542
service = connect(**opts.kwargs)
4643
socket.setdefaulttimeout(None)
47-
response = service.jobs.create(search, exec_mode="oneshot")
44+
response = service.jobs.oneshot(search)
4845

4946
pretty(response)
5047

examples/search.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,12 @@ def main(argv):
7777

7878
job = service.jobs.create(search, **kwargs_create)
7979
while True:
80-
stats = job.refresh()(
81-
'isDone',
82-
'doneProgress',
83-
'scanCount',
84-
'eventCount',
85-
'resultCount')
80+
job.refresh()
81+
stats = {'isDone': job['isDone'],
82+
'doneProgress': job['doneProgress'],
83+
'scanCount': job['scanCount'],
84+
'eventCount': job['eventCount'],
85+
'resultCount': job['resultCount']}
8686
progress = float(stats['doneProgress'])*100
8787
scanned = int(stats['scanCount'])
8888
matched = int(stats['eventCount'])

examples/spurl.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626

2727
# Invoke the url using the given opts parameters
2828
def invoke(path, **kwargs):
29-
message = { "method": kwargs.get("method", "GET"), }
30-
return binding.connect(**kwargs).request(path, message)
29+
method = kwargs.get("method", "GET")
30+
return binding.connect(**kwargs).request(path, method=method)
3131

3232
def print_response(response):
3333
if response.status != 200:

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",

0 commit comments

Comments
 (0)