├── .coveragerc
├── docs
├── CSS
│ ├── splunk_customizations.css
│ └── epub.css
├── _templates
│ └── layout.html
├── results.rst
├── data.rst
├── munge_links.sh
├── searchcommandsvalidators.rst
├── Makefile
├── modularinput.rst
├── binding.rst
├── searchcommands.rst
├── client.rst
├── index.rst
└── make.bat
├── tests
├── unit
│ ├── searchcommands
│ │ ├── apps
│ │ │ ├── app_with_logging_configuration
│ │ │ │ ├── bin
│ │ │ │ │ └── empty-directory
│ │ │ │ ├── logging.conf
│ │ │ │ └── default
│ │ │ │ │ ├── logging.conf
│ │ │ │ │ └── alternative-logging.conf
│ │ │ └── app_without_logging_configuration
│ │ │ │ ├── bin
│ │ │ │ └── empty-directory
│ │ │ │ └── default
│ │ │ │ └── empty-directory
│ │ ├── test_multibyte_processing.py
│ │ ├── __init__.py
│ │ ├── test_reporting_command.py
│ │ ├── chunked_data_stream.py
│ │ ├── test_generator_command.py
│ │ └── test_streaming_command.py
│ ├── modularinput
│ │ ├── data
│ │ │ ├── validation_error.xml
│ │ │ ├── validation.xml
│ │ │ ├── conf_with_0_inputs.xml
│ │ │ ├── conf_with_2_inputs.xml
│ │ │ ├── conf_with_invalid_inputs.xml
│ │ │ ├── event_minimal.xml
│ │ │ ├── argument_with_defaults.xml
│ │ │ ├── event_maximal.xml
│ │ │ ├── argument_without_defaults.xml
│ │ │ ├── stream_with_one_event.xml
│ │ │ ├── scheme_with_defaults.xml
│ │ │ ├── stream_with_two_events.xml
│ │ │ ├── scheme_without_defaults.xml
│ │ │ └── scheme_without_defaults_and_argument_title.xml
│ │ ├── modularinput_testlib.py
│ │ ├── test_validation_definition.py
│ │ ├── test_input_definition.py
│ │ └── test_scheme.py
│ ├── data
│ │ ├── custom_search
│ │ │ ├── multibyte_input.gz
│ │ │ ├── v1_search_input.gz
│ │ │ ├── usercount.baseline
│ │ │ └── tophashtags.baseline
│ │ ├── services.server.info.xml
│ │ └── services.xml
│ └── test_utils.py
├── system
│ ├── test_apps
│ │ ├── modularinput_app
│ │ │ ├── default
│ │ │ │ ├── inputs.conf
│ │ │ │ └── app.conf
│ │ │ ├── README
│ │ │ │ └── inputs.conf.spec
│ │ │ └── bin
│ │ │ │ └── modularinput.py
│ │ ├── eventing_app
│ │ │ ├── default
│ │ │ │ ├── commands.conf
│ │ │ │ └── app.conf
│ │ │ ├── metadata
│ │ │ │ └── default.meta
│ │ │ └── bin
│ │ │ │ └── eventingcsc.py
│ │ ├── reporting_app
│ │ │ ├── default
│ │ │ │ ├── commands.conf
│ │ │ │ └── app.conf
│ │ │ ├── metadata
│ │ │ │ └── default.meta
│ │ │ └── bin
│ │ │ │ └── reportingcsc.py
│ │ ├── streaming_app
│ │ │ ├── default
│ │ │ │ ├── commands.conf
│ │ │ │ └── app.conf
│ │ │ ├── metadata
│ │ │ │ └── default.meta
│ │ │ └── bin
│ │ │ │ └── streamingcsc.py
│ │ ├── generating_app
│ │ │ ├── default
│ │ │ │ ├── commands.conf
│ │ │ │ └── app.conf
│ │ │ ├── metadata
│ │ │ │ └── default.meta
│ │ │ └── bin
│ │ │ │ └── generatingcsc.py
│ │ └── cre_app
│ │ │ ├── default
│ │ │ ├── restmap.conf
│ │ │ └── app.conf
│ │ │ └── bin
│ │ │ └── execute.py
│ ├── test_modularinput_app.py
│ └── test_cre_apps.py
└── integration
│ ├── data
│ └── streaming_results.xml
│ ├── test_logger.py
│ ├── test_message.py
│ ├── test_kvstore_batch.py
│ ├── test_modular_input_kinds.py
│ ├── test_user.py
│ ├── test_event_type.py
│ ├── test_fired_alert.py
│ ├── test_conf.py
│ ├── test_kvstore_conf.py
│ ├── test_app.py
│ ├── test_kvstore_data.py
│ └── test_role.py
├── pytest.ini
├── .github
├── ISSUE_TEMPLATE
│ ├── custom.md
│ ├── feature_request.md
│ └── bug_report.md
├── dependabot.yaml
├── workflows
│ ├── fossa.yml
│ ├── pre-release.yml
│ ├── release.yml
│ └── test.yml
└── PULL_REQUEST_TEMPLATE
│ └── pr_template.md
├── .env
├── splunklib
├── modularinput
│ ├── __init__.py
│ ├── input_definition.py
│ ├── utils.py
│ ├── validation_definition.py
│ ├── scheme.py
│ ├── event_writer.py
│ ├── argument.py
│ └── event.py
├── __init__.py
├── utils.py
├── searchcommands
│ ├── environment.py
│ └── eventing_command.py
└── results.py
├── sitecustomize.py
├── Makefile
├── CONTRIBUTING.md
├── docker-compose.yml
├── pyproject.toml
└── utils
├── __init__.py
└── cmdopts.py
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | omit =
3 | tests/*
4 |
--------------------------------------------------------------------------------
/docs/CSS/splunk_customizations.css:
--------------------------------------------------------------------------------
1 | a.headerlink { display: none; }
2 |
3 |
--------------------------------------------------------------------------------
/tests/unit/searchcommands/apps/app_with_logging_configuration/bin/empty-directory:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/unit/searchcommands/apps/app_without_logging_configuration/bin/empty-directory:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/unit/searchcommands/apps/app_without_logging_configuration/default/empty-directory:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/system/test_apps/modularinput_app/default/inputs.conf:
--------------------------------------------------------------------------------
1 | [modularinput]
2 | python.version = python3
3 |
--------------------------------------------------------------------------------
/tests/unit/modularinput/data/validation_error.xml:
--------------------------------------------------------------------------------
1 | Big fat validation error!
--------------------------------------------------------------------------------
/tests/system/test_apps/modularinput_app/README/inputs.conf.spec:
--------------------------------------------------------------------------------
1 | [modularinput://]
2 |
3 | endpoint =
4 |
--------------------------------------------------------------------------------
/tests/unit/modularinput/data/validation.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splunk/splunk-sdk-python/HEAD/tests/unit/modularinput/data/validation.xml
--------------------------------------------------------------------------------
/tests/system/test_apps/eventing_app/default/commands.conf:
--------------------------------------------------------------------------------
1 | [eventingcsc]
2 | filename = eventingcsc.py
3 | chunked = true
4 | python.version = python3
5 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | markers =
3 | app: requires sdk-app-collection
4 | smoke: essential smoke tests
5 |
6 | junit_family =
7 | xunit2
8 |
--------------------------------------------------------------------------------
/tests/system/test_apps/reporting_app/default/commands.conf:
--------------------------------------------------------------------------------
1 | [reportingcsc]
2 | filename = reportingcsc.py
3 | chunked = true
4 | python.version = python3
5 |
--------------------------------------------------------------------------------
/tests/system/test_apps/streaming_app/default/commands.conf:
--------------------------------------------------------------------------------
1 | [streamingcsc]
2 | filename = streamingcsc.py
3 | chunked = true
4 | python.version = python3
5 |
--------------------------------------------------------------------------------
/tests/unit/data/custom_search/multibyte_input.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splunk/splunk-sdk-python/HEAD/tests/unit/data/custom_search/multibyte_input.gz
--------------------------------------------------------------------------------
/tests/unit/data/custom_search/v1_search_input.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splunk/splunk-sdk-python/HEAD/tests/unit/data/custom_search/v1_search_input.gz
--------------------------------------------------------------------------------
/tests/system/test_apps/generating_app/default/commands.conf:
--------------------------------------------------------------------------------
1 | [generatingcsc]
2 | filename = generatingcsc.py
3 | chunked = true
4 | python.version = python3
5 |
--------------------------------------------------------------------------------
/tests/unit/modularinput/data/conf_with_0_inputs.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splunk/splunk-sdk-python/HEAD/tests/unit/modularinput/data/conf_with_0_inputs.xml
--------------------------------------------------------------------------------
/tests/unit/modularinput/data/conf_with_2_inputs.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splunk/splunk-sdk-python/HEAD/tests/unit/modularinput/data/conf_with_2_inputs.xml
--------------------------------------------------------------------------------
/docs/_templates/layout.html:
--------------------------------------------------------------------------------
1 | {% extends '!layout.html' %}
2 |
3 | {% set css_files = css_files + ["_static/splunk_customizations.css"] %}
4 |
5 | {% set reldelim2 = "" %}
6 |
--------------------------------------------------------------------------------
/docs/results.rst:
--------------------------------------------------------------------------------
1 | splunklib.results
2 | -----------------
3 |
4 | .. automodule:: splunklib.results
5 |
6 | .. autoclass:: Message
7 |
8 | .. autoclass:: JSONResultsReader
9 |
--------------------------------------------------------------------------------
/tests/unit/modularinput/data/conf_with_invalid_inputs.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/splunk/splunk-sdk-python/HEAD/tests/unit/modularinput/data/conf_with_invalid_inputs.xml
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/custom.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Custom issue template
3 | about: Describe this issue template's purpose here.
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
--------------------------------------------------------------------------------
/tests/system/test_apps/cre_app/default/restmap.conf:
--------------------------------------------------------------------------------
1 | [script:execute]
2 | match = /execute
3 | scripttype = python
4 | handler = execute.Handler
5 |
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | target-branch: "develop"
6 | schedule:
7 | interval: "weekly"
8 |
--------------------------------------------------------------------------------
/tests/unit/data/custom_search/usercount.baseline:
--------------------------------------------------------------------------------
1 | coreaudiod,daemon,itay,locationd,mdnsresponder,root,spotlight,usbmuxd,windowserver,www
2 | 1,1,73,1,1,37,1,1,1,1
3 | 1,1,73,1,1,37,1,1,1,1
4 |
--------------------------------------------------------------------------------
/.github/workflows/fossa.yml:
--------------------------------------------------------------------------------
1 | name: Fossa OSS Scan
2 | on: [push]
3 |
4 | jobs:
5 | fossa-scan:
6 | uses: splunk/oss-scanning-public/.github/workflows/oss-scan.yml@main
7 | secrets: inherit
8 |
--------------------------------------------------------------------------------
/tests/unit/data/custom_search/tophashtags.baseline:
--------------------------------------------------------------------------------
1 | count,hashtag,percentage
2 | 1,#2,0.3333333333333333333333333333
3 | 1,#redsox,0.3333333333333333333333333333
4 | 1,#mlb,0.3333333333333333333333333333
5 |
--------------------------------------------------------------------------------
/tests/unit/modularinput/data/event_minimal.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | This is a test of the emergency broadcast system.
4 |
5 |
--------------------------------------------------------------------------------
/docs/data.rst:
--------------------------------------------------------------------------------
1 | splunklib.data
2 | --------------
3 |
4 | .. automodule:: splunklib.data
5 |
6 | .. autofunction:: load
7 |
8 | .. autofunction:: record
9 |
10 | .. autoclass:: Record
11 | :members:
12 |
13 |
--------------------------------------------------------------------------------
/docs/munge_links.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | TARGET=$1
4 |
5 | for file in $TARGET/*.html; do
6 | echo ${file}
7 | sed -i -e 's/class="reference external"/class="reference external" target="_blank"/g' "${file}"
8 | done
9 |
--------------------------------------------------------------------------------
/tests/unit/modularinput/data/argument_with_defaults.xml:
--------------------------------------------------------------------------------
1 |
2 | string
3 | false
4 | false
5 |
--------------------------------------------------------------------------------
/docs/searchcommandsvalidators.rst:
--------------------------------------------------------------------------------
1 | splunklib.searchcommands.validators
2 | -----------------------------------
3 |
4 | .. automodule:: splunklib.searchcommands.validators
5 |
6 | .. autoclass:: Fieldname
7 | :members:
8 | :inherited-members:
9 |
10 | .. autoclass:: Validator
11 | :members:
12 | :inherited-members:
13 |
--------------------------------------------------------------------------------
/tests/unit/modularinput/data/event_maximal.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | hilda
4 | misc
5 | main
6 | localhost
7 | This is a test of the emergency broadcast system.
8 |
9 |
--------------------------------------------------------------------------------
/tests/unit/modularinput/data/argument_without_defaults.xml:
--------------------------------------------------------------------------------
1 |
2 | 쎼 and 쎶 and <&> für
3 | is_pos_int('some_name')
4 | boolean
5 | true
6 | true
7 |
--------------------------------------------------------------------------------
/tests/system/test_apps/eventing_app/default/app.conf:
--------------------------------------------------------------------------------
1 | #
2 | # Splunk app configuration file
3 | #
4 |
5 | [install]
6 | is_configured = 0
7 |
8 | [ui]
9 | is_visible = 1
10 | label = [EXAMPLE] Eventing CSC App
11 |
12 | [launcher]
13 | description = Example app for eventing Custom Search Commands
14 | version = 1.0.0
15 | author = Splunk
16 |
17 |
--------------------------------------------------------------------------------
/tests/system/test_apps/generating_app/default/app.conf:
--------------------------------------------------------------------------------
1 | #
2 | # Splunk app configuration file
3 | #
4 |
5 | [install]
6 | is_configured = 0
7 |
8 | [ui]
9 | is_visible = 1
10 | label = [EXAMPLE] Generating CSC App
11 |
12 | [launcher]
13 | description = Example app for generating Custom Search Commands
14 | version = 1.0.0
15 | author = Splunk
16 |
17 |
--------------------------------------------------------------------------------
/tests/system/test_apps/reporting_app/default/app.conf:
--------------------------------------------------------------------------------
1 | #
2 | # Splunk app configuration file
3 | #
4 |
5 | [install]
6 | is_configured = 0
7 |
8 | [ui]
9 | is_visible = 1
10 | label = [EXAMPLE] Reporting CSC App
11 |
12 | [launcher]
13 | description = Example app for reporting Custom Search Commands
14 | version = 1.0.0
15 | author = Splunk
16 |
17 |
--------------------------------------------------------------------------------
/tests/system/test_apps/streaming_app/default/app.conf:
--------------------------------------------------------------------------------
1 | #
2 | # Splunk app configuration file
3 | #
4 |
5 | [install]
6 | is_configured = 0
7 |
8 | [ui]
9 | is_visible = 1
10 | label = [EXAMPLE] Streaming CSC App
11 |
12 | [launcher]
13 | description = Example app for streaming Custom Search Commands
14 | version = 1.0.0
15 | author = Splunk
16 |
17 |
--------------------------------------------------------------------------------
/tests/unit/modularinput/data/stream_with_one_event.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | hilda
5 | misc
6 | main
7 | localhost
8 | This is a test of the emergency broadcast system.
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/system/test_apps/modularinput_app/default/app.conf:
--------------------------------------------------------------------------------
1 | #
2 | # Splunk app configuration file
3 | #
4 |
5 | [install]
6 | is_configured = 0
7 |
8 | [ui]
9 | is_visible = 1
10 | label = [EXAMPLE] Modular Input Test App
11 |
12 | [launcher]
13 | description = Example app for Modular Inputs
14 | version = 1.0.0
15 | author = Splunk
16 |
17 | [package]
18 | check_for_updates = false
19 |
--------------------------------------------------------------------------------
/tests/unit/modularinput/data/scheme_with_defaults.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | abcd
4 | true
5 | false
6 | xml
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/system/test_apps/cre_app/default/app.conf:
--------------------------------------------------------------------------------
1 | [id]
2 | name = cre_app
3 | version = 0.1.0
4 |
5 | [package]
6 | id = cre_app
7 | check_for_updates = False
8 |
9 | [install]
10 | is_configured = 0
11 | state = enabled
12 |
13 | [ui]
14 | is_visible = 1
15 | label = [EXAMPLE] CRE app
16 |
17 | [launcher]
18 | description = Example app that exposes custom rest endpoints
19 | version = 0.0.1
20 | author = Splunk
21 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | # Splunk host (default: localhost)
2 | host=localhost
3 | # Splunk admin port (default: 8089)
4 | port=8089
5 | # Splunk username
6 | username=admin
7 | # Splunk password
8 | password=changed!
9 | # Access scheme (default: https)
10 | scheme=https
11 | # Your version of Splunk (default: 6.2)
12 | version=9.0
13 | # Bearer token for authentication
14 | #splunkToken=""
15 | # Session key for authentication
16 | #token=""
17 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | #
2 | # Makefile for Sphinx docs generation
3 | #
4 |
5 | SPHINXBUILD = sphinx-build
6 | BUILDDIR = ./_build
7 | HTMLDIR = ${BUILDDIR}/html
8 |
9 | # Internal variables
10 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees .
11 |
12 | .PHONY: html
13 | html:
14 | @rm -rf $(BUILDDIR)
15 | @$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(HTMLDIR)
16 | @sh munge_links.sh $(HTMLDIR)
17 | @echo
18 | @echo "Build finished. HTML pages available at docs/$(HTMLDIR)."
--------------------------------------------------------------------------------
/splunklib/modularinput/__init__.py:
--------------------------------------------------------------------------------
1 | """The following imports allow these classes to be imported via
2 | the splunklib.modularinput package like so:
3 |
4 | from splunklib.modularinput import *
5 | """
6 |
7 | from .argument import Argument
8 | from .event import Event
9 | from .event_writer import EventWriter
10 | from .input_definition import InputDefinition
11 | from .scheme import Scheme
12 | from .script import Script
13 | from .validation_definition import ValidationDefinition
14 |
--------------------------------------------------------------------------------
/docs/modularinput.rst:
--------------------------------------------------------------------------------
1 | splunklib.modularinput
2 | ----------------------
3 |
4 | .. automodule:: splunklib.modularinput
5 |
6 | .. autoclass:: Argument
7 | :members:
8 |
9 | .. autoclass:: Event
10 | :members:
11 |
12 | .. autoclass:: EventWriter
13 | :members:
14 |
15 | .. autoclass:: InputDefinition
16 | :members:
17 |
18 | .. autoclass:: Scheme
19 | :members:
20 |
21 | .. autoclass:: Script
22 | :members:
23 |
24 | .. autoclass:: ValidationDefinition
25 | :members:
26 |
--------------------------------------------------------------------------------
/docs/binding.rst:
--------------------------------------------------------------------------------
1 | splunklib.binding
2 | -----------------
3 |
4 | .. automodule:: splunklib.binding
5 |
6 | .. autofunction:: connect
7 |
8 | .. autofunction:: handler
9 |
10 | .. autofunction:: namespace
11 |
12 | .. autoclass:: AuthenticationError
13 | :members:
14 |
15 | .. autoclass:: Context
16 | :members: connect, delete, get, get_cookies, has_cookies, login, logout, post, request
17 |
18 | .. autoclass:: HTTPError
19 | :members:
20 |
21 | .. autoclass:: HttpLib
22 | :members: delete, get, post, request
23 |
24 | .. autoclass:: ResponseReader
25 | :members: close, empty, peek, read
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/tests/system/test_apps/eventing_app/metadata/default.meta:
--------------------------------------------------------------------------------
1 |
2 | # Application-level permissions
3 |
4 | []
5 | access = read : [ * ], write : [ admin, power ]
6 |
7 | ### EVENT TYPES
8 |
9 | [eventtypes]
10 | export = system
11 |
12 |
13 | ### PROPS
14 |
15 | [props]
16 | export = system
17 |
18 |
19 | ### TRANSFORMS
20 |
21 | [transforms]
22 | export = system
23 |
24 |
25 | ### LOOKUPS
26 |
27 | [lookups]
28 | export = system
29 |
30 |
31 | ### VIEWSTATES: even normal users should be able to create shared viewstates
32 |
33 | [viewstates]
34 | access = read : [ * ], write : [ * ]
35 | export = system
36 |
37 | [commands/eventingcsc]
38 | access = read : [ * ], write : [ * ]
39 | export = system
40 | owner = Splunk
41 |
--------------------------------------------------------------------------------
/tests/system/test_apps/generating_app/metadata/default.meta:
--------------------------------------------------------------------------------
1 |
2 | # Application-level permissions
3 |
4 | []
5 | access = read : [ * ], write : [ admin, power ]
6 |
7 | ### EVENT TYPES
8 |
9 | [eventtypes]
10 | export = system
11 |
12 |
13 | ### PROPS
14 |
15 | [props]
16 | export = system
17 |
18 |
19 | ### TRANSFORMS
20 |
21 | [transforms]
22 | export = system
23 |
24 |
25 | ### LOOKUPS
26 |
27 | [lookups]
28 | export = system
29 |
30 |
31 | ### VIEWSTATES: even normal users should be able to create shared viewstates
32 |
33 | [viewstates]
34 | access = read : [ * ], write : [ * ]
35 | export = system
36 |
37 | [commands/generatingcsc]
38 | access = read : [ * ], write : [ * ]
39 | export = system
40 | owner = nobody
41 |
--------------------------------------------------------------------------------
/tests/system/test_apps/reporting_app/metadata/default.meta:
--------------------------------------------------------------------------------
1 |
2 | # Application-level permissions
3 |
4 | []
5 | access = read : [ * ], write : [ admin, power ]
6 |
7 | ### EVENT TYPES
8 |
9 | [eventtypes]
10 | export = system
11 |
12 |
13 | ### PROPS
14 |
15 | [props]
16 | export = system
17 |
18 |
19 | ### TRANSFORMS
20 |
21 | [transforms]
22 | export = system
23 |
24 |
25 | ### LOOKUPS
26 |
27 | [lookups]
28 | export = system
29 |
30 |
31 | ### VIEWSTATES: even normal users should be able to create shared viewstates
32 |
33 | [viewstates]
34 | access = read : [ * ], write : [ * ]
35 | export = system
36 |
37 | [commands/reportingcsc]
38 | access = read : [ * ], write : [ * ]
39 | export = system
40 | owner = nobody
41 |
--------------------------------------------------------------------------------
/tests/system/test_apps/streaming_app/metadata/default.meta:
--------------------------------------------------------------------------------
1 |
2 | # Application-level permissions
3 |
4 | []
5 | access = read : [ * ], write : [ admin, power ]
6 |
7 | ### EVENT TYPES
8 |
9 | [eventtypes]
10 | export = system
11 |
12 |
13 | ### PROPS
14 |
15 | [props]
16 | export = system
17 |
18 |
19 | ### TRANSFORMS
20 |
21 | [transforms]
22 | export = system
23 |
24 |
25 | ### LOOKUPS
26 |
27 | [lookups]
28 | export = system
29 |
30 |
31 | ### VIEWSTATES: even normal users should be able to create shared viewstates
32 |
33 | [viewstates]
34 | access = read : [ * ], write : [ * ]
35 | export = system
36 |
37 | [commands/streamingcsc]
38 | access = read : [ * ], write : [ * ]
39 | export = system
40 | owner = nobody
41 |
--------------------------------------------------------------------------------
/tests/unit/modularinput/data/stream_with_two_events.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | hilda
5 | misc
6 | main
7 | localhost
8 | This is a test of the emergency broadcast system.
9 |
10 |
11 |
12 |
13 | hilda
14 | misc
15 | main
16 | localhost
17 | This is a test of the emergency broadcast system.
18 |
19 |
20 |
--------------------------------------------------------------------------------
/sitecustomize.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright © 2011-2024 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | # This file is required for running coverage.py
18 |
19 | try:
20 | import coverage
21 |
22 | coverage.process_startup()
23 | except:
24 | pass
25 |
--------------------------------------------------------------------------------
/.github/workflows/pre-release.yml:
--------------------------------------------------------------------------------
1 | name: Publish SDK to Test PyPI
2 | on: [workflow_dispatch]
3 |
4 | env:
5 | PYTHON_VERSION: 3.9
6 |
7 | jobs:
8 | publish-sdk-test-pypi:
9 | runs-on: ubuntu-latest
10 | permissions:
11 | id-token: write
12 | environment:
13 | name: splunk-test-pypi
14 | steps:
15 | - name: Checkout source
16 | uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e
17 | - name: Set up Python ${{ env.PYTHON_VERSION }}
18 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
19 | with:
20 | python-version: ${{ env.PYTHON_VERSION }}
21 | - name: Install dependencies
22 | run: python -m pip install . --group build
23 | - name: Build packages for distribution
24 | run: python -m build
25 | - name: Publish packages to Test PyPI
26 | uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
27 | with:
28 | repository-url: https://test.pypi.org/legacy/
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 |
15 | 1. Go to '...'
16 | 2. Click on '...'
17 | 3. Scroll down to '...'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Logs or Screenshots**
24 | If applicable, add logs or screenshots to help explain your problem.
25 |
26 | **Splunk (please complete the following information):**
27 |
28 | - Version: [e.g. 8.0.5]
29 | - OS: [e.g. Ubuntu 20.04.1]
30 | - Deployment: [e.g. single-instance]
31 |
32 | **SDK (please complete the following information):**
33 |
34 | - Version: [e.g. 1.6.14]
35 | - Language Runtime Version: [e.g. Python 3.7]
36 | - OS: [e.g. MacOS 10.15.7]
37 |
38 | **Additional context**
39 | Add any other context about the problem here.
40 |
--------------------------------------------------------------------------------
/tests/unit/modularinput/data/scheme_without_defaults.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | abcd
4 | 쎼 and 쎶 and <&> für
5 | false
6 | true
7 | simple
8 |
9 |
10 |
11 | string
12 | false
13 | false
14 |
15 |
16 | 쎼 and 쎶 and <&> für
17 | is_pos_int('some_name')
18 | number
19 | true
20 | true
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/tests/unit/modularinput/modularinput_testlib.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright © 2011-2024 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | import io
18 | import os
19 | import sys
20 | import unittest
21 |
22 | sys.path.insert(0, os.path.join("../../splunklib", ".."))
23 |
24 | from splunklib.modularinput.utils import xml_compare, parse_xml_data, parse_parameters
25 |
26 |
27 | def data_open(filepath):
28 | return io.open(
29 | os.path.join(os.path.dirname(os.path.abspath(__file__)), filepath), "rb"
30 | )
31 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | CONTAINER_NAME := "splunk"
2 |
3 | .PHONY: docs
4 | docs:
5 | @make -C ./docs html
6 |
7 | .PHONY: test
8 | test:
9 | @python -m pytest ./tests
10 |
11 | .PHONY: test-unit
12 | test-unit:
13 | @python -m pytest ./tests/unit
14 |
15 | .PHONY: test-integration
16 | test-integration:
17 | @python -m pytest ./tests/integration ./tests/system
18 |
19 | .PHONY: docker-up
20 | docker-up:
21 | @docker-compose up -d
22 |
23 | .PHONY: docker-ensure-up
24 | docker-ensure-up:
25 | @for i in `seq 0 180`; do \
26 | if docker exec -it $(CONTAINER_NAME) /bin/bash -c "/sbin/checkstate.sh &> /dev/null"; then \
27 | break; \
28 | fi; \
29 | printf "\rWaiting for Splunk for %s seconds..." $$i; \
30 | sleep 1; \
31 | done
32 |
33 | .PHONY: docker-start
34 | docker-start: docker-up docker-ensure-up
35 |
36 | .PHONY: docker-down
37 | docker-down:
38 | @docker-compose stop
39 |
40 | .PHONY: docker-restart
41 | docker-restart: docker-down docker-start
42 |
43 | .PHONY: docker-remove
44 | docker-remove:
45 | @docker-compose rm -f -s
46 |
47 | .PHONY: docker-refresh
48 | docker-refresh: docker-remove docker-start
--------------------------------------------------------------------------------
/tests/unit/searchcommands/test_multibyte_processing.py:
--------------------------------------------------------------------------------
1 | import io
2 | import gzip
3 | import sys
4 |
5 | from os import path
6 |
7 | from splunklib.searchcommands import StreamingCommand, Configuration
8 |
9 |
10 | def build_test_command():
11 | @Configuration()
12 | class TestSearchCommand(StreamingCommand):
13 | def stream(self, records):
14 | for record in records:
15 | yield record
16 |
17 | return TestSearchCommand()
18 |
19 |
20 | def get_input_file(name):
21 | return path.join(
22 | path.dirname(path.dirname(__file__)), "data", "custom_search", name + ".gz"
23 | )
24 |
25 |
26 | def test_multibyte_chunked():
27 | data = gzip.open(get_input_file("multibyte_input"))
28 | data = io.TextIOWrapper(data)
29 | cmd = build_test_command()
30 | cmd._process_protocol_v2(sys.argv, data, sys.stdout)
31 |
32 |
33 | def test_v1_searchcommand():
34 | data = gzip.open(get_input_file("v1_search_input"))
35 | data = io.TextIOWrapper(data)
36 | cmd = build_test_command()
37 | cmd._process_protocol_v1(["test_script.py", "__EXECUTE__"], data, sys.stdout)
38 |
--------------------------------------------------------------------------------
/tests/unit/modularinput/data/scheme_without_defaults_and_argument_title.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | abcd
4 | 쎼 and 쎶 and <&> für
5 | false
6 | true
7 | simple
8 |
9 |
10 |
11 | string
12 | false
13 | false
14 |
15 |
16 | Argument for ``test_scheme``
17 | 쎼 and 쎶 and <&> für
18 | is_pos_int('some_name')
19 | number
20 | true
21 | true
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Publish SDK to PyPI
2 | on:
3 | release:
4 | types: [published]
5 |
6 | env:
7 | PYTHON_VERSION: 3.9
8 |
9 | jobs:
10 | publish-sdk-pypi:
11 | runs-on: ubuntu-latest
12 | permissions:
13 | id-token: write
14 | environment:
15 | name: splunk-pypi
16 | steps:
17 | - name: Checkout source
18 | uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e
19 | - name: Set up Python ${{ env.PYTHON_VERSION }}
20 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
21 | with:
22 | python-version: ${{ env.PYTHON_VERSION }}
23 | - name: Install dependencies
24 | run: python -m pip install . --group release
25 | - name: Build packages for distribution
26 | run: python -m build
27 | - name: Publish packages to PyPI
28 | uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
29 | - name: Generate API reference
30 | run: make -C ./docs html
31 | - name: Upload docs artifact
32 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
33 | with:
34 | name: python-sdk-docs
35 | path: docs/_build/html
36 |
--------------------------------------------------------------------------------
/tests/integration/data/streaming_results.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | count
5 |
6 |
7 |
8 | search context: user="admin", app="search", bs-pathname="/an/obviously/bogus/path/to/splunk/etc"
9 |
10 |
11 |
12 |
13 | 1
14 |
15 |
16 |
17 |
18 |
19 |
20 | count
21 |
22 |
23 |
24 | search context: user="admin", app="search", bs-pathname="/an/obviously/bogus/path/to/splunk/etc"
25 |
26 |
27 |
28 |
29 | 3
30 |
31 |
32 |
33 |
34 |
35 |
36 | count
37 |
38 |
39 |
40 | search context: user="admin", app="search", bs-pathname="/an/obviously/bogus/path/to/splunk/etc"
41 |
42 |
43 |
44 |
45 | 54
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/pr_template.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Pull Request Template
3 | about: Create a Pull Request to contribute to the SDK
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
9 | ## Description of PR
10 |
11 | Provide the **context and motivation** for this PR.
12 | Briefly explain the **type of changes** (bug fix, feature request, doc update, etc.) made in this PR. Provide reference to issue # fixed, if applicable.
13 |
14 | Describe the approach to the solution, the changes made, and any resulting change in behavior or impact to the user.
15 |
16 | ## Testing the changes
17 |
18 | Please ensure tests are added for your changes.
19 | Include details of **types of tests** written for the changes in the PR and any **test setup and configuration** required to run the tests.
20 | Mention the **versions of the SDK, language runtime, OS and details of Splunk deployment** used in testing.
21 |
22 | ## Documentation
23 |
24 | Please ensure **comments** are added for your changes and any **relevant docs** (readme, reference docs, etc.) are updated.
25 | Include any references to documentation related to the changes.
26 |
27 | ## Dependencies and other resources
28 |
29 | Provide references to PRs or things **dependent on this change** and any relevant PRs or resources like style guides and tools used in this PR.
30 |
--------------------------------------------------------------------------------
/splunklib/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2011-2024 Splunk, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | # not use this file except in compliance with the License. You may obtain
5 | # a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | # License for the specific language governing permissions and limitations
13 | # under the License.
14 |
15 | """Python library for Splunk."""
16 |
17 | import logging
18 |
19 | DEFAULT_LOG_FORMAT = (
20 | "%(asctime)s, Level=%(levelname)s, Pid=%(process)s, Logger=%(name)s, File=%(filename)s, "
21 | "Line=%(lineno)s, %(message)s"
22 | )
23 | DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S %Z"
24 |
25 |
26 | # To set the logging level of splunklib
27 | # ex. To enable debug logs, call this method with parameter 'logging.DEBUG'
28 | # default logging level is set to 'WARNING'
29 | def setup_logging(
30 | level, log_format=DEFAULT_LOG_FORMAT, date_format=DEFAULT_DATE_FORMAT
31 | ):
32 | logging.basicConfig(level=level, format=log_format, datefmt=date_format)
33 |
34 |
35 | __version_info__ = (2, 2, 0, "alpha")
36 | __version__ = ".".join(map(str, __version_info__))
37 |
--------------------------------------------------------------------------------
/tests/system/test_apps/streaming_app/bin/streamingcsc.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding=utf-8
3 | #
4 | # Copyright © 2011-2024 Splunk, Inc.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
7 | # not use this file except in compliance with the License. You may obtain
8 | # a copy of the License at
9 | #
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15 | # License for the specific language governing permissions and limitations
16 | # under the License.
17 |
18 | import sys
19 |
20 | from splunklib.searchcommands import (
21 | Configuration,
22 | StreamingCommand,
23 | dispatch,
24 | )
25 |
26 |
27 | @Configuration()
28 | class StreamingCSC(StreamingCommand):
29 | """
30 | The streamingapp command returns events with a one new field 'fahrenheit'.
31 |
32 | Example:
33 |
34 | ``| makeresults count=5 | eval celsius = random()%100 | streamingcsc``
35 |
36 | returns a records with one new filed 'fahrenheit'.
37 | """
38 |
39 | def stream(self, records):
40 | for record in records:
41 | record["fahrenheit"] = (float(record["celsius"]) * 1.8) + 32
42 | yield record
43 |
44 |
45 | dispatch(StreamingCSC, sys.argv, sys.stdin, sys.stdout, __name__)
46 |
--------------------------------------------------------------------------------
/splunklib/utils.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2011-2024 Splunk, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | # not use this file except in compliance with the License. You may obtain
5 | # a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | # License for the specific language governing permissions and limitations
13 | # under the License.
14 |
15 | """The **splunklib.utils** File for utility functions."""
16 |
17 |
18 | def ensure_binary(s, encoding="utf-8", errors="strict"):
19 | """
20 | - `str` -> encoded to `bytes`
21 | - `bytes` -> `bytes`
22 | """
23 | if isinstance(s, str):
24 | return s.encode(encoding, errors)
25 |
26 | if isinstance(s, bytes):
27 | return s
28 |
29 | raise TypeError(f"not expecting type '{type(s)}'")
30 |
31 |
32 | def ensure_str(s, encoding="utf-8", errors="strict"):
33 | """
34 | - `str` -> `str`
35 | - `bytes` -> decoded to `str`
36 | """
37 | if isinstance(s, bytes):
38 | return s.decode(encoding, errors)
39 |
40 | if isinstance(s, str):
41 | return s
42 |
43 | raise TypeError(f"not expecting type '{type(s)}'")
44 |
45 |
46 | def assertRegex(self, *args, **kwargs):
47 | return getattr(self, "assertRegex")(*args, **kwargs)
48 |
--------------------------------------------------------------------------------
/tests/unit/searchcommands/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding=utf-8
3 | #
4 | # Copyright © 2011-2024 Splunk, Inc.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
7 | # not use this file except in compliance with the License. You may obtain
8 | # a copy of the License at
9 | #
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15 | # License for the specific language governing permissions and limitations
16 | # under the License.
17 |
18 | from os import path
19 | import logging
20 |
21 | from splunklib.searchcommands import environment
22 | from splunklib import searchcommands
23 |
24 | package_directory = path.dirname(path.realpath(__file__))
25 | project_root = path.dirname(path.dirname(package_directory))
26 |
27 |
28 | def rebase_environment(name):
29 | environment.app_root = path.join(package_directory, "apps", name)
30 | logging.Logger.manager.loggerDict.clear()
31 | del logging.root.handlers[:]
32 |
33 | environment.splunklib_logger, environment.logging_configuration = (
34 | environment.configure_logging("splunklib")
35 | )
36 | searchcommands.logging_configuration = environment.logging_configuration
37 | searchcommands.splunklib_logger = environment.splunklib_logger
38 | searchcommands.app_root = environment.app_root
39 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Python CI
2 | on: [push, workflow_dispatch]
3 |
4 | jobs:
5 | run-test-suite:
6 | runs-on: ${{ matrix.os }}
7 | strategy:
8 | fail-fast: false
9 | matrix:
10 | os: [ubuntu-latest]
11 | python-version: [3.9]
12 | splunk-version: [9.4, latest]
13 | include:
14 | # Oldest possible configuration
15 | # Last Ubuntu version with Python 3.7 binaries available
16 | - os: ubuntu-22.04
17 | python-version: 3.7
18 | splunk-version: 9.1
19 | # Latest possible configuration
20 | - os: ubuntu-latest
21 | python-version: 3.13
22 | splunk-version: latest
23 | steps:
24 | - name: Checkout code
25 | uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e
26 | - name: Launch Splunk Docker instance
27 | run: SPLUNK_VERSION=${{ matrix.splunk-version }} docker compose up -d
28 | - name: Setup Python ${{ matrix.python-version }}
29 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548
30 | with:
31 | python-version: ${{ matrix.python-version }}
32 | - name: (Python 3.7) Install dependencies
33 | if: ${{ matrix.python-version == '3.7' }}
34 | run: python -m pip install python-dotenv pytest
35 | - name: (Python >= 3.9) Install dependencies
36 | if: ${{ matrix.python-version != '3.7' }}
37 | run: python -m pip install . --group test
38 | - name: Run entire test suite
39 | run: python -m pytest ./tests
40 |
--------------------------------------------------------------------------------
/tests/system/test_apps/cre_app/bin/execute.py:
--------------------------------------------------------------------------------
1 | import splunk.rest
2 | import json
3 |
4 |
5 | class Handler(splunk.rest.BaseRestHandler):
6 | def handle_GET(self):
7 | self.response.setHeader("Content-Type", "application/json")
8 | self.response.setHeader("x-foo", "bar")
9 | self.response.status = 200
10 | self.response.write(
11 | json.dumps(
12 | {
13 | "headers": self.headers(),
14 | "method": "GET",
15 | }
16 | )
17 | )
18 |
19 | def handle_DELETE(self):
20 | self.handle_with_payload("DELETE")
21 |
22 | def handle_POST(self):
23 | self.handle_with_payload("POST")
24 |
25 | def handle_PUT(self):
26 | self.handle_with_payload("PUT")
27 |
28 | def handle_PATCH(self):
29 | self.handle_with_payload("PATCH")
30 |
31 | def handle_with_payload(self, method):
32 | self.response.setHeader("Content-Type", "application/json")
33 | self.response.setHeader("x-foo", "bar")
34 | self.response.status = 200
35 | self.response.write(
36 | json.dumps(
37 | {
38 | "payload": self.request.get("payload"),
39 | "headers": self.headers(),
40 | "method": method,
41 | }
42 | )
43 | )
44 |
45 | def headers(self):
46 | return {
47 | k: v
48 | for k, v in self.request.get("headers", {}).items()
49 | if k.lower().startswith("x")
50 | }
51 |
--------------------------------------------------------------------------------
/tests/system/test_apps/generating_app/bin/generatingcsc.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding=utf-8
3 | #
4 | # Copyright © 2011-2024 Splunk, Inc.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
7 | # not use this file except in compliance with the License. You may obtain
8 | # a copy of the License at
9 | #
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15 | # License for the specific language governing permissions and limitations
16 | # under the License.
17 |
18 | import sys
19 | import time
20 |
21 | from splunklib.searchcommands import (
22 | Configuration,
23 | GeneratingCommand,
24 | Option,
25 | dispatch,
26 | validators,
27 | )
28 |
29 |
30 | @Configuration()
31 | class GeneratingCSC(GeneratingCommand):
32 | """
33 | The `generatingcsc` command generates a specific number of records.
34 |
35 | Example:
36 |
37 | ``| generatingcsc count=4``
38 |
39 | Returns a 4 records having text 'Test Event'.
40 | """
41 |
42 | count = Option(require=True, validate=validators.Integer(0))
43 |
44 | def generate(self):
45 | self.logger.debug("Generating %s events" % self.count)
46 | for i in range(1, self.count + 1):
47 | text = f"Test Event {i}"
48 | yield {"_time": time.time(), "event_no": i, "_raw": text}
49 |
50 |
51 | dispatch(GeneratingCSC, sys.argv, sys.stdin, sys.stdout, __name__)
52 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | ## How to contribute
4 |
5 | If you would like to contribute to this project, see [Contributions to Splunk](https://www.splunk.com/en_us/form/contributions.html) for more information.
6 |
7 | ## Issues and bug reports
8 |
9 | If you're seeing some unexpected behavior with this project, please create an [issue](https://github.com/splunk/splunk-sdk-python/issues) on GitHub with the following information:
10 |
11 | 1. Version of this project you're using (ex: 1.5.0)
12 | 2. Platform version (ex: Windows Server 2012 R2)
13 | 3. Framework version (ex: Python 3.7)
14 | 4. Splunk Enterprise version (ex: 9.0)
15 | 5. Other relevant information (ex: local/remote environment, Splunk network configuration, standalone or distributed deployment, are load balancers used)
16 |
17 | Alternatively, if you have a Splunk question please ask on [Splunk Answers](https://community.splunk.com/t5/Splunk-Development/ct-p/developer-tools).
18 |
19 | ## Pull requests
20 |
21 | We love to see pull requests!
22 |
23 | To create a pull request:
24 |
25 | 1. Fill out the [Individual Contributor Agreement](https://www.splunk.com/en_us/form/contributions.html).
26 | 2. Fork the [repository](https://github.com/splunk/splunk-sdk-python).
27 | 3. Make changes to the **develop** branch, preferably with tests.
28 | 4. Create a [pull request](https://github.com/splunk/splunk-sdk-python/pulls) against the **develop** branch.
29 |
30 | ## Contact us
31 |
32 | If you have a paid Splunk Enterprise or Splunk Cloud license, you can contact [Support](https://www.splunk.com/en_us/support-and-services.html) with questions.
33 |
34 | You can reach the Splunk Developer Platform team at _devinfo@splunk.com_.
35 |
36 |
--------------------------------------------------------------------------------
/tests/system/test_apps/eventing_app/bin/eventingcsc.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding=utf-8
3 | #
4 | # Copyright © 2011-2024 Splunk, Inc.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
7 | # not use this file except in compliance with the License. You may obtain
8 | # a copy of the License at
9 | #
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15 | # License for the specific language governing permissions and limitations
16 | # under the License.
17 |
18 | import sys
19 |
20 | from splunklib.searchcommands import (
21 | Configuration,
22 | EventingCommand,
23 | Option,
24 | dispatch,
25 | )
26 |
27 |
28 | @Configuration()
29 | class EventingCSC(EventingCommand):
30 | """
31 | The `eventingcsc` command filters records from the events stream
32 | returning only those for which the status is same as search query.
33 |
34 | Example:
35 |
36 | ``index="_internal" | head 4000 | eventingcsc status=200``
37 |
38 | Returns records having status 200 as mentioned in search query.
39 | """
40 |
41 | status = Option(
42 | doc="""**Syntax:** **status=****
43 | **Description:** record having same status value will be returned.""",
44 | require=True,
45 | )
46 |
47 | def transform(self, records):
48 | for record in records:
49 | if str(self.status) == record["status"]:
50 | yield record
51 |
52 |
53 | dispatch(EventingCSC, sys.argv, sys.stdin, sys.stdout, __name__)
54 |
--------------------------------------------------------------------------------
/tests/integration/test_logger.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright © 2011-2024 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | from tests import testlib
18 |
19 |
20 | LEVELS = ["INFO", "WARN", "ERROR", "DEBUG", "CRIT"]
21 |
22 |
23 | class LoggerTestCase(testlib.SDKTestCase):
24 | def check_logger(self, logger):
25 | self.check_entity(logger)
26 | self.assertTrue(logger["level"] in LEVELS)
27 |
28 | def test_read(self):
29 | for logger in self.service.loggers.list(count=10):
30 | self.check_logger(logger)
31 |
32 | def test_crud(self):
33 | self.assertTrue("AuditLogger" in self.service.loggers)
34 | logger = self.service.loggers["AuditLogger"]
35 |
36 | saved = logger["level"]
37 | for level in LEVELS:
38 | logger.update(level=level)
39 | logger.refresh()
40 | self.assertEqual(self.service.loggers["AuditLogger"]["level"], level)
41 |
42 | logger.update(level=saved)
43 | logger.refresh()
44 | self.assertEqual(self.service.loggers["AuditLogger"]["level"], saved)
45 |
46 |
47 | if __name__ == "__main__":
48 | import unittest
49 |
50 | unittest.main()
51 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | splunk:
3 | image: "splunk/splunk:${SPLUNK_VERSION}"
4 | container_name: splunk
5 | environment:
6 | - SPLUNK_START_ARGS=--accept-license
7 | - SPLUNK_GENERAL_TERMS=--accept-sgt-current-at-splunk-com
8 | - SPLUNK_HEC_TOKEN=11111111-1111-1111-1111-1111111111113
9 | - SPLUNK_PASSWORD=changed!
10 | - SPLUNK_APPS_URL=https://github.com/splunk/sdk-app-collection/releases/latest/download/sdkappcollection.tgz
11 | ports:
12 | - "8000:8000"
13 | - "8088:8088"
14 | - "8089:8089"
15 | healthcheck:
16 | test: ['CMD', 'curl', '-f', 'http://localhost:8000']
17 | interval: 5s
18 | timeout: 5s
19 | retries: 20
20 | volumes:
21 | - "./tests/system/test_apps/eventing_app:/opt/splunk/etc/apps/eventing_app"
22 | - "./tests/system/test_apps/generating_app:/opt/splunk/etc/apps/generating_app"
23 | - "./tests/system/test_apps/reporting_app:/opt/splunk/etc/apps/reporting_app"
24 | - "./tests/system/test_apps/streaming_app:/opt/splunk/etc/apps/streaming_app"
25 | - "./tests/system/test_apps/modularinput_app:/opt/splunk/etc/apps/modularinput_app"
26 | - "./tests/system/test_apps/cre_app:/opt/splunk/etc/apps/cre_app"
27 | - "./splunklib:/opt/splunk/etc/apps/eventing_app/bin/splunklib"
28 | - "./splunklib:/opt/splunk/etc/apps/generating_app/bin/splunklib"
29 | - "./splunklib:/opt/splunk/etc/apps/reporting_app/bin/splunklib"
30 | - "./splunklib:/opt/splunk/etc/apps/streaming_app/bin/splunklib"
31 | - "./splunklib:/opt/splunk/etc/apps/modularinput_app/bin/splunklib"
32 |
--------------------------------------------------------------------------------
/tests/unit/searchcommands/apps/app_with_logging_configuration/logging.conf:
--------------------------------------------------------------------------------
1 | #
2 | # The format and semantics of this file are described in this article at Python.org:
3 | #
4 | # [Configuration file format](https://docs.python.org/2/library/logging.config.html#configuration-file-format)
5 | #
6 | [loggers]
7 | keys = root, splunklib, SearchCommand
8 |
9 | [logger_root]
10 | # Default: WARNING
11 | level = WARNING
12 | # Default: stderr
13 | handlers = stderr
14 |
15 | [logger_splunklib]
16 | qualname = splunklib
17 | # Default: WARNING
18 | level = NOTSET
19 | # Default: stderr
20 | handlers = splunklib
21 | # Default: 1
22 | propagate = 0
23 |
24 | [logger_SearchCommand]
25 | qualname = SearchCommand
26 | # Default: WARNING
27 | level = NOTSET
28 | # Default: stderr
29 | handlers = app
30 | # Default: 1
31 | propagate = 0
32 |
33 | [handlers]
34 | # See [logging.handlers](https://docs.python.org/2/library/logging.handlers.html)
35 | keys = app, splunklib, stderr
36 |
37 | [handler_app]
38 | # Select this handler to log events to app.log
39 | class = logging.handlers.RotatingFileHandler
40 | level = NOTSET
41 | args = ('app.log', 'a', 524288000, 9, 'utf-8', True)
42 | formatter = searchcommands
43 |
44 | [handler_splunklib]
45 | # Select this handler to log events to splunklib.log
46 | class = logging.handlers.RotatingFileHandler
47 | args = ('splunklib.log', 'a', 524288000, 9, 'utf-8', True)
48 | level = NOTSET
49 | formatter = searchcommands
50 |
51 | [handler_stderr]
52 | # Select this handler to log events to stderr which splunkd redirects to the associated job's search.log file
53 | class = logging.StreamHandler
54 | level = NOTSET
55 | args = (sys.stderr,)
56 | formatter = searchcommands
57 |
58 | [formatters]
59 | keys = searchcommands
60 |
61 | [formatter_searchcommands]
62 | format = %(asctime)s, Level=%(levelname)s, Pid=%(process)s, File=%(filename)s, Line=%(lineno)s, %(message)s
63 |
--------------------------------------------------------------------------------
/tests/unit/searchcommands/apps/app_with_logging_configuration/default/logging.conf:
--------------------------------------------------------------------------------
1 | #
2 | # The format and semantics of this file are described in this article at Python.org:
3 | #
4 | # [Configuration file format](https://docs.python.org/2/library/logging.config.html#configuration-file-format)
5 | #
6 | [loggers]
7 | keys = root, splunklib, SearchCommand
8 |
9 | [logger_root]
10 | # Default: WARNING
11 | level = WARNING
12 | # Default: stderr
13 | handlers = stderr
14 |
15 | [logger_splunklib]
16 | qualname = splunklib
17 | # Default: WARNING
18 | level = NOTSET
19 | # Default: stderr
20 | handlers = splunklib
21 | # Default: 1
22 | propagate = 0
23 |
24 | [logger_SearchCommand]
25 | qualname = SearchCommand
26 | # Default: WARNING
27 | level = NOTSET
28 | # Default: stderr
29 | handlers = app
30 | # Default: 1
31 | propagate = 0
32 |
33 | [handlers]
34 | # See [logging.handlers](https://docs.python.org/2/library/logging.handlers.html)
35 | keys = app, splunklib, stderr
36 |
37 | [handler_app]
38 | # Select this handler to log events to app.log
39 | class = logging.handlers.RotatingFileHandler
40 | level = NOTSET
41 | args = ('app.log', 'a', 524288000, 9, 'utf-8', True)
42 | formatter = searchcommands
43 |
44 | [handler_splunklib]
45 | # Select this handler to log events to splunklib.log
46 | class = logging.handlers.RotatingFileHandler
47 | args = ('splunklib.log', 'a', 524288000, 9, 'utf-8', True)
48 | level = NOTSET
49 | formatter = searchcommands
50 |
51 | [handler_stderr]
52 | # Select this handler to log events to stderr which splunkd redirects to the associated job's search.log file
53 | class = logging.StreamHandler
54 | level = NOTSET
55 | args = (sys.stderr,)
56 | formatter = searchcommands
57 |
58 | [formatters]
59 | keys = searchcommands
60 |
61 | [formatter_searchcommands]
62 | format = %(asctime)s, Level=%(levelname)s, Pid=%(process)s, File=%(filename)s, Line=%(lineno)s, %(message)s
63 |
--------------------------------------------------------------------------------
/tests/unit/searchcommands/apps/app_with_logging_configuration/default/alternative-logging.conf:
--------------------------------------------------------------------------------
1 | #
2 | # The format and semantics of this file are described in this article at Python.org:
3 | #
4 | # [Configuration file format](https://docs.python.org/2/library/logging.config.html#configuration-file-format)
5 | #
6 | [loggers]
7 | keys = root, splunklib, SearchCommand
8 |
9 | [logger_root]
10 | # Default: WARNING
11 | level = WARNING
12 | # Default: stderr
13 | handlers = stderr
14 |
15 | [logger_splunklib]
16 | qualname = splunklib
17 | # Default: WARNING
18 | level = NOTSET
19 | # Default: stderr
20 | handlers = splunklib
21 | # Default: 1
22 | propagate = 0
23 |
24 | [logger_SearchCommand]
25 | qualname = SearchCommand
26 | # Default: WARNING
27 | level = NOTSET
28 | # Default: stderr
29 | handlers = app
30 | # Default: 1
31 | propagate = 0
32 |
33 | [handlers]
34 | # See [logging.handlers](https://docs.python.org/2/library/logging.handlers.html)
35 | keys = app, splunklib, stderr
36 |
37 | [handler_app]
38 | # Select this handler to log events to app.log
39 | class = logging.handlers.RotatingFileHandler
40 | level = NOTSET
41 | args = ('app.log', 'a', 524288000, 9, 'utf-8', True)
42 | formatter = searchcommands
43 |
44 | [handler_splunklib]
45 | # Select this handler to log events to splunklib.log
46 | class = logging.handlers.RotatingFileHandler
47 | args = ('splunklib.log', 'a', 524288000, 9, 'utf-8', True)
48 | level = NOTSET
49 | formatter = searchcommands
50 |
51 | [handler_stderr]
52 | # Select this handler to log events to stderr which splunkd redirects to the associated job's search.log file
53 | class = logging.StreamHandler
54 | level = NOTSET
55 | args = (sys.stderr,)
56 | formatter = searchcommands
57 |
58 | [formatters]
59 | keys = searchcommands
60 |
61 | [formatter_searchcommands]
62 | format = %(asctime)s, Level=%(levelname)s, Pid=%(process)s, File=%(filename)s, Line=%(lineno)s, %(message)s
63 |
--------------------------------------------------------------------------------
/tests/unit/modularinput/test_validation_definition.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright © 2011-2024 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 |
18 | from tests.unit.modularinput.modularinput_testlib import unittest, data_open
19 | from splunklib.modularinput.validation_definition import ValidationDefinition
20 |
21 |
22 | class ValidationDefinitionTestCase(unittest.TestCase):
23 | def test_validation_definition_parse(self):
24 | """Check that parsing produces expected result"""
25 | found = ValidationDefinition.parse(data_open("data/validation.xml"))
26 |
27 | expected = ValidationDefinition()
28 | expected.metadata = {
29 | "server_host": "tiny",
30 | "server_uri": "https://127.0.0.1:8089",
31 | "checkpoint_dir": "/opt/splunk/var/lib/splunk/modinputs",
32 | "session_key": "123102983109283019283",
33 | "name": "aaa",
34 | }
35 | expected.parameters = {
36 | "param1": "value1",
37 | "param2": "value2",
38 | "disabled": "0",
39 | "index": "default",
40 | "multiValue": ["value1", "value2"],
41 | "multiValue2": ["value3", "value4"],
42 | }
43 |
44 | self.assertEqual(expected, found)
45 |
46 |
47 | if __name__ == "__main__":
48 | unittest.main()
49 |
--------------------------------------------------------------------------------
/docs/CSS/epub.css:
--------------------------------------------------------------------------------
1 | /* Page background color */
2 | body { background: transparent }
3 |
4 | /* Headings */
5 | h1 { font-size: 145% }
6 |
7 | body {
8 | font-family: Arial, sans-serif;
9 | color: #555;
10 | font-size: 13px;
11 | line-height: 18px;
12 | margin:0 auto;
13 | padding-left:5px;
14 | padding-right:5px;
15 | }
16 |
17 | pre {font-weight: bold}
18 |
19 | table {
20 | border-width: 0px;
21 | border-spacing: 0px;
22 | border-style: outset;
23 | border-color: gray;
24 | border-collapse: collapse;
25 | background-color: white;
26 | /*margin-top:15px; */
27 | margin-bottom: 15px;
28 | }
29 |
30 | th {
31 | border-width: 0px;
32 | padding: 2px 6px 1px 6px;
33 | border-style: inset;
34 | border-color: #e2e2e2;
35 | background-color: #eeeeee;
36 | text-align: left;
37 | }
38 |
39 | td {
40 | border-width: 0px;
41 | padding: 2px 6px 1px 6px;
42 | border-style: inset;
43 | border-color: #e2e2e2;
44 | background-color: white;
45 | }
46 |
47 | a {
48 | color: rgb(8, 89, 130);
49 | text-decoration: none;
50 | }
51 |
52 | /* Font used in left-hand frame lists */
53 | .FrameTitleFont { font-size: 100%; font-family: Arial, sans-serif; color:#000000 }
54 | .FrameHeadingFont { font-size: 100%; font-family: Arial, sans-serif; color:#000000 }
55 | .FrameItemFont { font-size: 100%; font-family: Arial, sans-serif; color:#000000 }
56 |
57 |
58 | /* Navigation bar fonts and colors */
59 | .NavBarCell1 { background-color:#EEEEFF; color:#000000} /* Light mauve */
60 | .NavBarCell1Rev { background-color:#00008B; color:#FFFFFF} /* Dark Blue */
61 | .NavBarFont1 { font-family: Arial, Helvetica, sans-serif; color:#000000;color:#000000;}
62 | .NavBarFont1Rev { font-family: Arial, Helvetica, sans-serif; color:#FFFFFF;color:#FFFFFF;}
63 |
64 | .NavBarCell2 { font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF; color:#000000}
65 | .NavBarCell3 { font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF; color:#000000}
66 |
--------------------------------------------------------------------------------
/splunklib/modularinput/input_definition.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2011-2024 Splunk, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | # not use this file except in compliance with the License. You may obtain
5 | # a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | # License for the specific language governing permissions and limitations
13 | # under the License.
14 |
15 | import xml.etree.ElementTree as ET
16 | from .utils import parse_xml_data
17 |
18 |
19 | class InputDefinition:
20 | """``InputDefinition`` encodes the XML defining inputs that Splunk passes to
21 | a modular input script.
22 |
23 | **Example**::
24 |
25 | i = InputDefinition()
26 |
27 | """
28 |
29 | def __init__(self):
30 | self.metadata = {}
31 | self.inputs = {}
32 |
33 | def __eq__(self, other):
34 | if not isinstance(other, InputDefinition):
35 | return False
36 | return self.metadata == other.metadata and self.inputs == other.inputs
37 |
38 | @staticmethod
39 | def parse(stream):
40 | """Parse a stream containing XML into an ``InputDefinition``.
41 |
42 | :param stream: stream containing XML to parse.
43 | :return: definition: an ``InputDefinition`` object.
44 | """
45 | definition = InputDefinition()
46 |
47 | # parse XML from the stream, then get the root node
48 | root = ET.parse(stream).getroot()
49 |
50 | for node in root:
51 | if node.tag == "configuration":
52 | # get config for each stanza
53 | definition.inputs = parse_xml_data(node, "stanza")
54 | else:
55 | definition.metadata[node.tag] = node.text
56 |
57 | return definition
58 |
--------------------------------------------------------------------------------
/tests/system/test_apps/reporting_app/bin/reportingcsc.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding=utf-8
3 | #
4 | # Copyright © 2011-2024 Splunk, Inc.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
7 | # not use this file except in compliance with the License. You may obtain
8 | # a copy of the License at
9 | #
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15 | # License for the specific language governing permissions and limitations
16 | # under the License.
17 |
18 | import sys
19 |
20 | from splunklib.searchcommands import (
21 | Configuration,
22 | Option,
23 | ReportingCommand,
24 | dispatch,
25 | validators,
26 | )
27 |
28 |
29 | @Configuration(requires_preop=True)
30 | class ReportingCSC(ReportingCommand):
31 | """
32 | The `reportingcsc` command returns a count of students
33 | having higher total marks than cutoff marks.
34 |
35 | Example:
36 | ```
37 | | makeresults count=10
38 | | eval math=random()%100, eng=random()%100, cs=random()%100
39 | | reportingcsc cutoff=150 math eng cs
40 | ```
41 |
42 | Returns a count of students out of 10 having a higher total marks than cutoff.
43 | """
44 |
45 | cutoff = Option(require=True, validate=validators.Integer(0))
46 |
47 | @Configuration()
48 | def map(self, records):
49 | """returns a total marks of a students"""
50 | # list of subjects
51 | fieldnames = self.fieldnames
52 | for record in records:
53 | # store a total marks of a single student
54 | total = 0.0
55 | for fieldname in fieldnames:
56 | total += float(record[fieldname])
57 | yield {"totalMarks": total}
58 |
59 | def reduce(self, records):
60 | """returns a students count having a higher total marks than cutoff"""
61 | pass_student_cnt = 0
62 | for record in records:
63 | value = float(record["totalMarks"])
64 | if value >= float(self.cutoff):
65 | pass_student_cnt += 1
66 | yield {"student having total marks greater than cutoff ": pass_student_cnt}
67 |
68 |
69 | dispatch(ReportingCSC, sys.argv, sys.stdin, sys.stdout, __name__)
70 |
--------------------------------------------------------------------------------
/tests/integration/test_message.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright © 2011-2024 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | from tests import testlib
18 |
19 | from splunklib import client
20 |
21 |
22 | class MessageTest(testlib.SDKTestCase):
23 | def setUp(self):
24 | testlib.SDKTestCase.setUp(self)
25 | self.message_name = testlib.tmpname()
26 | self.message = self.service.messages.create(
27 | self.message_name, value="Test message created by the SDK"
28 | )
29 |
30 | def tearDown(self):
31 | testlib.SDKTestCase.tearDown(self)
32 | self.service.messages.delete(self.message_name)
33 |
34 |
35 | class TestCreateDelete(testlib.SDKTestCase):
36 | def test_create_delete(self):
37 | message_name = testlib.tmpname()
38 | message_value = "Test message"
39 | message = self.service.messages.create(message_name, value=message_value)
40 | self.assertTrue(message_name in self.service.messages)
41 | self.assertEqual(message.value, message_value)
42 | self.check_entity(message)
43 | self.service.messages.delete(message_name)
44 | self.assertFalse(message_name in self.service.messages)
45 |
46 | def test_invalid_name(self):
47 | self.assertRaises(
48 | client.InvalidNameException,
49 | self.service.messages.create,
50 | None,
51 | value="What?",
52 | )
53 | self.assertRaises(
54 | client.InvalidNameException,
55 | self.service.messages.create,
56 | 42,
57 | value="Who, me?",
58 | )
59 | self.assertRaises(
60 | client.InvalidNameException,
61 | self.service.messages.create,
62 | [1, 2, 3],
63 | value="Who, me?",
64 | )
65 |
66 |
67 | if __name__ == "__main__":
68 | import unittest
69 |
70 | unittest.main()
71 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project.urls]
2 | homepage = "https://pypi.org/project/splunk-sdk"
3 | documentation = "https://docs.splunk.com/Documentation/PythonSDK/2.1.1"
4 | issues = "https://github.com/splunk/splunk-sdk-python/issues"
5 | changelog = "https://github.com/splunk/splunk-sdk-python/blob/master/CHANGELOG.md"
6 | source = "https://github.com/splunk/splunk-sdk-python.git"
7 | download = "https://github.com/splunk/splunk-sdk-python/releases/latest"
8 |
9 | [project]
10 | name = "splunk-sdk"
11 | dynamic = ["version"]
12 | description = "Splunk Software Development Kit for Python"
13 | readme = "README.md"
14 | requires-python = ">=3.7"
15 | license = { text = "Apache-2.0" }
16 | authors = [{ name = "Splunk, Inc.", email = "devinfo@splunk.com" }]
17 | keywords = ["splunk", "sdk"]
18 | classifiers = [
19 | "Programming Language :: Python :: 3",
20 | "Programming Language :: Python :: 3 :: Only",
21 | "Programming Language :: Python :: 3.7",
22 | "Programming Language :: Python :: 3.9",
23 | "Programming Language :: Python :: 3.13",
24 | "Development Status :: 6 - Mature",
25 | "Environment :: Other Environment",
26 | "Intended Audience :: Developers",
27 | "Operating System :: OS Independent",
28 | "Topic :: Software Development :: Libraries :: Python Modules",
29 | "Topic :: Software Development :: Libraries :: Application Frameworks",
30 | ]
31 |
32 | dependencies = ["python-dotenv>=0.21.1"]
33 | optional-dependencies = { compat = ["six>=1.17.0"] }
34 |
35 | [dependency-groups]
36 | build = ["build>=1.1.1", "twine>=4.0.2"]
37 | # Can't pin `sphinx` otherwise installation fails on python>=3.7
38 | docs = ["sphinx", "jinja2>=3.1.6"]
39 | lint = ["mypy>=1.4.1", "ruff>=0.13.1"]
40 | test = ["pytest>=7.4.4", "pytest-cov>=4.1.0"]
41 | release = [{ include-group = "build" }, { include-group = "docs" }]
42 | dev = [
43 | { include-group = "test" },
44 | { include-group = "lint" },
45 | { include-group = "build" },
46 | { include-group = "docs" },
47 | ]
48 |
49 | [build-system]
50 | requires = ["setuptools"]
51 | build-backend = "setuptools.build_meta"
52 |
53 | [tool.setuptools]
54 | packages = ["splunklib", "splunklib.modularinput", "splunklib.searchcommands"]
55 |
56 | [tool.setuptools.dynamic]
57 | version = { attr = "splunklib.__version__" }
58 |
59 | # https://docs.astral.sh/ruff/configuration/
60 | [tool.ruff.lint]
61 | fixable = ["ALL"]
62 | select = [
63 | "F", # pyflakes
64 | "E", # pycodestyle
65 | "I", # isort
66 | "ANN", # flake8 type annotations
67 | "RUF", # ruff-specific rules
68 | ]
69 |
--------------------------------------------------------------------------------
/tests/unit/searchcommands/test_reporting_command.py:
--------------------------------------------------------------------------------
1 | import io
2 |
3 | from splunklib import searchcommands
4 | from . import chunked_data_stream as chunky
5 |
6 |
7 | def test_simple_reporting_command():
8 | @searchcommands.Configuration()
9 | class TestReportingCommand(searchcommands.ReportingCommand):
10 | def reduce(self, records):
11 | value = 0
12 | for record in records:
13 | value += int(record["value"])
14 | yield {"sum": value}
15 |
16 | cmd = TestReportingCommand()
17 | ifile = io.BytesIO()
18 | data = []
19 | for i in range(0, 10):
20 | data.append({"value": str(i)})
21 | ifile.write(chunky.build_getinfo_chunk())
22 | ifile.write(chunky.build_data_chunk(data))
23 | ifile.seek(0)
24 | ofile = io.BytesIO()
25 | cmd._process_protocol_v2([], ifile, ofile)
26 | ofile.seek(0)
27 | chunk_stream = chunky.ChunkedDataStream(ofile)
28 | getinfo_response = chunk_stream.read_chunk()
29 | assert getinfo_response.meta["type"] == "reporting"
30 | data_chunk = chunk_stream.read_chunk()
31 | assert data_chunk.meta["finished"] is True # Should only be one row
32 | data = list(data_chunk.data)
33 | assert len(data) == 1
34 | assert int(data[0]["sum"]) == sum(range(0, 10))
35 |
36 |
37 | def test_simple_reporting_command_with_map():
38 | @searchcommands.Configuration()
39 | class MapAndReduceReportingCommand(searchcommands.ReportingCommand):
40 | def map(self, records):
41 | for record in records:
42 | record["value"] = str(int(record["value"]) * 2)
43 | yield record
44 |
45 | def reduce(self, records):
46 | total = 0
47 | for record in records:
48 | total += int(record["value"])
49 | yield {"sum": total}
50 |
51 | cmd = MapAndReduceReportingCommand()
52 | ifile = io.BytesIO()
53 |
54 | input_data = [{"value": str(i)} for i in range(5)]
55 |
56 | mapped_data = list(cmd.map(input_data))
57 |
58 | ifile.write(chunky.build_getinfo_chunk())
59 | ifile.write(chunky.build_data_chunk(mapped_data))
60 | ifile.seek(0)
61 |
62 | ofile = io.BytesIO()
63 | cmd._process_protocol_v2([], ifile, ofile)
64 |
65 | ofile.seek(0)
66 | chunk_stream = chunky.ChunkedDataStream(ofile)
67 | chunk_stream.read_chunk()
68 | data_chunk = chunk_stream.read_chunk()
69 | assert data_chunk.meta["finished"] is True
70 |
71 | result = list(data_chunk.data)
72 | expected_sum = sum(i * 2 for i in range(5))
73 | assert int(result[0]["sum"]) == expected_sum
74 |
--------------------------------------------------------------------------------
/tests/system/test_apps/modularinput_app/bin/modularinput.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # Copyright © 2011-2025 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | import sys
18 | from urllib import parse
19 |
20 | from splunklib.modularinput import Argument, Event, Scheme, Script
21 |
22 |
23 | class ModularInput(Script):
24 | """
25 | This app provides an example of a modular input that
26 | can be used in Settings => Data inputs => Local inputs => modularinput
27 | """
28 |
29 | endpoint_arg = "endpoint"
30 |
31 | def get_scheme(self):
32 | scheme = Scheme("modularinput")
33 |
34 | scheme.use_external_validation = True
35 | scheme.use_single_instance = True
36 |
37 | endpoint = Argument(self.endpoint_arg)
38 | endpoint.title = "URL"
39 | endpoint.data_type = Argument.data_type_string
40 | endpoint.description = "URL"
41 | endpoint.required_on_create = True
42 | scheme.add_argument(endpoint)
43 |
44 | return scheme
45 |
46 | def validate_input(self, definition):
47 | self.check_service_access()
48 |
49 | url = definition.parameters[self.endpoint_arg]
50 | parsed = parse.urlparse(url)
51 | if parsed.scheme != "https":
52 | raise ValueError(f"non-supported scheme {parsed.scheme}")
53 |
54 | def stream_events(self, inputs, ew):
55 | self.check_service_access()
56 |
57 | for input_name, input_item in list(inputs.inputs.items()):
58 | event = Event()
59 | event.stanza = input_name
60 | event.data = "example message"
61 | ew.write_event(event)
62 |
63 | def check_service_access(self):
64 | # Both validate_input and stream_events should have access to the Splunk
65 | # instance that executed the modular input.
66 | if self.service is None:
67 | raise Exception("self.Service == None")
68 | self.service.info # make sure that we are properly authenticated and self.service works
69 |
70 |
71 | if __name__ == "__main__":
72 | sys.exit(ModularInput().run(sys.argv))
73 |
--------------------------------------------------------------------------------
/tests/unit/data/services.server.info.xml:
--------------------------------------------------------------------------------
1 |
2 | server-info
3 | https://localhost/services/server/info
4 | 2011-07-01T16:53:53-07:00
5 |
6 |
7 | Splunk
8 |
9 | 1
10 | 30
11 | 0
12 |
13 |
14 | server-info
15 | https://localhost/services/server/info/server-info
16 | 2011-07-01T16:53:53-07:00
17 |
18 |
19 | system
20 |
21 |
22 |
23 |
24 | 101089
25 | i386
26 | 00system*system
27 | 3FD18BC4-010D-4845-AD51-27963CB17412
28 | 0
29 | 0
30 | E4BF242DB47AB54B5F9DC69D00E65A4BD4E89F917B93D0FA611EDF85ABFC639D
31 | 3af106e3af0db73ca69339684cad26eb
32 | OK
33 | 3FD18BC4-010D-4845-AD51-27963CB17412
34 | normal
35 | Darwin Kernel Version 10.8.0: Tue Jun 7 16:33:36 PDT 2011; root:xnu-1504.15.3~1/RELEASE_I386
36 | Darwin
37 | 10.8.0
38 | blovering.local
39 | 4.3
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/tests/integration/test_kvstore_batch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright 2011-2024 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | from tests import testlib
18 |
19 |
20 | class KVStoreBatchTestCase(testlib.SDKTestCase):
21 | def setUp(self):
22 | super().setUp()
23 | self.service.namespace["app"] = "search"
24 | confs = self.service.kvstore
25 | if "test" in confs:
26 | confs["test"].delete()
27 | confs.create("test")
28 |
29 | self.col = confs["test"].data
30 |
31 | def test_insert_find_update_data(self):
32 | data = [{"_key": str(x), "data": "#" + str(x), "num": x} for x in range(1000)]
33 | self.col.batch_save(*data)
34 |
35 | testData = self.col.query(sort="num")
36 | self.assertEqual(len(testData), 1000)
37 |
38 | for x in range(1000):
39 | self.assertEqual(testData[x]["_key"], str(x))
40 | self.assertEqual(testData[x]["data"], "#" + str(x))
41 | self.assertEqual(testData[x]["num"], x)
42 |
43 | data = [
44 | {"_key": str(x), "data": "#" + str(x + 1), "num": x + 1}
45 | for x in range(1000)
46 | ]
47 | self.col.batch_save(*data)
48 |
49 | testData = self.col.query(sort="num")
50 | self.assertEqual(len(testData), 1000)
51 |
52 | for x in range(1000):
53 | self.assertEqual(testData[x]["_key"], str(x))
54 | self.assertEqual(testData[x]["data"], "#" + str(x + 1))
55 | self.assertEqual(testData[x]["num"], x + 1)
56 |
57 | query = [{"query": {"num": x + 1}} for x in range(100)]
58 | testData = self.col.batch_find(*query)
59 |
60 | self.assertEqual(len(testData), 100)
61 | testData.sort(key=lambda x: x[0]["num"])
62 |
63 | for x in range(100):
64 | self.assertEqual(testData[x][0]["_key"], str(x))
65 | self.assertEqual(testData[x][0]["data"], "#" + str(x + 1))
66 | self.assertEqual(testData[x][0]["num"], x + 1)
67 |
68 | def tearDown(self):
69 | confs = self.service.kvstore
70 | if "test" in confs:
71 | confs["test"].delete()
72 |
73 |
74 | if __name__ == "__main__":
75 | import unittest
76 |
77 | unittest.main()
78 |
--------------------------------------------------------------------------------
/splunklib/modularinput/utils.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2011-2024 Splunk, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | # not use this file except in compliance with the License. You may obtain
5 | # a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | # License for the specific language governing permissions and limitations
13 | # under the License.
14 |
15 | # File for utility functions
16 |
17 |
18 | def xml_compare(expected, found):
19 | """Checks equality of two ``ElementTree`` objects.
20 |
21 | :param expected: An ``ElementTree`` object.
22 | :param found: An ``ElementTree`` object.
23 | :return: ``Boolean``, whether the two objects are equal.
24 | """
25 |
26 | # if comparing the same ET object
27 | if expected == found:
28 | return True
29 |
30 | # compare element attributes, ignoring order
31 | if set(expected.items()) != set(found.items()):
32 | return False
33 |
34 | # check for equal number of children
35 | expected_children = list(expected)
36 | found_children = list(found)
37 | if len(expected_children) != len(found_children):
38 | return False
39 |
40 | # compare children
41 | if not all(xml_compare(a, b) for a, b in zip(expected_children, found_children)):
42 | return False
43 |
44 | # compare elements, if there is no text node, return True
45 | if (expected.text is None or expected.text.strip() == "") and (
46 | found.text is None or found.text.strip() == ""
47 | ):
48 | return True
49 | return (
50 | expected.tag == found.tag
51 | and expected.text == found.text
52 | and expected.attrib == found.attrib
53 | )
54 |
55 |
56 | def parse_parameters(param_node):
57 | if param_node.tag == "param":
58 | return param_node.text
59 | if param_node.tag == "param_list":
60 | parameters = []
61 | for mvp in param_node:
62 | parameters.append(mvp.text)
63 | return parameters
64 | raise ValueError(f"Invalid configuration scheme, {param_node.tag} tag unexpected.")
65 |
66 |
67 | def parse_xml_data(parent_node, child_node_tag):
68 | data = {}
69 | for child in parent_node:
70 | child_name = child.get("name")
71 | if child.tag == child_node_tag:
72 | if child_node_tag == "stanza":
73 | data[child_name] = {"__app": child.get("app", None)}
74 | for param in child:
75 | data[child_name][param.get("name")] = parse_parameters(param)
76 | elif "item" == parent_node.tag:
77 | data[child_name] = parse_parameters(child)
78 | return data
79 |
--------------------------------------------------------------------------------
/splunklib/modularinput/validation_definition.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2011-2024 Splunk, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | # not use this file except in compliance with the License. You may obtain
5 | # a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | # License for the specific language governing permissions and limitations
13 | # under the License.
14 |
15 |
16 | import xml.etree.ElementTree as ET
17 |
18 | from .utils import parse_xml_data
19 |
20 |
21 | class ValidationDefinition:
22 | """This class represents the XML sent by Splunk for external validation of a
23 | new modular input.
24 |
25 | **Example**::
26 |
27 | v = ValidationDefinition()
28 |
29 | """
30 |
31 | def __init__(self):
32 | self.metadata = {}
33 | self.parameters = {}
34 |
35 | def __eq__(self, other):
36 | if not isinstance(other, ValidationDefinition):
37 | return False
38 | return self.metadata == other.metadata and self.parameters == other.parameters
39 |
40 | @staticmethod
41 | def parse(stream):
42 | """Creates a ``ValidationDefinition`` from a provided stream containing XML.
43 |
44 | The XML typically will look like this:
45 |
46 | .. code-block:: xml
47 |
48 |
49 | myHost
50 | https://127.0.0.1:8089
51 | 123102983109283019283
52 | /opt/splunk/var/lib/splunk/modinputs
53 | -
54 | value1
55 |
56 | value2
57 | value3
58 | value4
59 |
60 |
61 |
62 |
63 | :param stream: ``Stream`` containing XML to parse.
64 | :return: A ``ValidationDefinition`` object.
65 |
66 | """
67 |
68 | definition = ValidationDefinition()
69 |
70 | # parse XML from the stream, then get the root node
71 | root = ET.parse(stream).getroot()
72 |
73 | for node in root:
74 | # lone item node
75 | if node.tag == "item":
76 | # name from item node
77 | definition.metadata["name"] = node.get("name")
78 | definition.parameters = parse_xml_data(node, "")
79 | else:
80 | # Store anything else in metadata
81 | definition.metadata[node.tag] = node.text
82 |
83 | return definition
84 |
--------------------------------------------------------------------------------
/tests/unit/modularinput/test_input_definition.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright © 2011-2024 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | from tests.unit.modularinput.modularinput_testlib import unittest, data_open
18 | from splunklib.modularinput.input_definition import InputDefinition
19 |
20 |
21 | class InputDefinitionTestCase(unittest.TestCase):
22 | def test_parse_inputdef_with_zero_inputs(self):
23 | """Check parsing of XML that contains only metadata"""
24 |
25 | found = InputDefinition.parse(data_open("data/conf_with_0_inputs.xml"))
26 |
27 | expectedDefinition = InputDefinition()
28 | expectedDefinition.metadata = {
29 | "server_host": "tiny",
30 | "server_uri": "https://127.0.0.1:8089",
31 | "checkpoint_dir": "/some/dir",
32 | "session_key": "123102983109283019283",
33 | }
34 |
35 | self.assertEqual(found, expectedDefinition)
36 |
37 | def test_parse_inputdef_with_two_inputs(self):
38 | """Check parsing of XML that contains 2 inputs"""
39 |
40 | found = InputDefinition.parse(data_open("data/conf_with_2_inputs.xml"))
41 |
42 | expectedDefinition = InputDefinition()
43 | expectedDefinition.metadata = {
44 | "server_host": "tiny",
45 | "server_uri": "https://127.0.0.1:8089",
46 | "checkpoint_dir": "/some/dir",
47 | "session_key": "123102983109283019283",
48 | }
49 | expectedDefinition.inputs["foobar://aaa"] = {
50 | "__app": "search",
51 | "param1": "value1",
52 | "param2": "value2",
53 | "disabled": "0",
54 | "index": "default",
55 | }
56 | expectedDefinition.inputs["foobar://bbb"] = {
57 | "__app": "my_app",
58 | "param1": "value11",
59 | "param2": "value22",
60 | "disabled": "0",
61 | "index": "default",
62 | "multiValue": ["value1", "value2"],
63 | "multiValue2": ["value3", "value4"],
64 | }
65 |
66 | self.assertEqual(expectedDefinition, found)
67 |
68 | def test_attempt_to_parse_malformed_input_definition_will_throw_exception(self):
69 | """Does malformed XML cause the expected exception."""
70 |
71 | with self.assertRaises(ValueError):
72 | InputDefinition.parse(data_open("data/conf_with_invalid_inputs.xml"))
73 |
74 |
75 | if __name__ == "__main__":
76 | unittest.main()
77 |
--------------------------------------------------------------------------------
/tests/system/test_modularinput_app.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright © 2011-2025 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | from splunklib import results
18 | from tests import testlib
19 | from splunklib.binding import HTTPError
20 |
21 |
22 | class ModularInput(testlib.SDKTestCase):
23 | index_name = "test_modular_input"
24 | input_name = "test_modular_input"
25 | input_kind = "modularinput"
26 |
27 | def setUp(self):
28 | super().setUp()
29 |
30 | app_found = False
31 | for kind in self.service.modular_input_kinds:
32 | if kind.name == self.input_kind:
33 | app_found = True
34 |
35 | self.assertTrue(app_found, f"{self.input_kind} modular input not installed")
36 | self.clean()
37 |
38 | def tearDown(self):
39 | super().tearDown()
40 | self.clean()
41 |
42 | def clean(self):
43 | for input in self.service.inputs:
44 | if input.name == self.input_name and input.kind == self.input_kind:
45 | self.service.inputs.delete(self.input_name, self.input_kind)
46 |
47 | for index in self.service.indexes:
48 | if index.name == self.input_name:
49 | self.service.indexes.delete(self.input_name)
50 |
51 | def test_modular_input(self):
52 | self.service.indexes.create(self.index_name)
53 |
54 | self.service.inputs.create(
55 | self.input_name,
56 | self.input_kind,
57 | endpoint="https://example.com/api/endpoint",
58 | index=self.index_name,
59 | )
60 |
61 | def query():
62 | stream = self.service.jobs.oneshot(
63 | f'search index="{self.index_name}"', output_mode="json"
64 | )
65 | reader = results.JSONResultsReader(stream)
66 | return list(reader)
67 |
68 | # Wait until the modular input is executed by splunk.
69 | self.assertEventuallyTrue(lambda: len(query()) != 0, timeout=10)
70 |
71 | items = query()
72 | self.assertTrue(len(items) == 1)
73 | self.assertEqual(items[0]["_raw"], "example message")
74 |
75 | def test_external_validator(self):
76 | def create():
77 | self.service.inputs.create(
78 | self.input_name,
79 | self.input_kind,
80 | endpoint="http://example.com/api/endpoint",
81 | index=self.index_name,
82 | )
83 |
84 | self.assertRaisesRegex(HTTPError, "non-supported scheme http", create)
85 |
86 |
87 | if __name__ == "__main__":
88 | import unittest
89 |
90 | unittest.main()
91 |
--------------------------------------------------------------------------------
/tests/integration/test_modular_input_kinds.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright © 2011-2024 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | import pytest
18 |
19 | from tests import testlib
20 |
21 | from splunklib import client
22 |
23 |
24 | class ModularInputKindTestCase(testlib.SDKTestCase):
25 | @pytest.mark.app
26 | def test_list_arguments(self):
27 | self.install_app_from_collection("modular_inputs")
28 |
29 | if self.service.splunk_version[0] < 5:
30 | # Not implemented before 5.0
31 | return
32 |
33 | test1 = self.service.modular_input_kinds["test1"]
34 |
35 | expected_args = {
36 | "name",
37 | "resname",
38 | "key_id",
39 | "no_description",
40 | "empty_description",
41 | "arg_required_on_edit",
42 | "not_required_on_edit",
43 | "required_on_create",
44 | "not_required_on_create",
45 | "number_field",
46 | "string_field",
47 | "boolean_field",
48 | }
49 | found_args = set(test1.arguments.keys())
50 |
51 | self.assertEqual(expected_args, found_args)
52 |
53 | @pytest.mark.app
54 | def test_update_raises_exception(self):
55 | self.install_app_from_collection("modular_inputs")
56 |
57 | if self.service.splunk_version[0] < 5:
58 | # Not implemented before 5.0
59 | return
60 |
61 | test1 = self.service.modular_input_kinds["test1"]
62 | self.assertRaises(client.IllegalOperationException, test1.update, a="b")
63 |
64 | def check_modular_input_kind(self, m):
65 | if m.name == "test1":
66 | self.assertEqual('Test "Input" - 1', m["title"])
67 | self.assertEqual("xml", m["streaming_mode"])
68 | elif m.name == "test2":
69 | self.assertEqual("test2", m["title"])
70 | self.assertEqual("simple", m["streaming_mode"])
71 |
72 | @pytest.mark.app
73 | def test_list_modular_inputs(self):
74 | self.install_app_from_collection("modular_inputs")
75 |
76 | if self.service.splunk_version[0] < 5:
77 | # Not implemented before 5.0
78 | return
79 |
80 | inputs = self.service.inputs
81 | if ("abcd", "test2") not in inputs:
82 | inputs.create("abcd", "test2", field1="boris")
83 |
84 | input = inputs["abcd", "test2"]
85 | self.assertEqual(input.field1, "boris")
86 |
87 | for m in self.service.modular_input_kinds:
88 | self.check_modular_input_kind(m)
89 |
90 |
91 | if __name__ == "__main__":
92 | import unittest
93 |
94 | unittest.main()
95 |
--------------------------------------------------------------------------------
/tests/unit/searchcommands/chunked_data_stream.py:
--------------------------------------------------------------------------------
1 | import collections
2 | import csv
3 | import io
4 | import json
5 |
6 | import splunklib.searchcommands.internals
7 | from splunklib.utils import ensure_binary, ensure_str
8 |
9 |
10 | class Chunk:
11 | def __init__(self, version, meta, data):
12 | self.version = ensure_str(version)
13 | self.meta = json.loads(meta)
14 | dialect = splunklib.searchcommands.internals.CsvDialect
15 | self.data = csv.DictReader(io.StringIO(data.decode("utf-8")), dialect=dialect)
16 |
17 |
18 | class ChunkedDataStreamIter(collections.abc.Iterator):
19 | def __init__(self, chunk_stream):
20 | self.chunk_stream = chunk_stream
21 |
22 | def __next__(self):
23 | return self.next()
24 |
25 | def next(self):
26 | try:
27 | return self.chunk_stream.read_chunk()
28 | except EOFError:
29 | raise StopIteration
30 |
31 |
32 | class ChunkedDataStream(collections.abc.Iterable):
33 | def __iter__(self):
34 | return ChunkedDataStreamIter(self)
35 |
36 | def __init__(self, stream):
37 | empty = stream.read(0)
38 | assert isinstance(empty, bytes)
39 | self.stream = stream
40 |
41 | def read_chunk(self):
42 | header = self.stream.readline()
43 |
44 | while len(header) > 0 and header.strip() == b"":
45 | header = self.stream.readline() # Skip empty lines
46 | if len(header) == 0:
47 | raise EOFError
48 |
49 | version, meta, data = header.rstrip().split(b",")
50 | metabytes = self.stream.read(int(meta))
51 | databytes = self.stream.read(int(data))
52 | return Chunk(version, metabytes, databytes)
53 |
54 |
55 | def build_chunk(keyval, data=None):
56 | metadata = ensure_binary(json.dumps(keyval))
57 | data_output = _build_data_csv(data)
58 | return b"chunked 1.0,%d,%d\n%s%s" % (
59 | len(metadata),
60 | len(data_output),
61 | metadata,
62 | data_output,
63 | )
64 |
65 |
66 | def build_empty_searchinfo():
67 | return {
68 | "earliest_time": 0,
69 | "latest_time": 0,
70 | "search": "",
71 | "dispatch_dir": "",
72 | "sid": "",
73 | "args": [],
74 | "splunk_version": "42.3.4",
75 | }
76 |
77 |
78 | def build_getinfo_chunk():
79 | return build_chunk(
80 | {"action": "getinfo", "preview": False, "searchinfo": build_empty_searchinfo()}
81 | )
82 |
83 |
84 | def build_data_chunk(data, finished=True):
85 | return build_chunk({"action": "execute", "finished": finished}, data)
86 |
87 |
88 | def _build_data_csv(data):
89 | if data is None:
90 | return b""
91 | if isinstance(data, bytes):
92 | return data
93 | csvout = io.StringIO()
94 |
95 | headers = set()
96 | for datum in data:
97 | headers.update(datum.keys())
98 | writer = csv.DictWriter(
99 | csvout, headers, dialect=splunklib.searchcommands.internals.CsvDialect
100 | )
101 | writer.writeheader()
102 | for datum in data:
103 | writer.writerow(datum)
104 | return ensure_binary(csvout.getvalue())
105 |
--------------------------------------------------------------------------------
/splunklib/modularinput/scheme.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2011-2024 Splunk, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | # not use this file except in compliance with the License. You may obtain
5 | # a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | # License for the specific language governing permissions and limitations
13 | # under the License.
14 |
15 | import xml.etree.ElementTree as ET
16 |
17 |
18 | class Scheme:
19 | """Class representing the metadata for a modular input kind.
20 |
21 | A ``Scheme`` specifies a title, description, several options of how Splunk should run modular inputs of this
22 | kind, and a set of arguments which define a particular modular input's properties.
23 |
24 | The primary use of ``Scheme`` is to abstract away the construction of XML to feed to Splunk.
25 | """
26 |
27 | # Constant values, do not change
28 | # These should be used for setting the value of a Scheme object's streaming_mode field.
29 | streaming_mode_simple = "SIMPLE"
30 | streaming_mode_xml = "XML"
31 |
32 | def __init__(self, title):
33 | """
34 | :param title: ``string`` identifier for this Scheme in Splunk.
35 | """
36 | self.title = title
37 | self.description = None
38 | self.use_external_validation = True
39 | self.use_single_instance = False
40 | self.streaming_mode = Scheme.streaming_mode_xml
41 |
42 | # list of Argument objects, each to be represented by an tag
43 | self.arguments = []
44 |
45 | def add_argument(self, arg):
46 | """Add the provided argument, ``arg``, to the ``self.arguments`` list.
47 |
48 | :param arg: An ``Argument`` object to add to ``self.arguments``.
49 | """
50 | self.arguments.append(arg)
51 |
52 | def to_xml(self):
53 | """Creates an ``ET.Element`` representing self, then returns it.
54 |
55 | :returns: an ``ET.Element`` representing this scheme.
56 | """
57 | root = ET.Element("scheme")
58 |
59 | ET.SubElement(root, "title").text = self.title
60 |
61 | # add a description subelement if it's defined
62 | if self.description is not None:
63 | ET.SubElement(root, "description").text = self.description
64 |
65 | # add all other subelements to this Scheme, represented by (tag, text)
66 | subelements = [
67 | ("use_external_validation", self.use_external_validation),
68 | ("use_single_instance", self.use_single_instance),
69 | ("streaming_mode", self.streaming_mode),
70 | ]
71 | for name, value in subelements:
72 | ET.SubElement(root, name).text = str(value).lower()
73 |
74 | endpoint = ET.SubElement(root, "endpoint")
75 |
76 | args = ET.SubElement(endpoint, "args")
77 |
78 | # add arguments as subelements to the element
79 | for arg in self.arguments:
80 | arg.add_to_document(args)
81 |
82 | return root
83 |
--------------------------------------------------------------------------------
/tests/unit/searchcommands/test_generator_command.py:
--------------------------------------------------------------------------------
1 | import io
2 | import time
3 |
4 | from splunklib.searchcommands import Configuration, GeneratingCommand
5 | from . import chunked_data_stream as chunky
6 |
7 |
8 | def test_simple_generator():
9 | @Configuration()
10 | class GeneratorTest(GeneratingCommand):
11 | def generate(self):
12 | for num in range(1, 10):
13 | yield {"_time": time.time(), "event_index": num}
14 |
15 | generator = GeneratorTest()
16 | in_stream = io.BytesIO()
17 | in_stream.write(chunky.build_getinfo_chunk())
18 | in_stream.write(chunky.build_chunk({"action": "execute"}))
19 | in_stream.seek(0)
20 | out_stream = io.BytesIO()
21 | generator._process_protocol_v2([], in_stream, out_stream)
22 | out_stream.seek(0)
23 |
24 | ds = chunky.ChunkedDataStream(out_stream)
25 | is_first_chunk = True
26 | finished_seen = False
27 | expected = set(str(i) for i in range(1, 10))
28 | seen = set()
29 | for chunk in ds:
30 | if is_first_chunk:
31 | assert chunk.meta["generating"] is True
32 | assert chunk.meta["type"] == "stateful"
33 | is_first_chunk = False
34 | finished_seen = chunk.meta.get("finished", False)
35 | for row in chunk.data:
36 | seen.add(row["event_index"])
37 | print(out_stream.getvalue())
38 | print(expected)
39 | print(seen)
40 | assert expected.issubset(seen)
41 | assert finished_seen
42 |
43 |
44 | def test_allow_empty_input_for_generating_command():
45 | """
46 | Passing allow_empty_input for generating command will cause an error
47 | """
48 |
49 | @Configuration()
50 | class GeneratorTest(GeneratingCommand):
51 | def generate(self):
52 | for num in range(1, 3):
53 | yield {"_index": num}
54 |
55 | generator = GeneratorTest()
56 | in_stream = io.BytesIO()
57 | out_stream = io.BytesIO()
58 |
59 | try:
60 | generator.process([], in_stream, out_stream, allow_empty_input=False)
61 | except ValueError as error:
62 | assert str(error) == "allow_empty_input cannot be False for Generating Commands"
63 |
64 |
65 | def test_all_fieldnames_present_for_generated_records():
66 | @Configuration()
67 | class GeneratorTest(GeneratingCommand):
68 | def generate(self):
69 | yield self.gen_record(_time=time.time(), one=1)
70 | yield self.gen_record(_time=time.time(), two=2)
71 | yield self.gen_record(_time=time.time(), three=3)
72 | yield self.gen_record(_time=time.time(), four=4)
73 | yield self.gen_record(_time=time.time(), five=5)
74 |
75 | generator = GeneratorTest()
76 | in_stream = io.BytesIO()
77 | in_stream.write(chunky.build_getinfo_chunk())
78 | in_stream.write(chunky.build_chunk({"action": "execute"}))
79 | in_stream.seek(0)
80 | out_stream = io.BytesIO()
81 | generator._process_protocol_v2([], in_stream, out_stream)
82 | out_stream.seek(0)
83 |
84 | ds = chunky.ChunkedDataStream(out_stream)
85 | fieldnames_expected = {"_time", "one", "two", "three", "four", "five"}
86 | fieldnames_actual = set()
87 | for chunk in ds:
88 | for row in chunk.data:
89 | fieldnames_actual |= set(row.keys())
90 | assert fieldnames_expected.issubset(fieldnames_actual)
91 |
--------------------------------------------------------------------------------
/tests/integration/test_user.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright © 2011-2024 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | from tests import testlib
18 |
19 | from splunklib import client
20 |
21 |
22 | class UserTestCase(testlib.SDKTestCase):
23 | def check_user(self, user):
24 | self.check_entity(user)
25 | # Verify expected fields exist
26 | [user[f] for f in ["email", "password", "realname", "roles"]]
27 |
28 | def setUp(self):
29 | super().setUp()
30 | self.username = testlib.tmpname()
31 | self.user = self.service.users.create(
32 | self.username, password="changeme!", roles=["power", "user"]
33 | )
34 |
35 | def tearDown(self):
36 | super().tearDown()
37 | for user in self.service.users:
38 | if user.name.startswith("delete-me"):
39 | self.service.users.delete(user.name)
40 |
41 | def test_read(self):
42 | for user in self.service.users:
43 | self.check_user(user)
44 | for role in user.role_entities:
45 | self.assertTrue(isinstance(role, client.Entity))
46 | self.assertTrue(role.name in self.service.roles)
47 | self.assertEqual(user.roles, [role.name for role in user.role_entities])
48 |
49 | def test_create(self):
50 | self.assertTrue(self.username in self.service.users)
51 | self.assertEqual(self.username, self.user.name)
52 |
53 | def test_delete(self):
54 | self.service.users.delete(self.username)
55 | self.assertFalse(self.username in self.service.users)
56 | with self.assertRaises(client.HTTPError):
57 | self.user.refresh()
58 |
59 | def test_update(self):
60 | self.assertTrue(self.user["email"] is None)
61 | self.user.update(email="foo@bar.com")
62 | self.user.refresh()
63 | self.assertTrue(self.user["email"] == "foo@bar.com")
64 |
65 | def test_in_is_case_insensitive(self):
66 | # Splunk lowercases user names, verify the casing works as expected
67 | users = self.service.users
68 | self.assertTrue(self.username in users)
69 | self.assertTrue(self.username.upper() in users)
70 |
71 | def test_username_in_create_is_case_insensitive(self):
72 | name = testlib.tmpname().lower()
73 | users = self.service.users
74 | user = users.create(name.upper(), password="changeme!", roles="power")
75 | self.assertTrue(user.name == name)
76 | self.assertTrue(name in users)
77 |
78 | def test_delete_is_case_insensitive(self):
79 | users = self.service.users
80 | users.delete(self.username.upper())
81 | self.assertFalse(self.username in users)
82 | self.assertFalse(self.username.upper() in users)
83 |
84 |
85 | if __name__ == "__main__":
86 | import unittest
87 |
88 | unittest.main()
89 |
--------------------------------------------------------------------------------
/tests/integration/test_event_type.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright © 2011-2024 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | from tests import testlib
18 |
19 |
20 | class TestRead(testlib.SDKTestCase):
21 | def test_read(self):
22 | for event_type in self.service.event_types.list(count=1):
23 | self.check_entity(event_type)
24 |
25 |
26 | class TestCreate(testlib.SDKTestCase):
27 | def test_create(self):
28 | self.event_type_name = testlib.tmpname()
29 | event_types = self.service.event_types
30 | self.assertFalse(self.event_type_name in event_types)
31 |
32 | kwargs = {}
33 | kwargs["search"] = "index=_internal *"
34 | kwargs["description"] = "An internal event"
35 | kwargs["disabled"] = 1
36 | kwargs["priority"] = 2
37 |
38 | event_type = event_types.create(self.event_type_name, **kwargs)
39 | self.assertTrue(self.event_type_name in event_types)
40 | self.assertEqual(self.event_type_name, event_type.name)
41 |
42 | def tearDown(self):
43 | super().setUp()
44 | try:
45 | self.service.event_types.delete(self.event_type_name)
46 | except KeyError:
47 | pass
48 |
49 |
50 | class TestEventType(testlib.SDKTestCase):
51 | def setUp(self):
52 | super().setUp()
53 | self.event_type_name = testlib.tmpname()
54 | self.event_type = self.service.event_types.create(
55 | self.event_type_name, search="index=_internal *"
56 | )
57 |
58 | def tearDown(self):
59 | super().setUp()
60 | try:
61 | self.service.event_types.delete(self.event_type_name)
62 | except KeyError:
63 | pass
64 |
65 | # def test_delete(self):
66 | # self.assertTrue(self.event_type_name in self.service.event_types)
67 | # self.service.event_types.delete(self.event_type_name)
68 | # self.assertFalse(self.event_type_name in self.service.event_types)
69 |
70 | def test_update(self):
71 | kwargs = {
72 | "search": "index=_audit *",
73 | "description": "An audit event",
74 | "priority": "3",
75 | }
76 | self.event_type.update(**kwargs)
77 | self.event_type.refresh()
78 | self.assertEqual(self.event_type["search"], kwargs["search"])
79 | self.assertEqual(self.event_type["description"], kwargs["description"])
80 | self.assertEqual(self.event_type["priority"], kwargs["priority"])
81 |
82 | def test_enable_disable(self):
83 | self.assertEqual(self.event_type["disabled"], "0")
84 | self.event_type.disable()
85 | self.event_type.refresh()
86 | self.assertEqual(self.event_type["disabled"], "1")
87 | self.event_type.enable()
88 | self.event_type.refresh()
89 | self.assertEqual(self.event_type["disabled"], "0")
90 |
91 |
92 | if __name__ == "__main__":
93 | import unittest
94 |
95 | unittest.main()
96 |
--------------------------------------------------------------------------------
/tests/unit/searchcommands/test_streaming_command.py:
--------------------------------------------------------------------------------
1 | import io
2 |
3 | from splunklib.searchcommands import StreamingCommand, Configuration
4 | from . import chunked_data_stream as chunky
5 |
6 |
7 | def test_simple_streaming_command():
8 | @Configuration()
9 | class TestStreamingCommand(StreamingCommand):
10 | def stream(self, records):
11 | for record in records:
12 | record["out_index"] = record["in_index"]
13 | yield record
14 |
15 | cmd = TestStreamingCommand()
16 | ifile = io.BytesIO()
17 | ifile.write(chunky.build_getinfo_chunk())
18 | data = []
19 | for i in range(0, 10):
20 | data.append({"in_index": str(i)})
21 | ifile.write(chunky.build_data_chunk(data, finished=True))
22 | ifile.seek(0)
23 | ofile = io.BytesIO()
24 | cmd._process_protocol_v2([], ifile, ofile)
25 | ofile.seek(0)
26 | output = chunky.ChunkedDataStream(ofile)
27 | getinfo_response = output.read_chunk()
28 | assert getinfo_response.meta["type"] == "streaming"
29 |
30 |
31 | def test_field_preservation_negative():
32 | @Configuration()
33 | class TestStreamingCommand(StreamingCommand):
34 | def stream(self, records):
35 | for index, record in enumerate(records):
36 | if index % 2 != 0:
37 | record["odd_field"] = True
38 | else:
39 | record["even_field"] = True
40 | yield record
41 |
42 | cmd = TestStreamingCommand()
43 | ifile = io.BytesIO()
44 | ifile.write(chunky.build_getinfo_chunk())
45 | data = []
46 | for i in range(0, 10):
47 | data.append({"in_index": str(i)})
48 | ifile.write(chunky.build_data_chunk(data, finished=True))
49 | ifile.seek(0)
50 | ofile = io.BytesIO()
51 | cmd._process_protocol_v2([], ifile, ofile)
52 | ofile.seek(0)
53 | output_iter = chunky.ChunkedDataStream(ofile).__iter__()
54 | next(output_iter)
55 | output_records = list(next(output_iter).data)
56 |
57 | # Assert that count of records having "odd_field" is 0
58 | assert len(list(r for r in output_records if "odd_field" in r)) == 0
59 |
60 | # Assert that count of records having "even_field" is 10
61 | assert len(list(r for r in output_records if "even_field" in r)) == 10
62 |
63 |
64 | def test_field_preservation_positive():
65 | @Configuration()
66 | class TestStreamingCommand(StreamingCommand):
67 | def stream(self, records):
68 | for index, record in enumerate(records):
69 | if index % 2 != 0:
70 | self.add_field(record, "odd_field", True)
71 | else:
72 | self.add_field(record, "even_field", True)
73 | yield record
74 |
75 | cmd = TestStreamingCommand()
76 | ifile = io.BytesIO()
77 | ifile.write(chunky.build_getinfo_chunk())
78 | data = []
79 | for i in range(0, 10):
80 | data.append({"in_index": str(i)})
81 | ifile.write(chunky.build_data_chunk(data, finished=True))
82 | ifile.seek(0)
83 | ofile = io.BytesIO()
84 | cmd._process_protocol_v2([], ifile, ofile)
85 | ofile.seek(0)
86 | output_iter = chunky.ChunkedDataStream(ofile).__iter__()
87 | next(output_iter)
88 | output_records = list(next(output_iter).data)
89 |
90 | # Assert that count of records having "odd_field" is 10
91 | assert len(list(r for r in output_records if "odd_field" in r)) == 10
92 |
93 | # Assert that count of records having "even_field" is 10
94 | assert len(list(r for r in output_records if "even_field" in r)) == 10
95 |
--------------------------------------------------------------------------------
/docs/searchcommands.rst:
--------------------------------------------------------------------------------
1 | splunklib.searchcommands
2 | ------------------------
3 |
4 | .. automodule:: splunklib.searchcommands
5 |
6 | .. autofunction:: dispatch(command_class[, argv=sys.argv, input_file=sys.stdin, output_file=sys.stdout, module_name=None, allow_empty_input=True])
7 |
8 | .. autoclass:: EventingCommand
9 | :members:
10 | :inherited-members:
11 | :exclude-members: ConfigurationSettings, process, transform
12 |
13 | .. autoclass:: splunklib.searchcommands::EventingCommand.ConfigurationSettings
14 | :members:
15 | :inherited-members:
16 | :exclude-members: configuration_settings, fix_up, items, keys
17 |
18 | .. automethod:: splunklib.searchcommands::EventingCommand.transform
19 |
20 | .. automethod:: splunklib.searchcommands::EventingCommand.process(args=sys.argv[, input_file=sys.stdin, output_file=sys.stdout])
21 |
22 | .. autoclass:: GeneratingCommand
23 | :members:
24 | :inherited-members:
25 | :exclude-members: ConfigurationSettings, generate, process
26 |
27 | .. autoclass:: splunklib.searchcommands::GeneratingCommand.ConfigurationSettings
28 | :members:
29 | :inherited-members:
30 | :exclude-members: configuration_settings, fix_up, items, keys
31 |
32 | .. automethod:: splunklib.searchcommands::GeneratingCommand.generate
33 |
34 | .. automethod:: splunklib.searchcommands::GeneratingCommand.process(args=sys.argv[, input_file=sys.stdin, output_file=sys.stdout, allow_empty_input=True])
35 |
36 | .. autoclass:: ReportingCommand
37 | :members:
38 | :inherited-members:
39 | :exclude-members: ConfigurationSettings, map, process, reduce
40 |
41 | .. autoclass:: splunklib.searchcommands::ReportingCommand.ConfigurationSettings
42 | :members:
43 | :inherited-members:
44 | :exclude-members: configuration_settings, fix_up, items, keys
45 |
46 | .. automethod:: splunklib.searchcommands::ReportingCommand.map
47 |
48 | .. automethod:: splunklib.searchcommands::ReportingCommand.process(args=sys.argv[, input_file=sys.stdin, output_file=sys.stdout])
49 |
50 | .. automethod:: splunklib.searchcommands::ReportingCommand.reduce
51 |
52 | .. autoclass:: StreamingCommand
53 | :members:
54 | :inherited-members:
55 | :exclude-members: ConfigurationSettings, process, stream
56 |
57 | .. autoclass:: splunklib.searchcommands::StreamingCommand.ConfigurationSettings
58 | :members:
59 | :inherited-members:
60 | :exclude-members: configuration_settings, fix_up, items, keys
61 |
62 | .. automethod:: splunklib.searchcommands::StreamingCommand.process(args=sys.argv[, input_file=sys.stdin, output_file=sys.stdout, allow_empty_input=True])
63 |
64 | .. automethod:: splunklib.searchcommands::StreamingCommand.stream
65 |
66 | .. autoclass:: Configuration
67 | :members:
68 | :inherited-members:
69 |
70 | .. autoclass:: Option
71 | :members:
72 | :inherited-members:
73 | :exclude-members: Item, View, fix_up
74 |
75 | .. autoclass:: Boolean
76 | :members:
77 | :inherited-members:
78 |
79 | .. autoclass:: Duration
80 | :members:
81 | :inherited-members:
82 |
83 | .. autoclass:: File
84 | :members:
85 | :inherited-members:
86 |
87 | .. autoclass:: Integer
88 | :members:
89 | :inherited-members:
90 |
91 | .. autoclass:: Float
92 | :members:
93 | :inherited-members:
94 |
95 | .. autoclass:: RegularExpression
96 | :members:
97 | :inherited-members:
98 |
99 | .. autoclass:: Set
100 | :members:
101 | :inherited-members:
102 |
103 |
--------------------------------------------------------------------------------
/tests/unit/test_utils.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright © 2011-2025 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | import os
18 | from pathlib import Path
19 | import unittest
20 |
21 | from utils import dslice
22 |
23 | TEST_DICT = {
24 | "username": "admin",
25 | "password": "changeme",
26 | "port": 8089,
27 | "host": "localhost",
28 | "scheme": "https",
29 | }
30 |
31 |
32 | class TestUtils(unittest.TestCase):
33 | # Test dslice when a dict is passed to change key names
34 | def test_dslice_dict_args(self):
35 | args = {
36 | "username": "user-name",
37 | "password": "new_password",
38 | "port": "admin_port",
39 | "foo": "bar",
40 | }
41 | expected = {
42 | "user-name": "admin",
43 | "new_password": "changeme",
44 | "admin_port": 8089,
45 | }
46 | self.assertTrue(expected == dslice(TEST_DICT, args))
47 |
48 | # Test dslice when a list is passed
49 | def test_dslice_list_args(self):
50 | test_list = ["username", "password", "port", "host", "foo"]
51 | expected = {
52 | "username": "admin",
53 | "password": "changeme",
54 | "port": 8089,
55 | "host": "localhost",
56 | }
57 | self.assertTrue(expected == dslice(TEST_DICT, test_list))
58 |
59 | # Test dslice when a single string is passed
60 | def test_dslice_arg(self):
61 | test_arg = "username"
62 | expected = {"username": "admin"}
63 | self.assertTrue(expected == dslice(TEST_DICT, test_arg))
64 |
65 | # Test dslice using all three types of arguments
66 | def test_dslice_all_args(self):
67 | test_args = [{"username": "new_username"}, ["password", "host"], "port"]
68 | expected = {
69 | "new_username": "admin",
70 | "password": "changeme",
71 | "host": "localhost",
72 | "port": 8089,
73 | }
74 | self.assertTrue(expected == dslice(TEST_DICT, *test_args))
75 |
76 |
77 | class FilePermissionTest(unittest.TestCase):
78 | def setUp(self):
79 | super().setUp()
80 |
81 | # Check for any change in the default file permission(i.e 644) for all files within splunklib
82 | def test_filePermissions(self):
83 | def checkFilePermissions(dir_path):
84 | for file in os.listdir(dir_path):
85 | if file.__contains__("pycache"):
86 | continue
87 | path = os.path.join(dir_path, file)
88 | if os.path.isfile(path):
89 | permission = oct(os.stat(path).st_mode)
90 | self.assertEqual(permission, "0o100644")
91 | else:
92 | checkFilePermissions(path)
93 |
94 | test_file_path = Path(__file__)
95 | # From tests/unit/test_file_permissions.py, go up 2 levels to project root, then to splunklib
96 | splunklib_path = test_file_path.parent.parent.parent / "splunklib"
97 | checkFilePermissions(str(splunklib_path))
98 |
99 |
100 | if __name__ == "__main__":
101 | import unittest
102 |
103 | unittest.main()
104 |
--------------------------------------------------------------------------------
/utils/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2011-2024 Splunk, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | # not use this file except in compliance with the License. You may obtain
5 | # a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | # License for the specific language governing permissions and limitations
13 | # under the License.
14 |
15 | """Utility module shared by the SDK unit tests."""
16 |
17 | from utils.cmdopts import *
18 |
19 |
20 | def config(option, opt, value, parser):
21 | assert opt == "--config"
22 | parser.load(value)
23 |
24 |
25 | # Default Splunk cmdline rules
26 | RULES_SPLUNK = {
27 | "config": {
28 | "flags": ["--config"],
29 | "action": "callback",
30 | "callback": config,
31 | "type": "string",
32 | "nargs": "1",
33 | "help": "Load options from config file",
34 | },
35 | "scheme": {
36 | "flags": ["--scheme"],
37 | "default": "https",
38 | "help": "Scheme (default 'https')",
39 | },
40 | "host": {
41 | "flags": ["--host"],
42 | "default": "localhost",
43 | "help": "Host name (default 'localhost')",
44 | },
45 | "port": {
46 | "flags": ["--port"],
47 | "default": "8089",
48 | "help": "Port number (default 8089)",
49 | },
50 | "app": {"flags": ["--app"], "help": "The app context (optional)"},
51 | "owner": {"flags": ["--owner"], "help": "The user context (optional)"},
52 | "username": {
53 | "flags": ["--username"],
54 | "default": None,
55 | "help": "Username to login with",
56 | },
57 | "password": {
58 | "flags": ["--password"],
59 | "default": None,
60 | "help": "Password to login with",
61 | },
62 | "version": {
63 | "flags": ["--version"],
64 | "default": None,
65 | "help": "Ignore. Used by JavaScript SDK.",
66 | },
67 | "splunkToken": {
68 | "flags": ["--bearerToken"],
69 | "default": None,
70 | "help": "Bearer token for authentication",
71 | },
72 | "token": {
73 | "flags": ["--sessionKey"],
74 | "default": None,
75 | "help": "Session key for authentication",
76 | },
77 | }
78 |
79 | FLAGS_SPLUNK = list(RULES_SPLUNK.keys())
80 |
81 |
82 | # value: dict, args: [(dict | list | str)*]
83 | def dslice(value, *args):
84 | """Returns a 'slice' of the given dictionary value containing only the
85 | requested keys. The keys can be requested in a variety of ways, as an
86 | arg list of keys, as a list of keys, or as a dict whose key(s) represent
87 | the source keys and whose corresponding values represent the resulting
88 | key(s) (enabling key rename), or any combination of the above."""
89 | result = {}
90 | for arg in args:
91 | if isinstance(arg, dict):
92 | for k, v in list(arg.items()):
93 | if k in value:
94 | result[v] = value[k]
95 | elif isinstance(arg, list):
96 | for k in arg:
97 | if k in value:
98 | result[k] = value[k]
99 | else:
100 | if arg in value:
101 | result[arg] = value[arg]
102 | return result
103 |
104 |
105 | def parse(argv, rules=None, config=None, **kwargs):
106 | """Parse the given arg vector with the default Splunk command rules."""
107 | parser_ = parser(rules, **kwargs)
108 | if config is not None:
109 | parser_.loadenv(config)
110 | return parser_.parse(argv).result
111 |
112 |
113 | def parser(rules=None, **kwargs):
114 | """Instantiate a parser with the default Splunk command rules."""
115 | rules = RULES_SPLUNK if rules is None else dict(RULES_SPLUNK, **rules)
116 | return Parser(rules, **kwargs)
117 |
--------------------------------------------------------------------------------
/splunklib/modularinput/event_writer.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2011-2024 Splunk, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | # not use this file except in compliance with the License. You may obtain
5 | # a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | # License for the specific language governing permissions and limitations
13 | # under the License.
14 |
15 | import sys
16 | import traceback
17 |
18 | from ..utils import ensure_str
19 | from .event import ET
20 |
21 |
22 | class EventWriter:
23 | """``EventWriter`` writes events and error messages to Splunk from a modular input.
24 | Its two important methods are ``writeEvent``, which takes an ``Event`` object,
25 | and ``log``, which takes a severity and an error message.
26 | """
27 |
28 | # Severities that Splunk understands for log messages from modular inputs.
29 | # Do not change these
30 | DEBUG = "DEBUG"
31 | INFO = "INFO"
32 | WARN = "WARN"
33 | ERROR = "ERROR"
34 | FATAL = "FATAL"
35 |
36 | def __init__(self, output=sys.stdout, error=sys.stderr):
37 | """
38 | :param output: Where to write the output; defaults to sys.stdout.
39 | :param error: Where to write any errors; defaults to sys.stderr.
40 | """
41 | self._out = output
42 | self._err = error
43 |
44 | # has the opening tag been written yet?
45 | self.header_written = False
46 |
47 | def write_event(self, event):
48 | """Writes an ``Event`` object to Splunk.
49 |
50 | :param event: An ``Event`` object.
51 | """
52 |
53 | if not self.header_written:
54 | self._out.write("")
55 | self.header_written = True
56 |
57 | event.write_to(self._out)
58 |
59 | def log(self, severity, message):
60 | """Logs messages about the state of this modular input to Splunk.
61 | These messages will show up in Splunk's internal logs.
62 |
63 | :param severity: ``string``, severity of message, see severities defined as class constants.
64 | :param message: ``string``, message to log.
65 | """
66 |
67 | self._err.write(f"{severity} {message}\n")
68 | self._err.flush()
69 |
70 | def log_exception(self, message, exception=None, severity=None):
71 | """Logs messages about the exception thrown by this modular input to Splunk.
72 | These messages will show up in Splunk's internal logs.
73 |
74 | :param message: ``string``, message to log.
75 | :param exception: ``Exception``, exception thrown by this modular input; if none, sys.exc_info() is used
76 | :param severity: ``string``, severity of message, see severities defined as class constants. Default severity: ERROR
77 | """
78 | if exception is not None:
79 | tb_str = traceback.format_exception(
80 | type(exception), exception, exception.__traceback__
81 | )
82 | else:
83 | tb_str = traceback.format_exc()
84 |
85 | if severity is None:
86 | severity = EventWriter.ERROR
87 |
88 | self._err.write(("%s %s - %s" % (severity, message, tb_str)).replace("\n", " "))
89 | self._err.flush()
90 |
91 | def write_xml_document(self, document):
92 | """Writes a string representation of an
93 | ``ElementTree`` object to the output stream.
94 |
95 | :param document: An ``ElementTree`` object.
96 | """
97 | self._out.write(ensure_str(ET.tostring(document), errors="replace"))
98 | self._out.flush()
99 |
100 | def close(self):
101 | """Write the closing tag to make this XML well formed."""
102 | if self.header_written:
103 | self._out.write("")
104 | self._out.flush()
105 |
--------------------------------------------------------------------------------
/tests/integration/test_fired_alert.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright © 2011-2024 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | from tests import testlib
18 |
19 |
20 | class FiredAlertTestCase(testlib.SDKTestCase):
21 | def setUp(self):
22 | super().setUp()
23 | self.index_name = testlib.tmpname()
24 | self.assertFalse(self.index_name in self.service.indexes)
25 | self.index = self.service.indexes.create(self.index_name)
26 | saved_searches = self.service.saved_searches
27 | self.saved_search_name = testlib.tmpname()
28 | self.assertFalse(self.saved_search_name in saved_searches)
29 | query = f"search index={self.index_name}"
30 | kwargs = {
31 | "alert_type": "always",
32 | "alert.severity": "3",
33 | "alert.suppress": "0",
34 | "alert.track": "1",
35 | "dispatch.earliest_time": "-1h",
36 | "dispatch.latest_time": "now",
37 | "is_scheduled": "1",
38 | "cron_schedule": "* * * * *",
39 | }
40 | self.saved_search = saved_searches.create(
41 | self.saved_search_name, query, **kwargs
42 | )
43 |
44 | def tearDown(self):
45 | super().tearDown()
46 | if self.service.splunk_version >= (5,):
47 | self.service.indexes.delete(self.index_name)
48 | for saved_search in self.service.saved_searches:
49 | if saved_search.name.startswith("delete-me"):
50 | self.service.saved_searches.delete(saved_search.name)
51 | self.assertFalse(saved_search.name in self.service.saved_searches)
52 | self.assertFalse(saved_search.name in self.service.fired_alerts)
53 |
54 | def test_new_search_is_empty(self):
55 | self.assertEqual(self.saved_search.alert_count, 0)
56 | self.assertEqual(len(self.saved_search.history()), 0)
57 | self.assertEqual(len(self.saved_search.fired_alerts), 0)
58 | self.assertFalse(self.saved_search_name in self.service.fired_alerts)
59 |
60 | def test_alerts_on_events(self):
61 | self.assertEqual(self.saved_search.alert_count, 0)
62 | self.assertEqual(len(self.saved_search.fired_alerts), 0)
63 |
64 | self.index.enable()
65 | self.assertEventuallyTrue(
66 | lambda: self.index.refresh() and self.index["disabled"] == "0", timeout=25
67 | )
68 |
69 | eventCount = int(self.index["totalEventCount"])
70 | self.assertEqual(self.index["sync"], "0")
71 | self.assertEqual(self.index["disabled"], "0")
72 | self.index.refresh()
73 | self.index.submit(
74 | "This is a test " + testlib.tmpname(), sourcetype="sdk_use", host="boris"
75 | )
76 |
77 | def f():
78 | self.index.refresh()
79 | return int(self.index["totalEventCount"]) == eventCount + 1
80 |
81 | self.assertEventuallyTrue(f, timeout=50)
82 |
83 | def g():
84 | self.saved_search.refresh()
85 | return self.saved_search.alert_count == 1
86 |
87 | self.assertEventuallyTrue(g, timeout=200)
88 |
89 | alerts = self.saved_search.fired_alerts
90 | self.assertEqual(len(alerts), 1)
91 |
92 | def test_read(self):
93 | for alert_group in self.service.fired_alerts:
94 | alert_group.count
95 | for alert in alert_group.alerts:
96 | alert.content
97 |
98 |
99 | if __name__ == "__main__":
100 | import unittest
101 |
102 | unittest.main()
103 |
--------------------------------------------------------------------------------
/tests/integration/test_conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright © 2011-2024 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | from tests import testlib
18 |
19 | from splunklib import client
20 |
21 |
22 | class TestRead(testlib.SDKTestCase):
23 | def test_read(self):
24 | service = client.connect(**self.opts.kwargs)
25 |
26 | confs = service.confs
27 |
28 | # Make sure the collection contains some of the expected entries
29 | self.assertTrue("eventtypes" in confs)
30 | self.assertTrue("indexes" in confs)
31 | self.assertTrue("inputs" in confs)
32 | self.assertTrue("props" in confs)
33 | self.assertTrue("transforms" in confs)
34 | self.assertTrue("savedsearches" in confs)
35 |
36 | for stanza in confs["indexes"].list(count=5):
37 | self.check_entity(stanza)
38 |
39 |
40 | class TestConfs(testlib.SDKTestCase):
41 | def setUp(self):
42 | super().setUp()
43 | self.app_name = testlib.tmpname()
44 | self.app = self.service.apps.create(self.app_name)
45 | # Connect using the test app context
46 | kwargs = self.opts.kwargs.copy()
47 | kwargs["app"] = self.app_name
48 | kwargs["owner"] = "nobody"
49 | kwargs["sharing"] = "app"
50 | self.app_service = client.connect(**kwargs)
51 |
52 | def tearDown(self):
53 | self.service.apps.delete(self.app_name)
54 | self.clear_restart_message()
55 |
56 | def test_confs(self):
57 | confs = self.app_service.confs
58 | conf_name = testlib.tmpname()
59 | self.assertRaises(KeyError, confs.__getitem__, conf_name)
60 | self.assertFalse(conf_name in confs)
61 |
62 | conf = confs.create(conf_name)
63 | self.assertTrue(conf_name in confs)
64 | self.assertEqual(conf.name, conf_name)
65 |
66 | # New conf should be empty
67 | stanzas = conf.list()
68 | self.assertEqual(len(stanzas), 0)
69 |
70 | # Creating a stanza works
71 | count = len(conf)
72 | stanza_name = testlib.tmpname()
73 | stanza = conf.create(stanza_name)
74 | self.assertEqual(len(conf), count + 1)
75 | self.assertTrue(stanza_name in conf)
76 |
77 | # New stanzas are empty
78 | self.assertEqual(len(stanza), 0)
79 |
80 | # Update works
81 | key = testlib.tmpname()
82 | val = testlib.tmpname()
83 | stanza.update(**{key: val})
84 | self.assertEventuallyTrue(
85 | lambda: stanza.refresh() and len(stanza) == 1, pause_time=0.2
86 | )
87 | self.assertEqual(len(stanza), 1)
88 | self.assertTrue(key in stanza)
89 |
90 | values = {
91 | testlib.tmpname(): testlib.tmpname(),
92 | testlib.tmpname(): testlib.tmpname(),
93 | }
94 | stanza.submit(values)
95 | stanza.refresh()
96 | for key, value in values.items():
97 | self.assertTrue(key in stanza)
98 | self.assertEqual(value, stanza[key])
99 |
100 | count = len(conf)
101 | conf.delete(stanza_name)
102 | self.assertFalse(stanza_name in conf)
103 | self.assertEqual(len(conf), count - 1)
104 |
105 | # Can't actually delete configuration files directly, at least
106 | # not in current versions of Splunk.
107 | self.assertRaises(client.IllegalOperationException, confs.delete, conf_name)
108 | self.assertTrue(conf_name in confs)
109 |
110 |
111 | if __name__ == "__main__":
112 | import unittest
113 |
114 | unittest.main()
115 |
--------------------------------------------------------------------------------
/tests/integration/test_kvstore_conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright 2011-2020 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | import json
18 | from tests import testlib
19 | from splunklib import client
20 |
21 |
22 | class KVStoreConfTestCase(testlib.SDKTestCase):
23 | def setUp(self):
24 | super().setUp()
25 | self.service.namespace["app"] = "search"
26 | self.confs = self.service.kvstore
27 | if "test" in self.confs:
28 | self.confs["test"].delete()
29 |
30 | def test_owner_restriction(self):
31 | self.service.kvstore_owner = "admin"
32 | self.assertRaises(client.HTTPError, lambda: self.confs.list())
33 | self.service.kvstore_owner = "nobody"
34 |
35 | def test_create_delete_collection(self):
36 | self.confs.create("test")
37 | self.assertTrue("test" in self.confs)
38 | self.confs["test"].delete()
39 | self.assertTrue("test" not in self.confs)
40 |
41 | def test_create_fields(self):
42 | self.confs.create(
43 | "test", accelerated_fields={"ind1": {"a": 1}}, fields={"a": "number1"}
44 | )
45 | self.assertEqual(self.confs["test"]["field.a"], "number1")
46 | self.assertEqual(self.confs["test"]["accelerated_fields.ind1"], {"a": 1})
47 | self.confs["test"].delete()
48 |
49 | def test_update_collection(self):
50 | self.confs.create("test")
51 | val = {"a": 1}
52 | self.confs["test"].post(
53 | **{"accelerated_fields.ind1": json.dumps(val), "field.a": "number"}
54 | )
55 | self.assertEqual(self.confs["test"]["field.a"], "number")
56 | self.assertEqual(self.confs["test"]["accelerated_fields.ind1"], {"a": 1})
57 | self.confs["test"].delete()
58 |
59 | def test_update_accelerated_fields(self):
60 | self.confs.create("test", accelerated_fields={"ind1": {"a": 1}})
61 | self.assertEqual(self.confs["test"]["accelerated_fields.ind1"], {"a": 1})
62 | # update accelerated_field value
63 | self.confs["test"].update_accelerated_field("ind1", {"a": -1})
64 | self.assertEqual(self.confs["test"]["accelerated_fields.ind1"], {"a": -1})
65 | self.confs["test"].delete()
66 |
67 | def test_update_fields(self):
68 | self.confs.create("test")
69 | self.confs["test"].post(**{"field.a": "number"})
70 | self.assertEqual(self.confs["test"]["field.a"], "number")
71 | self.confs["test"].update_field("a", "string")
72 | self.assertEqual(self.confs["test"]["field.a"], "string")
73 | self.confs["test"].delete()
74 |
75 | def test_create_unique_collection(self):
76 | self.confs.create("test")
77 | self.assertTrue("test" in self.confs)
78 | self.assertRaises(client.HTTPError, lambda: self.confs.create("test"))
79 | self.confs["test"].delete()
80 |
81 | def test_overlapping_collections(self):
82 | self.service.namespace["app"] = "system"
83 | self.confs.create("test")
84 | self.service.namespace["app"] = "search"
85 | self.confs.create("test")
86 | self.assertEqual(self.confs["test"]["eai:appName"], "search")
87 | self.service.namespace["app"] = "system"
88 | self.assertEqual(self.confs["test"]["eai:appName"], "system")
89 | self.service.namespace["app"] = "search"
90 | self.confs["test"].delete()
91 | self.confs["test"].delete()
92 |
93 | def tearDown(self):
94 | if "test" in self.confs:
95 | self.confs["test"].delete()
96 |
97 |
98 | if __name__ == "__main__":
99 | import unittest
100 |
101 | unittest.main()
102 |
--------------------------------------------------------------------------------
/tests/integration/test_app.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright © 2011-2024 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 |
18 | import logging
19 | from tests import testlib
20 | from splunklib import client
21 |
22 |
23 | class TestApp(testlib.SDKTestCase):
24 | app = None
25 | app_name = None
26 |
27 | def setUp(self):
28 | super().setUp()
29 | if self.app is None:
30 | for app in self.service.apps:
31 | if app.name.startswith("delete-me"):
32 | self.service.apps.delete(app.name)
33 | # Creating apps takes 0.8s, which is too long to wait for
34 | # each test in this test suite. Therefore we create one
35 | # app and reuse it. Since apps are rather less fraught
36 | # than entities like indexes, this is okay.
37 | self.app_name = testlib.tmpname()
38 | self.app = self.service.apps.create(self.app_name)
39 | logging.debug(f"Creating app {self.app_name}")
40 | else:
41 | logging.debug(f"App {self.app_name} already exists. Skipping creation.")
42 | if self.service.restart_required:
43 | self.restart_splunk()
44 |
45 | def tearDown(self):
46 | super().tearDown()
47 | # The rest of this will leave Splunk in a state requiring a restart.
48 | # It doesn't actually matter, though.
49 | self.service = client.connect(**self.opts.kwargs)
50 | app_name = ""
51 | for app in self.service.apps:
52 | app_name = app.name
53 | if app_name.startswith("delete-me"):
54 | self.service.apps.delete(app_name)
55 | self.assertEventuallyTrue(lambda: app_name not in self.service.apps)
56 | self.clear_restart_message()
57 |
58 | def test_app_integrity(self):
59 | self.check_entity(self.app)
60 | self.app.setupInfo
61 | self.app["setupInfo"]
62 |
63 | def test_disable_enable(self):
64 | self.app.disable()
65 | self.app.refresh()
66 | self.assertEqual(self.app["disabled"], "1")
67 | self.app.enable()
68 | self.app.refresh()
69 | self.assertEqual(self.app["disabled"], "0")
70 |
71 | def test_update(self):
72 | kwargs = {
73 | "author": "Me",
74 | "description": "Test app description",
75 | "label": "SDK Test",
76 | "version": "1.2",
77 | "visible": True,
78 | }
79 | self.app.update(**kwargs)
80 | self.app.refresh()
81 | self.assertEqual(self.app["author"], "Me")
82 | self.assertEqual(self.app["label"], "SDK Test")
83 | self.assertEqual(self.app["version"], "1.2")
84 | self.assertEqual(self.app["visible"], "1")
85 |
86 | def test_delete(self):
87 | name = testlib.tmpname()
88 | self.service.apps.create(name)
89 | self.assertTrue(name in self.service.apps)
90 | self.service.apps.delete(name)
91 | self.assertFalse(name in self.service.apps)
92 | self.clear_restart_message() # We don't actually have to restart here.
93 |
94 | def test_package(self):
95 | p = self.app.package()
96 | self.assertEqual(p.name, self.app_name)
97 | self.assertTrue(p.path.endswith(self.app_name + ".spl"))
98 | # Assert string due to deprecation of this property in new Splunk versions
99 | self.assertIsInstance(p.url, str)
100 |
101 | def test_updateInfo(self):
102 | p = self.app.updateInfo()
103 | self.assertTrue(p is not None)
104 |
105 |
106 | if __name__ == "__main__":
107 | import unittest
108 |
109 | unittest.main()
110 |
--------------------------------------------------------------------------------
/docs/client.rst:
--------------------------------------------------------------------------------
1 | splunklib.client
2 | ----------------
3 |
4 | .. automodule:: splunklib.client
5 |
6 | .. autofunction:: connect
7 |
8 | .. autoclass:: AmbiguousReferenceException
9 | :members:
10 |
11 | .. autoclass:: Application
12 | :members: setupInfo, package, updateInfo
13 | :inherited-members:
14 |
15 | .. autoclass:: AlertGroup
16 | :members: alerts, count
17 | :inherited-members:
18 |
19 | .. autoclass:: Collection
20 | :members: create, delete
21 | :inherited-members:
22 |
23 | .. autoclass:: ConfigurationFile
24 | :inherited-members:
25 |
26 | .. autoclass:: Configurations
27 | :members: create, delete
28 | :inherited-members:
29 |
30 | .. autoclass:: Endpoint
31 | :members: get, post
32 | :inherited-members:
33 |
34 | .. autoclass:: Entity
35 | :members: access, delete, disable, enable, fields, get, links, name, namespace, post, refresh, reload, update
36 | :inherited-members:
37 |
38 | .. autoclass:: IllegalOperationException
39 | :members:
40 |
41 | .. autoclass:: IncomparableException
42 | :members:
43 |
44 | .. autoclass:: Index
45 | :members: attach, attached_socket, clean, disable, enable, roll_hot_buckets, submit, upload
46 | :inherited-members:
47 |
48 | .. autoclass:: Indexes
49 | :members: default, delete
50 | :inherited-members:
51 |
52 | .. autoclass:: Input
53 | :members: update
54 | :inherited-members:
55 |
56 | .. autoclass:: Inputs
57 | :members: create, delete, itemmeta, kinds, kindpath, list, iter, oneshot
58 | :inherited-members:
59 |
60 | .. autoclass:: InvalidNameException
61 | :members:
62 |
63 | .. autoclass:: Job
64 | :members: cancel, disable_preview, enable_preview, events, finalize, is_done, is_ready, name, pause, refresh, results, preview, searchlog, set_priority, summary, timeline, touch, set_ttl, unpause
65 | :inherited-members:
66 |
67 | .. autoclass:: Jobs
68 | :members: create, export, itemmeta, oneshot
69 | :inherited-members:
70 |
71 | .. autoclass:: KVStoreCollection
72 | :members: data, update_index, update_field
73 | :inherited-members:
74 |
75 | .. autoclass:: KVStoreCollectionData
76 | :members: query, query_by_id, insert, delete, delete_by_id, update, batch_save
77 | :inherited-members:
78 |
79 | .. autoclass:: KVStoreCollections
80 | :members: create
81 | :inherited-members:
82 |
83 | .. autoclass:: Loggers
84 | :members: itemmeta
85 | :inherited-members:
86 |
87 | .. autoclass:: Message
88 | :members: value
89 | :inherited-members:
90 |
91 | .. autoclass:: ModularInputKind
92 | :members: arguments, update
93 | :inherited-members:
94 |
95 | .. autoclass:: NoSuchCapability
96 | :members:
97 |
98 | .. autoclass:: NotSupportedError
99 | :members:
100 |
101 | .. autoclass:: OperationError
102 | :members:
103 |
104 | .. autoclass:: ReadOnlyCollection
105 | :members: itemmeta, iter, list, names
106 | :inherited-members:
107 |
108 | .. autoclass:: Role
109 | :members: grant, revoke
110 | :inherited-members:
111 |
112 | .. autoclass:: Roles
113 | :members: create, delete
114 | :inherited-members:
115 |
116 | .. autoclass:: SavedSearch
117 | :members: acknowledge, alert_count, dispatch, fired_alerts, history, scheduled_times, suppress, suppressed, unsuppress, update
118 | :inherited-members:
119 |
120 | .. autoclass:: SavedSearches
121 | :members: create
122 | :inherited-members:
123 |
124 | .. autoclass:: Service
125 | :members: apps, confs, capabilities, event_types, fired_alerts, indexes, info, inputs, job, jobs, kvstore, loggers, messages, modular_input_kinds, parse, restart, restart_required, roles, search, saved_searches, settings, splunk_version, storage_passwords, users
126 | :inherited-members:
127 |
128 | .. autoclass:: Settings
129 | :members: update
130 | :inherited-members:
131 |
132 | .. autoclass:: Stanza
133 | :members: submit
134 | :inherited-members:
135 |
136 | .. autoclass:: StoragePassword
137 | :members: clear_password, encrypted_password, realm, username
138 | :inherited-members:
139 |
140 | .. autoclass:: StoragePasswords
141 | :members: create, delete
142 | :inherited-members:
143 |
144 | .. autoclass:: User
145 | :members: role_entities
146 | :inherited-members:
147 |
148 | .. autoclass:: Users
149 | :members: create, delete
150 | :inherited-members:
151 |
--------------------------------------------------------------------------------
/tests/integration/test_kvstore_data.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright © 2011-2024 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | import json
18 | from tests import testlib
19 |
20 | from splunklib import client
21 |
22 |
23 | class KVStoreDataTestCase(testlib.SDKTestCase):
24 | def setUp(self):
25 | super().setUp()
26 | self.service.namespace["app"] = "search"
27 | self.confs = self.service.kvstore
28 | if "test" in self.confs:
29 | self.confs["test"].delete()
30 | self.confs.create("test")
31 |
32 | self.col = self.confs["test"].data
33 |
34 | def test_insert_query_delete_data(self):
35 | for x in range(50):
36 | self.col.insert(
37 | json.dumps({"_key": str(x), "data": "#" + str(x), "num": x})
38 | )
39 | self.assertEqual(len(self.col.query()), 50)
40 | self.assertEqual(len(self.col.query(query='{"num": 10}')), 1)
41 | self.assertEqual(self.col.query(query='{"num": 10}')[0]["data"], "#10")
42 | self.col.delete(json.dumps({"num": {"$gt": 39}}))
43 | self.assertEqual(len(self.col.query()), 40)
44 | self.col.delete()
45 | self.assertEqual(len(self.col.query()), 0)
46 |
47 | def test_update_delete_data(self):
48 | for x in range(50):
49 | self.col.insert(
50 | json.dumps({"_key": str(x), "data": "#" + str(x), "num": x})
51 | )
52 | self.assertEqual(len(self.col.query()), 50)
53 | self.assertEqual(self.col.query(query='{"num": 49}')[0]["data"], "#49")
54 | self.col.update(str(49), json.dumps({"data": "#50", "num": 50}))
55 | self.assertEqual(len(self.col.query()), 50)
56 | self.assertEqual(self.col.query(query='{"num": 50}')[0]["data"], "#50")
57 | self.assertEqual(len(self.col.query(query='{"num": 49}')), 0)
58 | self.col.delete_by_id(49)
59 | self.assertEqual(len(self.col.query(query='{"num": 50}')), 0)
60 |
61 | def test_query_data(self):
62 | if "test1" in self.confs:
63 | self.confs["test1"].delete()
64 | self.confs.create("test1")
65 | self.col = self.confs["test1"].data
66 | for x in range(10):
67 | self.col.insert(
68 | json.dumps({"_key": str(x), "data": "#" + str(x), "num": x})
69 | )
70 | data = self.col.query(sort="data:-1", skip=9)
71 | self.assertEqual(len(data), 1)
72 | self.assertEqual(data[0]["data"], "#0")
73 | data = self.col.query(sort="data:1")
74 | self.assertEqual(data[0]["data"], "#0")
75 | data = self.col.query(limit=2, skip=9)
76 | self.assertEqual(len(data), 1)
77 |
78 | def test_invalid_insert_update(self):
79 | self.assertRaises(client.HTTPError, lambda: self.col.insert("NOT VALID DATA"))
80 | id = self.col.insert(json.dumps({"foo": "bar"}))["_key"]
81 | self.assertRaises(
82 | client.HTTPError, lambda: self.col.update(id, "NOT VALID DATA")
83 | )
84 | self.assertEqual(self.col.query_by_id(id)["foo"], "bar")
85 |
86 | def test_params_data_type_conversion(self):
87 | self.confs["test"].post(
88 | **{"field.data": "number", "accelerated_fields.data": '{"data": -1}'}
89 | )
90 | for x in range(50):
91 | self.col.insert(json.dumps({"_key": str(x), "data": str(x), "ignore": x}))
92 | data = self.col.query(sort="data:-1", limit=20, fields="data,_id:0", skip=10)
93 | self.assertEqual(len(data), 20)
94 | for x in range(20):
95 | self.assertEqual(data[x]["data"], 39 - x)
96 | self.assertTrue("ignore" not in data[x])
97 | self.assertTrue("_key" not in data[x])
98 |
99 | def tearDown(self):
100 | if "test" in self.confs:
101 | self.confs["test"].delete()
102 |
103 |
104 | if __name__ == "__main__":
105 | import unittest
106 |
107 | unittest.main()
108 |
--------------------------------------------------------------------------------
/tests/integration/test_role.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright © 2011-2024 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | from tests import testlib
18 | import logging
19 |
20 | from splunklib import client
21 |
22 |
23 | class RoleTestCase(testlib.SDKTestCase):
24 | def setUp(self):
25 | super().setUp()
26 | self.role_name = testlib.tmpname()
27 | self.role = self.service.roles.create(self.role_name)
28 |
29 | def tearDown(self):
30 | super().tearDown()
31 | for role in self.service.roles:
32 | if role.name.startswith("delete-me"):
33 | self.service.roles.delete(role.name)
34 |
35 | def check_role(self, role):
36 | self.check_entity(role)
37 | capabilities = role.service.capabilities
38 | for capability in role.content.capabilities:
39 | self.assertTrue(capability in capabilities)
40 |
41 | def test_read(self):
42 | for role in self.service.roles:
43 | self.check_role(role)
44 | role.refresh()
45 | self.check_role(role)
46 |
47 | def test_read_case_insensitive(self):
48 | for role in self.service.roles:
49 | a = self.service.roles[role.name.upper()]
50 | b = self.service.roles[role.name.lower()]
51 | self.assertEqual(a.name, b.name)
52 |
53 | def test_create(self):
54 | self.assertTrue(self.role_name in self.service.roles)
55 | self.check_entity(self.role)
56 |
57 | def test_delete(self):
58 | self.assertTrue(self.role_name in self.service.roles)
59 | self.service.roles.delete(self.role_name)
60 | self.assertFalse(self.role_name in self.service.roles)
61 | self.assertRaises(client.HTTPError, self.role.refresh)
62 |
63 | def test_grant_and_revoke(self):
64 | self.assertFalse("edit_user" in self.role.capabilities)
65 | self.role.grant("edit_user")
66 | self.role.refresh()
67 | self.assertTrue("edit_user" in self.role.capabilities)
68 |
69 | self.assertFalse("change_own_password" in self.role.capabilities)
70 | self.role.grant("change_own_password")
71 | self.role.refresh()
72 | self.assertTrue("edit_user" in self.role.capabilities)
73 | self.assertTrue("change_own_password" in self.role.capabilities)
74 |
75 | self.role.revoke("edit_user")
76 | self.role.refresh()
77 | self.assertFalse("edit_user" in self.role.capabilities)
78 | self.assertTrue("change_own_password" in self.role.capabilities)
79 |
80 | self.role.revoke("change_own_password")
81 | self.role.refresh()
82 | self.assertFalse("edit_user" in self.role.capabilities)
83 | self.assertFalse("change_own_password" in self.role.capabilities)
84 |
85 | def test_invalid_grant(self):
86 | self.assertRaises(
87 | client.NoSuchCapability, self.role.grant, "i-am-an-invalid-capability"
88 | )
89 |
90 | def test_invalid_revoke(self):
91 | self.assertRaises(
92 | client.NoSuchCapability, self.role.revoke, "i-am-an-invalid-capability"
93 | )
94 |
95 | def test_revoke_capability_not_granted(self):
96 | self.role.revoke("change_own_password")
97 |
98 | def test_update(self):
99 | kwargs = {}
100 | if "user" in self.role["imported_roles"]:
101 | kwargs["imported_roles"] = ""
102 | else:
103 | kwargs["imported_roles"] = ["user"]
104 | if self.role["srchJobsQuota"] is not None:
105 | kwargs["srchJobsQuota"] = int(self.role["srchJobsQuota"]) + 1
106 | self.role.update(**kwargs)
107 | self.role.refresh()
108 | self.assertEqual(self.role["imported_roles"], kwargs["imported_roles"])
109 | self.assertEqual(int(self.role["srchJobsQuota"]), kwargs["srchJobsQuota"])
110 |
111 |
112 | if __name__ == "__main__":
113 | import unittest
114 |
115 | unittest.main()
116 |
--------------------------------------------------------------------------------
/splunklib/modularinput/argument.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2011-2024 Splunk, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | # not use this file except in compliance with the License. You may obtain
5 | # a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | # License for the specific language governing permissions and limitations
13 | # under the License.
14 |
15 | import xml.etree.ElementTree as ET
16 |
17 |
18 | class Argument:
19 | """Class representing an argument to a modular input kind.
20 |
21 | ``Argument`` is meant to be used with ``Scheme`` to generate an XML
22 | definition of the modular input kind that Splunk understands.
23 |
24 | ``name`` is the only required parameter for the constructor.
25 |
26 | **Example with least parameters**::
27 |
28 | arg1 = Argument(name="arg1")
29 |
30 | **Example with all parameters**::
31 |
32 | arg2 = Argument(
33 | name="arg2",
34 | description="This is an argument with lots of parameters",
35 | validation="is_pos_int('some_name')",
36 | data_type=Argument.data_type_number,
37 | required_on_edit=True,
38 | required_on_create=True
39 | )
40 | """
41 |
42 | # Constant values, do not change.
43 | # These should be used for setting the value of an Argument object's data_type field.
44 | data_type_boolean = "BOOLEAN"
45 | data_type_number = "NUMBER"
46 | data_type_string = "STRING"
47 |
48 | def __init__(
49 | self,
50 | name,
51 | description=None,
52 | validation=None,
53 | data_type=data_type_string,
54 | required_on_edit=False,
55 | required_on_create=False,
56 | title=None,
57 | ):
58 | """
59 | :param name: ``string``, identifier for this argument in Splunk.
60 | :param description: ``string``, human-readable description of the argument.
61 | :param validation: ``string`` specifying how the argument should be validated, if using internal validation.
62 | If using external validation, this will be ignored.
63 | :param data_type: ``string``, data type of this field; use the class constants.
64 | "data_type_boolean", "data_type_number", or "data_type_string".
65 | :param required_on_edit: ``Boolean``, whether this arg is required when editing an existing modular input of this kind.
66 | :param required_on_create: ``Boolean``, whether this arg is required when creating a modular input of this kind.
67 | :param title: ``String``, a human-readable title for the argument.
68 | """
69 | self.name = name
70 | self.description = description
71 | self.validation = validation
72 | self.data_type = data_type
73 | self.required_on_edit = required_on_edit
74 | self.required_on_create = required_on_create
75 | self.title = title
76 |
77 | def add_to_document(self, parent):
78 | """Adds an ``Argument`` object to this ElementTree document.
79 |
80 | Adds an subelement to the parent element, typically
81 | and sets up its subelements with their respective text.
82 |
83 | :param parent: An ``ET.Element`` to be the parent of a new subelement
84 | :returns: An ``ET.Element`` object representing this argument.
85 | """
86 | arg = ET.SubElement(parent, "arg")
87 | arg.set("name", self.name)
88 |
89 | if self.title is not None:
90 | ET.SubElement(arg, "title").text = self.title
91 |
92 | if self.description is not None:
93 | ET.SubElement(arg, "description").text = self.description
94 |
95 | if self.validation is not None:
96 | ET.SubElement(arg, "validation").text = self.validation
97 |
98 | # add all other subelements to this Argument, represented by (tag, text)
99 | subelements = [
100 | ("data_type", self.data_type),
101 | ("required_on_edit", self.required_on_edit),
102 | ("required_on_create", self.required_on_create),
103 | ]
104 |
105 | for name, value in subelements:
106 | ET.SubElement(arg, name).text = str(value).lower()
107 |
108 | return arg
109 |
--------------------------------------------------------------------------------
/tests/unit/data/services.xml:
--------------------------------------------------------------------------------
1 |
2 | services
3 | https://localhost/services/
4 | 2011-07-01T16:52:51-07:00
5 |
6 |
7 | Splunk
8 |
9 |
10 | alerts
11 | https://localhost/services/alerts
12 | 2011-07-01T16:52:51-07:00
13 |
14 |
15 |
16 | apps
17 | https://localhost/services/apps
18 | 2011-07-01T16:52:51-07:00
19 |
20 |
21 |
22 | authentication
23 | https://localhost/services/authentication
24 | 2011-07-01T16:52:51-07:00
25 |
26 |
27 |
28 | authorization
29 | https://localhost/services/authorization
30 | 2011-07-01T16:52:51-07:00
31 |
32 |
33 |
34 | data
35 | https://localhost/services/data
36 | 2011-07-01T16:52:51-07:00
37 |
38 |
39 |
40 | deployment
41 | https://localhost/services/deployment
42 | 2011-07-01T16:52:51-07:00
43 |
44 |
45 |
46 | licenser
47 | https://localhost/services/licenser
48 | 2011-07-01T16:52:51-07:00
49 |
50 |
51 |
52 | messages
53 | https://localhost/services/messages
54 | 2011-07-01T16:52:51-07:00
55 |
56 |
57 |
58 | configs
59 | https://localhost/services/configs
60 | 2011-07-01T16:52:51-07:00
61 |
62 |
63 |
64 | saved
65 | https://localhost/services/saved
66 | 2011-07-01T16:52:51-07:00
67 |
68 |
69 |
70 | scheduled
71 | https://localhost/services/scheduled
72 | 2011-07-01T16:52:51-07:00
73 |
74 |
75 |
76 | search
77 | https://localhost/services/search
78 | 2011-07-01T16:52:51-07:00
79 |
80 |
81 |
82 | server
83 | https://localhost/services/server
84 | 2011-07-01T16:52:51-07:00
85 |
86 |
87 |
88 | streams
89 | https://localhost/services/streams
90 | 2011-07-01T16:52:51-07:00
91 |
92 |
93 |
94 | broker
95 | https://localhost/services/broker
96 | 2011-07-01T16:52:51-07:00
97 |
98 |
99 |
100 | clustering
101 | https://localhost/services/clustering
102 | 2011-07-01T16:52:51-07:00
103 |
104 |
105 |
106 | masterlm
107 | https://localhost/services/masterlm
108 | 2011-07-01T16:52:51-07:00
109 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/tests/system/test_cre_apps.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright © 2011-2025 Splunk, Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
6 | # not use this file except in compliance with the License. You may obtain
7 | # a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | # License for the specific language governing permissions and limitations
15 | # under the License.
16 |
17 | import json
18 | import pytest
19 |
20 | from tests import testlib
21 | from splunklib import results
22 |
23 |
24 | class TestJSONCustomRestEndpointsSpecialMethodHelpers(testlib.SDKTestCase):
25 | app_name = "cre_app"
26 |
27 | def test_GET(self):
28 | resp = self.service.get(
29 | app=self.app_name,
30 | path_segment="execute",
31 | headers=[("x-bar", "baz")],
32 | )
33 | self.assertIn(("x-foo", "bar"), resp.headers)
34 | self.assertEqual(resp.status, 200)
35 | self.assertEqual(
36 | json.loads(str(resp.body)),
37 | {
38 | "headers": {"x-bar": "baz"},
39 | "method": "GET",
40 | },
41 | )
42 |
43 | def test_POST(self):
44 | body = json.dumps({"foo": "bar"})
45 | resp = self.service.post(
46 | app=self.app_name,
47 | path_segment="execute",
48 | body=body,
49 | headers=[("x-bar", "baz")],
50 | )
51 | self.assertIn(("x-foo", "bar"), resp.headers)
52 | self.assertEqual(resp.status, 200)
53 | self.assertEqual(
54 | json.loads(str(resp.body)),
55 | {
56 | "payload": '{"foo": "bar"}',
57 | "headers": {"x-bar": "baz"},
58 | "method": "POST",
59 | },
60 | )
61 |
62 | def test_DELETE(self):
63 | # delete does allow specifying body and custom headers.
64 | resp = self.service.delete(
65 | app=self.app_name,
66 | path_segment="execute",
67 | )
68 | self.assertIn(("x-foo", "bar"), resp.headers)
69 | self.assertEqual(resp.status, 200)
70 | self.assertEqual(
71 | json.loads(str(resp.body)),
72 | {
73 | "payload": "",
74 | "headers": {},
75 | "method": "DELETE",
76 | },
77 | )
78 |
79 |
80 | class TestJSONCustomRestEndpointGenericRequest(testlib.SDKTestCase):
81 | app_name = "cre_app"
82 |
83 | def test_no_str_body_GET(self):
84 | def with_body():
85 | self.service.request(
86 | app=self.app_name, method="GET", path_segment="execute", body="str"
87 | )
88 |
89 | self.assertRaisesRegex(
90 | Exception, "Unable to set body on GET request", with_body
91 | )
92 |
93 | def test_GET(self):
94 | resp = self.service.request(
95 | app=self.app_name,
96 | method="GET",
97 | path_segment="execute",
98 | headers=[("x-bar", "baz")],
99 | )
100 | self.assertIn(("x-foo", "bar"), resp.headers)
101 | self.assertEqual(resp.status, 200)
102 | self.assertEqual(
103 | json.loads(str(resp.body)),
104 | {
105 | "headers": {"x-bar": "baz"},
106 | "method": "GET",
107 | },
108 | )
109 |
110 | def test_POST(self):
111 | self.method("POST")
112 |
113 | def test_PUT(self):
114 | self.method("PUT")
115 |
116 | def test_PATCH(self):
117 | if self.service.splunk_version[0] < 10:
118 | self.skipTest("PATCH custom REST endpoints not supported on splunk < 10")
119 | self.method("PATCH")
120 |
121 | def test_DELETE(self):
122 | self.method("DELETE")
123 |
124 | def method(self, method: str):
125 | body = json.dumps({"foo": "bar"})
126 | resp = self.service.request(
127 | app=self.app_name,
128 | method=method,
129 | path_segment="execute",
130 | body=body,
131 | headers=[("x-bar", "baz")],
132 | )
133 | self.assertIn(("x-foo", "bar"), resp.headers)
134 | self.assertEqual(resp.status, 200)
135 | self.assertEqual(
136 | json.loads(str(resp.body)),
137 | {
138 | "payload": '{"foo": "bar"}',
139 | "headers": {"x-bar": "baz"},
140 | "method": method,
141 | },
142 | )
143 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Welcome to the API reference for the Splunk SDK for Python, which describes the modules that are included in the SDK.
2 | For more information, see the `Splunk Developer Portal `_.
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 | :name: SDK for Python API Reference
7 |
8 | binding
9 | client
10 | data
11 | results
12 | modularinput
13 | searchcommands
14 | searchcommandsvalidators
15 |
16 |
17 | :doc:`binding`
18 | --------------
19 |
20 | :func:`~splunklib.binding.connect` function
21 |
22 | :func:`~splunklib.binding.namespace` function
23 |
24 | :class:`~splunklib.binding.Context` class
25 |
26 | :class:`~splunklib.binding.ResponseReader` class
27 |
28 |
29 | **Exceptions**
30 |
31 | :func:`~splunklib.binding.handler` function
32 |
33 | :class:`~splunklib.binding.AuthenticationError` class
34 |
35 | **Custom HTTP handler**
36 |
37 | :class:`~splunklib.binding.HTTPError` class
38 |
39 | :class:`~splunklib.binding.HttpLib` class
40 |
41 |
42 | :doc:`client`
43 | -------------
44 |
45 | :func:`~splunklib.client.connect` function
46 |
47 | :class:`~splunklib.client.Service` class
48 |
49 | :class:`~splunklib.client.Endpoint` base class
50 |
51 |
52 | **Entities and collections**
53 |
54 | :class:`~splunklib.client.Entity` class
55 |
56 | :class:`~splunklib.client.Collection` class
57 |
58 | :class:`~splunklib.client.ReadOnlyCollection` class
59 |
60 | :class:`~splunklib.client.Application` class
61 |
62 | :class:`~splunklib.client.AlertGroup` class
63 |
64 | :class:`~splunklib.client.ConfigurationFile` class
65 |
66 | :class:`~splunklib.client.Stanza` class
67 |
68 | :class:`~splunklib.client.Configurations` class
69 |
70 | :class:`~splunklib.client.Index` class
71 |
72 | :class:`~splunklib.client.Indexes` class
73 |
74 | :class:`~splunklib.client.Input` class
75 |
76 | :class:`~splunklib.client.Inputs` class
77 |
78 | :class:`~splunklib.client.Job` class
79 |
80 | :class:`~splunklib.client.Jobs` class
81 |
82 | :class:`~splunklib.client.KVStoreCollection` class
83 |
84 | :class:`~splunklib.client.KVStoreCollectionData` class
85 |
86 | :class:`~splunklib.client.KVStoreCollections` class
87 |
88 | :class:`~splunklib.client.Loggers` class
89 |
90 | :class:`~splunklib.client.Message` class
91 |
92 | :class:`~splunklib.client.ModularInputKind` class
93 |
94 | :class:`~splunklib.client.Role` class
95 |
96 | :class:`~splunklib.client.Roles` class
97 |
98 | :class:`~splunklib.client.SavedSearch` class
99 |
100 | :class:`~splunklib.client.SavedSearches` class
101 |
102 | :class:`~splunklib.client.Settings` class
103 |
104 | :class:`~splunklib.client.StoragePassword` class
105 |
106 | :class:`~splunklib.client.StoragePasswords` class
107 |
108 | :class:`~splunklib.client.User` class
109 |
110 | :class:`~splunklib.client.Users` class
111 |
112 |
113 | **Exceptions**
114 |
115 | :class:`~splunklib.client.AmbiguousReferenceException` class
116 |
117 | :class:`~splunklib.client.IllegalOperationException` class
118 |
119 | :class:`~splunklib.client.IncomparableException` class
120 |
121 | :class:`~splunklib.client.InvalidNameException` class
122 |
123 | :class:`~splunklib.client.NoSuchCapability` class
124 |
125 | :class:`~splunklib.client.NotSupportedError` class
126 |
127 | :class:`~splunklib.client.OperationError` class
128 |
129 |
130 | :doc:`data`
131 | -----------
132 |
133 | :func:`~splunklib.data.load` function
134 |
135 | :func:`~splunklib.data.record` function
136 |
137 | :class:`~splunklib.data.Record` class
138 |
139 | :doc:`results`
140 | --------------
141 |
142 | :class:`~splunklib.results.ResultsReader` class
143 |
144 | :class:`~splunklib.results.Message` class
145 |
146 | :doc:`modularinput`
147 | -------------------
148 |
149 | :class:`~splunklib.modularinput.Argument` class
150 |
151 | :class:`~splunklib.modularinput.Event` class
152 |
153 | :class:`~splunklib.modularinput.EventWriter` class
154 |
155 | :class:`~splunklib.modularinput.InputDefinition` class
156 |
157 | :class:`~splunklib.modularinput.Scheme` class
158 |
159 | :class:`~splunklib.modularinput.Script` class
160 |
161 | :class:`~splunklib.modularinput.ValidationDefinition` class
162 |
163 | :doc:`searchcommands`
164 | ---------------------
165 |
166 | :class:`~splunklib.searchcommands.EventingCommand` class
167 |
168 | :class:`~splunklib.searchcommands.GeneratingCommand` class
169 |
170 | :class:`~splunklib.searchcommands.ReportingCommand` class
171 |
172 | :class:`~splunklib.searchcommands.StreamingCommand` class
173 |
174 | :class:`~splunklib.searchcommands.Option` class
175 |
--------------------------------------------------------------------------------
/utils/cmdopts.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2011-2024 Splunk, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | # not use this file except in compliance with the License. You may obtain
5 | # a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | # License for the specific language governing permissions and limitations
13 | # under the License.
14 |
15 | """Command line utilities shared by command line tools & unit tests."""
16 |
17 | from os import path
18 | from optparse import OptionParser
19 | import sys
20 | from dotenv import dotenv_values
21 |
22 | __all__ = ["error", "Parser", "cmdline"]
23 |
24 |
25 | # Print the given message to stderr, and optionally exit
26 | def error(message, exitcode=None):
27 | print(f"Error: {message}", file=sys.stderr)
28 | if exitcode is not None:
29 | sys.exit(exitcode)
30 |
31 |
32 | class record(dict):
33 | def __getattr__(self, name):
34 | try:
35 | return self[name]
36 | except KeyError:
37 | raise AttributeError(name)
38 |
39 | def __setattr__(self, name, value):
40 | self[name] = value
41 |
42 |
43 | class Parser(OptionParser):
44 | def __init__(self, rules=None, **kwargs):
45 | OptionParser.__init__(self, **kwargs)
46 | self.dests = set({})
47 | self.result = record({"args": [], "kwargs": record()})
48 | if rules is not None:
49 | self.init(rules)
50 |
51 | def init(self, rules):
52 | """Initialize the parser with the given command rules."""
53 | # Initialize the option parser
54 | for dest in rules.keys():
55 | rule = rules[dest]
56 |
57 | # Assign defaults ourselves here, instead of in the option parser
58 | # itself in order to allow for multiple calls to parse (dont want
59 | # subsequent calls to override previous values with default vals).
60 | if "default" in rule:
61 | self.result["kwargs"][dest] = rule["default"]
62 |
63 | flags = rule["flags"]
64 | kwargs = {"action": rule.get("action", "store")}
65 | # NOTE: Don't provision the parser with defaults here, per above.
66 | for key in ["callback", "help", "metavar", "type"]:
67 | if key in rule:
68 | kwargs[key] = rule[key]
69 | self.add_option(*flags, dest=dest, **kwargs)
70 |
71 | # Remember the dest vars that we see, so that we can merge results
72 | self.dests.add(dest)
73 |
74 | # Load command options from '.env' file
75 | def load(self, filepath):
76 | argv = []
77 | try:
78 | filedata = dotenv_values(filepath)
79 | except:
80 | error("Unable to open '%s'" % filepath, 2)
81 |
82 | # update result kwargs value with .env file data
83 | for key, value in filedata.items():
84 | value = value.strip()
85 | if len(value) == 0 or value is None:
86 | continue # Skip blank value
87 | elif key in self.dests:
88 | self.result["kwargs"][key] = value
89 | else:
90 | raise NameError("No such option --" + key)
91 |
92 | return self
93 |
94 | def loadif(self, filepath):
95 | """Load the given filepath if it exists, otherwise ignore."""
96 | if path.isfile(filepath):
97 | self.load(filepath)
98 | return self
99 |
100 | def loadenv(self, filename):
101 | dir_path = path.dirname(path.realpath(__file__))
102 | filepath = path.join(dir_path, "..", filename)
103 | self.loadif(filepath)
104 | return self
105 |
106 | def parse(self, argv):
107 | """Parse the given argument vector."""
108 | kwargs, args = self.parse_args(argv)
109 | self.result["args"] += args
110 | # Annoying that parse_args doesn't just return a dict
111 | for dest in self.dests:
112 | value = getattr(kwargs, dest)
113 | if value is not None:
114 | self.result["kwargs"][dest] = value
115 | return self
116 |
117 | def format_epilog(self, formatter):
118 | return self.epilog or ""
119 |
120 |
121 | def cmdline(argv, rules=None, config=None, **kwargs):
122 | """Simplified cmdopts interface that does not default any parsing rules
123 | and that does not allow compounding calls to the parser."""
124 | parser = Parser(rules, **kwargs)
125 | if config is not None:
126 | parser.loadenv(config)
127 | return parser.parse(argv).result
128 |
--------------------------------------------------------------------------------
/splunklib/modularinput/event.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2011-2024 Splunk, Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | # not use this file except in compliance with the License. You may obtain
5 | # a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | # License for the specific language governing permissions and limitations
13 | # under the License.
14 |
15 | from io import TextIOBase
16 | import xml.etree.ElementTree as ET
17 |
18 | from ..utils import ensure_str
19 |
20 |
21 | class Event:
22 | """Represents an event or fragment of an event to be written by this modular input to Splunk.
23 |
24 | To write an input to a stream, call the ``write_to`` function, passing in a stream.
25 | """
26 |
27 | def __init__(
28 | self,
29 | data=None,
30 | stanza=None,
31 | time=None,
32 | host=None,
33 | index=None,
34 | source=None,
35 | sourcetype=None,
36 | done=True,
37 | unbroken=True,
38 | ):
39 | """There are no required parameters for constructing an Event
40 |
41 | **Example with minimal configuration**::
42 |
43 | my_event = Event(
44 | data="This is a test of my new event.",
45 | stanza="myStanzaName",
46 | time="%.3f" % 1372187084.000
47 | )
48 |
49 | **Example with full configuration**::
50 |
51 | excellent_event = Event(
52 | data="This is a test of my excellent event.",
53 | stanza="excellenceOnly",
54 | time="%.3f" % 1372274622.493,
55 | host="localhost",
56 | index="main",
57 | source="Splunk",
58 | sourcetype="misc",
59 | done=True,
60 | unbroken=True
61 | )
62 |
63 | :param data: ``string``, the event's text.
64 | :param stanza: ``string``, name of the input this event should be sent to.
65 | :param time: ``float``, time in seconds, including up to 3 decimal places to represent milliseconds.
66 | :param host: ``string``, the event's host, ex: localhost.
67 | :param index: ``string``, the index this event is specified to write to, or None if default index.
68 | :param source: ``string``, the source of this event, or None to have Splunk guess.
69 | :param sourcetype: ``string``, source type currently set on this event, or None to have Splunk guess.
70 | :param done: ``boolean``, is this a complete ``Event``? False if an ``Event`` fragment.
71 | :param unbroken: ``boolean``, Is this event completely encapsulated in this ``Event`` object?
72 | """
73 | self.data = data
74 | self.done = done
75 | self.host = host
76 | self.index = index
77 | self.source = source
78 | self.sourceType = sourcetype
79 | self.stanza = stanza
80 | self.time = time
81 | self.unbroken = unbroken
82 |
83 | def write_to(self, stream):
84 | """Write an XML representation of self, an ``Event`` object, to the given stream.
85 |
86 | The ``Event`` object will only be written if its data field is defined,
87 | otherwise a ``ValueError`` is raised.
88 |
89 | :param stream: stream to write XML to.
90 | """
91 | if self.data is None:
92 | raise ValueError(
93 | "Events must have at least the data field set to be written to XML."
94 | )
95 |
96 | event = ET.Element("event")
97 | if self.stanza is not None:
98 | event.set("stanza", self.stanza)
99 | event.set("unbroken", str(int(self.unbroken)))
100 |
101 | # if a time isn't set, let Splunk guess by not creating a