Skip to content

Commit 3dc0909

Browse files
author
Frederick Ross
committed
Merge branch 'fross-feature/naming' into develop
2 parents c546e18 + 9c93c43 commit 3dc0909

31 files changed

+3042
-994
lines changed

CHANGELOG.md

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

3+
## 0.8.5
4+
5+
36
## 0.8.0 (beta)
47

58
### Features

docs/tutorial.rst

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
Introduction
2+
------------
3+
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.::
5+
6+
import splunklib.client as client
7+
c = client.connect(host="localhost",
8+
port=8089,
9+
scheme="https",
10+
username="admin",
11+
password="changeme")
12+
saved_searches = c.saved_searches
13+
mss = saved_searches.create("my_saved_search", "search * | head 10")
14+
assert "my_saved_search" in saved_searches
15+
saved_searches.delete("my_saved_search")
16+
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.
22+
23+
Note that in the example code we did not assert::
24+
25+
mss == saved_searches["my_saved_search"]
26+
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.
28+
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]]
30+
31+
.. [#] Calling ``refresh`` on an entity that has already been deleted raises an ``HTTPError``.
32+
33+
You can access the fields of an entity either as if they were keys in a dictionary, or fields of an object::
34+
35+
mss['search'] == "search * | head 10"
36+
mss.search == "search * | head 10"
37+
38+
mss['action.email'] == '0'
39+
mss.action.email == '0'
40+
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.::
42+
43+
mss['action'] == {'email': '0',
44+
'populate_lookup': '0',
45+
'rss': '0',
46+
'script': '0',
47+
'summary_index': '0'}
48+
mss.action == {'email': '0',
49+
'populate_lookup': '0',
50+
'rss': '0',
51+
'script': '0',
52+
'summary_index': '0'}
53+
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.::
55+
56+
mss.dispatch() # Runs the saved search.
57+
mss.suppress(30) # Suppress all alerts from this saved search for 30 seconds
58+
59+
This should be enough information to understand the reference documentation and start using the SDK productively.
60+
61+
Roundtrips and caching
62+
----------------------
63+
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.
65+
66+
67+
68+
69+

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:

0 commit comments

Comments
 (0)