linuxfabrik-lib 4.2.0__tar.gz → 4.3.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 (50) hide show
  1. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/PKG-INFO +1 -1
  2. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/db_mysql.py +9 -8
  3. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/db_sqlite.py +4 -7
  4. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/disk.py +224 -2
  5. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/dmidecode.py +4 -6
  6. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/endoflifedate.py +68 -55
  7. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/huawei.py +3 -2
  8. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/human.py +2 -5
  9. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/icinga.py +2 -2
  10. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/lftest.py +63 -12
  11. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/linuxfabrik_lib.egg-info/PKG-INFO +1 -1
  12. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/linuxfabrik_lib.egg-info/SOURCES.txt +2 -0
  13. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/net.py +117 -4
  14. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/pyproject.toml +6 -1
  15. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/redfish.py +10 -5
  16. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/rocket.py +7 -7
  17. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/shell.py +23 -1
  18. linuxfabrik_lib-4.3.0/ssh.py +248 -0
  19. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/time.py +2 -5
  20. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/txt.py +15 -4
  21. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/uptimerobot.py +2 -2
  22. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/url.py +62 -25
  23. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/veeam.py +8 -5
  24. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/LICENSE +0 -0
  25. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/README.md +0 -0
  26. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/__init__.py +0 -0
  27. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/args.py +0 -0
  28. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/base.py +0 -0
  29. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/cache.py +0 -0
  30. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/distro.py +0 -0
  31. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/feedparser.py +0 -0
  32. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/globals.py +0 -0
  33. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/grassfish.py +0 -0
  34. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/infomaniak.py +0 -0
  35. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/jitsi.py +0 -0
  36. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/keycloak.py +0 -0
  37. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/librenms.py +0 -0
  38. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/linuxfabrik_lib.egg-info/dependency_links.txt +0 -0
  39. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/linuxfabrik_lib.egg-info/requires.txt +0 -0
  40. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/linuxfabrik_lib.egg-info/top_level.txt +0 -0
  41. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/nextcloud.py +0 -0
  42. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/nodebb.py +0 -0
  43. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/powershell.py +0 -0
  44. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/psutil.py +0 -0
  45. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/qts.py +0 -0
  46. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/setup.cfg +0 -0
  47. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/smb.py +0 -0
  48. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/version.py +0 -0
  49. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/wildfly.py +0 -0
  50. {linuxfabrik_lib-4.2.0 → linuxfabrik_lib-4.3.0}/winrm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linuxfabrik-lib
3
- Version: 4.2.0
3
+ Version: 4.3.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.
@@ -10,19 +10,18 @@
10
10
 
11
11
  """Library for accessing MySQL/MariaDB servers."""
12
12
 
13
- import warnings
14
-
15
- warnings.filterwarnings('ignore', category=UserWarning, module='pymysql')
16
-
17
- __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
18
- __version__ = '2026051803'
19
-
20
13
  import re
21
14
  import sys
15
+ import warnings
22
16
 
23
17
  from . import base
24
18
  from .globals import STATE_UNKNOWN
25
19
 
20
+ warnings.filterwarnings('ignore', category=UserWarning, module='pymysql')
21
+
22
+ __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
23
+ __version__ = '2026060201'
24
+
26
25
  try:
27
26
  import pymysql.cursors
28
27
  except ImportError:
@@ -538,7 +537,9 @@ def get_server_info(banner=None):
538
537
  return {'flavor': flavor, 'version': version}
539
538
  return None
540
539
 
