Skip to content

Commit 86a964c

Browse files
author
Frederick Ross
committed
Initial support for modular inputs.
Fixed version handling.
1 parent c5aa3d6 commit 86a964c

File tree

6 files changed

+198
-137
lines changed

6 files changed

+198
-137
lines changed

splunklib/client.py

Lines changed: 161 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
PATH_JOBS = "search/jobs/"
9898
PATH_LOGGER = "server/logger/"
9999
PATH_MESSAGES = "messages/"
100+
PATH_MODULAR_INPUTS = "data/modular-inputs"
100101
PATH_ROLES = "authentication/roles/"
101102
PATH_SAVED_SEARCHES = "saved/searches/"
102103
PATH_STANZA = "configs/conf-%s/%s" # (file, stanza)
@@ -334,6 +335,7 @@ class Service(Context):
334335
"""
335336
def __init__(self, **kwargs):
336337
Context.__init__(self, **kwargs)
338+
self._splunk_version = None
337339

338340
@property
339341
def apps(self):
@@ -396,6 +398,14 @@ def messages(self):
396398
"""Returns a collection of service messages."""
397399
return Collection(self, PATH_MESSAGES, item=Message)
398400

401+
@property
402+
def modular_input_kinds(self):
403+
"""Returns a collection of the modular input kinds on this Splunk instance."""
404+
if self.splunk_version[0] >= 5:
405+
return ReadOnlyCollection(self, PATH_MODULAR_INPUTS, item=Entity)
406+
else:
407+
raise IllegalOperationException("Modular inputs are not supported before Splunk version 5.")
408+
399409
# kwargs: enable_lookups, reload_macros, parse_only, output_mode
400410
def parse(self, query, **kwargs):
401411
"""Parses a search query and returns a semantic map of the search.
@@ -454,6 +464,12 @@ def settings(self):
454464
"""Returns configuration settings for the service."""
455465
return Settings(self)
456466

467+
@property
468+
def splunk_version(self):
469+
if self._splunk_version is None:
470+
self._splunk_version = tuple([int(p) for p in self.info['version'].split('.')])
471+
return self._splunk_version
472+
457473
@property
458474
def users(self):
459475
"""Returns a collection of users."""
@@ -891,47 +907,12 @@ def update(self, **kwargs):
891907
self.post(**kwargs)
892908
return self
893909

894-
class Collection(Endpoint):
895-
"""A collection of entities in the Splunk instance.
896-
897-
Splunk provides a number of different collections of distinct
898-
entity types: applications, saved searches, fired alerts, and a
899-
number of others. Each particular type is available separately
900-
from the Splunk instance, and the entities of that type are
901-
returned in a :class:`Collection`.
902-
903-
:class:`Collection`'s interface does not quite match either
904-
``list`` or ``dict`` in Python, since there are enough semantic
905-
mismatches with either to make its behavior surprising. A unique
906-
element in a :class:`Collection` is defined by a string giving its
907-
name plus a namespace object (though the namespace is optional if
908-
the name is unique).::
909-
910-
import splunklib.client as client
911-
s = client.connect(...)
912-
c = s.saved_searches # c is a Collection
913-
m = c['my_search', client.namespace(owner='boris', app='natasha', sharing='user')]
914-
# Or if there is only one search visible named 'my_search'
915-
m = c['my_search']
916-
917-
Similarly, ``"name" in c`` works as you expect (though you cannot
918-
currently pass a namespace to the ``in`` operator), as does
919-
``len(c)``.
920-
921-
However, as an aggregate, :class:`Collection` behaves more like a
922-
list. If you iterate over a :class:`Collection`, you get an
923-
iterator over the entities, not the names and namespaces::
924-
925-
for entity in c:
926-
assert isinstance(entity, client.Entity)
927-
928-
The :meth:`create` and :meth:`delete` methods create and delete
929-
entities in this collection. The access control list and other
930-
metadata of the collection is returned by the :meth:`itemmeta`
931-
method.
910+
class ReadOnlyCollection(Endpoint):
911+
"""A read-only collection of entities in the Splunk instance.
932912
933-
:class:`Collection` does no caching. Each call makes at least one
934-
round trip to the server to fetch data.
913+
See the documentation for :class:`Collection` for most usage. The only
914+
difference is that it lacks :class:`ReadOnlyCollection` lacks
915+
:method:`create`, :method:`delete`
935916
"""
936917
def __init__(self, service, path, item=Entity):
937918
Endpoint.__init__(self, service, path)
@@ -1132,97 +1113,6 @@ def contains(self, name):
11321113
"""
11331114
return name in self
11341115

