Skip to content

Commit 8c16a69

Browse files
committed
Update Entity metadata interface -- Entity.metadata => Entity.access & Entity.fields plus improved entity units (found/fixed a bug in Message entity).
1 parent e4adff8 commit 8c16a69

19 files changed

+224
-167
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ properties:
5050

5151
* `Entity.state` returns the entire state record
5252
* `Entity.content` returns the content field of the state record
53-
* `Entity.metadata` returns the metadata field of the state record
53+
* `Entity.access` returns entity access metadata
54+
* `Entity.fields` returns entity content metadata
5455

5556
`Entity.refresh` is a new method that issues a round-trip to the server
5657
and updates the local, cached state record.

docs/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ splunklib.client
9797
:inherited-members:
9898
:undoc-members:
9999

100+
.. autoclass:: Loggers
101+
:members:
102+
:inherited-members:
103+
100104
.. autoclass:: Message
101105
:members:
102106
:inherited-members:

examples/index.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,11 @@ def create(self, argv):
7070
print "Index '%s' already exists" % name
7171
return
7272

73-
# Read item metadata and construct command line parser rules that
73+
# Read index metadata and construct command line parser rules that
7474
# correspond to each editable field.
7575

7676
# Request editable fields
77-
itemmeta = self.service.indexes.itemmeta()
78-
fields = itemmeta['eai:attributes'].optionalFields
77+
fields = self.service.indexes.itemmeta().fields.optional
7978

8079
# Build parser rules
8180
rules = dict([(field, {'flags': ["--%s" % field]}) for field in fields])
@@ -142,15 +141,16 @@ def update(self, argv):
142141
if len(argv) == 0:
143142
error("Command requires an index name", 2)
144143
name = argv[0]
144+
145145
if not self.service.indexes.contains(name):
146146
error("Index '%s' does not exist" % name, 2)
147147
index = self.service.indexes[name]
148148

149-
# Read entity metadata and construct command line parser rules that
149+
# Read index metadata and construct command line parser rules that
150150
# correspond to each editable field.
151151

152152
# Request editable fields
153-
fields = index.readmeta()['eai:attributes'].optionalFields
153+
fields = self.service.indexes.itemmeta().fields.optional
154154

155155
# Build parser rules
156156
rules = dict([(field, {'flags': ["--%s" % field]}) for field in fields])

splunklib/client.py

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -108,17 +108,37 @@ def _parse_atom_entry(entry):
108108
elink = elink if isinstance(elink, list) else [elink]
109109
links = record((link.rel, link.href) for link in elink)
110110

111-
econtent = entry.get('content', {})
112-
content = record((k, v) for k, v in econtent.iteritems()
111+
# Retrieve entity content values
112+
content = entry.get('content', {})
113+
114+
# Host entry metadata
115+
metadata = _parse_atom_metadata(content)
116+
117+
# Filter some of the noise out of the content record
118+
content = record((k, v) for k, v in content.iteritems()
113119
if k not in ['eai:acl', 'eai:attributes', 'type'])
114-
metadata = dict((k, econtent.get(k, None))
115-
for k in ['eai:acl', 'eai:attributes'])
116120

117121
return record({
118122
'title': title,
119123
'links': links,
120-
'content': content,
121-
'metadata': metadata })
124+
'access': metadata.access,
125+
'fields': metadata.fields,
126+
'content': content
127+
})
128+
129+
# Parse the metadata fields out of the given atom entry content record
130+
def _parse_atom_metadata(content):
131+
# Hoist access metadata
132+
access = content.get('eai:acl', None)
133+
134+
# Hoist content metadata (and cleanup some naming)
135+
attributes = content.get('eai:attributes', {})
136+
fields = record({
137+
'required': attributes.get('requiredFields', []),
138+
'optional': attributes.get('optionalFields', []),
139+
'wildcard': attributes.get('wildcardFields', [])})
140+
141+
return record({'access': access, 'fields': fields})
122142

123143
# kwargs: scheme, host, port, app, owner, username, password
124144
def connect(**kwargs):
@@ -220,7 +240,7 @@ def jobs(self):
220240
def loggers(self):
221241
"""Returns a collection of service logging categories and their status.
222242
"""
223-
return Collection(self, PATH_LOGGER)
243+
return Loggers(self)
224244

