linuxfabrik-lib 2.3.0__tar.gz → 2.4.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 (49) hide show
  1. {linuxfabrik_lib-2.3.0/linuxfabrik_lib.egg-info → linuxfabrik_lib-2.4.0}/PKG-INFO +1 -1
  2. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/args.py +12 -1
  3. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/base.py +15 -8
  4. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/dmidecode.py +129 -44
  5. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/endoflifedate.py +441 -206
  6. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0/linuxfabrik_lib.egg-info}/PKG-INFO +1 -1
  7. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/pyproject.toml +1 -1
  8. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/redfish.py +122 -29
  9. linuxfabrik_lib-2.4.0/rocket.py +462 -0
  10. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/time.py +21 -0
  11. linuxfabrik_lib-2.3.0/rocket.py +0 -149
  12. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/LICENSE.txt +0 -0
  13. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/README.md +0 -0
  14. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/__init__.py +0 -0
  15. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/cache.py +0 -0
  16. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/db_mysql.py +0 -0
  17. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/db_sqlite.py +0 -0
  18. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/disk.py +0 -0
  19. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/distro.py +0 -0
  20. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/feedparser.py +0 -0
  21. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/globals.py +0 -0
  22. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/grassfish.py +0 -0
  23. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/huawei.py +0 -0
  24. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/human.py +0 -0
  25. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/icinga.py +0 -0
  26. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/infomaniak.py +0 -0
  27. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/jitsi.py +0 -0
  28. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/keycloak.py +0 -0
  29. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/lftest.py +0 -0
  30. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/librenms.py +0 -0
  31. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/linuxfabrik_lib.egg-info/SOURCES.txt +0 -0
  32. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/linuxfabrik_lib.egg-info/dependency_links.txt +0 -0
  33. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/linuxfabrik_lib.egg-info/requires.txt +0 -0
  34. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/linuxfabrik_lib.egg-info/top_level.txt +0 -0
  35. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/net.py +0 -0
  36. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/nodebb.py +0 -0
  37. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/powershell.py +0 -0
  38. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/psutil.py +0 -0
  39. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/qts.py +0 -0
  40. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/setup.cfg +0 -0
  41. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/shell.py +0 -0
  42. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/smb.py +0 -0
  43. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/txt.py +0 -0
  44. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/uptimerobot.py +0 -0
  45. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/url.py +0 -0
  46. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/veeam.py +0 -0
  47. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/version.py +0 -0
  48. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/wildfly.py +0 -0
  49. {linuxfabrik_lib-2.3.0 → linuxfabrik_lib-2.4.0}/winrm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linuxfabrik-lib
3
- Version: 2.3.0
3
+ Version: 2.4.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
  Project-URL: Homepage, https://github.com/Linuxfabrik/lib
@@ -12,7 +12,7 @@
12
12
  """
13
13
 
14
14
  __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
15
- __version__ = '2025042002'
15
+ __version__ = '2025091501'
16
16
 
17
17
 
18
18
  HELP_TEXTS = {
@@ -25,6 +25,17 @@ HELP_TEXTS = {
25
25
  '`(?: ... )*` is a non-capturing group that matches any sequence of characters '
26
26
  'that satisfy the condition inside it, zero or more times.'
27
27
  ),
28
+ '--stratum': (
29
+ 'Warns if the determined stratum of the time server is greater than or equal to this '
30
+ 'value. '
31
+ 'Stratum 1 indicates a computer with a locally attached reference clock. A computer that '
32
+ 'is synchronised to a stratum 1 computer is at stratum 2. A computer that is synchronised '
33
+ 'to a stratum 2 computer is at stratum 3, and so on.'
34
+ ),
35
+ '--verbose': (
36
+ 'Makes this plugin verbose during the operation. Useful for debugging and seeing '
37
+ 'what\'s going on under the hood.'
38
+ ),
28
39
  }
29
40
 
30
41
  # Predefined sets for checking units and methods
@@ -12,7 +12,7 @@
12
12
  """
13
13
 
14
14
  __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
15
- __version__ = '2025042101'
15
+ __version__ = '2025070401'
16
16
 
17
17
  import collections
18
18
  import numbers
