Skip to content

Commit 546388b

Browse files
author
David Noble
committed
DVPL-3363: splunklib.searchcommands | Provide access to the contents of the file located at SearchCommand.input_headers['infoPath']
DVPL-3364: splunklib.searchcommands | Replace MessagesHeader data structure borrowed from Intersplunk.py + Unit tests are include with this commit + Verified for completeness and correctness in Splunk with ad-hoc integration testing Signed-off-by: David Noble <[email protected]>
1 parent e372a05 commit 546388b

File tree

6 files changed

+402
-100
lines changed

6 files changed

+402
-100
lines changed

examples/searchcommands_app/bin/simulate.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
dispatch, GeneratingCommand, Configuration, Option, validators
2424

2525

26-
@Configuration()
26+
@Configuration(requires_srinfo=True)
2727
class SimulateCommand(GeneratingCommand):
2828
""" Generates a sequence of events drawn from a CSV file using repeated
2929
random sampling
@@ -44,7 +44,7 @@ class SimulateCommand(GeneratingCommand):
4444
##Example
4545
4646
```
47-
| simulate csv=population.csv rate=200 interval=00:00:01 duration=00:00:30 |
47+
| simulate csv=population.csv rate=50 interval=00:00:01 duration=00:00:01 |
4848
countmatches fieldname=word_count pattern="\\w+" text |
4949
stats mean(word_count) stdev(word_count)
5050
```

examples/searchcommands_app/default/commands.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
filename = countmatches.py
66
supports_getinfo = true
77
supports_rawargs = true
8+
outputheader = true
89

910
[simulate]
1011
filename = simulate.py
@@ -16,3 +17,4 @@ outputheader = true
1617
filename = sum.py
1718
supports_getinfo = true
1819
supports_rawargs = true
20+
outputheader = true

splunklib/searchcommands/decorators.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,7 @@ def __call__(self, function):
133133

134134
@classmethod
135135
def fix_up(cls, command):
136-
""" TODO: Documentation
137136

138-
"""
139137
is_option = lambda attribute: isinstance(attribute, Option)
140138
command.option_definitions = getmembers(command, is_option)
141139
member_number = 0

splunklib/searchcommands/search_command_internals.py

Lines changed: 64 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,6 @@ def __init__(cls, module, name, bases, settings):
7272
class InputHeader(object):
7373
""" Represents a Splunk input header as a collection of name/value pairs.
7474
75-
TODO: Description
76-
7775
"""
7876
def __init__(self):
7977
self._settings = OrderedDict()
@@ -82,39 +80,66 @@ def __getitem__(self, name):
8280
return self._settings[name]
8381

8482
def __iter__(self):
85-
for item in self._settings.items():
86-
yield item
83+
return self.iterkeys()
84+
85+
def __len__(self):
86+
return len(self._settings)
8787

8888
def __repr__(self):
8989
return ''.join(
9090
[InputHeader.__name__, '(', repr(self._settings.items()), ')'])
9191

92+
def items(self):
93+
return self._settings.items()
94+
95+
def iteritems(self):
96+
return self._settings.iteritems()
97+
98+
def iterkeys(self):
99+
return self._settings.iterkeys()
100+
101+
def itervalues(self):
102+
return self._settings.itervalues()
103+
104+
def keys(self):
105+
return self._settings.keys()
106+
107+
def values(self):
108+
return self._settings.values()
109+
92110
def read(self, input_file):
93111
""" Reads an InputHeader from `input_file`.
94112
95-
The input header is read as a sequence of *<name>***:***<value>* pairs
113+
The input header is read as a sequence of *<key>***:***<value>* pairs
96114
separated by a newline. The end of the input header is signalled by an
97115
empty line or an end-of-file.
98116
99117
:param input_file: File-like object that supports iteration over lines
100118
101119
"""
102-
name = None
120+
key, value = None, None
121+
import sys
103122
for line in input_file:
104123
if line == '\n':
105124
break
106-
if line[-1] == '\n':
125+
if line[-1:] == '\n':
107126
line = line[:-1]
108-
value = line.split(':', 1)
109-
if len(value) == 2:
110-
name, value = value
111-
self._settings[name] = urllib.unquote(value)
112-
elif name is not None:
113-
# add new line to multi-line value
114-
self._settings[name] = '\n'.join(
115-
[self._settings[name], urllib.unquote(line)])
116-
else:
117-
pass # on unnamed multi-line value
127+
item = line.split(':', 1)
128+
if len(item) == 2:
129+
# start of a new item
130+
self._update(key, value)
131+
key, value = item[0], urllib.unquote(item[1])
132+
elif key is not None:
133+
# continuation of the current item
134+
value = '\n'.join([value, urllib.unquote(line)])
135+
136+
self._update(key, value)
137+
return
138+
139+
def _update(self, k, v):
140+
if k is not None:
141+
self._settings[k] = v if k != 'infoPath' else open(v, 'r')
142+
return
118143

119144

120145
class MessagesHeader(object):
@@ -126,10 +151,10 @@ class MessagesHeader(object):
126151
127152
Message levels include:
128153
154+
+ debug_message
129155
+ info_message
130156
+ warn_message
131157
+ error_messages
132-
+ TODO: ... (?)
133158
134159
The end of the messages header is signalled by the occurrence of a single
135160
blank line (`\r\n').
@@ -140,39 +165,42 @@ class MessagesHeader(object):
140165
"""
141166