225245
@property
226246
def messages(self):
@@ -233,7 +253,7 @@ def parse(self, query, **kwargs):
233253
234254
:param `query`: The search query to parse.
235255
:param `kwargs`: Optional arguments to pass to the ``search/parser``
236-
endpoint.
256+
endpoint.
237257
:return: A semantic map of the parsed search query.
238258
"""
239259
return self.get("search/parser", q=query, **kwargs)
@@ -302,17 +322,6 @@ def __call__(self, *args):
302322
def __getitem__(self, key):
303323
return self.content[key]
304324

305-
#
306-
# Given that update doesn't automatically refresh the cached local state,
307-
# this operation now violates the principle of least surprise, because it
308-
# allows you to do what appears to be a local assignment, which is then
309-
# not reflected in the value you see if you do a subsequent __getitem__
310-
# without an explicit refresh.
311-
#
312-
# def __setitem__(self, key, value):
313-
# self.update(**{ key: value })
314-
#
315-
316325
# Load the Atom entry record from the given response - this is a method
317326
# because the "entry" record varies slightly by entity and this allows
318327
# for a subclass to override and handle any special cases.
@@ -331,6 +340,11 @@ def refresh(self, state=None):
331340
self._state = state if state is not None else self.read()
332341
return self
333342

343+
@property
344+
def access(self):
345+
"""Returns entity access metadata."""
346+
return self.state.access
347+
334348
@property
335349
def content(self):
336350
"""Returns the contents of the entity."""
@@ -346,16 +360,16 @@ def enable(self):
346360
self.post("enable")
347361
return self
348362

363+
@property
364+
def fields(self):
365+
"""Returns entity content metadata."""
366+
return self.state.fields
367+
349368
@property
350369
def links(self):
351370
"""Returns a dictionary of related resources."""
352371
return self.state.links
353372

354-
@property
355-
def metadata(self):
356-
"""Returns the entity metadata."""
357-
return self.state.metadata
358-
359373
@property
360374
def name(self):
361375
"""Returns the entity name."""
@@ -448,10 +462,7 @@ def itemmeta(self):
448462
"""Returns metadata for members of the collection."""
449463
response = self.get("_new")
450464
content = _load_atom(response, MATCH_ENTRY_CONTENT)
451-
return {
452-
'eai:acl': content['eai:acl'],
453-
'eai:attributes': content['eai:attributes']
454-
}
465+
return _parse_atom_metadata(content)
455466

456467
# kwargs: count, offset, search, sort_dir, sort_key, sort_mode
457468
def list(self, count=-1, **kwargs):
@@ -674,10 +685,7 @@ def itemmeta(self, kind):
674685
"""Returns metadata for the members of a given kind."""
675686
response = self.get("%s/_new" % self._kindmap[kind])
676687
content = _load_atom(response, MATCH_ENTRY_CONTENT)
677-
return {
678-
'eai:acl': content['eai:acl'],
679-
'eai:attributes': content['eai:attributes']
680-
}
688+
return _parse_atom_metadata(content)
681689

682690
@property
683691
def kinds(self):
@@ -884,9 +892,17 @@ def create(self, query, **kwargs):
884892
def list(self, count=0, **kwargs):
885893
return Collection.list(self, count, **kwargs)
886894

895+
class Loggers(Collection):
896+
"""This class represents a collection of service logging categories."""
897+
def __init__(self, service):
898+
Collection.__init__(self, service, PATH_LOGGER)
899+
900+
def itemmeta(self):
901+
raise NotSupportedError
902+
887903
class Message(Entity):
888-
def __init__(self, service, name, **kwargs):
889-
Entity.__init__(self, service, _path(PATH_MESSAGES, name), **kwargs)
904+
def __init__(self, service, path, **kwargs):
905+
Entity.__init__(self, service, path, **kwargs)
890906

891907
@property
892908
def value(self):

tests/splunklib.client.baseline

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
['AlertGroup', 'Collection', 'Conf', 'Confs', 'Context', 'Endpoint', 'Entity', 'HTTPError', 'INPUT_KINDMAP', 'Index', 'Input', 'Inputs', 'Job', 'Jobs', 'MATCH_ENTRY_CONTENT', 'Message', 'NotSupportedError', 'OperationError', 'PATH_APPS', 'PATH_CAPABILITIES', 'PATH_CONF', 'PATH_CONFS', 'PATH_EVENT_TYPES', 'PATH_FIRED_ALERTS', 'PATH_INDEXES', 'PATH_INPUTS', 'PATH_JOBS', 'PATH_LOGGER', 'PATH_MESSAGES', 'PATH_ROLES', 'PATH_SAVED_SEARCHES', 'PATH_STANZA', 'PATH_USERS', 'SavedSearch', 'SavedSearches', 'Service', 'Settings', 'Stanza', 'Users', 'XNAMEF_ATOM', 'XNAME_CONTENT', 'XNAME_ENTRY', '__all__', '__builtins__', '__doc__', '__file__', '__name__', '__package__', '_filter_content', '_load_atom', '_load_atom_entries', '_load_sid', '_parse_atom_entry', '_path', '_path_conf', 'connect', 'data', 'quote', 'record', 'sleep', 'urlencode']
1+
['AlertGroup', 'Collection', 'Conf', 'Confs', 'Context', 'Endpoint', 'Entity', 'HTTPError', 'INPUT_KINDMAP', 'Index', 'Input', 'Inputs', 'Job', 'Jobs', 'Loggers', 'MATCH_ENTRY_CONTENT', 'Message', 'NotSupportedError', 'OperationError', 'PATH_APPS', 'PATH_CAPABILITIES', 'PATH_CONF', 'PATH_CONFS', 'PATH_EVENT_TYPES', 'PATH_FIRED_ALERTS', 'PATH_INDEXES', 'PATH_INPUTS', 'PATH_JOBS', 'PATH_LOGGER', 'PATH_MESSAGES', 'PATH_ROLES', 'PATH_SAVED_SEARCHES', 'PATH_STANZA', 'PATH_USERS', 'SavedSearch', 'SavedSearches', 'Service', 'Settings', 'Stanza', 'Users', 'XNAMEF_ATOM', 'XNAME_CONTENT', 'XNAME_ENTRY', '__all__', '__builtins__', '__doc__', '__file__', '__name__', '__package__', '_filter_content', '_load_atom', '_load_atom_entries', '_load_sid', '_parse_atom_entry', '_parse_atom_metadata', '_path', '_path_conf', 'connect', 'data', 'quote', 'record', 'sleep', 'urlencode']
22

tests/test_app.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@
2020

2121
class TestCase(testlib.TestCase):
2222
def check_app(self, app):
23-
app.name
24-
app.path
25-
app.content
26-
app.metadata
23+
self.check_entity(app)
2724

2825
def test_read(self):
2926
service = client.connect(**self.opts.kwargs)

tests/test_collection.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@
2121
class TestCase(testlib.TestCase):
2222
# Verify that the given collections interface behaves as expected
2323
def check_collection(self, collection):
24+
# Check item metadata
25+
try:
26+
metadata = collection.itemmeta()
27+
self.assertTrue(isinstance(metadata, dict))
28+
self.assertTrue(isinstance(metadata.access, dict))
29+
self.assertTrue(isinstance(metadata.fields, dict))
30+
except client.NotSupportedError: pass
31+
2432
# Check various collection options
2533
collection.list() # Default
2634
collection.list(search="title=*")

tests/test_conf.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,7 @@ def test_read(self):
3636
conf.name
3737
conf.path
3838
for stanza in conf:
39-
stanza.name
40-
stanza.path
41-
stanza.content
42-
stanza.metadata
39+
self.check_entity(stanza)
4340

4441
def test_crud(self):
4542
service = client.connect(**self.opts.kwargs)

tests/test_event_type.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,26 @@
1919
import testlib
2020

2121
class TestCase(testlib.TestCase):
22-
def check_content(self, entity, **kwargs):
23-
for k, v in kwargs.iteritems():
24-
self.assertEqual(entity[k], str(v))
22+
def check_event_type(self, event_type):
23+
self.check_entity(event_type)
24+
keys = ['description', 'priority', 'search']
25+
for key in keys: self.assertTrue(key, event_type)
2526

26-
def test(self):
27-
event_types = client.connect(**self.opts.kwargs).event_types
27+
def test_read(self):
28+
service = client.connect(**self.opts.kwargs)
29+
30+
for event_type in service.event_types:
31+
self.check_event_type(event_type)
32+
33+
def test_crud(self):
34+
service = client.connect(**self.opts.kwargs)
35+
36+
event_types = service.event_types
2837

2938
if 'sdk-test' in event_types:
3039
event_types.delete('sdk-test')
3140
self.assertFalse('sdk-test' in event_types)
3241

33-
for event_type in event_types:
34-
event_type.content.description
35-
event_type.content.priority
36-
event_type.content.search
37-
3842
kwargs = {}
3943
kwargs['search'] = "index=_internal *"
4044
kwargs['description'] = "An internal event"

tests/test_fired_alert.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ def test_crud(self):
3131
searches = service.saved_searches
3232
fired_alerts = service.fired_alerts
3333

34+
if 'sdk-tests' not in service.indexes:
35+
service.indexes.create("sdk-tests")
36+
3437
# Clean out the test index
3538
index = service.indexes['sdk-tests']
3639
index.clean()

0 commit comments

Comments
 (0)