omdnotificationforwarder 4.1__tar.gz → 4.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omdnotificationforwarder-4.1/README.md → omdnotificationforwarder-4.3/PKG-INFO +52 -1
- omdnotificationforwarder-4.1/PKG-INFO → omdnotificationforwarder-4.3/README.md +31 -21
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/bin/notificationforwarder +2 -2
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/pyproject.toml +5 -3
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/baseclass.py +1 -1
- omdnotificationforwarder-4.3/src/notificationforwarder/nostr/__init__.py +1 -0
- omdnotificationforwarder-4.3/src/notificationforwarder/nostr/formatter.py +54 -0
- omdnotificationforwarder-4.3/src/notificationforwarder/nostr/forwarder.py +130 -0
- omdnotificationforwarder-4.3/src/notificationforwarder/oauth2webhook/__init__.py +0 -0
- omdnotificationforwarder-4.3/src/notificationforwarder/oauth2webhook/forwarder.py +98 -0
- omdnotificationforwarder-4.3/src/notificationforwarder/util.py +37 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/test_alertmanager.py +3 -2
- omdnotificationforwarder-4.3/tests/test_builtin_plugins_focus.py +279 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/test_formatter.py +58 -1
- omdnotificationforwarder-4.3/tests/test_nostr.py +355 -0
- omdnotificationforwarder-4.3/tests/test_oauth2webhook.py +206 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/test_reporter.py +8 -3
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/.gitignore +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/component_loader.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/email/formatter.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/email/forwarder.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/example/formatter.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/example/forwarder.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/json/__init__.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/json/logger.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/naemonlog/reporter.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/rabbitmq/formatter.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/rabbitmq/forwarder.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/runtime_config.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/runtime_flow.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/spool.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/syslog/formatter.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/syslog/forwarder.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/telegram/forwarder.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/text/__init__.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/text/logger.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/src/notificationforwarder/webhook/forwarder.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/lib/python/notificationforwarder/split1/forwarder.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/lib/python/notificationforwarder/split2/formatter.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/lib/python/notificationforwarder/split2/forwarder.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/lib/python/notificationforwarder/split3/formatter.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/lib/python/notificationforwarder/split3/forwarder.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/local/lib/python/notificationforwarder/alertmanager_servicenow/formatter.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/local/lib/python/notificationforwarder/bayern/formatter.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/local/lib/python/notificationforwarder/datadup/formatter.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/local/lib/python/notificationforwarder/datapost/formatter.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/local/lib/python/notificationforwarder/discard/formatter.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/local/lib/python/notificationforwarder/split1/formatter.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/local/lib/python/notificationforwarder/split2/forwarder.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/local/lib/python/notificationforwarder/split3/formatter.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/local/lib/python/notificationforwarder/split3/forwarder.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/local/lib/python/notificationforwarder/split4/formatter.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/local/lib/python/notificationforwarder/split4/forwarder.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/local/lib/python/notificationforwarder/ticketsystem/forwarder.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/local/lib/python/notificationforwarder/ticketsystem/reporter.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/pythonpath/local/lib/python/notificationforwarder/vong/formatter.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/test_classes.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/test_delivery_resilience.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/test_discard.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/test_logger.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/test_package.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/test_paths.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/test_runtime_foundation.py +0 -0
- {omdnotificationforwarder-4.1 → omdnotificationforwarder-4.3}/tests/test_webhook.py +0 -0
|
@@ -1,8 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: omdnotificationforwarder
|
|
3
|
+
Version: 4.3
|
|
4
|
+
Summary: A framework for notification scripts for OMD
|
|
5
|
+
Project-URL: Homepage, https://github.com/lausser/noteventificationforhandlerwarder
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/lausser/noteventificationforhandlerwarder/issues
|
|
7
|
+
Author-email: Gerhard Lausser <lausser@yahoo.com>
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.6.1
|
|
12
|
+
Requires-Dist: jinja2
|
|
13
|
+
Provides-Extra: nostr
|
|
14
|
+
Requires-Dist: nostr-sdk; extra == 'nostr'
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: flake8; extra == 'test'
|
|
17
|
+
Requires-Dist: pytest; extra == 'test'
|
|
18
|
+
Requires-Dist: pytest-cov; extra == 'test'
|
|
19
|
+
Requires-Dist: requests; extra == 'test'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
1
22
|
# notificationforwarder
|
|
2
23
|
|
|
3
24
|
In this framework, two aspects are in the focus. How to transport a notification to the recipient system and in which format.
|
|
4
25
|
In the beginning, Naemon or one of the other monitoring cores will execute a command line. The actual script and the individual command line parameters are defined in a command definition. Typical parameters are (i use the notation of Nagios macros) HOSTNAME, SERVICEDESC, SERVICESTATE, SERVICEOUTPUT. These snippets need to be put together to some kind of payload suitable for the receiving system. And then this payload must be transported to it. We call the two components *formatter* and *forwarder*. The formatter takes the raw input data and creates a payload and the forwarder transmits the payload to the destination.
|
|
5
26
|
What the framework does for you behind the scenes: When forwarding to a recipient fails, the event is saved in a local sqlite database for a certain time and re-sent when the script is called next time and the recipient is available again. Logging of successful and of course failed deliveries is also done automatically.
|
|
27
|
+
Logging uses the framework's native setup with the standard logfile naming and format used by the built-in tools.
|
|
6
28
|
There is also a component *reporter* which will rarely be used. It's purpose is to run additional code after a successful or failed delivery.
|
|
7
29
|
|
|
8
30
|
Let me list some of the formatter/forwarder combinations which are usually found in enterprise environments:
|
|
@@ -18,6 +40,8 @@ Let me list some of the formatter/forwarder combinations which are usually found
|
|
|
18
40
|
|json |Splunk HEC |
|
|
19
41
|
|json |RabbitMQ |
|
|
20
42
|
|
|
43
|
+
|markdown-like Nostr note|Nostr relays |
|
|
44
|
+
|
|
21
45
|
Of course json is not json, the attributes and values are different depending on the recipient.
|
|
22
46
|
|
|
23
47
|
|
|
@@ -240,6 +264,34 @@ There is also a SyslogFormatter, which creates the log line as:
|
|
|
240
264
|
|
|
241
265
|
If you want a different format, then copy *lib/python/notificationforwarder/syslog/formatter.py* to *local/lib/python/notificationforwarder/syslog/formatter.py* and modify it like you want. Or, with *--formatter*, you can use whatever formatter is suitable, as long as it's payload attribute consists of a line of text.
|
|
242
266
|
|
|
267
|
+
### Nostr
|
|
268
|
+
|
|
269
|
+
The Nostr plugin pair publishes an encrypted direct message with labeled lines and default monitoring tags so the alert appears in chat-oriented clients.
|
|
270
|
+
|
|
271
|
+
Install the optional Nostr support with `pip install notificationforwarder[nostr]`.
|
|
272
|
+
|
|
273
|
+
|parameter|description|default|
|
|
274
|
+
|---------|-----------|-------|
|
|
275
|
+
|relays|comma-separated relay URLs|`wss://relay.damus.io`|
|
|
276
|
+
|nsec|bech32 private key for signing|required|
|
|
277
|
+
|tags|extra Nostr tags to include|`monitoring` plus host/service/state tags|
|
|
278
|
+
|
|
279
|
+
Example:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
$USER1$/notificationforwarder \
|
|
283
|
+
--forwarder nostr \
|
|
284
|
+
--forwarderopt relays='wss://relay.damus.io,wss://nostr-pub.wellorder.net' \
|
|
285
|
+
--forwarderopt nsec='$NSEC$' \
|
|
286
|
+
--formatter nostr \
|
|
287
|
+
--eventopt HOSTNAME='$HOSTNAME$' \
|
|
288
|
+
--eventopt SERVICEDESC='$SERVICEDESC$' \
|
|
289
|
+
--eventopt SERVICESTATE='$SERVICESTATE$' \
|
|
290
|
+
--eventopt SERVICEOUTPUT='$SERVICEOUTPUT$'
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The recipient `p` tag can be provided through the `tags` forwarder option or custom formatter output. The message content is encrypted before relay publication, so it shows up as a DM in clients that support NIP-04 direct messages.
|
|
294
|
+
|
|
243
295
|
## Loggers
|
|
244
296
|
|
|
245
297
|
The framework uses a modular logging architecture similar to formatters, forwarders, and reporters. By default, notificationforwarder uses **text format logging** - you don't need to do anything, logging works exactly as it did before. The traditional text format is backward compatible with all existing installations.
|
|
@@ -411,4 +463,3 @@ define command{
|
|
|
411
463
|
>> $USER4$/var/log/notificationforwarder_errors.log 2>&1
|
|
412
464
|
}
|
|
413
465
|
```
|
|
414
|
-
|
|
@@ -1,28 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: omdnotificationforwarder
|
|
3
|
-
Version: 4.1
|
|
4
|
-
Summary: A framework for notification scripts for OMD
|
|
5
|
-
Project-URL: Homepage, https://github.com/lausser/noteventificationforhandlerwarder
|
|
6
|
-
Project-URL: Bug Tracker, https://github.com/lausser/noteventificationforhandlerwarder/issues
|
|
7
|
-
Author-email: Gerhard Lausser <lausser@yahoo.com>
|
|
8
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
-
Classifier: Operating System :: OS Independent
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Requires-Python: >=3.6.1
|
|
12
|
-
Requires-Dist: coshsh
|
|
13
|
-
Requires-Dist: jinja2
|
|
14
|
-
Provides-Extra: test
|
|
15
|
-
Requires-Dist: flake8; extra == 'test'
|
|
16
|
-
Requires-Dist: pytest; extra == 'test'
|
|
17
|
-
Requires-Dist: pytest-cov; extra == 'test'
|
|
18
|
-
Requires-Dist: requests; extra == 'test'
|
|
19
|
-
Description-Content-Type: text/markdown
|
|
20
|
-
|
|
21
1
|
# notificationforwarder
|
|
22
2
|
|
|
23
3
|
In this framework, two aspects are in the focus. How to transport a notification to the recipient system and in which format.
|
|
24
4
|
In the beginning, Naemon or one of the other monitoring cores will execute a command line. The actual script and the individual command line parameters are defined in a command definition. Typical parameters are (i use the notation of Nagios macros) HOSTNAME, SERVICEDESC, SERVICESTATE, SERVICEOUTPUT. These snippets need to be put together to some kind of payload suitable for the receiving system. And then this payload must be transported to it. We call the two components *formatter* and *forwarder*. The formatter takes the raw input data and creates a payload and the forwarder transmits the payload to the destination.
|
|
25
5
|
What the framework does for you behind the scenes: When forwarding to a recipient fails, the event is saved in a local sqlite database for a certain time and re-sent when the script is called next time and the recipient is available again. Logging of successful and of course failed deliveries is also done automatically.
|
|
6
|
+
Logging uses the framework's native setup with the standard logfile naming and format used by the built-in tools.
|
|
26
7
|
There is also a component *reporter* which will rarely be used. It's purpose is to run additional code after a successful or failed delivery.
|
|
27
8
|
|
|
28
9
|
Let me list some of the formatter/forwarder combinations which are usually found in enterprise environments:
|
|
@@ -38,6 +19,8 @@ Let me list some of the formatter/forwarder combinations which are usually found
|
|
|
38
19
|
|json |Splunk HEC |
|
|
39
20
|
|json |RabbitMQ |
|
|
40
21
|
|
|
22
|
+
|markdown-like Nostr note|Nostr relays |
|
|
23
|
+
|
|
41
24
|
Of course json is not json, the attributes and values are different depending on the recipient.
|
|
42
25
|
|
|
43
26
|
|
|
@@ -260,6 +243,34 @@ There is also a SyslogFormatter, which creates the log line as:
|
|
|
260
243
|
|
|
261
244
|
If you want a different format, then copy *lib/python/notificationforwarder/syslog/formatter.py* to *local/lib/python/notificationforwarder/syslog/formatter.py* and modify it like you want. Or, with *--formatter*, you can use whatever formatter is suitable, as long as it's payload attribute consists of a line of text.
|
|
262
245
|
|
|
246
|
+
### Nostr
|
|
247
|
+
|
|
248
|
+
The Nostr plugin pair publishes an encrypted direct message with labeled lines and default monitoring tags so the alert appears in chat-oriented clients.
|
|
249
|
+
|
|
250
|
+
Install the optional Nostr support with `pip install notificationforwarder[nostr]`.
|
|
251
|
+
|
|
252
|
+
|parameter|description|default|
|
|
253
|
+
|---------|-----------|-------|
|
|
254
|
+
|relays|comma-separated relay URLs|`wss://relay.damus.io`|
|
|
255
|
+
|nsec|bech32 private key for signing|required|
|
|
256
|
+
|tags|extra Nostr tags to include|`monitoring` plus host/service/state tags|
|
|
257
|
+
|
|
258
|
+
Example:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
$USER1$/notificationforwarder \
|
|
262
|
+
--forwarder nostr \
|
|
263
|
+
--forwarderopt relays='wss://relay.damus.io,wss://nostr-pub.wellorder.net' \
|
|
264
|
+
--forwarderopt nsec='$NSEC$' \
|
|
265
|
+
--formatter nostr \
|
|
266
|
+
--eventopt HOSTNAME='$HOSTNAME$' \
|
|
267
|
+
--eventopt SERVICEDESC='$SERVICEDESC$' \
|
|
268
|
+
--eventopt SERVICESTATE='$SERVICESTATE$' \
|
|
269
|
+
--eventopt SERVICEOUTPUT='$SERVICEOUTPUT$'
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The recipient `p` tag can be provided through the `tags` forwarder option or custom formatter output. The message content is encrypted before relay publication, so it shows up as a DM in clients that support NIP-04 direct messages.
|
|
273
|
+
|
|
263
274
|
## Loggers
|
|
264
275
|
|
|
265
276
|
The framework uses a modular logging architecture similar to formatters, forwarders, and reporters. By default, notificationforwarder uses **text format logging** - you don't need to do anything, logging works exactly as it did before. The traditional text format is backward compatible with all existing installations.
|
|
@@ -431,4 +442,3 @@ define command{
|
|
|
431
442
|
>> $USER4$/var/log/notificationforwarder_errors.log 2>&1
|
|
432
443
|
}
|
|
433
444
|
```
|
|
434
|
-
|
|
@@ -6,7 +6,7 @@ import logging.handlers
|
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
8
|
#import logging
|
|
9
|
-
from
|
|
9
|
+
from notificationforwarder.util import setup_logging
|
|
10
10
|
from notificationforwarder import baseclass
|
|
11
11
|
import traceback
|
|
12
12
|
|
|
@@ -78,7 +78,7 @@ Example for an HTTP-based reporter:
|
|
|
78
78
|
help='Logger type: text (default) or json, or custom logger module',
|
|
79
79
|
default='text')
|
|
80
80
|
parser.add_argument('--version', action='version',
|
|
81
|
-
version=f'%(prog)s 4.
|
|
81
|
+
version=f'%(prog)s 4.3')
|
|
82
82
|
|
|
83
83
|
args = parser.parse_args()
|
|
84
84
|
if not hasattr(args, 'formatter'):
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling"
|
|
2
|
+
requires = ["hatchling"]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[tool.hatch.build]
|
|
@@ -21,7 +21,7 @@ packages = ["src/notificationforwarder"]
|
|
|
21
21
|
|
|
22
22
|
[project]
|
|
23
23
|
name = "omdnotificationforwarder"
|
|
24
|
-
version = "4.
|
|
24
|
+
version = "4.3"
|
|
25
25
|
authors = [
|
|
26
26
|
{ name="Gerhard Lausser", email="lausser@yahoo.com" },
|
|
27
27
|
]
|
|
@@ -34,11 +34,13 @@ classifiers = [
|
|
|
34
34
|
"Operating System :: OS Independent",
|
|
35
35
|
]
|
|
36
36
|
dependencies = [
|
|
37
|
-
"coshsh",
|
|
38
37
|
"jinja2",
|
|
39
38
|
]
|
|
40
39
|
|
|
41
40
|
[project.optional-dependencies]
|
|
41
|
+
nostr = [
|
|
42
|
+
"nostr-sdk",
|
|
43
|
+
]
|
|
42
44
|
test = [
|
|
43
45
|
"requests",
|
|
44
46
|
"flake8",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Nostr formatter and forwarder for notificationforwarder."""
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from notificationforwarder.baseclass import NotificationFormatter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NostrFormatter(NotificationFormatter):
|
|
7
|
+
def _pick(self, event, *names, default=""):
|
|
8
|
+
for name in names:
|
|
9
|
+
value = event.eventopts.get(name)
|
|
10
|
+
if value not in (None, ""):
|
|
11
|
+
return value
|
|
12
|
+
return default
|
|
13
|
+
|
|
14
|
+
def _build_tags(self, host, service, state, extra_tags=None):
|
|
15
|
+
tags = [["t", "monitoring"]]
|
|
16
|
+
if host:
|
|
17
|
+
tags.append(["t", "host=" + host])
|
|
18
|
+
if service:
|
|
19
|
+
tags.append(["t", "service=" + service])
|
|
20
|
+
if state:
|
|
21
|
+
tags.append(["t", "state=" + state])
|
|
22
|
+
if isinstance(extra_tags, str):
|
|
23
|
+
try:
|
|
24
|
+
extra_tags = json.loads(extra_tags)
|
|
25
|
+
except Exception:
|
|
26
|
+
extra_tags = []
|
|
27
|
+
for tag in extra_tags or []:
|
|
28
|
+
if isinstance(tag, (list, tuple)) and tag:
|
|
29
|
+
tags.append(list(tag))
|
|
30
|
+
return tags
|
|
31
|
+
|
|
32
|
+
def format_event(self, event):
|
|
33
|
+
host = self._pick(event, "HOSTNAME", "HOST", "HOSTNAME_SHORT", "HOSTNAME_FULL")
|
|
34
|
+
service = self._pick(event, "SERVICEDESC", "SERVICE", default="")
|
|
35
|
+
state = self._pick(event, "SERVICESTATE", "HOSTSTATE", "STATE", default="")
|
|
36
|
+
output = self._pick(event, "SERVICEOUTPUT", "HOSTOUTPUT", "OUTPUT", default="")
|
|
37
|
+
|
|
38
|
+
lines = [
|
|
39
|
+
f"Host: {host}",
|
|
40
|
+
f"Service: {service or '-'}",
|
|
41
|
+
f"State: {state or '-'}",
|
|
42
|
+
f"Output: {output or '-'}",
|
|
43
|
+
]
|
|
44
|
+
content = "\n".join(lines)
|
|
45
|
+
event.payload = {
|
|
46
|
+
"content": content,
|
|
47
|
+
"tags": self._build_tags(
|
|
48
|
+
host,
|
|
49
|
+
service,
|
|
50
|
+
state,
|
|
51
|
+
event.forwarderopts.get("tags"),
|
|
52
|
+
),
|
|
53
|
+
}
|
|
54
|
+
event.summary = f"{host or 'unknown'}/{service or '-'}: {state or '-'}"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from nostr_sdk import Client, Keys, NostrSigner, PublicKey, RelayUrl, Tag, uniffi_set_event_loop
|
|
5
|
+
except ImportError as exc:
|
|
6
|
+
raise ImportError("missing nostr-sdk") from exc
|
|
7
|
+
|
|
8
|
+
from notificationforwarder.baseclass import NotificationForwarder, timeout
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NostrForwarder(NotificationForwarder):
|
|
12
|
+
def __init__(self, opts):
|
|
13
|
+
super(self.__class__, self).__init__(opts)
|
|
14
|
+
setattr(self, "relays", getattr(self, "relays", "wss://relay.damus.io"))
|
|
15
|
+
setattr(self, "relay_urls", getattr(self, "relay_urls", self.relays))
|
|
16
|
+
setattr(self, "nsec", getattr(self, "nsec", None))
|
|
17
|
+
setattr(self, "timeout_seconds", int(getattr(self, "timeout_seconds", 6)))
|
|
18
|
+
|
|
19
|
+
def _relay_list(self):
|
|
20
|
+
relays = self.relay_urls if self.relay_urls is not None else self.relays
|
|
21
|
+
if isinstance(relays, str):
|
|
22
|
+
return [relay.strip() for relay in relays.split(",") if relay.strip()]
|
|
23
|
+
if isinstance(relays, (list, tuple)):
|
|
24
|
+
return [str(relay).strip() for relay in relays if str(relay).strip()]
|
|
25
|
+
return []
|
|
26
|
+
|
|
27
|
+
def _keys(self):
|
|
28
|
+
if not self.nsec:
|
|
29
|
+
raise ValueError("nsec is required for Nostr publishing")
|
|
30
|
+
return Keys.parse(self.nsec)
|
|
31
|
+
|
|
32
|
+
def _public_key(self, value):
|
|
33
|
+
if isinstance(value, PublicKey):
|
|
34
|
+
return value
|
|
35
|
+
if not isinstance(value, str):
|
|
36
|
+
raise ValueError("receiver public key must be a string or PublicKey")
|
|
37
|
+
return PublicKey.parse(value)
|
|
38
|
+
|
|
39
|
+
def _tags(self, formatted_event):
|
|
40
|
+
tags = []
|
|
41
|
+
payload = formatted_event.payload or {}
|
|
42
|
+
tag_values = payload.get("tags", [])
|
|
43
|
+
if isinstance(tag_values, str):
|
|
44
|
+
import json
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
tag_values = json.loads(tag_values)
|
|
48
|
+
except Exception:
|
|
49
|
+
tag_values = []
|
|
50
|
+
if isinstance(tag_values, (list, tuple)):
|
|
51
|
+
for tag in tag_values:
|
|
52
|
+
if isinstance(tag, (list, tuple)) and tag:
|
|
53
|
+
tags.append(Tag.parse(list(tag)))
|
|
54
|
+
return tags
|
|
55
|
+
|
|
56
|
+
def _configured_tags(self):
|
|
57
|
+
tag_values = getattr(self, "tags", [])
|
|
58
|
+
if isinstance(tag_values, str):
|
|
59
|
+
import json
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
tag_values = json.loads(tag_values)
|
|
63
|
+
except Exception:
|
|
64
|
+
tag_values = []
|
|
65
|
+
return tag_values if isinstance(tag_values, (list, tuple)) else []
|
|
66
|
+
|
|
67
|
+
def _nip_mode(self):
|
|
68
|
+
return getattr(self, "nip_mode", "nip04")
|
|
69
|
+
|
|
70
|
+
async def _send_private_msg(self, relay_urls, receiver_pubkey, message, tags):
|
|
71
|
+
uniffi_set_event_loop(asyncio.get_running_loop())
|
|
72
|
+
keys = self._keys()
|
|
73
|
+
signer = NostrSigner.keys(keys)
|
|
74
|
+
client = Client(signer)
|
|
75
|
+
relay_urls = [RelayUrl.parse(url) for url in relay_urls]
|
|
76
|
+
|
|
77
|
+
for relay_url in relay_urls:
|
|
78
|
+
await client.add_relay(relay_url)
|
|
79
|
+
|
|
80
|
+
await client.connect()
|
|
81
|
+
return await client.send_private_msg_to(relay_urls, receiver_pubkey, message, tags)
|
|
82
|
+
|
|
83
|
+
async def _send_nip04_dm(self, relay_urls, receiver_pubkey, message):
|
|
84
|
+
uniffi_set_event_loop(asyncio.get_running_loop())
|
|
85
|
+
keys = self._keys()
|
|
86
|
+
signer = NostrSigner.keys(keys)
|
|
87
|
+
client = Client(signer)
|
|
88
|
+
relay_urls = [RelayUrl.parse(url) for url in relay_urls]
|
|
89
|
+
|
|
90
|
+
for relay_url in relay_urls:
|
|
91
|
+
await client.add_relay(relay_url)
|
|
92
|
+
|
|
93
|
+
await client.connect()
|
|
94
|
+
return await client.send_private_msg_to(relay_urls, receiver_pubkey, message, [])
|
|
95
|
+
|
|
96
|
+
@timeout(30)
|
|
97
|
+
def submit(self, event):
|
|
98
|
+
relay_urls = self._relay_list()
|
|
99
|
+
if not relay_urls:
|
|
100
|
+
logger.critical("Nostr publish failed: no relays configured")
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
payload = event.payload or {}
|
|
104
|
+
nip_mode = self._nip_mode()
|
|
105
|
+
tags = self._configured_tags() or payload.get("tags", [])
|
|
106
|
+
recipient_pubkey = None
|
|
107
|
+
for tag in tags or []:
|
|
108
|
+
if isinstance(tag, (list, tuple)) and len(tag) > 1 and tag[0] == "p":
|
|
109
|
+
recipient_pubkey = self._public_key(tag[1])
|
|
110
|
+
break
|
|
111
|
+
if recipient_pubkey is None:
|
|
112
|
+
raise ValueError("Nostr DM requires a recipient p tag")
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
message = payload.get("content", event.summary or str(payload))
|
|
116
|
+
nip_mode = self._nip_mode()
|
|
117
|
+
logger.debug("Nostr NIP mode: {}".format(nip_mode))
|
|
118
|
+
if nip_mode == "nip17":
|
|
119
|
+
# For NIP-17, use tags from payload (like [["t", "monitoring"]])
|
|
120
|
+
tags = self._tags(event)
|
|
121
|
+
asyncio.run(self._send_private_msg(relay_urls, recipient_pubkey, message, tags))
|
|
122
|
+
else:
|
|
123
|
+
# NIP-04: send without tags (nostr-sdk doesn't support NIP-04 tags easily)
|
|
124
|
+
asyncio.run(self._send_nip04_dm(relay_urls, recipient_pubkey, message))
|
|
125
|
+
logger.info("published Nostr DM: {}".format(event.summary))
|
|
126
|
+
self.no_more_logging()
|
|
127
|
+
return True
|
|
128
|
+
except Exception as exc:
|
|
129
|
+
logger.critical("Nostr publish failed: {}".format(str(exc)))
|
|
130
|
+
return False
|
|
File without changes
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import fcntl
|
|
5
|
+
import requests
|
|
6
|
+
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
|
|
7
|
+
|
|
8
|
+
import notificationforwarder.webhook.forwarder as _webhook_module
|
|
9
|
+
from notificationforwarder.webhook.forwarder import WebhookForwarder
|
|
10
|
+
from notificationforwarder.baseclass import NotificationForwarder, timeout
|
|
11
|
+
|
|
12
|
+
logger = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Oauth2WebhookForwarder(WebhookForwarder):
|
|
16
|
+
def __init__(self, opts):
|
|
17
|
+
# Call NotificationForwarder directly to avoid infinite recursion from
|
|
18
|
+
# WebhookForwarder's super(self.__class__, self) pattern when subclassed.
|
|
19
|
+
NotificationForwarder.__init__(self, opts)
|
|
20
|
+
# Webhook attribute defaults
|
|
21
|
+
setattr(self, "url", getattr(self, "url", "http://localhost:12345"))
|
|
22
|
+
setattr(self, "username", getattr(self, "username", None))
|
|
23
|
+
setattr(self, "password", getattr(self, "password", None))
|
|
24
|
+
setattr(self, "insecure", getattr(self, "insecure", "yes"))
|
|
25
|
+
setattr(self, "headers", getattr(self, "headers", None))
|
|
26
|
+
# OAuth2 attribute defaults
|
|
27
|
+
setattr(self, "token_url", getattr(self, "token_url", None))
|
|
28
|
+
setattr(self, "client_id", getattr(self, "client_id", None))
|
|
29
|
+
setattr(self, "client_secret", getattr(self, "client_secret", None))
|
|
30
|
+
setattr(self, "token_scope", getattr(self, "token_scope", None))
|
|
31
|
+
setattr(self, "token_grant_type", getattr(self, "token_grant_type", "client_credentials"))
|
|
32
|
+
setattr(self, "token_expiry_buffer", int(getattr(self, "token_expiry_buffer", 30)))
|
|
33
|
+
|
|
34
|
+
def _token_cache_path(self):
|
|
35
|
+
return os.path.join(
|
|
36
|
+
os.environ["OMD_ROOT"], "var", "tmp",
|
|
37
|
+
"notificationforwarder_{}_oauth2token.json".format(self.forwarder_name),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def acquire_token(self):
|
|
41
|
+
cache_path = self._token_cache_path()
|
|
42
|
+
lock_path = cache_path + ".lock"
|
|
43
|
+
|
|
44
|
+
with open(lock_path, "w") as lf:
|
|
45
|
+
fcntl.flock(lf, fcntl.LOCK_EX)
|
|
46
|
+
try:
|
|
47
|
+
if os.path.exists(cache_path):
|
|
48
|
+
with open(cache_path) as f:
|
|
49
|
+
cached = json.load(f)
|
|
50
|
+
if time.time() < cached.get("expires_at", 0) - self.token_expiry_buffer:
|
|
51
|
+
logger.debug("oauth2: using cached token")
|
|
52
|
+
return cached["access_token"]
|
|
53
|
+
|
|
54
|
+
logger.debug("oauth2: acquiring new token from {}".format(self.token_url))
|
|
55
|
+
data = {
|
|
56
|
+
"grant_type": self.token_grant_type,
|
|
57
|
+
"client_id": self.client_id,
|
|
58
|
+
"client_secret": self.client_secret,
|
|
59
|
+
}
|
|
60
|
+
if self.token_scope:
|
|
61
|
+
data["scope"] = self.token_scope
|
|
62
|
+
|
|
63
|
+
resp = requests.post(
|
|
64
|
+
self.token_url,
|
|
65
|
+
data=data,
|
|
66
|
+
timeout=10,
|
|
67
|
+
verify=(self.insecure != "yes"),
|
|
68
|
+
)
|
|
69
|
+
resp.raise_for_status()
|
|
70
|
+
token_data = resp.json()
|
|
71
|
+
token = token_data["access_token"]
|
|
72
|
+
expires_in = int(token_data.get("expires_in", 3600))
|
|
73
|
+
expires_at = time.time() + expires_in
|
|
74
|
+
|
|
75
|
+
with open(cache_path, "w") as f:
|
|
76
|
+
json.dump({"access_token": token, "expires_at": expires_at}, f)
|
|
77
|
+
|
|
78
|
+
logger.debug("oauth2: token acquired, valid for {}s".format(expires_in))
|
|
79
|
+
return token
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.critical("oauth2: token acquisition failed: {}".format(e))
|
|
82
|
+
raise
|
|
83
|
+
finally:
|
|
84
|
+
fcntl.flock(lf, fcntl.LOCK_UN)
|
|
85
|
+
|
|
86
|
+
def submit_one(self, event):
|
|
87
|
+
# Sync logger into the webhook module so WebhookForwarder.submit_one can use it.
|
|
88
|
+
_webhook_module.logger = logger
|
|
89
|
+
try:
|
|
90
|
+
token = self.acquire_token()
|
|
91
|
+
except Exception:
|
|
92
|
+
return False
|
|
93
|
+
hdrs = event.forwarderopts.get("headers") or {}
|
|
94
|
+
if isinstance(hdrs, str):
|
|
95
|
+
hdrs = json.loads(hdrs)
|
|
96
|
+
hdrs["Authorization"] = "Bearer {}".format(token)
|
|
97
|
+
event.forwarderopts["headers"] = hdrs
|
|
98
|
+
return super().submit_one(event)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from logging.handlers import RotatingFileHandler
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def setup_logging(logdir=".", logfile="notificationforwarder.log", scrnloglevel=logging.INFO, txtloglevel=logging.INFO, format="%(asctime)s %(process)d - %(levelname)s - %(message)s", backup_count=3):
|
|
8
|
+
logdir_path = Path(logdir)
|
|
9
|
+
logdir_path.mkdir(parents=True, exist_ok=True)
|
|
10
|
+
|
|
11
|
+
logfile_path = Path(logfile)
|
|
12
|
+
abs_logfile = logfile_path if logfile_path.is_absolute() else logdir_path / logfile_path
|
|
13
|
+
abs_logfile.touch(exist_ok=True)
|
|
14
|
+
logger_name = abs_logfile.name[:-4] if abs_logfile.name.endswith(".log") else abs_logfile.name
|
|
15
|
+
logger = logging.getLogger(logger_name)
|
|
16
|
+
logger.setLevel(logging.DEBUG)
|
|
17
|
+
logger.propagate = False
|
|
18
|
+
|
|
19
|
+
for handler in list(logger.handlers):
|
|
20
|
+
handler.close()
|
|
21
|
+
logger.removeHandler(handler)
|
|
22
|
+
|
|
23
|
+
formatter = logging.Formatter(format)
|
|
24
|
+
|
|
25
|
+
file_handler = RotatingFileHandler(str(abs_logfile), maxBytes=20 * 1024 * 1024, backupCount=backup_count, delay=True)
|
|
26
|
+
file_handler.setFormatter(formatter)
|
|
27
|
+
file_handler.setLevel(txtloglevel)
|
|
28
|
+
logger.addHandler(file_handler)
|
|
29
|
+
|
|
30
|
+
console_handler = logging.StreamHandler(sys.stderr)
|
|
31
|
+
console_handler.setFormatter(formatter)
|
|
32
|
+
console_handler.setLevel(scrnloglevel)
|
|
33
|
+
logger.addHandler(console_handler)
|
|
34
|
+
|
|
35
|
+
logger.debug("Logger initialized.")
|
|
36
|
+
|
|
37
|
+
return logger
|
|
@@ -149,7 +149,7 @@ def test_alertmanager_forwarder(setup):
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
|
|
152
|
-
amgw = notificationforwarder.baseclass.new("webhook", None, "alertmanager_servicenow",
|
|
152
|
+
amgw = notificationforwarder.baseclass.new("webhook", None, "alertmanager_servicenow", True, True, forwarderopts)
|
|
153
153
|
famgw = amgw.new_formatter()
|
|
154
154
|
assert famgw.__class__.__name__ == "AlertmanagerServicenowFormatter"
|
|
155
155
|
assert famgw.__module_file__.endswith("pythonpath/local/lib/python/notificationforwarder/alertmanager_servicenow/formatter.py")
|
|
@@ -158,9 +158,10 @@ def test_alertmanager_forwarder(setup):
|
|
|
158
158
|
|
|
159
159
|
amgw.forward(eventopts)
|
|
160
160
|
log = open(get_logfile(amgw)).read()
|
|
161
|
+
assert "event has no node" in log
|
|
162
|
+
assert "replace node with 4490.example.net" in log
|
|
161
163
|
assert re.search(r'success: alertmanager sends 2 alarms.*result is .*"records": 2.*4490.*4491.*', log, flags=re.MULTILINE)
|
|
162
164
|
|
|
163
165
|
http_server.shutdown()
|
|
164
166
|
|
|
165
167
|
|
|
166
|
-
|