142167
def __init__(self):
143-
self._messages = OrderedDict(
144-
[('warn_message', []), ('info_message', []), ('error_message', [])])
168+
self._messages = []
145169

146-
def __iadd__(self, level, text):
147-
self.append(level, text)
170+
def __iadd__(self, (message_level, message_text)):
171+
self._messages.append((message_level, message_text))
172+
return self
148173

149174
def __iter__(self):
150-
for message_level in self._messages:
151-
for message_text in self._messages[message_level]:
152-
yield (message_level, message_text)
175+
return self._messages.__iter__()
176+
177+
def __len__(self):
178+
return len(self._messages)
153179

154180
def __repr__(self):
155-
messages = [message for message in self]
156-
return ''.join([MessagesHeader.__name__, '(', repr(messages), ')'])
181+
return ''.join([MessagesHeader.__name__, '(', repr(self._messages), ')'])
157182

158-
def append(self, level, text):
183+
def append(self, message_level, message_text):
159184
""" Adds a message level/text pair to this MessagesHeader """
160-
if not level in self._messages.keys():
161-
raise ValueError('level="%s"' % level)
162-
self._messages[level].append(text)
185+
if not message_level in MessagesHeader._message_levels:
186+
raise ValueError('message_level="%s"' % message_level)
187+
self._messages.append((message_level, message_text))
163188

164189
def write(self, output_file):
165190
""" Writes this MessageHeader to an output stream.
166191
167-
Messages are written as a sequence of *<message-level>***=**
168-
*<message-text>* pairs separated by '\r\n'. The sequence is terminated
169-
by a pair of '\r\n' sequences.
192+
Messages are written as a sequence of *<message_text-message_level>***=**
193+
*<message_text-text>* pairs separated by '\r\n'. The sequence is
194+
terminated by a pair of '\r\n' sequences.
170195
171196
"""
172-
for level, message in self:
173-
output_file.write('%s=%s\r\n' % (level, message))
197+
for message_level, message_text in self:
198+
output_file.write('%s=%s\r\n' % (message_level, message_text))
174199
output_file.write('\r\n')
175200

201+
_message_levels = [
202+
'debug_message', 'warn_message', 'info_message', 'error_message']
203+
176204

177205
class SearchCommandParser(object):
178206
""" Parses the arguments to a search command.

tests/test_searchcommands_app.py

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -209,66 +209,6 @@ def setUp(self):
209209
self.maxDiff = 2 * 65535
210210
return
211211

212-
def test_command_parser(self):
213-
from splunklib.searchcommands.search_command_internals import \
214-
SearchCommandParser
215-
parser = SearchCommandParser()
216-
encoder = JSONEncoder()
217-
file_path = TestSearchCommandsApp._data_file(os.path.join('input', 'counts.csv'))
218-
219-
options = [
220-
'boolean=true',
221-
'duration=00:00:10',
222-
'fieldname=word_count',
223-
'file=%s' % encoder.encode(file_path),
224-
'integer=10',
225-
'optionname=foo_bar',
226-
'regularexpression="\\\\w+"',
227-
'set=foo',
228-
'show_configuration=true']
229-
fields = ['field_1', 'field_2', 'field_3']
230-
231-
command = StubbedStreamingCommand() # All options are required
232-
parser.parse(options + fields, command)
233-
command_line = str(command)
234-
235-
self.assertEqual(
236-
'stubbedstreaming boolean=true duration=10 fieldname="word_count" file=%s integer=10 optionname="foo_bar" regularexpression="\\\\w+" set="foo" show_configuration=true field_1 field_2 field_3' % encoder.encode(file_path),
237-
command_line)
238-
239-
for option in options:
240-
self.assertRaises(ValueError, parser.parse, [x for x in options if x != option] + ['field_1', 'field_2', 'field_3'], command)
241-
242-
command = StubbedReportingCommand() # No options are required
243-
parser.parse(options + fields, command)
244-
245-
for option in options:
246-
try:
247-
parser.parse([x for x in options if x != option] + ['field_1', 'field_2', 'field_3'], command)
248-
except Exception as e:
249-
self.assertFalse("Unexpected exception: %s" % e)
250-
251-
try:
252-
parser.parse(options, command)
253-
except Exception as e:
254-
self.assertFalse("Unexpected exception: %s" % e)
255-
256-
for option in command.options.itervalues():
257-
self.assertTrue(option.is_set)
258-
259-
self.assertEqual(len(command.fieldnames), 0)
260-
261-
try:
262-
parser.parse(fields, command)
263-
except Exception as e:
264-
self.assertFalse("Unexpected exception: %s" % e)
265-
266-
for option in command.options.itervalues():
267-
self.assertFalse(option.is_set)
268-
269-
self.assertListEqual(fields, command.fieldnames)
270-
return
271-
272212
def disable_test_option_logging_configuration(self):
273213
self._run(
274214
'simulate', [

0 commit comments

Comments
 (0)