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.
Files changed (64) hide show
  1. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/PKG-INFO +7 -3
  2. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/README.md +6 -2
  3. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/db_sqlite.py +7 -5
  4. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/feedparser.py +21 -18
  5. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/grassfish.py +10 -1
  6. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/huawei_dorado.py +114 -36
  7. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/icinga.py +6 -2
  8. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/keycloak.py +3 -1
  9. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/lftest.py +38 -3
  10. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/linuxfabrik_lib.egg-info/PKG-INFO +7 -3
  11. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/psi.py +10 -3
  12. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/pyproject.toml +1 -1
  13. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/redfish.py +8 -2
  14. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/rocket.py +16 -4
  15. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/shell.py +21 -15
  16. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/url.py +148 -58
  17. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/LICENSE +0 -0
  18. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/__init__.py +0 -0
  19. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/args.py +0 -0
  20. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/base.py +0 -0
  21. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/bexio.py +0 -0
  22. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/cache.py +0 -0
  23. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/container.py +0 -0
  24. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/db_mysql.py +0 -0
  25. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/disk.py +0 -0
  26. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/distro.py +0 -0
  27. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/dmidecode.py +0 -0
  28. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/endoflifedate.py +0 -0
  29. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/globals.py +0 -0
  30. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/huawei_pacific.py +0 -0
  31. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/human.py +0 -0
  32. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/infomaniak.py +0 -0
  33. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/jitsi.py +0 -0
  34. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/kvm.py +0 -0
  35. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/librenms.py +0 -0
  36. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/linuxfabrik_lib.egg-info/SOURCES.txt +0 -0
  37. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/linuxfabrik_lib.egg-info/dependency_links.txt +0 -0
  38. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/linuxfabrik_lib.egg-info/requires.txt +0 -0
  39. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/linuxfabrik_lib.egg-info/top_level.txt +0 -0
  40. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/logmatch.py +0 -0
  41. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/logsource.py +0 -0
  42. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/lvm.py +0 -0
  43. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/mail.py +0 -0
  44. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/net.py +0 -0
  45. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/nextcloud.py +0 -0
  46. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/nodebb.py +0 -0
  47. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/openmetrics.py +0 -0
  48. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/openstack.py +0 -0
  49. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/powershell.py +0 -0
  50. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/psutil.py +0 -0
  51. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/qts.py +0 -0
  52. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/setup.cfg +0 -0
  53. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/smb.py +0 -0
  54. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/ssh.py +0 -0
  55. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/task.py +0 -0
  56. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/time.py +0 -0
  57. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/txt.py +0 -0
  58. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/uptimerobot.py +0 -0
  59. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/user.py +0 -0
  60. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/veeam.py +0 -0
  61. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/version.py +0 -0
  62. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/wildfly.py +0 -0
  63. {linuxfabrik_lib-8.0.0 → linuxfabrik_lib-8.2.0}/winrm.py +0 -0
  64. {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.0.0
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 (STATE_CRIT, STATE_OK, STATE_UNKNOWN, STATE_WARN)
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('usage', result['usage'], '%', args.warning, args.critical, 0, 100)
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 (STATE_CRIT, STATE_OK, STATE_UNKNOWN, STATE_WARN)
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('usage', result['usage'], '%', args.warning, args.critical, 0, 100)
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(lib.db_sqlite.select(
24
- ... conn,
25
- ... 'SELECT * FROM perfdata WHERE name = :name ORDER BY timestamp DESC LIMIT 2',
26
- ... {'name': disk},
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__ = '2026082501'
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 or the body is not a feed.
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
- attempt = 0
84
- while True:
85
- success, xml = url.fetch(
86
- feed_url,
87
- encoding=encoding,
88
- insecure=insecure,
89
- no_proxy=no_proxy,
90
- proxy=proxy,
91
- timeout=timeout,
92
- )
93
- result = parse_soup(xml, feed_url) if success else (False, xml)
94
-
95
- if result[0] or attempt >= retries:
96
- return result
97
- attempt += 1
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 or the body is not a feed.
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, host, port, uri, version, func, insecure=False, no_proxy=False, proxy=None, timeout=8
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__ = '2026092101'
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, forcing a fresh login before the second one, and waits one
1052
- second between attempts. The retry count is kept low on purpose, so one call stays within
1053
- the caller's own timeout: the worst case is three requests plus one login,
1054
- plus two seconds of waiting. This budget is per call. A caller that chains several calls
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
- # On the second attempt, drop the cached session and log in again; a
1082
- # rejected request is most likely an expired session that retrying
1083
- # with the same token cannot fix. The third attempt then reuses that
1084
- # fresh token to absorb a remaining transient error.
1085
- ibasetoken, cookie, device_id = get_creds(args, force_relogin=attempt == 2)
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 a faulty or invalid one, `STATE_WARN`
1412
- for every other code, including one the enumeration does not know and a missing value.
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()`: the codes are renumbered from `0` up and every
1418
- one of them collides with the shared enumeration, so `get_running_status_state()` cannot
1419
- be used here.
1420
- - Split (`3`) warns rather than going critical. A split domain no longer mirrors, but each
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(0) == STATE_OK
1505
+ >>> get_hypermetro_domain_running_status_state(1) == STATE_OK
1427
1506
  True
1428
1507
 
1429
- >>> get_hypermetro_domain_running_status_state('2') == STATE_CRIT
1508
+ >>> get_hypermetro_domain_running_status_state('35') == STATE_CRIT
1430
1509
  True
1431
1510
  """
1432
1511
  code = as_code(rs)
1433
- if code == 0:
1512
+ if code == 1:
1434
1513
  return STATE_OK
1435
- if code in (2, 5):
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
- # Two conversions are worth naming. The response times are documented in microseconds and
1875
- # go out in seconds, the unit Nagios knows. The bandwidths and I/O sizes are documented as
1876
- # MB/s and KB, and are read as binary multiples: every capacity this API reports is binary
1877
- # (sectors of 512 bytes), and so is what DeviceManager itself displays.
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', 1024),
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', 1024),
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` and `dr_star`. Both renumber `RUNNINGSTATUS` instead
2103
- of sharing this enumeration, and both collide on the low codes this function is most
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, args.USERNAME, args.PASSWORD, data=data,
88
- ... method_override='GET', timeout=3
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(lib.keycloak.get_data(args, admin_token, '/admin/serverinfo'))
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__ = '2026092101'
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 prepare_container_env():
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.0.0
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 (STATE_CRIT, STATE_OK, STATE_UNKNOWN, STATE_WARN)
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('usage', result['usage'], '%', args.warning, args.critical, 0, 100)
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(lib.psi.get_summary(
38
- pressure, 'memory', 'memory', ('some', 'full'), 'full', states,
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.0.0"
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__ = '2026092103'
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
- # no cached token: create a new session via the SessionService
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(rc_url, auth_token, user_id, insecure=False, no_proxy=False, proxy=None, timeout=3):
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(rc_url, user, password, insecure=False, no_proxy=False, proxy=None, timeout=3):
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(rc_url, webhook, data, insecure=False, no_proxy=False, proxy=None, timeout=3):
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, data, header=None, insecure=False, no_proxy=False, proxy=None, timeout=3
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).