1135-
def create(self, name, **params):
1136-
"""Create a new entity in this collection.
1137-
1138-
This function makes either one or two roundtrips to the
1139-
server, depending on the type of the entities in this
1140-
collection, plus at most two more if autologin is enabled.
1141-
1142-
:param name: The name of the entity to create.
1143-
:type name: string
1144-
:param namespace: A namespace, as created by the :func:`namespace`
1145-
function (optional). If you wish, you can set
1146-
``owner``, ``app``, and ``sharing`` directly.
1147-
:type namespace: :class:`Record` with keys ``'owner'``, ``'app'``, and
1148-
``'sharing'``
1149-
:param params: Additional entity-specific arguments (optional).
1150-
:return: The new entity.
1151-
:rtype: subclass of ``Entity``, chosen by ``self.item`` in ``Collection``
1152-
1153-
**Example**::
1154-
1155-
import splunklib.client as client
1156-
s = client.connect(...)
1157-
applications = s.apps
1158-
new_app = applications.create("my_fake_app")
1159-
"""
1160-
if not isinstance(name, basestring):
1161-
raise InvalidNameException("%s is not a valid name for an entity." % name)
1162-
if 'namespace' in params:
1163-
namespace = params.pop('namespace')
1164-
params['owner'] = namespace.owner
1165-
params['app'] = namespace.app
1166-
params['sharing'] = namespace.sharing
1167-
response = self.post(name=name, **params)
1168-
atom = _load_atom(response, XNAME_ENTRY)
1169-
if atom is None:
1170-
# This endpoint doesn't return the content of the new
1171-
# item. We have to go fetch it ourselves.
1172-
return self[name]
1173-
else:
1174-
entry = atom.entry
1175-
state = _parse_atom_entry(entry)
1176-
entity = self.item(
1177-
self.service,
1178-
self._entity_path(state),
1179-
state=state)
1180-
return entity
1181-
1182-
def delete(self, name, **params):
1183-
"""Delete the entity *name* from the collection.
1184-
1185-
:param name: The name of the entity to delete.
1186-
:type name: string
1187-
:rtype: the collection ``self``.
1188-
1189-
This method is implemented for consistency with the REST
1190-
interface's DELETE method.
1191-
1192-
If there is no entity named *name* on the server, then throws
1193-
a ``KeyError``. This function always makes a roundtrip to the
1194-
server.
1195-
1196-
**Example**::
1197-
1198-
import splunklib.client as client
1199-
c = client.connect(...)
1200-
saved_searches = c.saved_searches
1201-
saved_searches.create('my_saved_search',
1202-
'search * | head 1')
1203-
assert 'my_saved_search' in saved_searches
1204-
saved_searches.delete('my_saved_search')
1205-
assert 'my_saved_search' not in saved_searches
1206-
"""
1207-
# If you update the documentation here, be sure you do so on
1208-
# __delitem__ as well.
1209-
if 'namespace' in params:
1210-
namespace = params.pop('namespace')
1211-
params['owner'] = namespace.owner
1212-
params['app'] = namespace.app
1213-
params['sharing'] = namespace.sharing
1214-
try:
1215-
self.service.delete(_path(self.path, name), **params)
1216-
except HTTPError as he:
1217-
# An HTTPError with status code 404 means that the entity
1218-
# has already been deleted, and we reraise it as a
1219-
# KeyError.
1220-
if he.status == 404:
1221-
raise KeyError("No such entity %s" % name)
1222-
else:
1223-
raise
1224-
return self
1225-
12261116
def itemmeta(self):
12271117
"""Returns metadata for members of the collection.
12281118
@@ -1339,6 +1229,137 @@ def names(self, count=None, **kwargs):
13391229
"""
13401230
return [ent.name for ent in self.iter(count=count, **kwargs)]
13411231

