linuxfabrik-lib 8.0.0__tar.gz → 8.2.0__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.
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/PKG-INFO +7 -3
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/README.md +6 -2
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/db_sqlite.py +7 -5
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/feedparser.py +21 -18
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/grassfish.py +10 -1
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/huawei_dorado.py +114 -36
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/icinga.py +6 -2
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/keycloak.py +3 -1
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/lftest.py +38 -3
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/linuxfabrik_lib.egg-info/PKG-INFO +7 -3
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/psi.py +10 -3
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/pyproject.toml +1 -1
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/redfish.py +8 -2
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/rocket.py +16 -4
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/shell.py +21 -15
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/url.py +148 -58
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/LICENSE +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/__init__.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/args.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/base.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/bexio.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/cache.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/container.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/db_mysql.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/disk.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/distro.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/dmidecode.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/endoflifedate.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/globals.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/huawei_pacific.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/human.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/infomaniak.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/jitsi.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/kvm.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/librenms.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/linuxfabrik_lib.egg-info/SOURCES.txt +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/linuxfabrik_lib.egg-info/dependency_links.txt +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/linuxfabrik_lib.egg-info/requires.txt +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/linuxfabrik_lib.egg-info/top_level.txt +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/logmatch.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/logsource.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/lvm.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/mail.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/net.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/nextcloud.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/nodebb.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/openmetrics.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/openstack.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/powershell.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/psutil.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/qts.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/setup.cfg +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/smb.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/ssh.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/task.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/time.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/txt.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/uptimerobot.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/user.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/veeam.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/version.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/wildfly.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/winrm.py +0 -0
- {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/wordpress.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: linuxfabrik-lib
|
|
3
|
-
Version: 8.
|
|
3
|
+
Version: 8.2.0
|
|
4
4
|
Summary: Python libraries used in various Linuxfabrik projects, including the 'Linuxfabrik Monitoring Plugins' project.
|
|
5
5
|
Author-email: "Linuxfabrik GmbH, Zurich, Switzerland" <info@linuxfabrik.ch>
|
|
6
6
|
License: This is free and unencumbered software released into the public domain.
|
|
@@ -222,7 +222,8 @@ A typical monitoring plugin using these libraries:
|
|
|
222
222
|
import lib.args
|
|
223
223
|
import lib.base
|
|
224
224
|
import lib.url
|
|
225
|
-
from lib.globals import
|
|
225
|
+
from lib.globals import STATE_CRIT, STATE_OK, STATE_UNKNOWN, STATE_WARN
|
|
226
|
+
|
|
226
227
|
|
|
227
228
|
def main():
|
|
228
229
|
# Parse arguments with custom threshold types
|
|
@@ -237,11 +238,14 @@ def main():
|
|
|
237
238
|
|
|
238
239
|
# Evaluate thresholds
|
|
239
240
|
state = lib.base.get_state(result['usage'], args.warning, args.critical)
|
|
240
|
-
perfdata = lib.base.get_perfdata(
|
|
241
|
+
perfdata = lib.base.get_perfdata(
|
|
242
|
+
'usage', result['usage'], '%', args.warning, args.critical, 0, 100
|
|
243
|
+
)
|
|
241
244
|
|
|
242
245
|
# Output and exit
|
|
243
246
|
lib.base.oao('Usage is {}%'.format(result['usage']), state, perfdata)
|
|
244
247
|
|
|
248
|
+
|
|
245
249
|
if __name__ == '__main__':
|
|
246
250
|
main()
|
|
247
251
|
```
|
|
@@ -151,7 +151,8 @@ A typical monitoring plugin using these libraries:
|
|
|
151
151
|
import lib.args
|
|
152
152
|
import lib.base
|
|
153
153
|
import lib.url
|
|
154
|
-
from lib.globals import
|
|
154
|
+
from lib.globals import STATE_CRIT, STATE_OK, STATE_UNKNOWN, STATE_WARN
|
|
155
|
+
|
|
155
156
|
|
|
156
157
|
def main():
|
|
157
158
|
# Parse arguments with custom threshold types
|
|
@@ -166,11 +167,14 @@ def main():
|
|
|
166
167
|
|
|
167
168
|
# Evaluate thresholds
|
|
168
169
|
state = lib.base.get_state(result['usage'], args.warning, args.critical)
|
|
169
|
-
perfdata = lib.base.get_perfdata(
|
|
170
|
+
perfdata = lib.base.get_perfdata(
|
|
171
|
+
'usage', result['usage'], '%', args.warning, args.critical, 0, 100
|
|
172
|
+
)
|
|
170
173
|
|
|
171
174
|
# Output and exit
|
|
172
175
|
lib.base.oao('Usage is {}%'.format(result['usage']), state, perfdata)
|
|
173
176
|
|
|
177
|
+
|
|
174
178
|
if __name__ == '__main__':
|
|
175
179
|
main()
|
|
176
180
|
```
|
|
@@ -20,11 +20,13 @@ This is one typical use case of this library (taken from `disk-io`):
|
|
|
20
20
|
>>> lib.base.coe(lib.db_sqlite.cut(conn, _max=args.COUNT * len(disks)))
|
|
21
21
|
>>> lib.base.coe(lib.db_sqlite.commit(conn))
|
|
22
22
|
|
|
23
|
-
>>> result = lib.base.coe(
|
|
24
|
-
...
|
|
25
|
-
...
|
|
26
|
-
...
|
|
27
|
-
...
|
|
23
|
+
>>> result = lib.base.coe(
|
|
24
|
+
... lib.db_sqlite.select(
|
|
25
|
+
... conn,
|
|
26
|
+
... 'SELECT * FROM perfdata WHERE name = :name ORDER BY timestamp DESC LIMIT 2',
|
|
27
|
+
... {'name': disk},
|
|
28
|
+
... )
|
|
29
|
+
... )
|
|
28
30
|
|
|
29
31
|
>>> lib.db_sqlite.close(conn)
|
|
30
32
|
"""
|
|
@@ -14,7 +14,7 @@ Time zone handling is not implemented.
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
|
|
17
|
-
__version__ = '
|
|
17
|
+
__version__ = '2026092201'
|
|
18
18
|
|
|
19
19
|
import sys
|
|
20
20
|
|
|
@@ -65,7 +65,8 @@ def fetch_soup(
|
|
|
65
65
|
encoding : str, optional
|
|
66
66
|
Encoding to use for the URL fetch operation. Default is `'urlencode'`.
|
|
67
67
|
retries : int, optional
|
|
68
|
-
How many extra attempts to make if the request fails
|
|
68
|
+
How many extra attempts to make if the request fails in a way that may clear up
|
|
69
|
+
(see `url.fetch()`) or the body is not a feed.
|
|
69
70
|
`0` (default) means a single attempt. Useful against flaky endpoints (e.g. a status
|
|
70
71
|
page behind a load balancer) that occasionally answer with something else.
|
|
71
72
|
|
|
@@ -80,21 +81,22 @@ def fetch_soup(
|
|
|
80
81
|
--------
|
|
81
82
|
>>> success, soup = fetch_soup('https://example.com/feed.xml', retries=3)
|
|
82
83
|
"""
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
84
|
+
# `fetch()` owns the retries: it repeats a request that may succeed next time and,
|
|
85
|
+
# through `retry_if`, an answer that arrived but is not a feed. A 404 or a refused
|
|
86
|
+
# redirect comes back the same and is reported at once.
|
|
87
|
+
success, xml = url.fetch(
|
|
88
|
+
feed_url,
|
|
89
|
+
encoding=encoding,
|
|
90
|
+
insecure=insecure,
|
|
91
|
+
no_proxy=no_proxy,
|
|
92
|
+
proxy=proxy,
|
|
93
|
+
retries=retries,
|
|
94
|
+
retry_if=lambda body: not parse_soup(body, feed_url)[0],
|
|
95
|
+
timeout=timeout,
|
|
96
|
+
)
|
|
97
|
+
if not success:
|
|
98
|
+
return (False, xml)
|
|
99
|
+
return parse_soup(xml, feed_url)
|
|
98
100
|
|
|
99
101
|
|
|
100
102
|
def parse(
|
|
@@ -128,7 +130,8 @@ def parse(
|
|
|
128
130
|
encoding : str, optional
|
|
129
131
|
Encoding to use for the URL fetch operation. Default is `'urlencode'`.
|
|
130
132
|
retries : int, optional
|
|
131
|
-
How many extra attempts to make if the request fails
|
|
133
|
+
How many extra attempts to make if the request fails in a way that may clear up
|
|
134
|
+
(see `url.fetch()`) or the body is not a feed.
|
|
132
135
|
`0` (default) means a single attempt. See `fetch_soup()`.
|
|
133
136
|
|
|
134
137
|
Returns
|
|
@@ -21,7 +21,16 @@ from . import url
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def fetch_json(
|
|
24
|
-
token,
|
|
24
|
+
token,
|
|
25
|
+
host,
|
|
26
|
+
port,
|
|
27
|
+
uri,
|
|
28
|
+
version,
|
|
29
|
+
func,
|
|
30
|
+
insecure=False,
|
|
31
|
+
no_proxy=False,
|
|
32
|
+
proxy=None,
|
|
33
|
+
timeout=8,
|
|
25
34
|
):
|
|
26
35
|
"""
|
|
27
36
|
Fetch JSON data from a Grassfish API endpoint.
|
|
@@ -23,7 +23,7 @@ readable label instead of `'Unknown'`, regardless of which firmware answers.
|
|
|
23
23
|
# pylint: disable=C0302
|
|
24
24
|
|
|
25
25
|
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
|
|
26
|
-
__version__ = '
|
|
26
|
+
__version__ = '2026092202'
|
|
27
27
|
|
|
28
28
|
import json
|
|
29
29
|
from time import sleep as _sleep
|
|
@@ -1048,11 +1048,14 @@ def get_data(endpoint, args, max_attempts=3):
|
|
|
1048
1048
|
|
|
1049
1049
|
Notes
|
|
1050
1050
|
-----
|
|
1051
|
-
- Makes at most three attempts
|
|
1052
|
-
|
|
1053
|
-
the caller's own timeout: the worst case is three
|
|
1054
|
-
plus two seconds of waiting. This budget is per call. A caller
|
|
1055
|
-
has to size its own timeout for the sum.
|
|
1051
|
+
- Makes at most three attempts and waits one second between attempts. The first attempt
|
|
1052
|
+
the appliance rejects is followed by one fresh login. The retry count is kept low on
|
|
1053
|
+
purpose, so one call stays within the caller's own timeout: the worst case is three
|
|
1054
|
+
requests plus one login, plus two seconds of waiting. This budget is per call. A caller
|
|
1055
|
+
that chains several calls has to size its own timeout for the sum.
|
|
1056
|
+
- A request that got no answer at all (a timeout, a refused connection) does not force a
|
|
1057
|
+
fresh login. It says nothing about the session, and on a slow appliance the extra login
|
|
1058
|
+
only makes the next attempt slower while discarding a session that is still valid.
|
|
1056
1059
|
- A rejected request is retried instead of aborting the caller: a transport failure, an HTTP
|
|
1057
1060
|
error status and a response that is not the documented `{'error': {'code': ...}}` envelope
|
|
1058
1061
|
all count as a failed attempt and are handed back in that envelope. The caller therefore
|
|
@@ -1076,13 +1079,18 @@ def get_data(endpoint, args, max_attempts=3):
|
|
|
1076
1079
|
}
|
|
1077
1080
|
"""
|
|
1078
1081
|
result = {}
|
|
1082
|
+
relogged = False
|
|
1079
1083
|
|
|
1080
1084
|
for attempt in range(1, max_attempts + 1):
|
|
1081
|
-
#
|
|
1082
|
-
# rejected request is most likely an expired session that retrying
|
|
1083
|
-
#
|
|
1084
|
-
#
|
|
1085
|
-
|
|
1085
|
+
# After the appliance rejected a request, drop the cached session and log in again
|
|
1086
|
+
# once; a rejected request is most likely an expired session that retrying with
|
|
1087
|
+
# the same token cannot fix. A request that never got an answer (a timeout, a
|
|
1088
|
+
# refused connection) says nothing about the session, so it is retried with the
|
|
1089
|
+
# cached one: logging in again would cost a round trip on an appliance that is
|
|
1090
|
+
# already slow, and throw a valid session away.
|
|
1091
|
+
force_relogin = attempt > 1 and not relogged and get_error_code(result) != 'n/a'
|
|
1092
|
+
relogged = relogged or force_relogin
|
|
1093
|
+
ibasetoken, cookie, device_id = get_creds(args, force_relogin=force_relogin)
|
|
1086
1094
|
# The device ID comes from the session, not from `args`: it may have been the
|
|
1087
1095
|
# appliance's own answer rather than a caller-supplied value, and a re-login can
|
|
1088
1096
|
# in principle hand back a different one.
|
|
@@ -1244,6 +1252,40 @@ def get_error_code(result):
|
|
|
1244
1252
|
return error
|
|
1245
1253
|
|
|
1246
1254
|
|
|
1255
|
+
def get_expboard_model(em):
|
|
1256
|
+
"""
|
|
1257
|
+
Convert a Huawei expansion board model code into a human-readable description.
|
|
1258
|
+
|
|
1259
|
+
Parameters
|
|
1260
|
+
----------
|
|
1261
|
+
em : int or str
|
|
1262
|
+
The expansion board model code to interpret.
|
|
1263
|
+
A missing or malformed value renders as `'Unknown'`.
|
|
1264
|
+
|
|
1265
|
+
Returns
|
|
1266
|
+
-------
|
|
1267
|
+
str
|
|
1268
|
+
A human-readable description of the expansion board model.
|
|
1269
|
+
Returns `'Unknown'` if the code is not recognized.
|
|
1270
|
+
|
|
1271
|
+
Examples
|
|
1272
|
+
--------
|
|
1273
|
+
>>> get_expboard_model(0)
|
|
1274
|
+
'SAS'
|
|
1275
|
+
|
|
1276
|
+
>>> get_expboard_model('7')
|
|
1277
|
+
'Smart NVMe'
|
|
1278
|
+
"""
|
|
1279
|
+
mapping = {
|
|
1280
|
+
0: 'SAS',
|
|
1281
|
+
1: 'FC',
|
|
1282
|
+
5: 'NVMe',
|
|
1283
|
+
6: 'Smart SAS',
|
|
1284
|
+
7: 'Smart NVMe',
|
|
1285
|
+
}
|
|
1286
|
+
return mapping.get(as_code(em), 'Unknown')
|
|
1287
|
+
|
|
1288
|
+
|
|
1247
1289
|
def get_health_status(hs):
|
|
1248
1290
|
"""
|
|
1249
1291
|
Convert a Huawei health status code into a human-readable description.
|
|
@@ -1396,6 +1438,45 @@ def get_host_access_state(has):
|
|
|
1396
1438
|
return mapping.get(as_code(has), 'Unknown')
|
|
1397
1439
|
|
|
1398
1440
|
|
|
1441
|
+
def get_hypermetro_domain_running_status(rs):
|
|
1442
|
+
"""
|
|
1443
|
+
Convert a HyperMetro domain's `RUNNINGSTATUS` code into a human-readable description.
|
|
1444
|
+
|
|
1445
|
+
Parameters
|
|
1446
|
+
----------
|
|
1447
|
+
rs : int or str
|
|
1448
|
+
The running status code to interpret.
|
|
1449
|
+
A missing or malformed value renders as `'Unknown'`.
|
|
1450
|
+
|
|
1451
|
+
Returns
|
|
1452
|
+
-------
|
|
1453
|
+
str
|
|
1454
|
+
A human-readable description including the original code in brackets.
|
|
1455
|
+
Returns `'Unknown'` if the code is not recognized.
|
|
1456
|
+
|
|
1457
|
+
Notes
|
|
1458
|
+
-----
|
|
1459
|
+
- Covers the block `HyperMetroDomain` object (type `15362`). Both REST Interface
|
|
1460
|
+
References list only these three codes for it. The file system HyperMetro domain
|
|
1461
|
+
(`FsHyperMetroDomain`, type `162`) is a different object with an enumeration of its
|
|
1462
|
+
own that runs from `0` up, so its codes must not be read through this function.
|
|
1463
|
+
|
|
1464
|
+
Examples
|
|
1465
|
+
--------
|
|
1466
|
+
>>> get_hypermetro_domain_running_status(1)
|
|
1467
|
+
'Normal (1)'
|
|
1468
|
+
|
|
1469
|
+
>>> get_hypermetro_domain_running_status('35')
|
|
1470
|
+
'Invalid (35)'
|
|
1471
|
+
"""
|
|
1472
|
+
mapping = {
|
|
1473
|
+
1: 'Normal (1)',
|
|
1474
|
+
33: 'To be recovered (33)',
|
|
1475
|
+
35: 'Invalid (35)',
|
|
1476
|
+
}
|
|
1477
|
+
return mapping.get(as_code(rs), 'Unknown')
|
|
1478
|
+
|
|
1479
|
+
|
|
1399
1480
|
def get_hypermetro_domain_running_status_state(rs):
|
|
1400
1481
|
"""
|
|
1401
1482
|
Convert a HyperMetro domain's `RUNNINGSTATUS` code into the state a consumer reports for it.
|
|
@@ -1408,31 +1489,29 @@ def get_hypermetro_domain_running_status_state(rs):
|
|
|
1408
1489
|
Returns
|
|
1409
1490
|
-------
|
|
1410
1491
|
int
|
|
1411
|
-
`STATE_OK` for a normal domain, `STATE_CRIT` for
|
|
1412
|
-
|
|
1492
|
+
`STATE_OK` for a normal domain, `STATE_CRIT` for an invalid one, `STATE_WARN` for
|
|
1493
|
+
every other code, including one the enumeration does not know and a missing value.
|
|
1413
1494
|
|
|
1414
1495
|
Notes
|
|
1415
1496
|
-----
|
|
1416
|
-
- Scoped to the `HyperMetroDomain` object for the same reason as
|
|
1417
|
-
`get_hypermetro_domain_running_status()
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
side still serves its own I/O, and an administrator splits a domain deliberately during
|
|
1422
|
-
maintenance.
|
|
1497
|
+
- Scoped to the block `HyperMetroDomain` object for the same reason as
|
|
1498
|
+
`get_hypermetro_domain_running_status()`.
|
|
1499
|
+
- Verified against V700R001C10SPH128 on a Dorado 6000 V6: a healthy domain reports `1`.
|
|
1500
|
+
- To be recovered (`33`) warns rather than going critical. The domain is still there and
|
|
1501
|
+
the appliance is working its way back to a mirrored state on its own.
|
|
1423
1502
|
|
|
1424
1503
|
Examples
|
|
1425
1504
|
--------
|
|
1426
|
-
>>> get_hypermetro_domain_running_status_state(
|
|
1505
|
+
>>> get_hypermetro_domain_running_status_state(1) == STATE_OK
|
|
1427
1506
|
True
|
|
1428
1507
|
|
|
1429
|
-
>>> get_hypermetro_domain_running_status_state('
|
|
1508
|
+
>>> get_hypermetro_domain_running_status_state('35') == STATE_CRIT
|
|
1430
1509
|
True
|
|
1431
1510
|
"""
|
|
1432
1511
|
code = as_code(rs)
|
|
1433
|
-
if code ==
|
|
1512
|
+
if code == 1:
|
|
1434
1513
|
return STATE_OK
|
|
1435
|
-
if code
|
|
1514
|
+
if code == 35:
|
|
1436
1515
|
return STATE_CRIT
|
|
1437
1516
|
return STATE_WARN
|
|
1438
1517
|
|
|
@@ -1871,20 +1950,23 @@ def get_performance(uuid, data_ids, args):
|
|
|
1871
1950
|
# `(label, uom, factor)`. The label is the metric name a consumer emits, `uom` is the
|
|
1872
1951
|
# Nagios unit of measurement, and `factor` converts the vendor's unit into it.
|
|
1873
1952
|
#
|
|
1874
|
-
#
|
|
1875
|
-
# go out in seconds, the unit Nagios knows. The bandwidths
|
|
1876
|
-
#
|
|
1877
|
-
#
|
|
1953
|
+
# Three conversions are worth naming. The response times are documented in microseconds and
|
|
1954
|
+
# go out in seconds, the unit Nagios knows. The bandwidths are documented as MB/s and are
|
|
1955
|
+
# read as binary multiples: every capacity this API reports is binary (sectors of 512
|
|
1956
|
+
# bytes), and so is what DeviceManager itself displays. The average I/O sizes are
|
|
1957
|
+
# documented as KB but arrive in bytes: on a Dorado 6000 V6 running V700R001C10SPH128,
|
|
1958
|
+
# a storage pool reading 590 MB/s at 22413 IOPS reported an average read I/O size of
|
|
1959
|
+
# 27648, which is 27 KiB per I/O in bytes and would be 27 MiB per I/O in KB.
|
|
1878
1960
|
PERFORMANCE_INDICATORS = {
|
|
1879
1961
|
18: ('usage_percent', '%', 1),
|
|
1880
1962
|
19: ('queue_length', None, 1),
|
|
1881
1963
|
21: ('block_bandwidth', 'B', 1024 * 1024),
|
|
1882
1964
|
22: ('total_iops', None, 1),
|
|
1883
1965
|
23: ('read_bandwidth', 'B', 1024 * 1024),
|
|
1884
|
-
24: ('avg_read_io_size', 'B',
|
|
1966
|
+
24: ('avg_read_io_size', 'B', 1),
|
|
1885
1967
|
25: ('read_iops', None, 1),
|
|
1886
1968
|
26: ('write_bandwidth', 'B', 1024 * 1024),
|
|
1887
|
-
27: ('avg_write_io_size', 'B',
|
|
1969
|
+
27: ('avg_write_io_size', 'B', 1),
|
|
1888
1970
|
28: ('write_iops', None, 1),
|
|
1889
1971
|
68: ('cpu_usage_percent', '%', 1),
|
|
1890
1972
|
69: ('cache_usage_percent', '%', 1),
|
|
@@ -2099,12 +2181,8 @@ def get_running_status(rs):
|
|
|
2099
2181
|
|
|
2100
2182
|
Notes
|
|
2101
2183
|
-----
|
|
2102
|
-
- Not scoped to `HyperMetroDomain
|
|
2103
|
-
|
|
2104
|
-
likely to be handed: `2` is faulty on a HyperMetro domain and disabled on a DR Star
|
|
2105
|
-
trio, not running. Read through this function either would look healthy, so they have
|
|
2106
|
-
their own mappings in `get_hypermetro_domain_running_status()` and
|
|
2107
|
-
`get_dr_star_running_status()`.
|
|
2184
|
+
- Not scoped to `HyperMetroDomain`. A HyperMetro domain has an enumeration of its own,
|
|
2185
|
+
so it has its own mapping in `get_hypermetro_domain_running_status()`.
|
|
2108
2186
|
- Code `94` is the one value whose meaning differs between the two documented firmware
|
|
2109
2187
|
generations: 6.1.0 defines it as error, V700R001C10 as faulty. The response carries no
|
|
2110
2188
|
firmware marker, so it renders as `'Error/Faulty'`, which holds under both revisions.
|
|
@@ -84,8 +84,12 @@ def api_post(
|
|
|
84
84
|
... }
|
|
85
85
|
>>> result = lib.base.coe(
|
|
86
86
|
... lib.icinga.api_post(
|
|
87
|
-
... uri,
|
|
88
|
-
...
|
|
87
|
+
... uri,
|
|
88
|
+
... args.USERNAME,
|
|
89
|
+
... args.PASSWORD,
|
|
90
|
+
... data=data,
|
|
91
|
+
... method_override='GET',
|
|
92
|
+
... timeout=3,
|
|
89
93
|
... )
|
|
90
94
|
... )
|
|
91
95
|
"""
|
|
@@ -19,7 +19,9 @@ Typical use case:
|
|
|
19
19
|
# obtain an admin access token and call the Admin REST API (fetch the realm's details).
|
|
20
20
|
oidc_config = lib.base.coe(lib.keycloak.discover_oidc_endpoints(args))
|
|
21
21
|
admin_token = lib.base.coe(lib.keycloak.obtain_admin_token(args, oidc_config))
|
|
22
|
-
server_info = lib.base.coe(
|
|
22
|
+
server_info = lib.base.coe(
|
|
23
|
+
lib.keycloak.get_data(args, admin_token, '/admin/serverinfo')
|
|
24
|
+
)
|
|
23
25
|
"""
|
|
24
26
|
|
|
25
27
|
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
|
|
@@ -21,7 +21,7 @@ import tempfile
|
|
|
21
21
|
from . import base, disk, shell
|
|
22
22
|
|
|
23
23
|
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
|
|
24
|
-
__version__ = '
|
|
24
|
+
__version__ = '2026092201'
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def attach_each(test_class, items, action, id_func=str):
|
|
@@ -108,7 +108,23 @@ RUN_ID_ENV = 'LFTEST_RUN_ID'
|
|
|
108
108
|
RUN_ID_LABEL = 'ch.linuxfabrik.lftest.run'
|
|
109
109
|
|
|
110
110
|
|
|
111
|
-
def
|
|
111
|
+
def in_test_process():
|
|
112
|
+
"""Return whether this process is a unit test rather than a monitoring plugin.
|
|
113
|
+
|
|
114
|
+
A plugin imports this module for its `--test` fixtures and must not be given a
|
|
115
|
+
test environment, so anything that changes how a plugin talks to the outside
|
|
116
|
+
world asks here first. A test is recognised by `unittest`, which every
|
|
117
|
+
`unit-test/run` imports before it reaches this module, and by the markers the
|
|
118
|
+
runners put into the environment.
|
|
119
|
+
"""
|
|
120
|
+
return (
|
|
121
|
+
'unittest' in sys.modules
|
|
122
|
+
or NO_CONTAINER_ENV in os.environ
|
|
123
|
+
or RUN_ID_ENV in os.environ
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def prepare_container_env(force=False):
|
|
112
128
|
"""Point testcontainers at rootless podman, unless the caller said otherwise.
|
|
113
129
|
|
|
114
130
|
testcontainers-python reads `DOCKER_HOST` and nothing else, so a host that
|
|
@@ -122,8 +138,22 @@ def prepare_container_env():
|
|
|
122
138
|
Called on import, so it takes effect however a test was started: through
|
|
123
139
|
`tools/run-unit-tests` or by running `unit-test/run` directly. Values the
|
|
124
140
|
caller already set are kept, which is how a docker host or a running reaper
|
|
125
|
-
stays in charge.
|
|
141
|
+
stays in charge. A helper that starts a container calls it again with `force`,
|
|
142
|
+
so it does not depend on what the import saw.
|
|
143
|
+
|
|
144
|
+
Outside a test process this does nothing, unless `force` says otherwise.
|
|
145
|
+
`DOCKER_HOST` is read by the `docker` client as well, so a plugin that ends up
|
|
146
|
+
with the podman socket in it reports `Cannot connect to the Docker daemon` on a
|
|
147
|
+
host where docker is running fine.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
force : bool, optional
|
|
152
|
+
Prepare the environment even outside a test
|
|
153
|
+
process, for a caller that starts a container itself.
|
|
126
154
|
"""
|
|
155
|
+
if not force and not in_test_process():
|
|
156
|
+
return
|
|
127
157
|
if 'DOCKER_HOST' not in os.environ:
|
|
128
158
|
os.environ['DOCKER_HOST'] = os.environ.get(
|
|
129
159
|
'CONTAINER_HOST', f'unix:///run/user/{os.getuid()}/podman/podman.sock'
|
|
@@ -255,6 +285,8 @@ def network():
|
|
|
255
285
|
... ):
|
|
256
286
|
... pass
|
|
257
287
|
"""
|
|
288
|
+
# see run_container()
|
|
289
|
+
prepare_container_env(force=True)
|
|
258
290
|
require_container_runtime()
|
|
259
291
|
from testcontainers.core.network import Network
|
|
260
292
|
|
|
@@ -453,6 +485,9 @@ def run_container(
|
|
|
453
485
|
... )
|
|
454
486
|
... # point the executable at this url
|
|
455
487
|
"""
|
|
488
|
+
# A caller that starts a container needs the socket whatever this process
|
|
489
|
+
# looks like, so this does not lean on the import-time guess.
|
|
490
|
+
prepare_container_env(force=True)
|
|
456
491
|
require_container_runtime()
|
|
457
492
|
from datetime import timedelta
|
|
458
493
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: linuxfabrik-lib
|
|
3
|
-
Version: 8.
|
|
3
|
+
Version: 8.2.0
|
|
4
4
|
Summary: Python libraries used in various Linuxfabrik projects, including the 'Linuxfabrik Monitoring Plugins' project.
|
|
5
5
|
Author-email: "Linuxfabrik GmbH, Zurich, Switzerland" <info@linuxfabrik.ch>
|
|
6
6
|
License: This is free and unencumbered software released into the public domain.
|
|
@@ -222,7 +222,8 @@ A typical monitoring plugin using these libraries:
|
|
|
222
222
|
import lib.args
|
|
223
223
|
import lib.base
|
|
224
224
|
import lib.url
|
|
225
|
-
from lib.globals import
|
|
225
|
+
from lib.globals import STATE_CRIT, STATE_OK, STATE_UNKNOWN, STATE_WARN
|
|
226
|
+
|
|
226
227
|
|
|
227
228
|
def main():
|
|
228
229
|
# Parse arguments with custom threshold types
|
|
@@ -237,11 +238,14 @@ def main():
|
|
|
237
238
|
|
|
238
239
|
# Evaluate thresholds
|
|
239
240
|
state = lib.base.get_state(result['usage'], args.warning, args.critical)
|
|
240
|
-
perfdata = lib.base.get_perfdata(
|
|
241
|
+
perfdata = lib.base.get_perfdata(
|
|
242
|
+
'usage', result['usage'], '%', args.warning, args.critical, 0, 100
|
|
243
|
+
)
|
|
241
244
|
|
|
242
245
|
# Output and exit
|
|
243
246
|
lib.base.oao('Usage is {}%'.format(result['usage']), state, perfdata)
|
|
244
247
|
|
|
248
|
+
|
|
245
249
|
if __name__ == '__main__':
|
|
246
250
|
main()
|
|
247
251
|
```
|
|
@@ -34,9 +34,16 @@ Typical use case:
|
|
|
34
34
|
else:
|
|
35
35
|
thresholds = {'avg60': ('5', '10')}
|
|
36
36
|
states = lib.psi.get_states(pressure, 'full', thresholds)
|
|
37
|
-
print(
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
print(
|
|
38
|
+
lib.psi.get_summary(
|
|
39
|
+
pressure,
|
|
40
|
+
'memory',
|
|
41
|
+
'memory',
|
|
42
|
+
('some', 'full'),
|
|
43
|
+
'full',
|
|
44
|
+
states,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
40
47
|
"""
|
|
41
48
|
|
|
42
49
|
import errno
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "linuxfabrik-lib"
|
|
9
|
-
version = "8.
|
|
9
|
+
version = "8.2.0"
|
|
10
10
|
description = "Python libraries used in various Linuxfabrik projects, including the 'Linuxfabrik Monitoring Plugins' project."
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
authors = [
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"""This library parses data returned from the Redfish API."""
|
|
12
12
|
|
|
13
13
|
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
|
|
14
|
-
__version__ = '
|
|
14
|
+
__version__ = '2026092201'
|
|
15
15
|
|
|
16
16
|
import atexit
|
|
17
17
|
import base64
|
|
@@ -1576,10 +1576,16 @@ def get_auth_header(args, cache_expire=0, cache_filename=CACHE_FILENAME):
|
|
|
1576
1576
|
f'no usable session token in the cache, logging in with up to {login_retries + 1} '
|
|
1577
1577
|
f'attempt(s)',
|
|
1578
1578
|
)
|
|
1579
|
-
#
|
|
1579
|
+
# No cached token: create a new session via the SessionService. The token and the
|
|
1580
|
+
# session's URL come from the X-Auth-Token and Location headers, which is all a
|
|
1581
|
+
# client needs (DSP0266 1.21.0, 13.3.4.2 "Session login"). The body with the new
|
|
1582
|
+
# Session resource is required there too, but not every service sends it: the DMTF
|
|
1583
|
+
# Redfish Mockup Server answers "204 No Content". Treating that as a failed login
|
|
1584
|
+
# would fall back to HTTP Basic although the headers carry a valid session.
|
|
1580
1585
|
success, result = _fetch_json(
|
|
1581
1586
|
'login',
|
|
1582
1587
|
f'{args.URL}/redfish/v1/SessionService/Sessions',
|
|
1588
|
+
allow_empty=True,
|
|
1583
1589
|
data={'UserName': args.USERNAME, 'Password': args.PASSWORD},
|
|
1584
1590
|
encoding='serialized-json',
|
|
1585
1591
|
extended=True,
|
|
@@ -273,7 +273,9 @@ def get_rooms_get(
|
|
|
273
273
|
return True, result
|
|
274
274
|
|
|
275
275
|
|
|
276
|
-
def get_stats(
|
|
276
|
+
def get_stats(
|
|
277
|
+
rc_url, auth_token, user_id, insecure=False, no_proxy=False, proxy=None, timeout=3
|
|
278
|
+
):
|
|
277
279
|
"""
|
|
278
280
|
Retrieve Rocket.Chat statistics using an API token.
|
|
279
281
|
|
|
@@ -340,7 +342,9 @@ def get_stats(rc_url, auth_token, user_id, insecure=False, no_proxy=False, proxy
|
|
|
340
342
|
return True, result
|
|
341
343
|
|
|
342
344
|
|
|
343
|
-
def get_token(
|
|
345
|
+
def get_token(
|
|
346
|
+
rc_url, user, password, insecure=False, no_proxy=False, proxy=None, timeout=3
|
|
347
|
+
):
|
|
344
348
|
"""
|
|
345
349
|
Retrieve an API token from Rocket.Chat using user credentials.
|
|
346
350
|
|
|
@@ -410,7 +414,9 @@ def get_token(rc_url, user, password, insecure=False, no_proxy=False, proxy=None
|
|
|
410
414
|
return True, f'{auth_token}:{user_id}'
|
|
411
415
|
|
|
412
416
|
|
|
413
|
-
def send2webhook(
|
|
417
|
+
def send2webhook(
|
|
418
|
+
rc_url, webhook, data, insecure=False, no_proxy=False, proxy=None, timeout=3
|
|
419
|
+
):
|
|
414
420
|
"""
|
|
415
421
|
Send a JSON payload to a Rocket.Chat incoming webhook.
|
|
416
422
|
|
|
@@ -473,7 +479,13 @@ def send2webhook(rc_url, webhook, data, insecure=False, no_proxy=False, proxy=No
|
|
|
473
479
|
|
|
474
480
|
|
|
475
481
|
def send_message(
|
|
476
|
-
webhook_url,
|
|
482
|
+
webhook_url,
|
|
483
|
+
data,
|
|
484
|
+
header=None,
|
|
485
|
+
insecure=False,
|
|
486
|
+
no_proxy=False,
|
|
487
|
+
proxy=None,
|
|
488
|
+
timeout=3,
|
|
477
489
|
):
|
|
478
490
|
"""
|
|
479
491
|
Post a JSON message payload to a Rocket.Chat incoming webhook (complete URL).
|