@@ -269,6 +269,7 @@ def get_table(data, cols, header=None, strip=True, sort_by_key=None, sort_order_
269
269
  """
270
270
  if not data:
271
271
  return ''
272
+ data = data.copy() # data has been passed by-reference - kick the reference
272
273
 
273
274
  if sort_by_key:
274
275
  data = sorted(data, key=operator.itemgetter(sort_by_key), reverse=sort_order_reverse)
@@ -393,7 +394,7 @@ def guess_type(v, consumer='python'):
393
394
  return str(v) if consumer == 'python' else 'text'
394
395
 
395
396
 
396
- def is_empty_list(l):
397
+ def is_empty_list(lst):
397
398
  """
398
399
  Check if a list only contains either empty elements or whitespace.
399
400
 
@@ -479,7 +480,8 @@ def match_range(value, spec):
479
480
 
480
481
  ### Returns
481
482
  - **bool**:
482
- - True if `value` is inside the bounds for a non-inverted `spec`, or outside the bounds for an inverted `spec`.
483
+ - True if `value` is inside the bounds for a non-inverted `spec`, or outside the bounds for an
484
+ inverted `spec`.
483
485
  - Otherwise, False.
484
486
 
485
487
  ### Example
@@ -626,7 +628,8 @@ def oao(msg, state=STATE_OK, perfdata='', always_ok=False):
626
628
  """
627
629
  msg = txt.sanitize_sensitive_data(msg.strip()).replace('|', '!')
628
630
  if always_ok and msg:
629
- parts = msg.split('\n', 1) # Instead of splitlines(), we just split('\n', 1), so only first line is touched.
631
+ # Instead of splitlines(), we just split('\n', 1), so only first line is touched.
632
+ parts = msg.split('\n', 1)
630
633
  parts[0] += ' (always ok)'
631
634
  msg = '\n'.join(parts)
632
635
  print(f'{msg}|{perfdata.strip()}' if perfdata else msg)
@@ -683,12 +686,16 @@ def sort(array, reverse=True, sort_by_key=False):
683
686
  If the input is not a dictionary, the original input is returned unmodified.
684
687
 
685
688
  ### Parameters
686
- - **array** (`dict` or `any`): The dictionary to be sorted. If not a dictionary, the input is returned as is.
687
- - **reverse** (`bool`, optional): If True, sort in descending order; if False, ascending. Defaults to True.
688
- - **sort_by_key** (`bool`, optional): If True, sort by dictionary keys; if False, by values. Defaults to False.
689
+ - **array** (`dict` or `any`): The dictionary to be sorted. If not a dictionary, the input is
690
+ returned as is.
691
+ - **reverse** (`bool`, optional): If True, sort in descending order; if False, ascending.
692
+ Defaults to True.
693
+ - **sort_by_key** (`bool`, optional): If True, sort by dictionary keys; if False, by values.
694
+ Defaults to False.
689
695
 
690
696
  ### Returns
691
- - **list** or **any**: A list of sorted (key, value) tuples if a dictionary is provided, otherwise the original input.
697
+ - **list** or **any**: A list of sorted (key, value) tuples if a dictionary is provided,
698
+ otherwise the original input.
692
699
 
693
700
  ### Example
694
701
  >>> sort({'a': 2, 'b': 1})
@@ -14,7 +14,7 @@ Copied and refactored from py-dmidecode (https://github.com/zaibon/py-dmidecode)
14
14
  """
15
15
 
16
16
  __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
17
- __version__ = '2025042001'
17
+ __version__ = '2025090901'
18
18
 
19
19
  import re
20
20
  import subprocess
@@ -219,49 +219,86 @@ def cpu_type(dmi):
219
219
 
220
220
  def dmidecode_parse(output):
221
221
  """
222
- Parse the raw output of `dmidecode` into a structured dictionary.
223
-
224
- This function processes the raw textual output of the `dmidecode` tool and extracts
225
- structured information about system hardware, organized by DMI handle.
226
-
227
- ### Parameters
228
- - **output** (`str`):
229
- The raw string output from the `dmidecode` command.
230
-
231
- ### Returns
232
- - **dict**:
233
- A dictionary keyed by DMI handle. Each value contains fields such as dminame, dmisize,
234
- dmitype, and parsed key-value pairs from the output.
235
-
236
- ### Notes
237
- - Records are separated by double newlines in the output.
238
- - Only records with at least three lines are considered valid.
239
- - Multi-line blocks are handled if needed.
222
+ Parse `dmidecode` output into a dict, collapsing near-duplicates in an admin-friendly way.
223
+
224
+ Type-aware dedupe rules:
225
+ - Type 4 (Processor Information): ignore per-thread/core/socket noise fields; merge;
226
+ add dedup_count/dedup_sockets
227
+ - Type 17 (Memory Device): drop unpopulated; ignore slot labels; merge;
228
+ add dedup_count/dedup_slots
229
+ - Other types: generic dedupe (exact content after normalization)
230
+
231
+ Returns:
232
+ { dmi_handle_tuple: parsed_record_dict, ... }
233
+ where parsed_record_dict includes keys: dminame, dmisize, dmitype, parsed fields,
234
+ and possibly:
235
+ - dedup_count (int >= 1)
236
+ - dedup_sockets / dedup_slots (sorted list of labels encountered)
237
+ - dedup_handles (list of original DMI handle strings that were merged)
238
+ """
239
+ data = {}
240
+ seen = {} # fp -> (first_handle, aggregated_record)
241
+
242
+ # --- helpers -------------------------------------------------------------
243
+ def _normalize(s):
244
+ if s is None:
245
+ return ''
246
+ s = str(s).strip()
247
+ # treat common "unknown" variants as empty so they don't block dedupe
248
+ if s.lower() in {'unknown', 'not specified', 'not provided', 'n/a'}:
249
+ return ''
250
+ # collapse whitespace
251
+ return ' '.join(s.split())
252
+
253
+ def _lower(s):
254
+ return _normalize(s).lower()
255
+
256
+ def _drop_unpopulated_type17(rec):
257
+ size = _lower(rec.get('Size'))
258
+ if not size:
259
+ return True
260
+ if 'no module installed' in size:
261
+ return True
262
+ # Sometimes vendors encode 0-sized entries
263
+ if size.startswith('0 ') or size == '0':
264
+ return True
265
+ return False
240
266
 
241
- ### Example
242
- >>> dmidecode_parse(dmidecode_output)
243
- {
244
- ('0xDA00', '218', '251'): {
245
- 'dminame': 'OEM-specific Type',
246
- 'dmisize': 251,
247
- 'dmitype': 218,
248
- 'H': 'D\t\t0\t\t0\t\t0\t\t0\t\t0\t\t0\t\t0\t\t0\t\t0\t\t0\t\t0\t\t0\t\t0\t\t0\t\t0'
267
+ # Fields to ignore by DMI type when constructing fingerprints (order-independent)
268
+ IGNORE_BY_TYPE = {
269
+ 4: { # Processor Information
270
+ 'Socket Designation', 'ID',
271
+ 'L1 Cache Handle', 'L2 Cache Handle', 'L3 Cache Handle',
272
+ 'Serial Number', 'Asset Tag', 'Part Number',
273
+ 'Core Count', 'Core Enabled', # often bogus or per-core
249
274
  },
250
- ('0x0001', '0', '26'): {
251
- 'dminame': 'BIOS Information',
252
- 'dmisize': 26,
253
- 'dmitype': 0,
254
- 'Vendor': 'Dell Inc.',
255
- 'Version': '1.7.1',
256
- 'Release Date': '12/06/2024',
257
- 'ROM Size': '64 MB',
258
- ...,
275
+ 17: { # Memory Device
276
+ 'Locator', 'Bank Locator', 'Device Locator',
277
+ 'Memory Array Mapped Address Handle', 'Mem Array Error Info Handle',
278
+ 'Total Width', 'Data Width', # width can vary by board reporting; not essential
279
+ 'Serial Number', # sometimes blank; can differ even for identical sticks
259
280
  },
260
- ...
261
281
  }
262
- """
263
- data = {}
264
282
 
283
+ def _fingerprint(rec):
284
+ """Build a stable, type-aware fingerprint for dedupe."""
285
+ dtype = int(rec.get('dmitype', -1))
286
+ ignore = IGNORE_BY_TYPE.get(dtype, set())
287
+ base = (_normalize(rec.get('dminame', '')), dtype)
288
+
289
+ # normalize all fields except ignored + meta
290
+ items = []
291
+ for k in sorted(rec.keys()):
292
+ if k in ('dminame', 'dmitype', 'dmisize'):
293
+ continue
294
+ if k in ignore:
295
+ continue
296
+ v = rec[k]
297
+ # Multi-line blocks were joined with tabs; normalize them
298
+ items.append((k, _normalize(v)))
299
+ return base + tuple(items)
300
+
301
+ # --- parse loop ----------------------------------------------------------
265
302
  for record in output.split('\n\n'):
266
303
  record_element = record.splitlines()
267
304
  if len(record_element) < 3:
@@ -271,8 +308,8 @@ def dmidecode_parse(output):
271
308
  if not handle_data:
272
309
  continue
273
310
 
274
- dmi_handle = handle_data[0]
275
- data[dmi_handle] = {
311
+ dmi_handle = handle_data[0] # ('0x0004','4','42')
312
+ current = {
276
313
  'dminame': record_element[1],
277
314
  'dmisize': int(dmi_handle[2]),
278
315
  'dmitype': int(dmi_handle[1]),
@@ -282,11 +319,11 @@ def dmidecode_parse(output):
282
319
  in_block_list = []
283
320
 
284
321
  for line in record_element[2:]:
285
- if in_block_element:
322
+ if in_block_element is not None:
286
323
  in_block_data = IN_BLOCK_RE.findall(line)
287
324
  if in_block_data:
288
325
  in_block_list.append(in_block_data[0][0])
289
- data[dmi_handle][in_block_element] = '\t\t'.join(in_block_list)
326
+ current[in_block_element] = '\t\t'.join(in_block_list)
290
327
  continue
291
328
  else:
292
329
  in_block_element = None
@@ -295,7 +332,7 @@ def dmidecode_parse(output):
295
332
  record_data = RECORD_RE.findall(line)
296
333
  if record_data:
297
334
  key, value = record_data[0]
298
- data[dmi_handle][key] = value
335
+ current[key] = value
299
336
  continue
300
337
 
301
338
  record_data2 = RECORD2_RE.findall(line)
@@ -303,6 +340,54 @@ def dmidecode_parse(output):
303
340
  in_block_element = record_data2[0][0]
304
341
  in_block_list = []
305
342
 
343
+ # Type-specific filters (drop obviously irrelevant entries)
344
+ dtype = int(current.get('dmitype', -1))
345
+ if dtype == 4:
346
+ # keep only populated/enabled when reported
347
+ status = _lower(current.get('Status'))
348
+ if status and not ('populated' in status and 'enabled' in status):
349
+ continue
350
+ if dtype == 17:
351
+ if _drop_unpopulated_type17(current):
352
+ continue
353
+
354
+ # Build type-aware fingerprint and aggregate
355
+ fp = _fingerprint(current)
356
+ if fp not in seen:
357
+ # first occurrence becomes the representative
358
+ # attach dedupe metadata containers up-front (lazy-friendly)
359
+ rep = dict(current)
360
+ rep['dedup_count'] = 1
361
+ rep['dedup_handles'] = [dmi_handle[0]]
362
+ # capture socket/slot labels if present for admin visibility
363
+ if dtype == 4 and 'Socket Designation' in current:
364
+ rep['dedup_sockets'] = [current['Socket Designation']]
365
+ if dtype == 17:
366
+ labels = []
367
+ for k in ('Locator', 'Device Locator', 'Bank Locator'):
368
+ if current.get(k):
369
+ labels.append(current[k])
370
+ if labels:
371
+ rep['dedup_slots'] = sorted({*labels})
372
+ seen[fp] = (dmi_handle, rep)
373
+ data[dmi_handle] = rep
374
+ else:
375
+ first_handle, rep = seen[fp]
376
+ rep['dedup_count'] = int(rep.get('dedup_count', 1)) + 1
377
+ rep['dedup_handles'].append(dmi_handle[0])
378
+ # enrich socket/slot lists
379
+ if dtype == 4 and current.get('Socket Designation'):
380
+ sockets = set(rep.get('dedup_sockets', []))
381
+ sockets.add(current['Socket Designation'])
382
+ rep['dedup_sockets'] = sorted(sockets)
383
+ if dtype == 17:
384
+ slots = set(rep.get('dedup_slots', []))
385
+ for k in ('Locator', 'Device Locator', 'Bank Locator'):
386
+ if current.get(k):
387
+ slots.add(current[k])
388
+ if slots:
389
+ rep['dedup_slots'] = sorted(slots)
390
+
306
391
  return data
307
392
 
308
393