Skip to content

Commit bd8d142

Browse files
committed
Merge pull request splunk#126 from splunk/feature/kvstore
Add KV Store support to the Python SDK
2 parents f92d8ef + fd977be commit bd8d142

File tree

9 files changed

+587
-2
lines changed

9 files changed

+587
-2
lines changed

docs/client.rst

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,18 @@ splunklib.client
6868
:members: create, export, itemmeta, oneshot
6969
:inherited-members:
7070

71+
.. autoclass:: KVStoreCollection
72+
:members: data, update_index, update_field
73+
:inherited-members:
74+
75+
.. autoclass:: KVStoreCollectionData
76+
:members: query, query_by_id, insert, delete, delete_by_id, update, batch_save
77+
:inherited-members:
78+
79+
.. autoclass:: KVStoreCollections
80+
:members: create
81+
:inherited-members:
82+
7183
.. autoclass:: Loggers
7284
:members: itemmeta
7385
:inherited-members:
@@ -110,7 +122,7 @@ splunklib.client
110122
:inherited-members:
111123

112124
.. autoclass:: Service
113-
:members: apps, confs, capabilities, event_types, fired_alerts, indexes, info, inputs, job, jobs, loggers, messages, modular_input_kinds, parse, restart, restart_required, roles, search, saved_searches, settings, splunk_version, storage_passwords, users
125+
:members: apps, confs, capabilities, event_types, fired_alerts, indexes, info, inputs, job, jobs, kvstore, loggers, messages, modular_input_kinds, parse, restart, restart_required, roles, search, saved_searches, settings, splunk_version, storage_passwords, users
114126
:inherited-members:
115127

116128
.. autoclass:: Settings

