Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ad5f21e

Browse files
authored
Merge pull request #402 from splunk/DVPL-10069
Added support to process empty records
2 parents 8a988f8 + fe1784f commit ad5f21e

File tree

4 files changed

+119
-7
lines changed

4 files changed

+119
-7
lines changed

splunklib/searchcommands/generating_command.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# under the License.
1616

1717
from __future__ import absolute_import, division, print_function, unicode_literals
18+
import sys
1819

1920
from .decorators import ConfigurationSetting
2021
from .search_command import SearchCommand
@@ -220,6 +221,35 @@ def _execute_chunk_v2(self, process, chunk):
220221
return
221222
self._finished = True
222223

224+
def process(self, argv=sys.argv, ifile=sys.stdin, ofile=sys.stdout, allow_empty_input=True):
225+
""" Process data.
226+
227+
:param argv: Command line arguments.
228+
:type argv: list or tuple
229+
230+
:param ifile: Input data file.
231+
:type ifile: file
232+
233+
:param ofile: Output data file.
234+
:type ofile: file
235+
236+
:param allow_empty_input: For generating commands, it must be true. Doing otherwise will cause an error.
237+
:type allow_empty_input: bool
238+
239+
:return: :const:`None`
240+
:rtype: NoneType
241+
242+
"""
243+
244+
# Generating commands are expected to run on an empty set of inputs as the first command being run in a search,
245+
# also this class implements its own separate _execute_chunk_v2 method which does not respect allow_empty_input
246+
# so ensure that allow_empty_input is always True
247+
248+
if not allow_empty_input:
249+
raise ValueError("allow_empty_input cannot be False for Generating Commands")
250+
else:
251+
return super(GeneratingCommand, self).process(argv=argv, ifile=ifile, ofile=ofile, allow_empty_input=True)
252+
223253
# endregion
224254

225255
# region Types

splunklib/searchcommands/search_command.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def __init__(self):
124124
self._default_logging_level = self._logger.level
125125
self._record_writer = None
126126
self._records = None
127+
self._allow_empty_input = True
127128

128129
def __str__(self):
129130
text = ' '.join(chain((type(self).name, str(self.options)), [] if self.fieldnames is None else self.fieldnames))
@@ -413,7 +414,7 @@ def prepare(self):
413414
"""
414415
pass
415416

416-
def process(self, argv=sys.argv, ifile=sys.stdin, ofile=sys.stdout):
417+
def process(self, argv=sys.argv, ifile=sys.stdin, ofile=sys.stdout, allow_empty_input=True):
417418
""" Process data.
418419
419420
:param argv: Command line arguments.
@@ -425,10 +426,16 @@ def process(self, argv=sys.argv, ifile=sys.stdin, ofile=sys.stdout):
425426
:param ofile: Output data file.
426427
:type ofile: file
427428
429+
:param allow_empty_input: Allow empty input records for the command, if False an Error will be returned if empty chunk body is encountered when read
430+
:type allow_empty_input: bool
431+
428432
:return: :const:`None`
429433
:rtype: NoneType
430434
431435
"""
436+
437+
self._allow_empty_input = allow_empty_input
438+
432439
if len(argv) > 1:
433440
self._process_protocol_v1(argv, ifile, ofile)
434441
else:
@@ -965,13 +972,14 @@ def _execute_v2(self, ifile, process):
965972
def _execute_chunk_v2(self, process, chunk):
966973
metadata, body = chunk
967974

968-
if len(body) <= 0:
969-
return
975+
if len(body) <= 0 and not self._allow_empty_input:
976+
raise ValueError(
977+
"No records found to process. Set allow_empty_input=True in dispatch function to move forward "
978+
"with empty records.")
970979

971980
records = self._read_csv_records(StringIO(body))
972981
self._record_writer.write_records(process(records))
973982

974-
975983
def _report_unexpected_error(self):
976984

977985
error_type, error, tb = sys.exc_info()
@@ -1063,8 +1071,7 @@ def iteritems(self):
10631071
SearchMetric = namedtuple('SearchMetric', ('elapsed_seconds', 'invocation_count', 'input_count', 'output_count'))
10641072

10651073

1066-
1067-
def dispatch(command_class, argv=sys.argv, input_file=sys.stdin, output_file=sys.stdout, module_name=None):
1074+
def dispatch(command_class, argv=sys.argv, input_file=sys.stdin, output_file=sys.stdout, module_name=None, allow_empty_input=True):
10681075
""" Instantiates and executes a search command class
10691076
10701077
This function implements a `conditional script stanza <https://docs.python.org/2/library/__main__.html>`_ based on the value of
@@ -1087,6 +1094,8 @@ def dispatch(command_class, argv=sys.argv, input_file=sys.stdin, output_file=sys
10871094
:type output_file: :code:`file`
10881095
:param module_name: Name of the module calling :code:`dispatch` or :const:`None`.
10891096
:type module_name: :code:`basestring`
1097+
:param allow_empty_input: Allow empty input records for the command, if False an Error will be returned if empty chunk body is encountered when read
1098+
:type allow_empty_input: bool
10901099
:returns: :const:`None`
10911100
10921101
**Example**
@@ -1124,4 +1133,4 @@ def stream(records):
11241133
assert issubclass(command_class, SearchCommand)
11251134

11261135
if module_name is None or module_name == '__main__':
1127-
command_class().process(argv, input_file, output_file)
1136+
command_class().process(argv, input_file, output_file, allow_empty_input)

tests/searchcommands/test_generator_command.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,21 @@ def generate(self):
4141
assert finished_seen
4242

4343

44+
def test_allow_empty_input_for_generating_command():
45+
"""
46+
Passing allow_empty_input for generating command will cause an error
47+
"""
48+
@Configuration()
49+
class GeneratorTest(GeneratingCommand):
50+
def generate(self):
51+
for num in range(1, 3):
52+
yield {"_index": num}
53+
generator = GeneratorTest()
54+
in_stream = io.BytesIO()
55+
out_stream = io.BytesIO()
56+
57+
try:
58+
generator.process([], in_stream, out_stream, allow_empty_input=False)
59+
except ValueError as error:
60+
assert str(error) == "allow_empty_input cannot be False for Generating Commands"
4461

tests/searchcommands/test_search_command.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,62 @@ def test_process_scpv2(self):
723723
r'\{(' + inspector + r',' + finished + r'|' + finished + r',' + inspector + r')\}')
724724