1232+
class Collection(ReadOnlyCollection):
1233+
"""A collection of entities in the Splunk instance.
1234+
1235+
Splunk provides a number of different collections of distinct
1236+
entity types: applications, saved searches, fired alerts, and a
1237+
number of others. Each particular type is available separately
1238+
from the Splunk instance, and the entities of that type are
1239+
returned in a :class:`Collection`.
1240+
1241+
:class:`Collection`'s interface does not quite match either
1242+
``list`` or ``dict`` in Python, since there are enough semantic
1243+
mismatches with either to make its behavior surprising. A unique
1244+
element in a :class:`Collection` is defined by a string giving its
1245+
name plus a namespace object (though the namespace is optional if
1246+
the name is unique).::
1247+
1248+
import splunklib.client as client
1249+
s = client.connect(...)
1250+
c = s.saved_searches # c is a Collection
1251+
m = c['my_search', client.namespace(owner='boris', app='natasha', sharing='user')]
1252+
# Or if there is only one search visible named 'my_search'
1253+
m = c['my_search']
1254+
1255+
Similarly, ``"name" in c`` works as you expect (though you cannot
1256+
currently pass a namespace to the ``in`` operator), as does
1257+
``len(c)``.
1258+
1259+
However, as an aggregate, :class:`Collection` behaves more like a
1260+
list. If you iterate over a :class:`Collection`, you get an
1261+
iterator over the entities, not the names and namespaces::
1262+
1263+
for entity in c:
1264+
assert isinstance(entity, client.Entity)
1265+
1266+
The :meth:`create` and :meth:`delete` methods create and delete
1267+
entities in this collection. The access control list and other
1268+
metadata of the collection is returned by the :meth:`itemmeta`
1269+
method.
1270+
1271+
:class:`Collection` does no caching. Each call makes at least one
1272+
round trip to the server to fetch data.
1273+
"""
1274+
def create(self, name, **params):
1275+
"""Create a new entity in this collection.
1276+
1277+
This function makes either one or two roundtrips to the
1278+
server, depending on the type of the entities in this
1279+
collection, plus at most two more if autologin is enabled.
1280+
1281+
:param name: The name of the entity to create.
1282+
:type name: string
1283+
:param namespace: A namespace, as created by the :func:`namespace`
1284+
function (optional). If you wish, you can set
1285+
``owner``, ``app``, and ``sharing`` directly.
1286+
:type namespace: :class:`Record` with keys ``'owner'``, ``'app'``, and
1287+
``'sharing'``
1288+
:param params: Additional entity-specific arguments (optional).
1289+
:return: The new entity.
1290+
:rtype: subclass of ``Entity``, chosen by ``self.item`` in ``Collection``
1291+
1292+
**Example**::
1293+
1294+
import splunklib.client as client
1295+
s = client.connect(...)
1296+
applications = s.apps
1297+
new_app = applications.create("my_fake_app")
1298+
"""
1299+
if not isinstance(name, basestring):
1300+
raise InvalidNameException("%s is not a valid name for an entity." % name)
1301+
if 'namespace' in params:
1302+
namespace = params.pop('namespace')
1303+
params['owner'] = namespace.owner
1304+
params['app'] = namespace.app
1305+
params['sharing'] = namespace.sharing
1306+
response = self.post(name=name, **params)
1307+
atom = _load_atom(response, XNAME_ENTRY)
1308+
if atom is None:
1309+
# This endpoint doesn't return the content of the new
1310+
# item. We have to go fetch it ourselves.
1311+
return self[name]
1312+
else:
1313+
entry = atom.entry
1314+
state = _parse_atom_entry(entry)
1315+
entity = self.item(
1316+
self.service,
1317+
self._entity_path(state),
1318+
state=state)
1319+
return entity
1320+
1321+
def delete(self, name, **params):
1322+
"""Delete the entity *name* from the collection.
1323+
1324+
:param name: The name of the entity to delete.
1325+
:type name: string
1326+
:rtype: the collection ``self``.
1327+
1328+
This method is implemented for consistency with the REST
1329+
interface's DELETE method.
1330+
1331+
If there is no entity named *name* on the server, then throws
1332+
a ``KeyError``. This function always makes a roundtrip to the
1333+
server.
1334+
1335+
**Example**::
1336+
1337+
import splunklib.client as client
1338+
c = client.connect(...)
1339+
saved_searches = c.saved_searches
1340+
saved_searches.create('my_saved_search',
1341+
'search * | head 1')
1342+
assert 'my_saved_search' in saved_searches
1343+
saved_searches.delete('my_saved_search')
1344+
assert 'my_saved_search' not in saved_searches
1345+
"""
1346+
if 'namespace' in params:
1347+
namespace = params.pop('namespace')
1348+
params['owner'] = namespace.owner
1349+
params['app'] = namespace.app
1350+
params['sharing'] = namespace.sharing
1351+
try:
1352+
self.service.delete(_path(self.path, name), **params)
1353+
except HTTPError as he:
1354+
# An HTTPError with status code 404 means that the entity
1355+
# has already been deleted, and we reraise it as a
1356+
# KeyError.
1357+
if he.status == 404:
1358+
raise KeyError("No such entity %s" % name)
1359+
else:
1360+
raise
1361+
return self
1362+
13421363
class ConfigurationFile(Collection):
13431364
"""This class contains a single configuration, which is a collection of
13441365
stanzas."""
@@ -1454,6 +1475,13 @@ def default(self):
14541475
index = self['_audit']
14551476
return index['defaultDatabase']
14561477

