linuxfabrik-lib 4.4.0__tar.gz → 5.0.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-4.4.0 → linuxfabrik_lib-5.0.0}/PKG-INFO +1 -1
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/base.py +8 -1
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/db_mysql.py +8 -6
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/disk.py +9 -2
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/distro.py +16 -11
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/dmidecode.py +2 -2
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/endoflifedate.py +22 -16
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/lftest.py +24 -13
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/linuxfabrik_lib.egg-info/PKG-INFO +1 -1
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/nextcloud.py +7 -5
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/pyproject.toml +1 -1
- linuxfabrik_lib-5.0.0/shell.py +204 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/ssh.py +74 -62
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/url.py +3 -4
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/version.py +18 -11
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/winrm.py +0 -0
- linuxfabrik_lib-4.4.0/shell.py +0 -235
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/LICENSE +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/README.md +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/__init__.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/args.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/cache.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/db_sqlite.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/feedparser.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/globals.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/grassfish.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/huawei.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/human.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/icinga.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/infomaniak.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/jitsi.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/keycloak.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/librenms.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/linuxfabrik_lib.egg-info/SOURCES.txt +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/linuxfabrik_lib.egg-info/dependency_links.txt +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/linuxfabrik_lib.egg-info/requires.txt +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/linuxfabrik_lib.egg-info/top_level.txt +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/net.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/nodebb.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/powershell.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/psutil.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/qts.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/redfish.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/rocket.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/setup.cfg +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/smb.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/time.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/txt.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/uptimerobot.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/veeam.py +0 -0
- {linuxfabrik_lib-4.4.0 → linuxfabrik_lib-5.0.0}/wildfly.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: linuxfabrik-lib
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.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.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"""Provides very common every-day functions."""
|
|
12
12
|
|
|
13
13
|
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
|
|
14
|
-
__version__ = '
|
|
14
|
+
__version__ = '2026061201'
|
|
15
15
|
|
|
16
16
|
import html
|
|
17
17
|
import numbers
|
|
@@ -119,6 +119,9 @@ def cu(msg=None):
|
|
|
119
119
|
tb = format_exc() if has_traceback else None
|
|
120
120
|
|
|
121
121
|
if msg is not None:
|
|
122
|
+
# Normalize line endings to LF (see oao); error output may also carry
|
|
123
|
+
# CRLF, for example when a Windows command prints its error to stdout.
|
|
124
|
+
msg = msg.replace('\r\n', '\n').replace('\r', '\n')
|
|
122
125
|
msg = (
|
|
123
126
|
txt.sanitize_sensitive_data(msg).strip().replace('<', "'").replace('>', "'")
|
|
124
127
|
)
|
|
@@ -628,6 +631,10 @@ def oao(msg, state=STATE_OK, perfdata='', always_ok=False):
|
|
|
628
631
|
(and exits with code 2)
|
|
629
632
|
|
|
630
633
|
"""
|
|
634
|
+
# Normalize line endings to LF. Output captured on Windows (or read from a
|
|
635
|
+
# file or HTTP response) can carry CRLF or stray CR, which a web UI showing
|
|
636
|
+
# the output with `white-space: pre-wrap` would render as an extra line break.
|
|
637
|
+
msg = msg.replace('\r\n', '\n').replace('\r', '\n')
|
|
631
638
|
msg = html.escape(
|
|
632
639
|
txt.sanitize_sensitive_data(msg.strip()),
|
|
633
640
|
quote=False,
|
|
@@ -20,7 +20,7 @@ from .globals import STATE_UNKNOWN
|
|
|
20
20
|
warnings.filterwarnings('ignore', category=UserWarning, module='pymysql')
|
|
21
21
|
|
|
22
22
|
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
|
|
23
|
-
__version__ = '
|
|
23
|
+
__version__ = '2026061201'
|
|
24
24
|
|
|
25
25
|
try:
|
|
26
26
|
import pymysql.cursors
|
|
@@ -302,7 +302,9 @@ def _align_session_collation(conn):
|
|
|
302
302
|
# Whitelist identifiers to keep the formatted `SET NAMES` safe from
|
|
303
303
|
# any caller-controlled value (the row comes from a system view, but
|
|
304
304
|
# defence in depth doesn't cost anything).
|
|
305
|
-
if not (
|
|
305
|
+
if not (
|
|
306
|
+
re.match(r'^[A-Za-z0-9_]+$', cs) and re.match(r'^[A-Za-z0-9_]+$', coll)
|
|
307
|
+
):
|
|
306
308
|
return
|
|
307
309
|
with conn.cursor() as cursor:
|
|
308
310
|
cursor.execute(f'set names {cs} collate {coll}')
|
|
@@ -541,10 +543,10 @@ def get_server_info(banner=None):
|
|
|
541
543
|
from . import shell
|
|
542
544
|
|
|
543
545
|
for command in (
|
|
544
|
-
'mysqld --version',
|
|
545
|
-
'mariadbd --version',
|
|
546
|
-
'mariadb --version',
|
|
547
|
-
'mysql --version',
|
|
546
|
+
['mysqld', '--version'],
|
|
547
|
+
['mariadbd', '--version'],
|
|
548
|
+
['mariadb', '--version'],
|
|
549
|
+
['mysql', '--version'],
|
|
548
550
|
):
|
|
549
551
|
success, result = shell.shell_exec(command)
|
|
550
552
|
if not success:
|
|
@@ -13,7 +13,7 @@ partitions, grepping a file, etc.
|
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
|
|
16
|
-
__version__ = '
|
|
16
|
+
__version__ = '2026061201'
|
|
17
17
|
|
|
18
18
|
import csv
|
|
19
19
|
import os
|
|
@@ -747,7 +747,14 @@ def udevadm(device, _property):
|
|
|
747
747
|
>>> udevadm('/dev/linuxfabrik', 'DEVNAME')
|
|
748
748
|
''
|
|
749
749
|
"""
|
|
750
|
-
|
|
750
|
+
# Only query real device nodes under /dev. Resolving the path first defends
|
|
751
|
+
# against a crafted `device` that points elsewhere or smuggles arguments.
|
|
752
|
+
device = os.path.realpath(device)
|
|
753
|
+
if not device.startswith('/dev/'):
|
|
754
|
+
return ''
|
|
755
|
+
success, result = shell.shell_exec(
|
|
756
|
+
['udevadm', 'info', '--query=property', f'--name={device}']
|
|
757
|
+
)
|
|
751
758
|
if not success:
|
|
752
759
|
return ''
|
|
753
760
|
stdout, _, _ = result
|
|
@@ -17,15 +17,13 @@ Source Code is taken, converted, shortened and modified from:
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
|
|
20
|
-
__version__ = '
|
|
20
|
+
__version__ = '2026061201'
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
import os
|
|
24
24
|
import platform
|
|
25
25
|
import re
|
|
26
26
|
|
|
27
|
-
from . import shell
|
|
28
|
-
|
|
29
27
|
# --- Static mappings ---
|
|
30
28
|
|
|
31
29
|
OSDIST_LIST = (
|
|
@@ -430,15 +428,22 @@ def get_distribution_facts():
|
|
|
430
428
|
distro = facts.get('distribution_file_variety', 'distribution')
|
|
431
429
|
facts['os_family'] = _map_os_family(distro) # returns 'RedHat', for example
|
|
432
430
|
|
|
433
|
-
#
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
431
|
+
# read NAME and VERSION from /etc/os-release
|
|
432
|
+
values = {}
|
|
433
|
+
try:
|
|
434
|
+
with open('/etc/os-release', encoding='utf-8') as os_release:
|
|
435
|
+
for line in os_release:
|
|
436
|
+
line = line.strip()
|
|
437
|
+
if not line or line.startswith('#') or '=' not in line:
|
|
438
|
+
continue
|
|
439
|
+
key, value = line.split('=', 1)
|
|
440
|
+
values[key] = value.strip().strip('"').strip("'")
|
|
441
|
+
except OSError:
|
|
437
442
|
return facts
|
|
438
443
|
|
|
439
|
-
|
|
440
|
-
facts['os_info'] = (
|
|
441
|
-
|
|
442
|
-
)
|
|
444
|
+
# returns 'Fedora Linux 41 (Workstation Edition)', for example
|
|
445
|
+
facts['os_info'] = ' '.join(
|
|
446
|
+
part for part in (values.get('NAME'), values.get('VERSION')) if part
|
|
447
|
+
)
|
|
443
448
|
|
|
444
449
|
return facts
|
|
@@ -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__ = '
|
|
17
|
+
__version__ = '2026061201'
|
|
18
18
|
|
|
19
19
|
import re
|
|
20
20
|
|
|
@@ -511,7 +511,7 @@ def get_data():
|
|
|
511
511
|
...
|
|
512
512
|
}
|
|
513
513
|
"""
|
|
514
|
-
success, result = shell.shell_exec('sudo dmidecode')
|
|
514
|
+
success, result = shell.shell_exec(['sudo', 'dmidecode'])
|
|
515
515
|
if not success:
|
|
516
516
|
return False
|
|
517
517
|
|
|
@@ -15,11 +15,11 @@ Internet."""
|
|
|
15
15
|
# pylint: disable=C0302
|
|
16
16
|
|
|
17
17
|
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
|
|
18
|
-
__version__ = '
|
|
18
|
+
__version__ = '2026061001' # adapt this if something in this script changed fundamentally
|
|
19
19
|
|
|
20
20
|
ENDOFLIFE_DATE = {
|
|
21
21
|
|
|
22
|
-
'https://endoflife.date/api/apache.json': [
|
|
22
|
+
'https://endoflife.date/api/apache-http-server.json': [
|
|
23
23
|
{ 'cycle': '2.4',
|
|
24
24
|
'eol': False,
|
|
25
25
|
'latest': '2.4.67',
|
|
@@ -503,22 +503,22 @@ ENDOFLIFE_DATE = {
|
|
|
503
503
|
'https://endoflife.date/api/gitlab.json': [
|
|
504
504
|
{ 'cycle': '19.0',
|
|
505
505
|
'eol': '2026-08-20',
|
|
506
|
-
'latest': '19.0.
|
|
507
|
-
'latestReleaseDate': '2026-
|
|
506
|
+
'latest': '19.0.2',
|
|
507
|
+
'latestReleaseDate': '2026-06-10',
|
|
508
508
|
'lts': False,
|
|
509
509
|
'releaseDate': '2026-05-21',
|
|
510
510
|
'support': '2026-06-18'},
|
|
511
511
|
{ 'cycle': '18.11',
|
|
512
512
|
'eol': '2026-07-16',
|
|
513
|
-
'latest': '18.11.
|
|
514
|
-
'latestReleaseDate': '2026-
|
|
513
|
+
'latest': '18.11.5',
|
|
514
|
+
'latestReleaseDate': '2026-06-10',
|
|
515
515
|
'lts': False,
|
|
516
516
|
'releaseDate': '2026-04-16',
|
|
517
517
|
'support': '2026-05-21'},
|
|
518
518
|
{ 'cycle': '18.10',
|
|
519
519
|
'eol': '2026-06-18',
|
|
520
|
-
'latest': '18.10.
|
|
521
|
-
'latestReleaseDate': '2026-
|
|
520
|
+
'latest': '18.10.8',
|
|
521
|
+
'latestReleaseDate': '2026-06-10',
|
|
522
522
|
'lts': False,
|
|
523
523
|
'releaseDate': '2026-03-19',
|
|
524
524
|
'support': '2026-04-16'},
|
|
@@ -956,7 +956,7 @@ ENDOFLIFE_DATE = {
|
|
|
956
956
|
{ 'cycle': '12.4',
|
|
957
957
|
'eol': '2027-05-24',
|
|
958
958
|
'latest': '12.4.4',
|
|
959
|
-
'latestReleaseDate': '2026-06-
|
|
959
|
+
'latestReleaseDate': '2026-06-09',
|
|
960
960
|
'lts': False,
|
|
961
961
|
'releaseDate': '2026-02-24',
|
|
962
962
|
'support': '2026-04-17'},
|
|
@@ -1927,8 +1927,8 @@ ENDOFLIFE_DATE = {
|
|
|
1927
1927
|
'https://endoflife.date/api/matomo.json': [
|
|
1928
1928
|
{ 'cycle': '5',
|
|
1929
1929
|
'eol': False,
|
|
1930
|
-
'latest': '5.11.
|
|
1931
|
-
'latestReleaseDate': '2026-06-
|
|
1930
|
+
'latest': '5.11.1',
|
|
1931
|
+
'latestReleaseDate': '2026-06-12',
|
|
1932
1932
|
'lts': False,
|
|
1933
1933
|
'releaseDate': '2023-12-18',
|
|
1934
1934
|
'support': True},
|
|
@@ -2297,6 +2297,12 @@ ENDOFLIFE_DATE = {
|
|
|
2297
2297
|
|
|
2298
2298
|
|
|
2299
2299
|
'https://endoflife.date/api/nextcloud.json': [
|
|
2300
|
+
{ 'cycle': '34',
|
|
2301
|
+
'eol': '2027-06-30',
|
|
2302
|
+
'latest': '34.0.0',
|
|
2303
|
+
'latestReleaseDate': '2026-06-08',
|
|
2304
|
+
'lts': False,
|
|
2305
|
+
'releaseDate': '2026-06-09'},
|
|
2300
2306
|
{ 'cycle': '33',
|
|
2301
2307
|
'eol': '2027-02-28',
|
|
2302
2308
|
'latest': '33.0.5',
|
|
@@ -2944,16 +2950,16 @@ ENDOFLIFE_DATE = {
|
|
|
2944
2950
|
'https://endoflife.date/api/python.json': [
|
|
2945
2951
|
{ 'cycle': '3.14',
|
|
2946
2952
|
'eol': '2030-10-31',
|
|
2947
|
-
'latest': '3.14.
|
|
2948
|
-
'latestReleaseDate': '2026-
|
|
2953
|
+
'latest': '3.14.6',
|
|
2954
|
+
'latestReleaseDate': '2026-06-10',
|
|
2949
2955
|
'lts': False,
|
|
2950
2956
|
'pep': 'PEP-0745',
|
|
2951
2957
|
'releaseDate': '2025-10-07',
|
|
2952
2958
|
'support': '2027-10-01'},
|
|
2953
2959
|
{ 'cycle': '3.13',
|
|
2954
2960
|
'eol': '2029-10-31',
|
|
2955
|
-
'latest': '3.13.
|
|
2956
|
-
'latestReleaseDate': '2026-
|
|
2961
|
+
'latest': '3.13.14',
|
|
2962
|
+
'latestReleaseDate': '2026-06-10',
|
|
2957
2963
|
'lts': False,
|
|
2958
2964
|
'pep': 'PEP-0719',
|
|
2959
2965
|
'releaseDate': '2024-10-07',
|
|
@@ -3273,7 +3279,7 @@ ENDOFLIFE_DATE = {
|
|
|
3273
3279
|
],
|
|
3274
3280
|
|
|
3275
3281
|
|
|
3276
|
-
'https://endoflife.date/api/
|
|
3282
|
+
'https://endoflife.date/api/rocket-chat.json': [
|
|
3277
3283
|
{ 'cycle': '8.4',
|
|
3278
3284
|
'eol': '2026-10-31',
|
|
3279
3285
|
'latest': '8.4.3',
|
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
import contextlib
|
|
14
14
|
import os
|
|
15
15
|
import re
|
|
16
|
+
import shlex
|
|
16
17
|
import tempfile
|
|
17
18
|
|
|
18
19
|
from . import base, disk, shell
|
|
19
20
|
|
|
20
21
|
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
|
|
21
|
-
__version__ = '
|
|
22
|
+
__version__ = '2026061201'
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
def run(test_instance, plugin, testcase):
|
|
@@ -73,8 +74,10 @@ def run(test_instance, plugin, testcase):
|
|
|
73
74
|
... with self.subTest(id=t['id']):
|
|
74
75
|
... lib.lftest.run(self, self.check, t)
|
|
75
76
|
"""
|
|
77
|
+
# params come from the trusted testcase definition; tokenize them the way a
|
|
78
|
+
# shell would so quoted multi-word values stay intact, then build the argv.
|
|
76
79
|
params = testcase.get('params', '')
|
|
77
|
-
cmd =
|
|
80
|
+
cmd = [plugin, *shlex.split(params), f'--test={testcase["test"]}']
|
|
78
81
|
stdout, stderr, retc = base.coe(shell.shell_exec(cmd))
|
|
79
82
|
|
|
80
83
|
test_instance.assertEqual(
|
|
@@ -133,7 +136,6 @@ def attach_tests(test_class, tests, plugin_attr='check'):
|
|
|
133
136
|
### Example
|
|
134
137
|
>>> class TestCheck(unittest.TestCase):
|
|
135
138
|
... check = '../my-plugin'
|
|
136
|
-
...
|
|
137
139
|
>>> attach_tests(TestCheck, TESTS)
|
|
138
140
|
>>>
|
|
139
141
|
>>> if __name__ == '__main__':
|
|
@@ -157,6 +159,7 @@ def attach_tests(test_class, tests, plugin_attr='check'):
|
|
|
157
159
|
def _make(captured_testcase):
|
|
158
160
|
def _method(self):
|
|
159
161
|
run(self, getattr(self, plugin_attr), captured_testcase)
|
|
162
|
+
|
|
160
163
|
return _method
|
|
161
164
|
|
|
162
165
|
setattr(test_class, method_name, _make(testcase))
|
|
@@ -222,6 +225,7 @@ def attach_each(test_class, items, action, id_func=str):
|
|
|
222
225
|
def _make(captured_item):
|
|
223
226
|
def _method(self):
|
|
224
227
|
action(self, captured_item)
|
|
228
|
+
|
|
225
229
|
return _method
|
|
226
230
|
|
|
227
231
|
setattr(test_class, method_name, _make(item))
|
|
@@ -339,8 +343,7 @@ def run_container(
|
|
|
339
343
|
from testcontainers.core.wait_strategies import LogMessageWaitStrategy
|
|
340
344
|
except ImportError as e:
|
|
341
345
|
raise RuntimeError(
|
|
342
|
-
'testcontainers is not installed; run '
|
|
343
|
-
"`pip install testcontainers`"
|
|
346
|
+
'testcontainers is not installed; run `pip install testcontainers`'
|
|
344
347
|
) from e
|
|
345
348
|
|
|
346
349
|
c = DockerContainer(image)
|
|
@@ -456,12 +459,15 @@ def _run_mysql_compatible_resolved(image_ref, command, seed):
|
|
|
456
459
|
# in favour of `mariadb`; sclorg c10s ships both; MySQL
|
|
457
460
|
# ships `mysql` only. Prefer `mariadb`, fall back to
|
|
458
461
|
# `mysql` so all flavours work.
|
|
459
|
-
container.exec(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
462
|
+
container.exec(
|
|
463
|
+
[
|
|
464
|
+
'sh',
|
|
465
|
+
'-c',
|
|
466
|
+
'if command -v mariadb >/dev/null 2>&1; then CLIENT=mariadb; '
|
|
467
|
+
'else CLIENT=mysql; fi; '
|
|
468
|
+
f'"$CLIENT" -utest -ptest test -e "{seed}"',
|
|
469
|
+
]
|
|
470
|
+
)
|
|
465
471
|
|
|
466
472
|
host = container.get_container_host_ip()
|
|
467
473
|
port = container.get_exposed_port(3306)
|
|
@@ -574,7 +580,8 @@ def run_mysql_compatible_from_containerfile(
|
|
|
574
580
|
... ) as (container, defaults_file):
|
|
575
581
|
... result = subprocess.run(
|
|
576
582
|
... ['python3', '../mysql-traffic', f'--defaults-file={defaults_file}'],
|
|
577
|
-
... capture_output=True,
|
|
583
|
+
... capture_output=True,
|
|
584
|
+
... text=True,
|
|
578
585
|
... )
|
|
579
586
|
"""
|
|
580
587
|
try:
|
|
@@ -592,6 +599,8 @@ def run_mysql_compatible_from_containerfile(
|
|
|
592
599
|
dockerfile_name = os.path.basename(abspath)
|
|
593
600
|
tag = f'lfmp-mysql-{dockerfile_name}'.lower().replace('_', '-')
|
|
594
601
|
|
|
602
|
+
# No parenthesized context managers: they are Python 3.10+ syntax and break
|
|
603
|
+
# parsing on RHEL 8's default Python 3.6.
|
|
595
604
|
with (
|
|
596
605
|
DockerImage(
|
|
597
606
|
path=build_dir,
|
|
@@ -600,7 +609,9 @@ def run_mysql_compatible_from_containerfile(
|
|
|
600
609
|
clean_up=False,
|
|
601
610
|
) as image,
|
|
602
611
|
_run_mysql_compatible_resolved(
|
|
603
|
-
str(image.tag),
|
|
612
|
+
str(image.tag),
|
|
613
|
+
command,
|
|
614
|
+
seed,
|
|
604
615
|
) as result,
|
|
605
616
|
):
|
|
606
617
|
yield result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: linuxfabrik-lib
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.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.
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
"""This library collects some Nextcloud related functions."""
|
|
12
12
|
|
|
13
13
|
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
|
|
14
|
-
__version__ = '
|
|
14
|
+
__version__ = '2026061201'
|
|
15
15
|
|
|
16
16
|
import json
|
|
17
17
|
import os
|
|
18
|
+
import shlex
|
|
18
19
|
import shutil
|
|
19
20
|
|
|
20
21
|
from . import disk, shell
|
|
@@ -80,16 +81,17 @@ def run_occ(path, cmd, _format='json'):
|
|
|
80
81
|
# get the owner of config.php
|
|
81
82
|
user = disk.get_owner(os.path.join(path, 'config/config.php'))
|
|
82
83
|
occ = os.path.join(path, 'occ')
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
sudo_cmd =
|
|
84
|
+
# Run occ as the numeric UID of the config.php owner. `sudo -u '#<uid>'` selects
|
|
85
|
+
# the user by UID; the `#` only needs escaping for a shell, which we do not use.
|
|
86
|
+
sudo_cmd = ['sudo', '-u', f'#{user}', php, occ, *shlex.split(cmd)]
|
|
86
87
|
|
|
87
88
|
success, result = shell.shell_exec(sudo_cmd)
|
|
88
89
|
stdout, stderr, rc = result
|
|
89
90
|
|
|
90
91
|
# Prefer the return code to decide success/failure, not stderr presence
|
|
91
92
|
if not success or rc != 0:
|
|
92
|
-
|
|
93
|
+
cmd_display = ' '.join(sudo_cmd)
|
|
94
|
+
return False, f'Error running `{cmd_display}`: rc={rc}\n{stderr or stdout}'
|
|
93
95
|
|
|
94
96
|
# If we expect JSON, try to parse it; otherwise return text
|
|
95
97
|
if str(_format).lower() == 'json':
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "linuxfabrik-lib"
|
|
9
|
-
version = "
|
|
9
|
+
version = "5.0.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 = [
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8; py-indent-offset: 4 -*-
|
|
3
|
+
#
|
|
4
|
+
# Author: Linuxfabrik GmbH, Zurich, Switzerland
|
|
5
|
+
# Contact: info (at) linuxfabrik (dot) ch
|
|
6
|
+
# https://www.linuxfabrik.ch/
|
|
7
|
+
# License: The Unlicense, see LICENSE file.
|
|
8
|
+
|
|
9
|
+
# https://github.com/Linuxfabrik/monitoring-plugins/blob/main/CONTRIBUTING.rst
|
|
10
|
+
|
|
11
|
+
"""Communicates with the Shell on Linux and Windows."""
|
|
12
|
+
|
|
13
|
+
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
|
|
14
|
+
__version__ = '2026061202'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess # nosec B404 - this library is the subprocess helper
|
|
20
|
+
|
|
21
|
+
from . import txt
|
|
22
|
+
|
|
23
|
+
RETC_SSHPASS = {
|
|
24
|
+
1: 'Invalid command line argument',
|
|
25
|
+
2: 'Conflicting arguments given',
|
|
26
|
+
3: 'General runtime error',
|
|
27
|
+
4: 'Unrecognized response from ssh (parse error)',
|
|
28
|
+
5: 'Invalid/incorrect password',
|
|
29
|
+
6: 'Host public key is unknown. sshpass exits without confirming the new key.',
|
|
30
|
+
7: 'IP public key changed. sshpass exits without confirming the new key.',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def shell_exec(cmd, env=None, stdin='', cwd=None, timeout=None, lc_all='C'):
|
|
35
|
+
"""
|
|
36
|
+
Execute a command in a subprocess, given as a list of arguments (argv).
|
|
37
|
+
|
|
38
|
+
The command is always run with `shell=False`, so no shell is involved: arguments are passed
|
|
39
|
+
verbatim to the executable and are never interpreted for pipes (`|`), redirection, globbing,
|
|
40
|
+
variable expansion or any other shell metacharacter. This makes the helper safe to call with
|
|
41
|
+
untrusted argument values: a value like `|reboot|` or `; rm -rf /` ends up as one literal
|
|
42
|
+
argument, not as a command.
|
|
43
|
+
|
|
44
|
+
Because there is no shell, `cmd` must be a list (argv). Passing a string raises `TypeError`.
|
|
45
|
+
Build the command as a list, for example `['df', '--human-readable', mountpoint]`, where
|
|
46
|
+
`mountpoint` may be untrusted. A genuine pipeline (`a | b`) has to be expressed in code by
|
|
47
|
+
running the stages and connecting them explicitly, or by post-processing the first command's
|
|
48
|
+
output; it can no longer be expressed as a shell string.
|
|
49
|
+
|
|
50
|
+
### Parameters
|
|
51
|
+
- **cmd** (`list`):
|
|
52
|
+
The command to execute, as a list of arguments (argv), e.g. `['ls', '-l', '/tmp']`.
|
|
53
|
+
The first element is the program, the rest are its arguments.
|
|
54
|
+
- **env** (`dict`, optional):
|
|
55
|
+
A dictionary of environment variables to merge with the current OS environment.
|
|
56
|
+
Defaults to the current environment.
|
|
57
|
+
- **stdin** (`str`, optional):
|
|
58
|
+
A string to pass as standard input to the command. Defaults to an empty string.
|
|
59
|
+
- **cwd** (`str`, optional):
|
|
60
|
+
Working directory in which to execute the command. Defaults to None (current directory).
|
|
61
|
+
- **timeout** (`int` or `float`, optional):
|
|
62
|
+
Maximum time (in seconds) to allow the command to run. If exceeded, the process is
|
|
63
|
+
terminated. Defaults to None (no timeout).
|
|
64
|
+
- **lc_all** (`str`, optional):
|
|
65
|
+
Value to set for the `LC_ALL` environment variable, forcing command output locale.
|
|
66
|
+
Defaults to `'C'` (POSIX "C" locale, i.e., English).
|
|
67
|
+
|
|
68
|
+
### Returns
|
|
69
|
+
- **tuple**:
|
|
70
|
+
- On success:
|
|
71
|
+
`(True, (stdout, stderr, return_code))`
|
|
72
|
+
- **stdout** (`str`): Standard output of the command (decoded to text).
|
|
73
|
+
- **stderr** (`str`): Standard error of the command (decoded to text).
|
|
74
|
+
- **return_code** (`int`): Exit status of the command.
|
|
75
|
+
- On failure:
|
|
76
|
+
`(False, error_message)` — a string describing the error.
|
|
77
|
+
|
|
78
|
+
### Notes
|
|
79
|
+
- The environment is merged with `env` and always includes `LC_ALL=<lc_all>`, forcing output
|
|
80
|
+
to the specified locale.
|
|
81
|
+
- Exceptions such as `OSError`, `ValueError`, or other execution errors during process
|
|
82
|
+
creation are caught and reported as `(False, <error message>)`.
|
|
83
|
+
- If the process exceeds the specified `timeout`, it is killed, and the function returns
|
|
84
|
+
`(False, "Timeout after <timeout> seconds.")`.
|
|
85
|
+
"""
|
|
86
|
+
if not isinstance(cmd, (list, tuple)):
|
|
87
|
+
raise TypeError(
|
|
88
|
+
'shell_exec() requires cmd as a list of arguments '
|
|
89
|
+
f'(for example ["df", "-h"]), got {type(cmd).__name__}.'
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
env = {**os.environ.copy(), **(env or {})}
|
|
93
|
+
env['LC_ALL'] = lc_all
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
p = subprocess.Popen( # nosec B603 - shell=False, cmd is an argv list, no shell interpretation
|
|
97
|
+
cmd,
|
|
98
|
+
stdin=subprocess.PIPE,
|
|
99
|
+
stdout=subprocess.PIPE,
|
|
100
|
+
stderr=subprocess.PIPE,
|
|
101
|
+
env=env,
|
|
102
|
+
shell=False,
|
|
103
|
+
cwd=cwd,
|
|
104
|
+
)
|
|
105
|
+
except (OSError, ValueError, Exception) as e:
|
|
106
|
+
return False, f'Error "{e}" while calling command "{cmd}"'
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
stdout, stderr = p.communicate(
|
|
110
|
+
input=txt.to_bytes(stdin) if stdin else None,
|
|
111
|
+
timeout=timeout,
|
|
112
|
+
)
|
|
113
|
+
except subprocess.TimeoutExpired:
|
|
114
|
+
p.kill()
|
|
115
|
+
p.communicate()
|
|
116
|
+
return False, f'Timeout after {timeout} seconds.'
|
|
117
|
+
|
|
118
|
+
# Decode the captured bytes. On Windows, console programs (query, schtasks,
|
|
119
|
+
# w32tm, ...) write their piped output in the OEM / console output code page
|
|
120
|
+
# (e.g. cp437, cp850), not UTF-8 and not the ANSI code page, so a username
|
|
121
|
+
# like "müller" would otherwise be mangled (Linuxfabrik/monitoring-plugins#681).
|
|
122
|
+
if os.name == 'nt':
|
|
123
|
+
encoding, errors = _windows_output_encoding(), 'replace'
|
|
124
|
+
else:
|
|
125
|
+
encoding, errors = 'utf-8', 'surrogateescape'
|
|
126
|
+
return True, (
|
|
127
|
+
txt.to_text(stdout, encoding=encoding, errors=errors),
|
|
128
|
+
txt.to_text(stderr, encoding=encoding, errors=errors),
|
|
129
|
+
p.returncode,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _windows_output_encoding():
|
|
134
|
+
"""
|
|
135
|
+
Best-effort code page name for decoding a Windows subprocess's piped output.
|
|
136
|
+
|
|
137
|
+
Console programs emit their pipe output in the OEM / console output code page, not UTF-8 and
|
|
138
|
+
not the ANSI code page (`chcp 65001` has no effect on a pipe; see PEP 528). Prefer the console
|
|
139
|
+
output code page, fall back to the OEM code page when the process has no console (for example
|
|
140
|
+
when run headless by a monitoring agent), and fall back to UTF-8 if neither is available.
|
|
141
|
+
|
|
142
|
+
### Returns
|
|
143
|
+
- **str**: A Python codec name such as `'cp437'`, or `'utf-8'` as a last resort.
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
import ctypes
|
|
147
|
+
|
|
148
|
+
kernel32 = ctypes.windll.kernel32
|
|
149
|
+
code_page = kernel32.GetConsoleOutputCP() or kernel32.GetOEMCP()
|
|
150
|
+
if code_page:
|
|
151
|
+
return f'cp{code_page}'
|
|
152
|
+
except (OSError, AttributeError, ValueError):
|
|
153
|
+
pass
|
|
154
|
+
return 'utf-8'
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def safe_cli_value(value, name='value'):
|
|
158
|
+
"""
|
|
159
|
+
Reject a value that a called program could misinterpret as an option.
|
|
160
|
+
|
|
161
|
+
Building a command as an argument list (argv) and running it with `shell_exec()` prevents
|
|
162
|
+
shell injection, but it does not stop a value that starts with `-` from being picked up as an
|
|
163
|
+
*option* by the program being run, for example an ssh destination `-oProxyCommand=...` (remote
|
|
164
|
+
code execution) or a `ping` target `-f` (flood). Use this for values that reach a command as a
|
|
165
|
+
positional argument or as a command target, where option-style values have no legitimate
|
|
166
|
+
meaning. Values that are bound to an explicit option (`--name=<value>` or `-H <value>`) do not
|
|
167
|
+
need this guard.
|
|
168
|
+
|
|
169
|
+
### Parameters
|
|
170
|
+
- **value** (`any`): The value to check. Non-string values pass through unchanged.
|
|
171
|
+
- **name** (`str`, optional): Human-readable name used in the error message. Defaults to
|
|
172
|
+
`'value'`.
|
|
173
|
+
|
|
174
|
+
### Returns
|
|
175
|
+
- **tuple**: `(True, value)` if the value is safe, else `(False, error_message)`. The shape
|
|
176
|
+
is suitable for `lib.base.coe()`.
|
|
177
|
+
|
|
178
|
+
### Example
|
|
179
|
+
>>> host = lib.base.coe(lib.shell.safe_cli_value(args.HOSTNAME, '--hostname'))
|
|
180
|
+
"""
|
|
181
|
+
if isinstance(value, str) and value.startswith('-'):
|
|
182
|
+
return False, f'Refusing {name} that starts with "-": {value}'
|
|
183
|
+
return True, value
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def which(name):
|
|
187
|
+
"""
|
|
188
|
+
Locate an executable in the system PATH, like the `which` command.
|
|
189
|
+
|
|
190
|
+
Thin wrapper around `shutil.which()` so callers do not need to import it
|
|
191
|
+
directly and the lookup stays consistent across consumers.
|
|
192
|
+
|
|
193
|
+
### Parameters
|
|
194
|
+
- **name** (`str`): Program name to look for (e.g. `lynis`).
|
|
195
|
+
|
|
196
|
+
### Returns
|
|
197
|
+
- **str or None**: The absolute path to the executable, or `None` if it is
|
|
198
|
+
not found in PATH.
|
|
199
|
+
|
|
200
|
+
### Example
|
|
201
|
+
>>> which('sh')
|
|
202
|
+
'/usr/bin/sh'
|
|
203
|
+
"""
|
|
204
|
+
return shutil.which(name)
|