725725
self.assertEqual(command.protocol_version, 2)
726+
727+
# 5. Different scenarios with allow_empty_input flag, default is True
728+
# Test preparation
729+
dispatch_dir = os.path.join(basedir, 'recordings', 'scpv2', 'Splunk-6.3', 'countmatches.dispatch_dir')
730+
logging_configuration = os.path.join(basedir, 'apps', 'app_with_logging_configuration', 'logging.conf')
731+
logging_level = 'ERROR'
732+
record = False
733+
show_configuration = True
734+
735+
getinfo_metadata = metadata.format(
736+
dispatch_dir=encode_string(dispatch_dir),
737+
logging_configuration=encode_string(logging_configuration)[1:-1],
738+
logging_level=logging_level,
739+
record=('true' if record is True else 'false'),
740+
show_configuration=('true' if show_configuration is True else 'false'))
741+
742+
execute_metadata = '{"action":"execute","finished":true}'
743+
command = TestCommand()
744+
result = BytesIO()
745+
argv = ['some-external-search-command.py']
746+
747+
# Scenario a) Empty body & allow_empty_input=False ==> Assert Error
748+
749+
execute_body = '' # Empty body
750+
input_file = build_command_input(getinfo_metadata, execute_metadata, execute_body)
751+
try:
752+
command.process(argv, input_file, ofile=result, allow_empty_input=False) # allow_empty_input=False
753+
except SystemExit as error:
754+
self.assertNotEqual(0, error.code)
755+
self.assertTrue(result.getvalue().decode("UTF-8").__contains__("No records found to process. Set "
756+
"allow_empty_input=True in dispatch "
757+
"function to move forward with empty "
758+
"records."))
759+
else:
760+
self.fail('Expected SystemExit, not a return from TestCommand.process: {}\n'.format(
761+
result.getvalue().decode('utf-8')))
762+
763+
# Scenario b) Empty body & allow_empty_input=True ==> Assert Success
764+
765+
execute_body = '' # Empty body
766+
input_file = build_command_input(getinfo_metadata, execute_metadata, execute_body)
767+
result = BytesIO()
768+
769+
try:
770+
command.process(argv, input_file, ofile=result) # By default allow_empty_input=True
771+
except SystemExit as error:
772+
self.fail('Unexpected exception: {}: {}'.format(type(error).__name__, error))
773+
774+
expected = (
775+
'chunked 1.0,68,0\n'
776+
'{"inspector":{"messages":[["INFO","test command configuration: "]]}}\n'
777+
'chunked 1.0,17,0\n'
778+
'{"finished":true}'
779+
)
780+
781+
self.assertEquals(result.getvalue().decode("UTF-8"), expected)
726782
return
727783

728784
_package_directory = os.path.dirname(os.path.abspath(__file__))

0 commit comments

Comments
 (0)