1478+
def delete(self):
1479+
if self.splunk_version[0] >= 5:
1480+
Collection.delete(self.service, self.name)
1481+
else:
1482+
raise IllegalOperationException("Deleting indexes via the REST API is "
1483+
"not supported before Splunk version 5.")
1484+
14571485
class Index(Entity):
14581486
"""This class is an index class used to access specific operations."""
14591487
def __init__(self, service, path, **kwargs):
@@ -1545,14 +1573,14 @@ def disable(self):
15451573
"""Disables this index."""
15461574
# Starting in Ace, we have to do this with specific sharing,
15471575
# unlike most other entities.
1548-
self.post("disable", sharing="system")
1576+
self.post("disable")
15491577
return self
15501578

15511579
def enable(self):
15521580
"""Enables this index."""
15531581
# Starting in Ace, we have to reenable this with a specific
15541582
# sharing unlike most other entities.
1555-
self.post("enable", sharing="system")
1583+
self.post("enable")
15561584
return self
15571585

15581586
def roll_hot_buckets(self):

tests/test_collection.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
class TestCase(testlib.TestCase):
4141
def setUp(self):
4242
super(TestCase, self).setUp()
43+
if self.service.splunk_version[0] >= 5 and 'modular_input_kinds' not in collections:
44+
collections.append('modular_input_kinds') # Not supported before Splunk 5.0
45+
else:
46+
logging.info("Skipping modular_input_kinds; not supported by Splunk %s" % \
47+
'.'.join(str(x) for x in self.service.splunk_version))
4348
for saved_search in self.service.saved_searches:
4449
if saved_search.name.startswith('delete-me'):
4550
try:
@@ -54,7 +59,7 @@ def test_metadata(self):
5459
self.assertRaises(client.NotSupportedError, self.service.loggers.itemmeta)
5560
self.assertRaises(TypeError, self.service.inputs.itemmeta)
5661
for c in collections:
57-
if c in ['jobs', 'loggers', 'inputs']:
62+
if c in ['jobs', 'loggers', 'inputs', 'modular_input_kinds']:
5863
continue
5964
coll = getattr(self.service, c)
6065
metadata = coll.itemmeta()

tests/test_index.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def tearDown(self):
3434
# 5.0. In 4.x, we just have to leave them lying around until
3535
# someone cares to go clean them up. Unique naming prevents
3636
# clashes, though.
37-
if self.splunk_version >= 5:
37+
if self.service.splunk_version[0] >= 5:
3838
self.service.indexes.delete(self.index_name)
3939
else:
4040
logging.warning("test_index.py:TestDeleteIndex: Skipped: cannot "

0 commit comments

Comments
 (0)