docs/index.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ For more information, see the `Splunk Developer Portal <http://dev.splunk.com/vi
6969

7070
:class:`~splunklib.client.Jobs` class
7171

72+
:class:`~splunklib.client.KVStoreCollection` class
73+
74+
:class:`~splunklib.client.KVStoreCollectionData` class
75+
76+
:class:`~splunklib.client.KVStoreCollections` class
77+
7278
:class:`~splunklib.client.Loggers` class
7379

7480
:class:`~splunklib.client.Message` class

examples/kvstore.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env python
2+
#
3+
# Copyright 2011-2015 Splunk, Inc.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"): you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
17+
"""A command line utility for interacting with Splunk KV Store Collections."""
18+
19+
import sys, os, json
20+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
21+
22+
from splunklib.client import connect
23+
24+
try:
25+
from utils import parse
26+
except ImportError:
27+
raise Exception("Add the SDK repository to your PYTHONPATH to run the examples "
28+
"(e.g., export PYTHONPATH=~/splunk-sdk-python.")
29+
30+
def main():
31+
opts = parse(sys.argv[1:], {}, ".splunkrc")
32+
opts.kwargs["owner"] = "nobody"
33+
opts.kwargs["app"] = "search"
34+
service = connect(**opts.kwargs)
35+
36+
print "KV Store Collections:"
37+
for collection in service.kvstore:
38+
print " %s" % collection.name
39+
40+
# Let's delete a collection if it already exists, and then create it
41+
collection_name = "example_collection"
42+
if collection_name in service.kvstore:
43+
service.kvstore.delete(collection_name)
44+
45+
# Let's create it and then make sure it exists
46+
service.kvstore.create(collection_name)
47+
collection = service.kvstore[collection_name]
48+
49+
# Let's make sure it doesn't have any data
50+
print "Should be empty: %s" % json.dumps(collection.data.query())
51+
52+
# Let's add some data
53+
collection.data.insert(json.dumps({"_key": "item1", "somekey": 1, "otherkey": "foo"}))
54+
collection.data.insert(json.dumps({"_key": "item2", "somekey": 2, "otherkey": "foo"}))
55+
collection.data.insert(json.dumps({"somekey": 3, "otherkey": "bar"}))
56+
57+
# Let's make sure it has the data we just entered
58+
print "Should have our data: %s" % json.dumps(collection.data.query(), indent=1)
59+
60+
# Let's run some queries
61+
print "Should return item1: %s" % json.dumps(collection.data.query_by_id("item1"), indent=1)
62+
63+
query = json.dumps({"otherkey": "foo"})
64+
print "Should return item1 and item2: %s" % json.dumps(collection.data.query(query=query), indent=1)
65+
66+
query = json.dumps({"otherkey": "bar"})
67+
print "Should return third item with auto-generated _key: %s" % json.dumps(collection.data.query(query=query), indent=1)
68+
69+
# Let's delete the collection
70+
collection.delete()
71+
72+
if __name__ == "__main__":
73+
main()
74+
75+

splunklib/binding.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1168,10 +1168,16 @@ def post(self, url, headers=None, **kwargs):
11681168
:rtype: ``dict``
11691169
"""
11701170
if headers is None: headers = []
1171-
headers.append(("Content-Type", "application/x-www-form-urlencoded")),
1171+
11721172
# We handle GET-style arguments and an unstructured body. This is here
11731173
# to support the receivers/stream endpoint.
11741174
if 'body' in kwargs:
1175+
# We only use application/x-www-form-urlencoded if there is no other
1176+
# Content-Type header present. This can happen in cases where we
1177+
# send requests as application/json, e.g. for KV Store.
1178+
if len(filter(lambda x: x[0].lower() == "content-type", headers)) == 0:
1179+
headers.append(("Content-Type", "application/x-www-form-urlencoded"))
1180+
11751181
body = kwargs.pop('body')
11761182
if len(kwargs) > 0:
11771183
url = url + UrlEncoded('?' + _encode(**kwargs), skip_encode=True)

splunklib/client.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,14 @@ def splunk_version(self):
653653
self._splunk_version = tuple([int(p) for p in self.info['version'].split('.')])
654654
return self._splunk_version
655655

656+
@property
657+
def kvstore(self):
658+
"""Returns the collection of KV Store collections.
659+
660+
:return: A :class:`KVStoreCollections` collection of :class:`KVStoreCollection` entities.
661+
"""
662+
return KVStoreCollections(self)
663+
656664
@property
657665
def users(self):
658666
"""Returns the collection of users.
@@ -3518,3 +3526,194 @@ def package(self):
35183526
def updateInfo(self):
35193527
"""Returns any update information that is available for the app."""
35203528
return self._run_action("update")
3529+
3530+
class KVStoreCollections(Collection):
3531+
def __init__(self, service):
3532+
Collection.__init__(self, service, 'storage/collections/config', item=KVStoreCollection)
3533+
3534+
def create(self, name, indexes = {}, fields = {}, **kwargs):
3535+
"""Creates a KV Store Collection.
3536+
3537+
:param name: name of collection to create
3538+
:type name: ``string``
3539+
:param indexes: dictionary of index definitions
3540+
:type indexes: ``dict``
3541+
:param fields: dictionary of field definitions
3542+
:type fields: ``dict``
3543+
:param kwargs: a dictionary of additional parameters specifying indexes and field definitions
3544+
:type kwargs: ``dict``
3545+
3546+
:return: Result of POST request
3547+
"""
3548+
for k, v in indexes.iteritems():
3549+
if isinstance(v, dict):
3550+
v = json.dumps(v)
3551+
kwargs['index.' + k] = v
3552+
for k, v in fields.iteritems():
3553+
kwargs['field.' + k] = v
3554+
return self.post(name=name, **kwargs)
3555+
3556+
class KVStoreCollection(Entity):
3557+
@property
3558+
def data(self):
3559+
"""Returns data object for this Collection.
3560+
3561+
:rtype: :class:`KVStoreData`
3562+
"""
3563+
return KVStoreCollectionData(self)
3564+
3565+
def update_index(self, name, value):
3566+
"""Changes the definition of a KV Store index.
3567+
3568+
:param name: name of index to change
3569+
:type name: ``string``
3570+
:param value: new index definition
3571+
:type value: ``dict`` or ``string``
3572+
3573+
:return: Result of POST request
3574+
"""
3575+
kwargs = {}
3576+
kwargs['index.' + name] = value if isinstance(value, basestring) else json.dumps(value)
3577+
return self.post(**kwargs)
3578+
3579+
def update_field(self, name, value):
3580+
"""Changes the definition of a KV Store field.
3581+
3582+
:param name: name of field to change
3583+
:type name: ``string``
3584+
:param value: new field definition
3585+
:type value: ``string``
3586+
3587+
:return: Result of POST request
3588+
"""
3589+
kwargs = {}
3590+
kwargs['field.' + name] = value
3591+
return self.post(**kwargs)
3592+
3593+
class KVStoreCollectionData(object):
3594+
"""This class represents the data endpoint for a KVStoreCollection.
3595+
3596+
Retrieve using :meth:`KVStoreCollection.data`
3597+
"""
3598+
JSON_HEADER = [('Content-Type', 'application/json')]
3599+
3600+
def __init__(self, collection):
3601+
self.service = collection.service
3602+
self.collection = collection
3603+
self.owner, self.app, self.sharing = collection._proper_namespace()
3604+
self.path = 'storage/collections/data/' + UrlEncoded(self.collection.name) + '/'
3605+
3606+
def _get(self, url, **kwargs):
3607+
return self.service.get(self.path + url, owner=self.owner, app=self.app, sharing=self.sharing, **kwargs)
3608+
3609+
def _post(self, url, **kwargs):
3610+
return self.service.post(self.path + url, owner=self.owner, app=self.app, sharing=self.sharing, **kwargs)
3611+
3612+
def _delete(self, url, **kwargs):
3613+
return self.service.delete(self.path + url, owner=self.owner, app=self.app, sharing=self.sharing, **kwargs)
3614+
3615+
def query(self, **query):
3616+
"""
3617+
Gets the results of query, with optional parameters sort, limit, skip, and fields.
3618+
3619+
:param query: Optional parameters. Valid options are sort, limit, skip, and fields
3620+
:type query: ``dict``
3621+
3622+
:return: Array of documents retrieved by query.
3623+
:rtype: ``array``
3624+
"""
3625+
return json.loads(self._get('', **query).body.read())
3626+
3627+
def query_by_id(self, id):
3628+
"""
3629+
Returns object with _id = id.
3630+
3631+
:param id: Value for ID. If not a string will be coerced to string.
3632+
:type id: ``string``
3633+
3634+
:return: Document with id
3635+
:rtype: ``dict``
3636+
"""
3637+
return json.loads(self._get(UrlEncoded(str(id))).body.read())
3638+
3639+
def insert(self, data):
3640+
"""
3641+
Inserts item into this collection. An _id field will be generated if not assigned in the data.
3642+
3643+
:param data: Document to insert
3644+
:type data: ``string``
3645+
3646+
:return: _id of inserted object
3647+
:rtype: ``dict``
3648+
"""
3649+
return json.loads(self._post('', headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read())
3650+
3651+
def delete(self, query=None):
3652+
"""
3653+
Deletes all data in collection if query is absent. Otherwise, deletes all data matched by query.
3654+
3655+
:param query: Query to select documents to delete
3656+
:type query: ``string``
3657+
3658+
:return: Result of DELETE request
3659+
"""
3660+
return self._delete('', **({'query': query}) if query else {})
3661+
3662+
def delete_by_id(self, id):
3663+
"""
3664+
Deletes document that has _id = id.
3665+
3666+
:param id: id of document to delete
3667+
:type id: ``string``
3668+
3669+
:return: Result of DELETE request
3670+
"""
3671+
return self._delete(UrlEncoded(str(id)))
3672+
3673+
def update(self, id, data):
3674+
"""
3675+
Replaces document with _id = id with data.
3676+
3677+
:param id: _id of document to update
3678+
:type id: ``string``
3679+
:param data: the new document to insert
3680+
:type data: ``string``
3681+
3682+
:return: id of replaced document
3683+
:rtype: ``dict``
3684+
"""
3685+
return json.loads(self._post(UrlEncoded(str(id)), headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read())
3686+
3687+
def batch_find(self, *dbqueries):
3688+
"""
3689+
Returns array of results from queries dbqueries.
3690+
3691+
:param dbqueries: Array of individual queries as dictionaries
3692+
:type dbqueries: ``array`` of ``dict``
3693+
3694+
:return: Results of each query
3695+
:rtype: ``array`` of ``array``
3696+
"""
3697+
if len(dbqueries) < 1:
3698+
raise Exception('Must have at least one query.')
3699+
3700+
data = json.dumps(dbqueries)
3701+
3702+
return json.loads(self._post('batch_find', headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read())
3703+
3704+
def batch_save(self, *documents):
3705+
"""
3706+
Inserts or updates every document specified in documents.
3707+
3708+
:param documents: Array of documents to save as dictionaries
3709+
:type documents: ``array`` of ``dict``
3710+
3711+
:return: Results of update operation as overall stats
3712+
:rtype: ``dict``
3713+
"""
3714+
if len(documents) < 1:
3715+
raise Exception('Must have at least one document.')
3716+
3717+
data = json.dumps(documents)
3718+
3719+
return json.loads(self._post('batch_save', headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read())

tests/test_examples.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ def test_job(self):
182182
"job.py",
183183
"job.py list",
184184
"job.py list @0")
185+
186+
def test_kvstore(self):
187+
self.check_commands(
188+
"kvstore.py --help",
189+
"kvstore.py")
185190

186191
def test_loggers(self):
187192
self.check_commands(

0 commit comments

Comments
 (0)