Skip to content

Commit 829d1cb

Browse files
feat: add bulletin_rest_client module to manage global Splunk messages (#369)
**Issue number:[ADDON-70528](https://splunk.atlassian.net/browse/ADDON-70528)** ## Summary Create REST client to manage bulletin messages in splunk. ### Changes * add bulletin_rest_client module with basic methods: CREATE, GET, GET ALL, DELETE ### User experience The user can better manage events in add-on by creating specific messages in the Splunk bulletin. Every message has 3 severity levels: info, warn, error. The user can also delete or read those messages at code lvl with the use of REST client. ## Checklist If your change doesn't seem to apply, please leave them unchecked. * [x] I have performed a self-review of this change * [x] Changes have been tested * [x] Changes are documented * [x] PR title follows [conventional commit semantics](https://www.conventionalcommits.org/en/v1.0.0/) --------- Co-authored-by: Artem Rys <rysartem@gmail.com>
1 parent 1bfeb15 commit 829d1cb

File tree

6 files changed

+327
-0
lines changed

6 files changed

+327
-0
lines changed

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

solnlib/__init__.py

Lines changed: 2 additions & 0 deletions
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",

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)
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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+
import context
18+
from splunklib import binding
19+
import pytest
20+
from solnlib import bulletin_rest_client as brc
21+
22+
23+
def _build_bulletin_manager(msg_name, session_key: str) -> brc.BulletinRestClient:
24+
return brc.BulletinRestClient(
25+
msg_name,
26+
session_key,
27+
"-",
28+
owner=context.owner,
29+
scheme=context.scheme,
30+
host=context.host,
31+
port=context.port,
32+
)
33+
34+
35+
def test_create_message():
36+
session_key = context.get_session_key()
37+
bulletin_client = _build_bulletin_manager("msg_name", session_key)
38+
39+
with pytest.raises(binding.HTTPError) as e:
40+
bulletin_client.create_message(
41+
"new message to bulletin",
42+
capabilities=["apps_restore", "unknown_cap"],
43+
roles=["admin"],
44+
)
45+
assert str(e.value.status) == "400"
46+
47+
with pytest.raises(binding.HTTPError) as e:
48+
bulletin_client.create_message(
49+
"new message to bulletin", roles=["unknown_role"]
50+
)
51+
assert str(e.value.status) == "400"
52+
53+
54+
def test_bulletin_rest_api():
55+
session_key = context.get_session_key()
56+
bulletin_client_1 = _build_bulletin_manager("msg_name_1", session_key)
57+
bulletin_client_2 = _build_bulletin_manager("msg_name_2", session_key)
58+
59+
bulletin_client_1.create_message(
60+
"new message to bulletin",
61+
capabilities=["apps_restore", "delete_messages"],
62+
roles=["admin"],
63+
)
64+
65+
get_msg_1 = bulletin_client_1.get_message()
66+
assert get_msg_1["entry"][0]["content"]["message"] == "new message to bulletin"
67+
assert get_msg_1["entry"][0]["content"]["severity"] == "warn"
68+
69+
bulletin_client_1.create_message(
70+
"new message to bulletin", bulletin_client_1.Severity.INFO
71+
)
72+
get_msg_1 = bulletin_client_1.get_message()
73+
assert get_msg_1["entry"][0]["content"]["severity"] == "info"
74+
75+
bulletin_client_1.create_message(
76+
"new message to bulletin", bulletin_client_1.Severity.ERROR
77+
)
78+
get_msg_1 = bulletin_client_1.get_message()
79+
assert get_msg_1["entry"][0]["content"]["severity"] == "error"
80+
81+
get_all_msg = bulletin_client_1.get_all_messages()
82+
assert len(get_all_msg["entry"]) == 1
83+
84+
bulletin_client_2.create_message("new message to bulletin 2")
85+
86+
get_msg_2 = bulletin_client_2.get_message()
87+
assert get_msg_2["entry"][0]["content"]["message"] == "new message to bulletin 2"
88+
89+
get_all_msg = bulletin_client_1.get_all_messages()
90+
assert len(get_all_msg["entry"]) == 2
91+
92+
bulletin_client_1.delete_message()
93+
94+
with pytest.raises(binding.HTTPError) as e:
95+
bulletin_client_1.get_message()
96+
assert str(e.value.status) == "404"
97+
98+
with pytest.raises(binding.HTTPError) as e:
99+
bulletin_client_1.delete_message()
100+
assert str(e.value.status) == "404"
101+
102+
get_all_msg = bulletin_client_1.get_all_messages()
103+
assert len(get_all_msg["entry"]) == 1
104+
105+
bulletin_client_2.delete_message()
106+
107+
get_all_msg = bulletin_client_1.get_all_messages()
108+
assert len(get_all_msg["entry"]) == 0
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
import pytest
18+
from solnlib.bulletin_rest_client import BulletinRestClient
19+
20+
21+
context = {"owner": "nobody", "scheme": "https", "host": "localhost", "port": 8089}
22+
23+
24+
def test_create_message(monkeypatch):
25+
session_key = "123"
26+
bulletin_client = BulletinRestClient(
27+
"msg_name_1",
28+
session_key,
29+
"_",
30+
**context,
31+
)
32+
33+
def new_post(*args, **kwargs) -> str:
34+
return "ok"
35+
36+
monkeypatch.setattr(bulletin_client._rest_client, "post", new_post)
37+
38+
bulletin_client.create_message(
39+
"new message to bulletin",
40+
capabilities=["apps_restore", "delete_messages"],
41+
roles=["admin"],
42+
)
43+
44+
with pytest.raises(ValueError, match="Severity must be one of"):
45+
bulletin_client.create_message(
46+
"new message to bulletin",
47+
severity="debug",
48+
capabilities=["apps_restore", "delete_messages", 1],
49+
roles=["admin"],
50+
)
51+
52+
with pytest.raises(ValueError, match="Capabilities must be a list of strings."):
53+
bulletin_client.create_message(
54+
"new message to bulletin",
55+
capabilities=["apps_restore", "delete_messages", 1],
56+
roles=["admin"],
57+
)
58+
59+
with pytest.raises(ValueError, match="Roles must be a list of strings."):
60+
bulletin_client.create_message(
61+
"new message to bulletin",
62+
capabilities=["apps_restore", "delete_messages"],
63+
roles=["admin", 1],
64+
)

0 commit comments

Comments
 (0)