541
- from . import shell # local import to keep db_mysql usable without shell at module load
540
+ # local import to keep db_mysql usable without shell at module load
541
+ from . import shell
542
+
542
543
  for command in (
543
544
  'mysqld --version',
544
545
  'mariadbd --version',
@@ -28,7 +28,7 @@ This is one typical use case of this library (taken from `disk-io`):
28
28
  """
29
29
 
30
30
  __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
31
- __version__ = '2026060102'
31
+ __version__ = '2026060201'
32
32
 
33
33
  import csv
34
34
  import hashlib
@@ -557,10 +557,7 @@ def delete(conn, sql, data=None, delete_db_on_operational_error=True):
557
557
 
558
558
  c = conn.cursor()
559
559
  try:
560
- if data:
561
- rowcount = c.execute(sql, data).rowcount
562
- else:
563
- rowcount = c.execute(sql).rowcount
560
+ rowcount = c.execute(sql, data).rowcount if data else c.execute(sql).rowcount
564
561
  return True, rowcount
565
562
  except sqlite3.OperationalError as e:
566
563
  if delete_db_on_operational_error:
@@ -938,7 +935,7 @@ def insert(conn, data, table='perfdata', delete_db_on_operational_error=True):
938
935
  # table is sanitized via __filter_str() above; keys come from the caller's
939
936
  # data dict (column names, not values); VALUES use parameterized binds
940
937
  keys = ','.join(data.keys())
941
- binds = ','.join(f':{key}' for key in data.keys())
938
+ binds = ','.join(f':{key}' for key in data)
942
939
  sql = f'INSERT INTO "{table}" ({keys}) VALUES ({binds});' # nosec B608
943
940
 
944
941
  c = conn.cursor()
@@ -1161,7 +1158,7 @@ def replace(conn, data, table='perfdata', delete_db_on_operational_error=True):
1161
1158
  table = __filter_str(table)
1162
1159
 
1163
1160
  keys = ','.join(data.keys())
1164
- binds = ','.join(f':{key}' for key in data.keys())
1161
+ binds = ','.join(f':{key}' for key in data)
1165
1162
  sql = f'REPLACE INTO "{table}" ({keys}) VALUES ({binds});'
1166
1163
 
1167
1164
  c = conn.cursor()
@@ -13,11 +13,12 @@ partitions, grepping a file, etc.
13
13
  """
14
14
 
15
15
  __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
16
- __version__ = '2026060101'
16
+ __version__ = '2026060601'
17
17
 
18
18
  import csv
19
19
  import os
20
20
  import re
21
+ import shutil
21
22
  import tempfile
22
23
 
23
24
  from . import shell
@@ -55,6 +56,65 @@ def bd2dmd(device):
55
56
  return mapped_device if os.path.islink(mapped_device) else ''
56
57
 
57
58
 
59
+ def copy_dir(src, dst):
60
+ """
61
+ Recursively copy a directory tree.
62
+
63
+ Wraps `shutil.copytree()` and reports the outcome in the same
64
+ `(success, error)` style as the other disk helpers, so callers do not have to
65
+ handle exceptions themselves.
66
+
67
+ ### Parameters
68
+ - **src** (`str`): Source directory.
69
+ - **dst** (`str`): Destination directory (must not exist yet).
70
+
71
+ ### Returns
72
+ - **tuple**:
73
+ - tuple[0] (**bool**): True if the copy succeeded, otherwise False.
74
+ - tuple[1] (**None or str**): None on success, otherwise an error message.
75
+
76
+ ### Example
77
+ >>> copy_dir('/usr/share/lynis', '/tmp/lynis')
78
+ (True, None)
79
+ """
80
+ try:
81
+ shutil.copytree(src, dst)
82
+ return True, None
83
+ except (OSError, shutil.Error) as e:
84
+ return False, f'Error copying directory {src} to {dst}: {e}'
85
+ except Exception as e:
86
+ return False, f'Unknown error copying directory {src} to {dst}: {e}'
87
+
88
+
89
+ def copy_file(src, dst):
90
+ """
91
+ Copy a single file, preserving its metadata.
92
+
93
+ Wraps `shutil.copy2()` and reports the outcome in the same `(success, error)`
94
+ style as the other disk helpers.
95
+
96
+ ### Parameters
97
+ - **src** (`str`): Source file.
98
+ - **dst** (`str`): Destination file or directory.
99
+
100
+ ### Returns
101
+ - **tuple**:
102
+ - tuple[0] (**bool**): True if the copy succeeded, otherwise False.
103
+ - tuple[1] (**None or str**): None on success, otherwise an error message.
104
+
105
+ ### Example
106
+ >>> copy_file('/etc/lynis/default.prf', '/tmp/lynis/default.prf')
107
+ (True, None)
108
+ """
109
+ try:
110
+ shutil.copy2(src, dst)
111
+ return True, None
112
+ except OSError as e:
113
+ return False, f'OS error "{e.strerror}" while copying {src} to {dst}'
114
+ except Exception as e:
115
+ return False, f'Unknown error copying {src} to {dst}: {e}'
116
+
117
+
58
118
  def dir_exists(path):
59
119
  """
60
120
  Check if a directory exists at the given path.
@@ -120,6 +180,79 @@ def file_exists(path, allow_empty=False):
120
180
  return os.path.getsize(path) > 0
121
181
 
122
182
 
183
+ # Block-device name prefixes that never carry meaningful I/O for monitoring
184
+ # (loopback, RAM disks, compressed RAM, floppy and optical devices). They are
185
+ # skipped by get_block_devices().
186
+ _PSEUDO_DEVICE_PREFIXES = ('fd', 'loop', 'ram', 'sr', 'zram')
187
+
188
+
189
+ def get_block_devices():
190
+ """
191
+ Return all local block devices that expose I/O counters, mounted or not.
192
+
193
+ Unlike `get_real_disks()`, which is limited to block devices that currently have a mounted
194
+ filesystem, this also includes devices without a mounted filesystem, for example raw devices
195
+ backing a database or storage layer, or unmounted multipath/SAN volumes. Devices are
196
+ enumerated from `/proc/diskstats`, so their names line up with the per-device I/O counters
197
+ exposed there.
198
+
199
+ Each device is represented as a dictionary with:
200
+ - 'bd' : Block device path (e.g. '/dev/sda' or '/dev/dm-7').
201
+ - 'dmd': Device-mapper path if the device is a device-mapper target (e.g.
202
+ '/dev/mapper/data'), otherwise an empty string.
203
+ - 'mp' : Mount point(s), space-separated if mounted in several places, or an empty string if
204
+ the device is not mounted.
205
+
206
+ Pseudo devices that never carry meaningful I/O are skipped by name prefix: loopback (`loop`),
207
+ RAM disks (`ram`), compressed RAM (`zram`), floppy (`fd`) and optical (`sr`) devices.
208
+
209
+ ### Parameters
210
+ - None
211
+
212
+ ### Returns
213
+ - **list of dict**: One entry per block device, including unmounted ones. Empty list on
214
+ systems without `/proc/diskstats` (e.g. non-Linux).
215
+
216
+ ### Example
217
+ >>> get_block_devices()
218
+ [{'bd': '/dev/dm-7', 'dmd': '/dev/mapper/data', 'mp': ''},
219
+ {'bd': '/dev/sda1', 'dmd': '', 'mp': '/boot'}]
220
+ """
221
+ success, diskstats = read_file('/proc/diskstats')
222
+ if not success:
223
+ return []
224
+
225
+ # map every mounted block-device path to its mount point(s)
226
+ mountpoints = {}
227
+ mounts_ok, mounts_content = read_file('/proc/mounts')
228
+ if mounts_ok:
229
+ for line in mounts_content.splitlines():
230
+ if not line.startswith('/dev/'):
231
+ continue
232
+ parts = line.split()
233
+ device_path, mount_point = parts[0], parts[1]
234
+ if device_path in mountpoints:
235
+ mountpoints[device_path] += f' {mount_point}'
236
+ else:
237
+ mountpoints[device_path] = mount_point
238
+
239
+ disks = []
240
+ for line in diskstats.splitlines():
241
+ parts = line.split()
242
+ if len(parts) < 3:
243
+ continue
244
+ name = parts[2]
245
+ if name.startswith(_PSEUDO_DEVICE_PREFIXES):
246
+ continue
247
+ bd = f'/dev/{name}'
248
+ dmd = bd2dmd(name)
249
+ # a device can be mounted under its block-device path or its device-mapper path
250
+ mp = mountpoints.get(bd, '') or (mountpoints.get(dmd, '') if dmd else '')
251
+ disks.append({'bd': bd, 'dmd': dmd, 'mp': mp})
252
+
253
+ return disks
254
+
255
+
123
256
  def get_cwd():
124
257
  """
125
258
  Get the current working directory.
@@ -168,7 +301,7 @@ def get_owner(file):
168
301
  """
169
302
  try:
170
303
  return os.stat(file).st_uid
171
- except:
304
+ except OSError:
172
305
  return -1
173
306
 
174
307
 
@@ -302,6 +435,67 @@ def grep_file(filename, pattern):
302
435
  return True, ''
303
436
 
304
437
 
438
+ def make_temp_dir(prefix=''):
439
+ """
440
+ Create a unique temporary directory and return its path.
441
+
442
+ Wraps `tempfile.mkdtemp()` and reports the outcome in the same
443
+ `(success, result)` style as the other disk helpers.
444
+
445
+ ### Parameters
446
+ - **prefix** (`str`, optional): Prefix for the directory name. Defaults to ''.
447
+
448
+ ### Returns
449
+ - **tuple**:
450
+ - tuple[0] (**bool**): True on success, otherwise False.
451
+ - tuple[1] (**str**): The created directory path on success, otherwise an
452
+ error message.
453
+
454
+ ### Example
455
+ >>> make_temp_dir(prefix='myapp-')
456
+ (True, '/tmp/myapp-abcd1234')
457
+ """
458
+ try:
459
+ return True, tempfile.mkdtemp(prefix=prefix)
460
+ except OSError as e:
461
+ return False, f'OS error "{e.strerror}" while creating a temporary directory'
462
+ except Exception as e:
463
+ return False, f'Unknown error creating a temporary directory: {e}'
464
+
465
+
466
+ def mkdir(path, mode=0o755, exist_ok=True):
467
+ """
468
+ Create a directory, including any missing parent directories.
469
+
470
+ Wraps `os.makedirs()` and reports the outcome in the same `(success, error)`
471
+ style as the other disk helpers, so callers do not have to handle exceptions
472
+ themselves.
473
+
474
+ ### Parameters
475
+ - **path** (`str`): Directory path to create.
476
+ - **mode** (`int`, optional): Permission bits for newly created directories.
477
+ Defaults to `0o755`.
478
+ - **exist_ok** (`bool`, optional): If `True`, an already existing directory is
479
+ not an error. Defaults to `True`.
480
+
481
+ ### Returns
482
+ - **tuple**:
483
+ - tuple[0] (**bool**): True if the directory exists afterwards, else False.
484
+ - tuple[1] (**None or str**): None on success, otherwise an error message.
485
+
486
+ ### Example
487
+ >>> mkdir('/tmp/example/sub')
488
+ (True, None)
489
+ """
490
+ try:
491
+ os.makedirs(path, mode=mode, exist_ok=exist_ok)
492
+ return True, None
493
+ except OSError as e:
494
+ return False, f'OS error "{e.strerror}" while creating {path}'
495
+ except Exception as e:
496
+ return False, f'Unknown error creating {path}: {e}'
497
+
498
+
305
499
  def read_csv(
306
500
  filename,
307
501
  delimiter=',',
@@ -444,6 +638,34 @@ def read_file(filename):
444
638
  return False, f'Unknown error opening or reading {filename}: {e}'
445
639
 
446
640
 
641
+ def rm_dir(path):
642
+ """
643
+ Recursively delete a directory tree.
644
+
645
+ Wraps `shutil.rmtree()` and reports the outcome in the same `(success, error)`
646
+ style as `rm_file()`.
647
+
648
+ ### Parameters
649
+ - **path** (`str`): Directory tree to delete.
650
+
651
+ ### Returns
652
+ - **tuple**:
653
+ - tuple[0] (**bool**): True if deletion succeeded, otherwise False.
654
+ - tuple[1] (**None or str**): None on success, otherwise an error message.
655
+
656
+ ### Example
657
+ >>> rm_dir('/tmp/lynis')
658
+ (True, None)
659
+ """
660
+ try:
661
+ shutil.rmtree(path)
662
+ return True, None
663
+ except OSError as e:
664
+ return False, f'OS error "{e.strerror}" while deleting {path}'
665
+ except Exception as e:
666
+ return False, f'Unknown error deleting {path}: {e}'
667
+
668
+
447
669
  def rm_file(filename):
448
670
  """
449
671
  Delete or remove a file.
@@ -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__ = '2025090901'
17
+ __version__ = '2026060201'
18
18
 
19
19
  import re
20
20
 
@@ -260,9 +260,7 @@ def dmidecode_parse(output):
260
260
  if 'no module installed' in size:
261
261
  return True
262
262
  # Sometimes vendors encode 0-sized entries
263
- if size.startswith('0 ') or size == '0':
264
- return True
265
- return False
263
+ return bool(size.startswith('0 ') or size == '0')
266
264
 
267
265
  # Fields to ignore by DMI type when constructing fingerprints (order-independent)
268
266
  IGNORE_BY_TYPE = {
@@ -382,7 +380,7 @@ def dmidecode_parse(output):
382
380
  seen[fp] = (dmi_handle, rep)
383
381
  data[dmi_handle] = rep
384
382
  else:
385
- first_handle, rep = seen[fp]
383
+ _, rep = seen[fp]
386
384
  rep['dedup_count'] = int(rep.get('dedup_count', 1)) + 1
387
385
  rep['dedup_handles'].append(dmi_handle[0])
388
386
  # enrich socket/slot lists
@@ -517,7 +515,7 @@ def get_data():
517
515
  if not success:
518
516
  return False
519
517
 
520
- stdout, stderr, retc = result
518
+ stdout, _, retc = result
521
519
  if retc != 0:
522
520
  return False
523
521