Skip to content

Commit 286577e

Browse files
feat: add support of transforming custom command (#1856)
**Issue number:** ADDON-82004 ### PR Type **What kind of change does this PR introduce?** * [x] Feature * [ ] Bug Fix * [ ] Refactoring (no functional or API changes) * [x] Documentation Update * [ ] Maintenance (dependency updates, CI, etc.) ## Summary ### Changes Since we were blocked by Splunk SDK for Python on this functionality and now are unblocked, UCC now supports creation of transforming custom commands as well. ### User experience Users would now be able to create transforming custom commands via UCC. ## Checklist If an item doesn't apply to your changes, leave it unchecked. ### Review * [x] self-review - I have performed a self-review of this change according to the [development guidelines](https://splunk.github.io/addonfactory-ucc-generator/contributing/#development-guidelines) * [x] Changes are documented. The documentation is understandable, examples work [(more info)](https://splunk.github.io/addonfactory-ucc-generator/contributing/#documentation-guidelines) * [x] PR title and description follows the [contributing principles](https://splunk.github.io/addonfactory-ucc-generator/contributing/#pull-requests) * [ ] meeting - I have scheduled a meeting or recorded a demo to explain these changes (if there is a video, put a link below and in the ticket) ### Tests See [the testing doc](https://splunk.github.io/addonfactory-ucc-generator/contributing/#build-and-test). * [x] Unit - tests have been added/modified to cover the changes * [x] Smoke - tests have been added/modified to cover the changes * [ ] UI - tests have been added/modified to cover the changes * [ ] coverage - I have checked the code coverage of my changes [(see more)](https://splunk.github.io/addonfactory-ucc-generator/contributing/#checking-the-code-coverage) **Demo/meeting:** *Reviewers are encouraged to request meetings or demos if any part of the change is unclear*
1 parent 6d06746 commit 286577e

File tree

18 files changed

+508
-6
lines changed

18 files changed

+508
-6
lines changed

docs/custom_search_commands.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ There are 4 types of Custom search commands:
99
- Transforming
1010
- Dataset processing
1111

12-
> Note: Currently UCC supports only three types of custom search command, that are `Generating`, `Streaming` and `Dataset processing`.
1312

1413
> Note: Eventing commands are being referred as Dataset processing commands [reference](https://dev.splunk.com/enterprise/docs/devtools/customsearchcommands/).
1514

splunk_add_on_ucc_framework/generators/python_files/create_custom_command_python.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
#
16+
import os
17+
import ast
18+
from pathlib import Path
1619
from typing import Any, Dict, List, Optional
17-
1820
from splunk_add_on_ucc_framework.generators.file_generator import FileGenerator
1921
from splunk_add_on_ucc_framework.global_config import GlobalConfig
2022

@@ -29,8 +31,25 @@ def __init__(
2931
self.commands_info = []
3032
for command in global_config.custom_search_commands:
3133
argument_list: List[str] = []
34+
import_map = False
3235
imported_file_name = command["fileName"].replace(".py", "")
3336
template = command["commandType"].replace(" ", "_") + ".template"
37+
if command["commandType"] == "transforming":
38+
module_path = Path(
39+
os.path.realpath(
40+
os.path.join(self._input_dir, "bin", command["fileName"])
41+
)
42+
)
43+
44+
if not module_path.is_file():
45+
raise FileNotFoundError(
46+
f"Module path '{module_path}' does not point to a valid file."
47+
)
48+
module_content = ast.parse(module_path.read_text(encoding="utf-8"))
49+
for node in module_content.body:
50+
if isinstance(node, ast.FunctionDef) and node.name == "map":
51+
import_map = True
52+
3453
for argument in command["arguments"]:
3554
argument_dict = {
3655
"name": argument["name"],
@@ -48,6 +67,7 @@ def __init__(
4867
"syntax": command.get("syntax"),
4968
"template": template,
5069
"list_arg": argument_list,
70+
"import_map": import_map,
5171
}
5272
)
5373

@@ -109,6 +129,7 @@ def generate(self) -> Optional[List[Dict[str, str]]]:
109129
description=command_info["description"],
110130
syntax=command_info["syntax"],
111131
list_arg=command_info["list_arg"],
132+
import_map=command_info["import_map"],
112133
)
113134
generated_files.append(
114135
{

splunk_add_on_ucc_framework/schema/schema.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,8 @@
448448
"enum": [
449449
"generating",
450450
"streaming",
451-
"dataset processing"
451+
"dataset processing",
452+
"transforming"
452453
]
453454
},
454455
"requiredSearchAssistant": {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import sys
2+
import import_declare_test
3+
4+
from splunklib.searchcommands import \
5+
dispatch, ReportingCommand, Configuration, Option, validators
6+
7+
{% if import_map %}
8+
from {{ imported_file_name }} import reduce, map
9+
{% else %}
10+
from {{ imported_file_name }} import reduce
11+
{% endif %}
12+
13+
@Configuration()
14+
class {{class_name}}Command(ReportingCommand):
15+
{% if syntax or description%}
16+
"""
17+
18+
{% if syntax %}
19+
##Syntax
20+
{{syntax}}
21+
{% endif %}
22+
23+
{% if description %}
24+
##Description
25+
{{description}}
26+
{% endif %}
27+
28+
"""
29+
{% endif %}
30+
31+
{% for arg in list_arg %}
32+
{{arg}}
33+
{% endfor %}
34+
35+
{% if import_map %}
36+
@Configuration()
37+
def map(self, events):
38+
return map(self, events)
39+
{% endif %}
40+
41+
def reduce(self, events):
42+
return reduce(self, events)
43+
44+
dispatch({{class_name}}Command, sys.argv, sys.stdin, sys.stdout, __name__)

tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/appserver/static/js/build/globalConfig.json

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2427,5 +2427,106 @@
24272427
}
24282428
],
24292429
"_uccVersion": "5.62.0"
2430-
}
2430+
},
2431+
"customSearchCommand": [
2432+
{
2433+
"commandName": "generatetextcommand",
2434+
"fileName": "generatetext.py",
2435+
"commandType": "generating",
2436+
"requiredSearchAssistant": true,
2437+
"description": " This command generates COUNT occurrences of a TEXT string.",
2438+
"syntax": "generatetextcommand count=<event_count> text=<string>",
2439+
"usage": "public",
2440+
"arguments": [
2441+
{
2442+
"name": "count",
2443+
"required": true,
2444+
"validate": {
2445+
"type": "Integer",
2446+
"minimum": 5,
2447+
"maximum": 10
2448+
}
2449+
},
2450+
{
2451+
"name": "text",
2452+
"required": true
2453+
}
2454+
]
2455+
},
2456+
{
2457+
"commandName": "filtercommand",
2458+
"fileName": "filter.py",
2459+
"commandType": "dataset processing",
2460+
"requiredSearchAssistant": true,
2461+
"description": "It filters records from the events stream returning only those which has :code:`contains` in them and replaces :code:`replace_array[0]` with :code:`replace_array[1]`.",
2462+
"syntax": "| filtercommand contains='value1' replace='value to be replaced,value to replace with'",
2463+
"usage": "public",
2464+
"arguments": [
2465+
{
2466+
"name": "contains"
2467+
},
2468+
{
2469+
"name": "replace_array"
2470+
}
2471+
]
2472+
},
2473+
{
2474+
"commandName": "sumcommand",
2475+
"fileName": "sum.py",
2476+
"commandType": "transforming",
2477+
"requiredSearchAssistant": true,
2478+
"description": "The total produced is sum(sum(fieldname, 1, n), 1, N) where n = number of fields, N = number of records.",
2479+
"syntax": "| sumcommand total=lines linecount",
2480+
"usage": "public",
2481+
"arguments": [
2482+
{
2483+
"name": "total",
2484+
"validate": {
2485+
"type": "Fieldname"
2486+
},
2487+
"required": true
2488+
}
2489+
]
2490+
},
2491+
{
2492+
"commandName": "sumtwocommand",
2493+
"fileName": "sum_without_map.py",
2494+
"commandType": "transforming",
2495+
"requiredSearchAssistant": true,
2496+
"description": "Computes sum(total, 1, N) and stores the result in 'total'",
2497+
"syntax": "| sumtwocommand total=lines linecount",
2498+
"usage": "public",
2499+
"arguments": [
2500+
{
2501+
"name": "total",
2502+
"validate": {
2503+
"type": "Fieldname"
2504+
},
2505+
"required": true
2506+
}
2507+
]
2508+
},
2509+
{
2510+
"commandName": "countmatchescommand",
2511+
"fileName": "countmatches.py",
2512+
"commandType": "streaming",
2513+
"requiredSearchAssistant": false,
2514+
"arguments": [
2515+
{
2516+
"name": "fieldname",
2517+
"validate": {
2518+
"type": "Fieldname"
2519+
},
2520+
"required": true
2521+
},
2522+
{
2523+
"name": "pattern",
2524+
"validate": {
2525+
"type": "RegularExpression"
2526+
},
2527+
"required": true
2528+
}
2529+
]
2530+
}
2531+
]
24312532
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import logging
2+
3+
def map(self, records):
4+
""" Computes sum(fieldname, 1, n) and stores the result in 'total' """
5+
fieldnames = self.fieldnames
6+
total = 0.0
7+
for record in records:
8+
for fieldname in fieldnames:
9+
total += float(record[fieldname])
10+
yield {self.total: total}
11+
12+
def reduce(self, records):
13+
""" Computes sum(total, 1, N) and stores the result in 'total' """
14+
fieldname = self.total
15+
total = 0.0
16+
for record in records:
17+
value = record[fieldname]
18+
try:
19+
total += float(value)
20+
except ValueError:
21+
logging.debug(' could not convert %s value to float: %s', fieldname, repr(value))
22+
yield {self.total: total}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import logging
2+
3+
def reduce(self, records):
4+
""" Computes sum(total, 1, N) and stores the result in 'total' """
5+
fieldname = self.total
6+
total = 0.0
7+
for record in records:
8+
value = record[fieldname]
9+
try:
10+
total += float(value)
11+
except ValueError:
12+
logging.debug(' could not convert %s value to float: %s', fieldname, repr(value))
13+
yield {self.total: total}
14+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import sys
2+
import import_declare_test
3+
4+
from splunklib.searchcommands import \
5+
dispatch, ReportingCommand, Configuration, Option, validators
6+
7+
from sum import reduce, map
8+
9+
@Configuration()
10+
class SumcommandCommand(ReportingCommand):
11+
"""
12+
13+
##Syntax
14+
| sumcommand total=lines linecount
15+
16+
##Description
17+
The total produced is sum(sum(fieldname, 1, n), 1, N) where n = number of fields, N = number of records.
18+
19+
"""
20+
21+
total = Option(name='total', require=True, validate=validators.Fieldname())
22+
23+
@Configuration()
24+
def map(self, events):
25+
return map(self, events)
26+
27+
def reduce(self, events):
28+
return reduce(self, events)
29+
30+
dispatch(SumcommandCommand, sys.argv, sys.stdin, sys.stdout, __name__)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import sys
2+
import import_declare_test
3+
4+
from splunklib.searchcommands import \
5+
dispatch, ReportingCommand, Configuration, Option, validators
6+
7+
from sum_without_map import reduce
8+
9+
@Configuration()
10+
class SumtwocommandCommand(ReportingCommand):
11+
"""
12+
13+
##Syntax
14+
| sumtwocommand total=lines linecount
15+
16+
##Description
17+
The total produced is sum(sum(fieldname, 1, n), 1, N) where n = number of fields, N = number of records.
18+
19+
"""
20+
21+
total = Option(name='total', require=True, validate=validators.Fieldname())
22+
23+
24+
def reduce(self, events):
25+
return reduce(self, events)
26+
27+
dispatch(SumtwocommandCommand, sys.argv, sys.stdin, sys.stdout, __name__)

tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/default/commands.conf

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ filename = filtercommand.py
88
chunked = true
99
python.version = python3
1010

11+
[sumcommand]
12+
filename = sumcommand.py
13+
chunked = true
14+
python.version = python3
15+
16+
[sumtwocommand]
17+
filename = sumtwocommand.py
18+
chunked = true
19+
python.version = python3
20+
1121
[countmatchescommand]
1222
filename = countmatchescommand.py
1323
chunked = true

0 commit comments

Comments
 (0)