Skip to content

Commit 2e24589

Browse files
chore: merge develop into main (#373)
feat: TLS communication for fetching serverInfo, bulletin_rest_client module to manage global Splunk messages
2 parents fe46c72 + b809b0a commit 2e24589

File tree

14 files changed

+478
-18
lines changed

14 files changed

+478
-18
lines changed

.github/workflows/build-test-release.yml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -119,23 +119,23 @@ jobs:
119119
export SPLUNK_HOME=/opt/splunk
120120
wget -qO /tmp/splunk.tgz "${SPLUNK_BUILD_URL}"
121121
sudo tar -C /opt -zxf /tmp/splunk.tgz
122-
sudo cp -r tests/integration/data/solnlib_demo $SPLUNK_HOME/etc/apps
123-
sudo cp -r solnlib $SPLUNK_HOME/etc/apps/solnlib_demo/bin/
124-
sudo mkdir -p $SPLUNK_HOME/etc/apps/Splunk_TA_test/default/
125-
sudo chown -R "$USER":"$USER" /opt/splunk
122+
sudo chown -R "$USER":"$USER" $SPLUNK_HOME
123+
cp -r tests/integration/data/solnlib_demo $SPLUNK_HOME/etc/apps
124+
cp -r solnlib $SPLUNK_HOME/etc/apps/solnlib_demo/bin/
125+
mkdir -p $SPLUNK_HOME/etc/apps/Splunk_TA_test/default/
126126
ls $SPLUNK_HOME/etc/apps/solnlib_demo/bin/
127-
echo -e "[user_info]\nUSERNAME=Admin\nPASSWORD=Chang3d"'!' | sudo tee -a /opt/splunk/etc/system/local/user-seed.conf
128-
echo 'OPTIMISTIC_ABOUT_FILE_LOCKING=1' | sudo tee -a /opt/splunk/etc/splunk-launch.conf
129-
sudo /opt/splunk/bin/splunk start --accept-license
130-
sudo /opt/splunk/bin/splunk cmd python -m pip install solnlib
131-
sudo /opt/splunk/bin/splunk set servername custom-servername -auth admin:Chang3d!
132-
sudo /opt/splunk/bin/splunk restart
127+
echo -e "[user_info]\nUSERNAME=Admin\nPASSWORD=Chang3d"'!' | tee -a $SPLUNK_HOME/etc/system/local/user-seed.conf
128+
echo 'OPTIMISTIC_ABOUT_FILE_LOCKING=1' | tee -a $SPLUNK_HOME/etc/splunk-launch.conf
129+
$SPLUNK_HOME/bin/splunk start --accept-license
130+
$SPLUNK_HOME/bin/splunk cmd python -m pip install solnlib
131+
$SPLUNK_HOME/bin/splunk set servername custom-servername -auth admin:Chang3d!
132+
$SPLUNK_HOME/bin/splunk restart
133133
until curl -k -s -u admin:Chang3d! https://localhost:8089/services/server/info\?output_mode\=json | jq '.entry[0].content.kvStoreStatus' | grep -o "ready" ; do echo -n "Waiting for KVStore to become ready-" && sleep 5 ; done
134134
timeout-minutes: 5
135135
- name: Run tests
136136
run: |
137137
poetry install
138-
SPLUNK_HOME=/opt/splunk/ poetry run pytest --junitxml=test-results/results.xml -v tests/integration
138+
SPLUNK_HOME=/opt/splunk SPLUNK_DB=$SPLUNK_HOME/var/lib/splunk poetry run pytest --junitxml=test-results/results.xml -v tests/integration
139139
- uses: actions/upload-artifact@v4
140140
with:
141141
name: test-splunk-${{ matrix.splunk.version }}

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
# IDE related files
22
*.idea
33
*.DS_Store*
4-
.venv/
4+
# ignore all virtual environments
5+
.venv*
56

67
# Compiled files
78
__pycache__
89
*.pyc
910
*.pyo
1011

1112
.coverage
13+
*.log
14+
events.pickle

docs/bulletin_rest_client.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
::: solnlib.bulletin_rest_client

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ nav:
4141
- "pattern.py": pattern.md
4242
- "server_info.py": server_info.md
4343
- "splunk_rest_client.py": splunk_rest_client.md
44+
- "bulletin_rest_client.py": bulletin_rest_client.md
4445
- "splunkenv.py": splunkenv.md
4546
- "time_parser.py": time_parser.md
4647
- "timer_queue.py": timer_queue.md

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
[tool.poetry]
1818
name = "solnlib"
19-
version = "5.0.0"
19+
version = "5.1.0-beta.2"
2020
description = "The Splunk Software Development Kit for Splunk Solutions"
2121
authors = ["Splunk <addonfactory@splunk.com>"]
2222
license = "Apache-2.0"

solnlib/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from . import (
2020
acl,
21+
bulletin_rest_client,
2122
conf_manager,
2223
credentials,
2324
file_monitor,
@@ -37,6 +38,7 @@
3738

3839
__all__ = [
3940
"acl",
41+
"bulletin_rest_client",
4042
"conf_manager",
4143
"credentials",
4244
"file_monitor",
@@ -54,4 +56,4 @@
5456
"utils",
5557
]
5658

57-
__version__ = "5.0.0"
59+
__version__ = "5.1.0-beta.2"

solnlib/bulletin_rest_client.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#
2+
# Copyright 2024 Splunk Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
17+
from solnlib import splunk_rest_client as rest_client
18+
from typing import Optional, List
19+
import json
20+
21+
__all__ = ["BulletinRestClient"]
22+
23+
24+
class BulletinRestClient:
25+
"""REST client for handling Bulletin messages."""
26+
27+
MESSAGES_ENDPOINT = "/services/messages"
28+
29+
headers = [("Content-Type", "application/json")]
30+
31+
class Severity:
32+
INFO = "info"
33+
WARNING = "warn"
34+
ERROR = "error"
35+
36+
def __init__(
37+
self,
38+
message_name: str,
39+
session_key: str,
40+
app: str,
41+
**context: dict,
42+
):
43+
"""Initializes BulletinRestClient.
44+
When creating a new bulletin message, you must provide a name, which is a kind of ID.
45+
If you try to create another message with the same name (ID), the API will not add another message
46+
to the bulletin, but it will overwrite the existing one. Similar behaviour applies to deletion.
47+
To delete a message, you must indicate the name (ID) of the message.
48+
To provide better and easier control over bulletin messages, this client works in such a way
49+
that there is one instance responsible for handling one specific message.
50+
If you need to add another message to bulletin create another instance
51+
with a different 'message_name'
52+
e.g.
53+
msg_1 = BulletinRestClient("message_1", "<some session key>")
54+
msg_2 = BulletinRestClient("message_2", "<some session key>")
55+
56+
Arguments:
57+
message_name: Name of the message in the Splunk's bulletin.
58+
session_key: Splunk access token.
59+
app: App name of namespace.
60+
context: Other configurations for Splunk rest client.
61+
"""
62+
63+
self.message_name = message_name
64+
self.session_key = session_key
65+
self.app = app
66+
67+
self._rest_client = rest_client.SplunkRestClient(
68+
self.session_key, app=self.app, **context
69+
)
70+
71+
def create_message(
72+
self,
73+
msg: str,
74+
severity: Severity = Severity.WARNING,
75+
capabilities: Optional[List[str]] = None,
76+
roles: Optional[List] = None,
77+
):
78+
"""Creates a message in the Splunk's bulletin. Calling this method
79+
multiple times for the same instance will overwrite existing message.
80+
81+
Arguments:
82+
msg: The message which will be displayed in the Splunk's bulletin
83+
severity: Severity level of the message. It has to be one of: 'info', 'warn', 'error'.
84+
If wrong severity is given, ValueError will be raised.
85+
capabilities: One or more capabilities that users must have to view the message.
86+
Capability names are validated.
87+
This argument should be provided as a list of string/s e.g. capabilities=['one', 'two'].
88+
If a non-existent capability is used, HTTP 400 BAD REQUEST exception will be raised.
89+
If argument is not a List[str] ValueError will be raised.
90+
roles: One or more roles that users must have to view the message. Role names are validated.
91+
This argument should be provided as a list of string/s e.g. roles=['user', 'admin'].
92+
If a non-existent role is used, HTTP 400 BAD REQUEST exception will be raised.
93+
If argument is not a List[str] ValueError will be raised.
94+
"""
95+
body = {
96+
"name": self.message_name,
97+
"value": msg,
98+
"severity": severity,
99+
"capability": [],
100+
"role": [],
101+
}
102+
103+
if severity not in (
104+
self.Severity.INFO,
105+
self.Severity.WARNING,
106+
self.Severity.ERROR,
107+
):
108+
raise ValueError(
109+
"Severity must be one of ("
110+
"'BulletinRestClient.Severity.INFO', "
111+
"'BulletinRestClient.Severity.WARNING', "
112+
"'BulletinRestClient.Severity.ERROR'"
113+
")."
114+
)
115+
116+
if capabilities:
117+
body["capability"] = self._validate_and_get_body_value(
118+
capabilities, "Capabilities must be a list of strings."
119+
)
120+
121+
if roles:
122+
body["role"] = self._validate_and_get_body_value(
123+
roles, "Roles must be a list of strings."
124+
)
125+
126+
self._rest_client.post(self.MESSAGES_ENDPOINT, body=body, headers=self.headers)
127+
128+
def get_message(self):
129+
"""Get specific message created by this instance."""
130+
endpoint = f"{self.MESSAGES_ENDPOINT}/{self.message_name}"
131+
response = self._rest_client.get(endpoint, output_mode="json").body.read()
132+
return json.loads(response)
133+
134+
def get_all_messages(self):
135+
"""Get all messages in the bulletin."""
136+
response = self._rest_client.get(
137+
self.MESSAGES_ENDPOINT, output_mode="json"
138+
).body.read()
139+
return json.loads(response)
140+
141+
def delete_message(self):
142+
"""Delete specific message created by this instance."""
143+
endpoint = f"{self.MESSAGES_ENDPOINT}/{self.message_name}"
144+
self._rest_client.delete(endpoint)
145+
146+
@staticmethod
147+
def _validate_and_get_body_value(arg, error_msg) -> List:
148+
if type(arg) is list and (all(isinstance(el, str) for el in arg)):
149+
return [el for el in arg]
150+
else:
151+
raise ValueError(error_msg)

solnlib/server_info.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616

1717
"""This module contains Splunk server info related functionalities."""
1818

19+
import os
1920
import json
2021
from typing import Any, Dict, Optional
2122

23+
from splunk.rest import getWebCertFile, getWebKeyFile
2224
from splunklib import binding
23-
2425
from solnlib import splunk_rest_client as rest_client
2526
from solnlib import utils
27+
from solnlib.splunkenv import get_splunkd_access_info
2628

2729
__all__ = ["ServerInfo", "ServerInfoException"]
2830

@@ -56,6 +58,28 @@ def __init__(
5658
port: The port number, default is None.
5759
context: Other configurations for Splunk rest client.
5860
"""
61+
is_localhost = False
62+
if not all([scheme, host, port]) and os.environ.get("SPLUNK_HOME"):
63+
scheme, host, port = get_splunkd_access_info()
64+
is_localhost = (
65+
host == "localhost" or host == "127.0.0.1" or host in ("::1", "[::1]")
66+
)
67+
68+
if getWebCertFile() and getWebKeyFile():
69+
context["cert_file"] = getWebCertFile()
70+
context["key_file"] = getWebKeyFile()
71+
72+
if all([is_localhost, context.get("verify") is None]):
73+
# NOTE: this is specifically for mTLS communication
74+
# ONLY if scheme, host, port aren't provided AND user hasn't provided server certificate
75+
# we set verify to off (similar to 'rest.simpleRequest' implementation)
76+
context["verify"] = False
77+
78+
elif getWebCertFile() is not None:
79+
context["cert_file"] = getWebCertFile()
80+
if all([is_localhost, context.get("verify") is None]):
81+
context["verify"] = False
82+
5983
self._rest_client = rest_client.SplunkRestClient(
6084
session_key, "-", scheme=scheme, host=host, port=port, **context
6185
)

solnlib/splunk_rest_client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,13 @@ def _request_handler(context):
8888
verify = context.get("verify", False)
8989

9090
if context.get("key_file") and context.get("cert_file"):
91-
# cert = ('/path/client.cert', '/path/client.key')
92-
cert = context["key_file"], context["cert_file"]
91+
# cert: if tuple, ('cert', 'key') pair as per requests library
92+
cert = context["cert_file"], context["key_file"]
9393
elif context.get("cert_file"):
9494
cert = context["cert_file"]
95+
elif context.get("cert"):
96+
# as the solnlib uses requests, we need to have a check for 'cert' key as well
97+
cert = context["cert"]
9598
else:
9699
cert = None
97100

tests/integration/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import os
2+
import sys
3+
4+
# path manipulation get the 'splunk' library for the imports while running on GH Actions
5+
sys.path.append(
6+
os.path.sep.join([os.environ["SPLUNK_HOME"], "lib", "python3.7", "site-packages"])
7+
)
8+
# TODO: 'python3.7' needs to be updated as and when Splunk has new folder for Python.

0 commit comments

Comments
 (0)