linuxfabrik-lib 8.2.0__tar.gz → 8.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.
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/PKG-INFO +39 -19
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/README.md +37 -16
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/args.py +151 -34
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/base.py +142 -32
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/container.py +96 -3
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/db_mysql.py +130 -23
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/db_sqlite.py +44 -63
- linuxfabrik_lib-8.3.0/disk.py +2508 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/dmidecode.py +59 -15
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/endoflifedate.py +2115 -326
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/feedparser.py +74 -46
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/huawei_dorado.py +30 -4
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/huawei_pacific.py +29 -14
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/human.py +55 -20
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/icinga.py +30 -6
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/infomaniak.py +9 -2
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/jitsi.py +4 -5
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/keycloak.py +24 -4
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/kvm.py +122 -3
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/lftest.py +195 -99
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/linuxfabrik_lib.egg-info/PKG-INFO +39 -19
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/linuxfabrik_lib.egg-info/SOURCES.txt +6 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/linuxfabrik_lib.egg-info/requires.txt +0 -1
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/logsource.py +482 -87
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/lvm.py +71 -60
- linuxfabrik_lib-8.3.0/lynis.py +226 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/mail.py +33 -18
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/net.py +312 -51
- linuxfabrik_lib-8.3.0/nextcloud.py +380 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/openstack.py +18 -11
- linuxfabrik_lib-8.3.0/powershell.py +87 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/pyproject.toml +2 -3
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/redfish.py +412 -269
- linuxfabrik_lib-8.3.0/restic.py +239 -0
- linuxfabrik_lib-8.3.0/rhelappstreams.py +1689 -0
- linuxfabrik_lib-8.3.0/shell.py +669 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/smb.py +79 -24
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/ssh.py +72 -14
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/task.py +136 -53
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/time.py +88 -7
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/txt.py +143 -34
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/uptimerobot.py +147 -52
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/url.py +613 -232
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/user.py +42 -11
- linuxfabrik_lib-8.3.0/version.py +741 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/winrm.py +322 -128
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/wordpress.py +146 -91
- linuxfabrik_lib-8.2.0/disk.py +0 -1469
- linuxfabrik_lib-8.2.0/nextcloud.py +0 -142
- linuxfabrik_lib-8.2.0/powershell.py +0 -82
- linuxfabrik_lib-8.2.0/shell.py +0 -373
- linuxfabrik_lib-8.2.0/version.py +0 -357
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/LICENSE +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/__init__.py +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/bexio.py +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/cache.py +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/distro.py +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/globals.py +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/grassfish.py +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/librenms.py +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/linuxfabrik_lib.egg-info/dependency_links.txt +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/linuxfabrik_lib.egg-info/top_level.txt +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/logmatch.py +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/nodebb.py +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/openmetrics.py +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/psi.py +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/psutil.py +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/qts.py +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/rocket.py +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/setup.cfg +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/veeam.py +0 -0
- {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/wildfly.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: linuxfabrik-lib
|
|
3
|
-
Version: 8.
|
|
4
|
-
Summary: Python
|
|
3
|
+
Version: 8.3.0
|
|
4
|
+
Summary: Python 3.9+ utility library: argument parsing, output formatting, HTTP, shell, disk, SQLite/MySQL, caching, version checks, plus API clients.
|
|
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.
|
|
7
7
|
|
|
@@ -63,7 +63,6 @@ Requires-Dist: lxml
|
|
|
63
63
|
Requires-Dist: psutil
|
|
64
64
|
Requires-Dist: pymysql
|
|
65
65
|
Requires-Dist: pypsrp
|
|
66
|
-
Requires-Dist: PySmbClient
|
|
67
66
|
Requires-Dist: pywinrm
|
|
68
67
|
Requires-Dist: smbprotocol
|
|
69
68
|
Requires-Dist: xmltodict
|
|
@@ -73,7 +72,7 @@ Dynamic: license-file
|
|
|
73
72
|
Linuxfabrik Python Libraries
|
|
74
73
|
</h1>
|
|
75
74
|
<p align="center">
|
|
76
|
-
Python 3.9+
|
|
75
|
+
Python 3.9+ utility library: argument parsing, output formatting, HTTP, shell, disk, SQLite/MySQL, caching, version checks, plus API clients. Available on PyPI.
|
|
77
76
|
<span>•</span>
|
|
78
77
|
<b>made by <a href="https://linuxfabrik.ch/">Linuxfabrik</a></b>
|
|
79
78
|
</p>
|
|
@@ -95,7 +94,7 @@ Dynamic: license-file
|
|
|
95
94
|
|
|
96
95
|
# Linuxfabrik Python Libraries
|
|
97
96
|
|
|
98
|
-
A mature, production-grade Python library collection
|
|
97
|
+
A mature, production-grade Python library collection for system administration, monitoring, and infrastructure automation. It does not assume a particular consumer: any Python tool can use it, from check plugins to deployment scripts.
|
|
99
98
|
|
|
100
99
|
> If these libraries help you developing your own monitoring plugins, system tools or infrastructure automation, please give it a star.
|
|
101
100
|
|
|
@@ -120,14 +119,14 @@ pip install --user linuxfabrik-lib
|
|
|
120
119
|
|
|
121
120
|
These libraries are built with a clear set of priorities:
|
|
122
121
|
|
|
123
|
-
* **Procedural by design.** The libraries deliberately use a procedural/functional style rather than object-oriented programming. Pure functions with explicit inputs and outputs are easier to read, test, and reason about.
|
|
122
|
+
* **Procedural by design.** The libraries deliberately use a procedural/functional style rather than object-oriented programming. Pure functions with explicit inputs and outputs are easier to read, test, and reason about. Most consumers are short-lived, linear processes with no complex state to manage over time, where unnecessary abstraction layers would add overhead without tangible benefit.
|
|
124
123
|
* **Broad compatibility.** Python 3.9+ is the minimum, ensuring the libraries work on RHEL 8 and every major distribution without requiring newer runtimes.
|
|
125
124
|
* **Cross-platform.** Core functions behave identically on Linux, Windows, and macOS. Platform-specific code (WinRM, PowerShell, SMB) is cleanly separated.
|
|
126
125
|
* **Minimal dependencies.** We avoid pulling in large dependency trees. External packages are used only when the alternative would be unreliable or significantly more complex.
|
|
127
|
-
* **Consistent error handling.** Most functions return `(success, result)` tuples. The caller decides whether to continue or exit
|
|
126
|
+
* **Consistent error handling.** Most functions return `(success, result)` tuples. The caller decides whether to continue or exit, the library never exits on its own. The `base.coe()` ("Continue or Exit") helper makes this pattern concise.
|
|
128
127
|
* **Automatic redaction.** Sensitive data (passwords, tokens, API keys) in error messages is automatically sanitized before output.
|
|
129
128
|
* **Nagios/Icinga conventions.** State constants, threshold evaluation, performance data formatting, and range parsing follow the [Monitoring Plugins Development Guidelines](https://www.monitoring-plugins.org/doc/guidelines.html).
|
|
130
|
-
* **Defensive defaults.** Functions use sensible timeouts, safe SSL settings, and locked-down defaults so that
|
|
129
|
+
* **Defensive defaults.** Functions use sensible timeouts, safe SSL settings, and locked-down defaults so that consumers work out of the box without extensive configuration.
|
|
131
130
|
|
|
132
131
|
|
|
133
132
|
## Library Index
|
|
@@ -138,7 +137,7 @@ These libraries are built with a clear set of priorities:
|
|
|
138
137
|
| Module | Description | Key Functions |
|
|
139
138
|
|--------|-------------|---------------|
|
|
140
139
|
| **args.py** | Extends `argparse` with custom input types for monitoring thresholds, a registry of reusable `--help` texts, and reading a secret from a file instead of the command line. | `csv()`, `help()`, `int_or_none()`, `load_secret()`, `number_unit_method()` |
|
|
141
|
-
| **base.py** | The central
|
|
140
|
+
| **base.py** | The central module for check-style tools. Provides state evaluation, threshold comparison, performance data formatting, ASCII table output, and the `coe()` error-handling pattern. | `coe()`, `cu()`, `get_perfdata()`, `get_state()`, `get_table()`, `get_worst()`, `oao()`, `state2str()` |
|
|
142
141
|
| **globals.py** | Defines the four Nagios/Icinga plugin states: `STATE_OK` (0), `STATE_WARN` (1), `STATE_CRIT` (2), `STATE_UNKNOWN` (3). | -- |
|
|
143
142
|
| **human.py** | Converts raw numbers, byte sizes, bit rates, and durations to human-readable representations and back. Supports binary/SI prefixes and Nagios range syntax with units. | `bits2human()`, `bps2human()`, `bytes2human()`, `human2bytes()`, `human2seconds()`, `humanrange2bytes()`, `number2human()`, `seconds2human()` |
|
|
144
143
|
| **lftest.py** | Test harness for data-driven plugin unit tests, spinning up throwaway containers (including MySQL/MariaDB) as fixtures. | `attach_each()`, `attach_tests()`, `run()`, `run_container()`, `run_mysql_compatible_from_containerfile()`, `test()`, `test_json()` |
|
|
@@ -146,28 +145,38 @@ These libraries are built with a clear set of priorities:
|
|
|
146
145
|
| **task.py** | Runs callables under a deadline that holds even where the work cannot be interrupted, each in a process of its own and all of them sharing one deadline. | `run()`, `run_each()` |
|
|
147
146
|
| **time.py** | Date/time conversions between UNIX epochs, ISO strings, and datetime objects, plus time-macro expansion and time differences. Timezone-aware. | `epoch2iso()`, `now()`, `timestr2datetime()`, `timestrdiff()` |
|
|
148
147
|
| **txt.py** | Text processing: regex compilation, substring extraction, multi-line parsing, sensitive data redaction, pluralization, shortening for display, HTML character reference resolution, and byte/text encoding conversion. | `compile_regex()`, `extract_str()`, `match_regex()`, `mltext2array()`, `pluralize()`, `shorten()`, `to_bytes()`, `to_text()`, `unescape()` |
|
|
149
|
-
| **version.py** | Software version parsing, comparison, and End-of-Life checking against [endoflife.date](https://endoflife.date). | `check_eol()`, `version()`, `version2float()` |
|
|
148
|
+
| **version.py** | Software version parsing, comparison, and End-of-Life checking against [endoflife.date](https://endoflife.date), or against the life cycle RHEL, Debian and Ubuntu give the software they ship. | `check_eol()`, `version()`, `version2float()` |
|
|
150
149
|
|
|
151
150
|
|
|
152
151
|
### Data Access & Caching
|
|
153
152
|
|
|
154
153
|
| Module | Description | Key Functions |
|
|
155
154
|
|--------|-------------|---------------|
|
|
156
|
-
| **cache.py** | A simple SQLite-based key-value store with optional key expiration. Used for persisting state between
|
|
155
|
+
| **cache.py** | A simple SQLite-based key-value store with optional key expiration. Used for persisting state between runs. | `get()`, `set()` |
|
|
157
156
|
| **db_mysql.py** | MySQL/MariaDB client with connection management, query execution, privilege checking, and server flavor/status/variable/replication inspection. | `check_privileges()`, `connect()`, `get_all_variables()`, `get_engines()`, `get_replica_status()`, `lod2dict()`, `select()` |
|
|
158
157
|
| **db_sqlite.py** | Full SQLite interface: table/index creation, CRUD operations, CSV import, regex support, and per-second counter deltas for time-series data. | `connect()`, `create_index()`, `create_table()`, `cut()`, `delete()`, `insert()`, `per_second_deltas()`, `select()` |
|
|
158
|
+
| **logmatch.py** | Keeps log findings in an SQLite state database across runs until they age out or are acknowledged on the monitoring server, together with the read position of the log. | `acknowledge()`, `connect()`, `key()`, `pending()`, `record()`, `service_acknowledged()` |
|
|
159
159
|
|
|
160
160
|
|
|
161
161
|
### System & OS
|
|
162
162
|
|
|
163
163
|
| Module | Description | Key Functions |
|
|
164
164
|
|--------|-------------|---------------|
|
|
165
|
+
| **container.py** | Runs a container engine command-line client (Docker, Podman) under a deadline and tells a missing permission apart from an engine that is down or does not answer. | `get_engine_error()`, `run()` |
|
|
165
166
|
| **disk.py** | File I/O, directory walking, CSV/environment file parsing, block device and partition listing, and temp directory management. | `dir_exists()`, `file_exists()`, `get_real_disks()`, `get_tmpdir()`, `grep_file()`, `read_file()` |
|
|
166
167
|
| **distro.py** | Linux distribution detection. Returns normalized facts including distribution name, version, and Ansible-compatible `os_family`. | `get_distribution_facts()` |
|
|
167
168
|
| **dmidecode.py** | Parses `dmidecode` output into structured data. Extracts CPU, RAM, firmware, serial number, manufacturer, and model information. | `cpu_speed()`, `cpu_type()`, `firmware()`, `get_data()`, `manufacturer()`, `model()`, `ram()`, `serno()` |
|
|
168
169
|
| **endoflifedate.py** | Bundled End-of-Life data from [endoflife.date](https://endoflife.date) for offline version checks when internet access is unavailable. | -- |
|
|
170
|
+
| **kvm.py** | libvirt domains, storage pools and volumes via `virsh` on a read-only connection, which needs neither root nor sudo. | `get_domains()`, `get_domstats()`, `get_pool_info()`, `get_pools()`, `get_volumes()`, `virsh()` |
|
|
171
|
+
| **logsource.py** | Reads a log incrementally from a file, a systemd unit in the journal, or a container, and hands back a position that survives rotation and truncation. | `parse()`, `read()`, `read_many()`, `timestamp()` |
|
|
172
|
+
| **lvm.py** | LVM logical volumes and volume groups via `lvs` and `vgs`, including thin pools, snapshots, health status and the thin pool metadata limit. | `get_logical_volumes()`, `get_volume_groups()`, `health()`, `is_snapshot_invalid()`, `is_thin_pool()`, `metadata_limit()` |
|
|
173
|
+
| **lynis.py** | The machine-readable report of a Lynis security audit (`/var/log/lynis-report.dat`): hardening index, findings and suggestions, and the list of them for the plugin output. | `get_findings_msg()`, `parse_findings()`, `parse_report()` |
|
|
174
|
+
| **psi.py** | Linux pressure stall information (PSI) from `/proc/pressure` for CPU, I/O, IRQ and memory, with state evaluation and report output. | `get_perfdata()`, `get_states()`, `get_summary()`, `get_table()`, `is_enabled()`, `read()` |
|
|
169
175
|
| **psutil.py** | Wrapper around `psutil` for retrieving mounted disk partitions with device, mount point, filesystem type and mount options. Lists them from the mount table alone, so a network filesystem whose server is gone cannot hold the listing up. | `get_partitions()` |
|
|
176
|
+
| **restic.py** | Builds the repository and password arguments for the `restic` backup client so a privileged caller cannot make it read an arbitrary file or run an external program. | `password_file_arg()`, `repo_args()` |
|
|
177
|
+
| **rhelappstreams.py** | Bundled life cycle of the RHEL Application Streams, generated weekly from the data Red Hat publishes in [digital-roadmap-backend](https://github.com/RedHatInsights/digital-roadmap-backend). | -- |
|
|
170
178
|
| **shell.py** | Runs external commands from an argv list without a shell, guards option-style CLI values, and locates executables in `PATH`. | `safe_cli_value()`, `shell_exec()`, `which()` |
|
|
179
|
+
| **user.py** | Local Unix accounts: UID/GID names, the system/regular account boundary, interactive shells and the state of the shadow password. | `get_gid_name()`, `get_interactive_shells()`, `get_shadow_password()`, `get_uid_min()`, `get_uid_name()`, `password_state()` |
|
|
171
180
|
|
|
172
181
|
|
|
173
182
|
### Networking & HTTP
|
|
@@ -205,6 +214,7 @@ These libraries are built with a clear set of priorities:
|
|
|
205
214
|
| **librenms.py** | [LibreNMS](https://www.librenms.org/) monitoring API, mapping its alert states to Nagios states. | `get_data()`, `get_state()` |
|
|
206
215
|
| **nextcloud.py** | [Nextcloud](https://nextcloud.com/) `occ` command execution as the `config.php` owner, parsing JSON or text output. | `run_occ()` |
|
|
207
216
|
| **nodebb.py** | [NodeBB](https://nodebb.org/) forum API, using a Bearer user token. | `get_data()` |
|
|
217
|
+
| **openstack.py** | [OpenStack](https://www.openstack.org/) Identity v3 authentication from an rc file, with the token reused across runs, and access to the service REST APIs. | `connect()`, `fetch()`, `fetch_json()` |
|
|
208
218
|
| **qts.py** | QNAP [QTS](https://www.qnap.com/) NAS API with session authentication. | `get_auth_sid()` |
|
|
209
219
|
| **redfish.py** | [Redfish](https://www.dmtf.org/standards/redfish) BMC API for chassis, systems, storage, managers, and sensors, deriving Nagios states and perfdata. | `get_auth_header()`, `get_perfdata()`, `get_state()`, `get_systems()` |
|
|
210
220
|
| **rocket.py** | [Rocket.Chat](https://www.rocket.chat/) REST API for login, room/group history, statistics, and incoming webhooks. | `get_groups_history()`, `get_rooms_get()`, `get_stats()`, `get_token()`, `send2webhook()` |
|
|
@@ -216,21 +226,31 @@ These libraries are built with a clear set of priorities:
|
|
|
216
226
|
|
|
217
227
|
## Usage Example
|
|
218
228
|
|
|
219
|
-
A typical
|
|
229
|
+
A typical check-style tool using these libraries:
|
|
220
230
|
|
|
221
231
|
```python
|
|
232
|
+
import argparse
|
|
233
|
+
|
|
222
234
|
import lib.args
|
|
223
235
|
import lib.base
|
|
224
236
|
import lib.url
|
|
225
|
-
from lib.globals import STATE_CRIT, STATE_OK, STATE_UNKNOWN, STATE_WARN
|
|
226
237
|
|
|
227
238
|
|
|
228
239
|
def main():
|
|
229
|
-
|
|
230
|
-
parser = lib.args.
|
|
231
|
-
parser.add_argument(
|
|
232
|
-
|
|
233
|
-
|
|
240
|
+
parser = argparse.ArgumentParser(description='Checks the usage reported by an API.')
|
|
241
|
+
parser.add_argument('--url', required=True, help=lib.args.help('--url'))
|
|
242
|
+
parser.add_argument(
|
|
243
|
+
'--warning',
|
|
244
|
+
type=float,
|
|
245
|
+
default=80,
|
|
246
|
+
help=lib.args.help('--warning') + ' Default: %(default)s',
|
|
247
|
+
)
|
|
248
|
+
parser.add_argument(
|
|
249
|
+
'--critical',
|
|
250
|
+
type=float,
|
|
251
|
+
default=90,
|
|
252
|
+
help=lib.args.help('--critical') + ' Default: %(default)s',
|
|
253
|
+
)
|
|
234
254
|
args = parser.parse_args()
|
|
235
255
|
|
|
236
256
|
# Fetch data (coe = "Continue or Exit")
|
|
@@ -243,7 +263,7 @@ def main():
|
|
|
243
263
|
)
|
|
244
264
|
|
|
245
265
|
# Output and exit
|
|
246
|
-
lib.base.oao('Usage is {
|
|
266
|
+
lib.base.oao(f'Usage is {result["usage"]}%', state, perfdata)
|
|
247
267
|
|
|
248
268
|
|
|
249
269
|
if __name__ == '__main__':
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Linuxfabrik Python Libraries
|
|
3
3
|
</h1>
|
|
4
4
|
<p align="center">
|
|
5
|
-
Python 3.9+
|
|
5
|
+
Python 3.9+ utility library: argument parsing, output formatting, HTTP, shell, disk, SQLite/MySQL, caching, version checks, plus API clients. Available on PyPI.
|
|
6
6
|
<span>•</span>
|
|
7
7
|
<b>made by <a href="https://linuxfabrik.ch/">Linuxfabrik</a></b>
|
|
8
8
|
</p>
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
# Linuxfabrik Python Libraries
|
|
26
26
|
|
|
27
|
-
A mature, production-grade Python library collection
|
|
27
|
+
A mature, production-grade Python library collection for system administration, monitoring, and infrastructure automation. It does not assume a particular consumer: any Python tool can use it, from check plugins to deployment scripts.
|
|
28
28
|
|
|
29
29
|
> If these libraries help you developing your own monitoring plugins, system tools or infrastructure automation, please give it a star.
|
|
30
30
|
|
|
@@ -49,14 +49,14 @@ pip install --user linuxfabrik-lib
|
|
|
49
49
|
|
|
50
50
|
These libraries are built with a clear set of priorities:
|
|
51
51
|
|
|
52
|
-
* **Procedural by design.** The libraries deliberately use a procedural/functional style rather than object-oriented programming. Pure functions with explicit inputs and outputs are easier to read, test, and reason about.
|
|
52
|
+
* **Procedural by design.** The libraries deliberately use a procedural/functional style rather than object-oriented programming. Pure functions with explicit inputs and outputs are easier to read, test, and reason about. Most consumers are short-lived, linear processes with no complex state to manage over time, where unnecessary abstraction layers would add overhead without tangible benefit.
|
|
53
53
|
* **Broad compatibility.** Python 3.9+ is the minimum, ensuring the libraries work on RHEL 8 and every major distribution without requiring newer runtimes.
|
|
54
54
|
* **Cross-platform.** Core functions behave identically on Linux, Windows, and macOS. Platform-specific code (WinRM, PowerShell, SMB) is cleanly separated.
|
|
55
55
|
* **Minimal dependencies.** We avoid pulling in large dependency trees. External packages are used only when the alternative would be unreliable or significantly more complex.
|
|
56
|
-
* **Consistent error handling.** Most functions return `(success, result)` tuples. The caller decides whether to continue or exit
|
|
56
|
+
* **Consistent error handling.** Most functions return `(success, result)` tuples. The caller decides whether to continue or exit, the library never exits on its own. The `base.coe()` ("Continue or Exit") helper makes this pattern concise.
|
|
57
57
|
* **Automatic redaction.** Sensitive data (passwords, tokens, API keys) in error messages is automatically sanitized before output.
|
|
58
58
|
* **Nagios/Icinga conventions.** State constants, threshold evaluation, performance data formatting, and range parsing follow the [Monitoring Plugins Development Guidelines](https://www.monitoring-plugins.org/doc/guidelines.html).
|
|
59
|
-
* **Defensive defaults.** Functions use sensible timeouts, safe SSL settings, and locked-down defaults so that
|
|
59
|
+
* **Defensive defaults.** Functions use sensible timeouts, safe SSL settings, and locked-down defaults so that consumers work out of the box without extensive configuration.
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
## Library Index
|
|
@@ -67,7 +67,7 @@ These libraries are built with a clear set of priorities:
|
|
|
67
67
|
| Module | Description | Key Functions |
|
|
68
68
|
|--------|-------------|---------------|
|
|
69
69
|
| **args.py** | Extends `argparse` with custom input types for monitoring thresholds, a registry of reusable `--help` texts, and reading a secret from a file instead of the command line. | `csv()`, `help()`, `int_or_none()`, `load_secret()`, `number_unit_method()` |
|
|
70
|
-
| **base.py** | The central
|
|
70
|
+
| **base.py** | The central module for check-style tools. Provides state evaluation, threshold comparison, performance data formatting, ASCII table output, and the `coe()` error-handling pattern. | `coe()`, `cu()`, `get_perfdata()`, `get_state()`, `get_table()`, `get_worst()`, `oao()`, `state2str()` |
|
|
71
71
|
| **globals.py** | Defines the four Nagios/Icinga plugin states: `STATE_OK` (0), `STATE_WARN` (1), `STATE_CRIT` (2), `STATE_UNKNOWN` (3). | -- |
|
|
72
72
|
| **human.py** | Converts raw numbers, byte sizes, bit rates, and durations to human-readable representations and back. Supports binary/SI prefixes and Nagios range syntax with units. | `bits2human()`, `bps2human()`, `bytes2human()`, `human2bytes()`, `human2seconds()`, `humanrange2bytes()`, `number2human()`, `seconds2human()` |
|
|
73
73
|
| **lftest.py** | Test harness for data-driven plugin unit tests, spinning up throwaway containers (including MySQL/MariaDB) as fixtures. | `attach_each()`, `attach_tests()`, `run()`, `run_container()`, `run_mysql_compatible_from_containerfile()`, `test()`, `test_json()` |
|
|
@@ -75,28 +75,38 @@ These libraries are built with a clear set of priorities:
|
|
|
75
75
|
| **task.py** | Runs callables under a deadline that holds even where the work cannot be interrupted, each in a process of its own and all of them sharing one deadline. | `run()`, `run_each()` |
|
|
76
76
|
| **time.py** | Date/time conversions between UNIX epochs, ISO strings, and datetime objects, plus time-macro expansion and time differences. Timezone-aware. | `epoch2iso()`, `now()`, `timestr2datetime()`, `timestrdiff()` |
|
|
77
77
|
| **txt.py** | Text processing: regex compilation, substring extraction, multi-line parsing, sensitive data redaction, pluralization, shortening for display, HTML character reference resolution, and byte/text encoding conversion. | `compile_regex()`, `extract_str()`, `match_regex()`, `mltext2array()`, `pluralize()`, `shorten()`, `to_bytes()`, `to_text()`, `unescape()` |
|
|
78
|
-
| **version.py** | Software version parsing, comparison, and End-of-Life checking against [endoflife.date](https://endoflife.date). | `check_eol()`, `version()`, `version2float()` |
|
|
78
|
+
| **version.py** | Software version parsing, comparison, and End-of-Life checking against [endoflife.date](https://endoflife.date), or against the life cycle RHEL, Debian and Ubuntu give the software they ship. | `check_eol()`, `version()`, `version2float()` |
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
### Data Access & Caching
|
|
82
82
|
|
|
83
83
|
| Module | Description | Key Functions |
|
|
84
84
|
|--------|-------------|---------------|
|
|
85
|
-
| **cache.py** | A simple SQLite-based key-value store with optional key expiration. Used for persisting state between
|
|
85
|
+
| **cache.py** | A simple SQLite-based key-value store with optional key expiration. Used for persisting state between runs. | `get()`, `set()` |
|
|
86
86
|
| **db_mysql.py** | MySQL/MariaDB client with connection management, query execution, privilege checking, and server flavor/status/variable/replication inspection. | `check_privileges()`, `connect()`, `get_all_variables()`, `get_engines()`, `get_replica_status()`, `lod2dict()`, `select()` |
|
|
87
87
|
| **db_sqlite.py** | Full SQLite interface: table/index creation, CRUD operations, CSV import, regex support, and per-second counter deltas for time-series data. | `connect()`, `create_index()`, `create_table()`, `cut()`, `delete()`, `insert()`, `per_second_deltas()`, `select()` |
|
|
88
|
+
| **logmatch.py** | Keeps log findings in an SQLite state database across runs until they age out or are acknowledged on the monitoring server, together with the read position of the log. | `acknowledge()`, `connect()`, `key()`, `pending()`, `record()`, `service_acknowledged()` |
|
|
88
89
|
|
|
89
90
|
|
|
90
91
|
### System & OS
|
|
91
92
|
|
|
92
93
|
| Module | Description | Key Functions |
|
|
93
94
|
|--------|-------------|---------------|
|
|
95
|
+
| **container.py** | Runs a container engine command-line client (Docker, Podman) under a deadline and tells a missing permission apart from an engine that is down or does not answer. | `get_engine_error()`, `run()` |
|
|
94
96
|
| **disk.py** | File I/O, directory walking, CSV/environment file parsing, block device and partition listing, and temp directory management. | `dir_exists()`, `file_exists()`, `get_real_disks()`, `get_tmpdir()`, `grep_file()`, `read_file()` |
|
|
95
97
|
| **distro.py** | Linux distribution detection. Returns normalized facts including distribution name, version, and Ansible-compatible `os_family`. | `get_distribution_facts()` |
|
|
96
98
|
| **dmidecode.py** | Parses `dmidecode` output into structured data. Extracts CPU, RAM, firmware, serial number, manufacturer, and model information. | `cpu_speed()`, `cpu_type()`, `firmware()`, `get_data()`, `manufacturer()`, `model()`, `ram()`, `serno()` |
|
|
97
99
|
| **endoflifedate.py** | Bundled End-of-Life data from [endoflife.date](https://endoflife.date) for offline version checks when internet access is unavailable. | -- |
|
|
100
|
+
| **kvm.py** | libvirt domains, storage pools and volumes via `virsh` on a read-only connection, which needs neither root nor sudo. | `get_domains()`, `get_domstats()`, `get_pool_info()`, `get_pools()`, `get_volumes()`, `virsh()` |
|
|
101
|
+
| **logsource.py** | Reads a log incrementally from a file, a systemd unit in the journal, or a container, and hands back a position that survives rotation and truncation. | `parse()`, `read()`, `read_many()`, `timestamp()` |
|
|
102
|
+
| **lvm.py** | LVM logical volumes and volume groups via `lvs` and `vgs`, including thin pools, snapshots, health status and the thin pool metadata limit. | `get_logical_volumes()`, `get_volume_groups()`, `health()`, `is_snapshot_invalid()`, `is_thin_pool()`, `metadata_limit()` |
|
|
103
|
+
| **lynis.py** | The machine-readable report of a Lynis security audit (`/var/log/lynis-report.dat`): hardening index, findings and suggestions, and the list of them for the plugin output. | `get_findings_msg()`, `parse_findings()`, `parse_report()` |
|
|
104
|
+
| **psi.py** | Linux pressure stall information (PSI) from `/proc/pressure` for CPU, I/O, IRQ and memory, with state evaluation and report output. | `get_perfdata()`, `get_states()`, `get_summary()`, `get_table()`, `is_enabled()`, `read()` |
|
|
98
105
|
| **psutil.py** | Wrapper around `psutil` for retrieving mounted disk partitions with device, mount point, filesystem type and mount options. Lists them from the mount table alone, so a network filesystem whose server is gone cannot hold the listing up. | `get_partitions()` |
|
|
106
|
+
| **restic.py** | Builds the repository and password arguments for the `restic` backup client so a privileged caller cannot make it read an arbitrary file or run an external program. | `password_file_arg()`, `repo_args()` |
|
|
107
|
+
| **rhelappstreams.py** | Bundled life cycle of the RHEL Application Streams, generated weekly from the data Red Hat publishes in [digital-roadmap-backend](https://github.com/RedHatInsights/digital-roadmap-backend). | -- |
|
|
99
108
|
| **shell.py** | Runs external commands from an argv list without a shell, guards option-style CLI values, and locates executables in `PATH`. | `safe_cli_value()`, `shell_exec()`, `which()` |
|
|
109
|
+
| **user.py** | Local Unix accounts: UID/GID names, the system/regular account boundary, interactive shells and the state of the shadow password. | `get_gid_name()`, `get_interactive_shells()`, `get_shadow_password()`, `get_uid_min()`, `get_uid_name()`, `password_state()` |
|
|
100
110
|
|
|
101
111
|
|
|
102
112
|
### Networking & HTTP
|
|
@@ -134,6 +144,7 @@ These libraries are built with a clear set of priorities:
|
|
|
134
144
|
| **librenms.py** | [LibreNMS](https://www.librenms.org/) monitoring API, mapping its alert states to Nagios states. | `get_data()`, `get_state()` |
|
|
135
145
|
| **nextcloud.py** | [Nextcloud](https://nextcloud.com/) `occ` command execution as the `config.php` owner, parsing JSON or text output. | `run_occ()` |
|
|
136
146
|
| **nodebb.py** | [NodeBB](https://nodebb.org/) forum API, using a Bearer user token. | `get_data()` |
|
|
147
|
+
| **openstack.py** | [OpenStack](https://www.openstack.org/) Identity v3 authentication from an rc file, with the token reused across runs, and access to the service REST APIs. | `connect()`, `fetch()`, `fetch_json()` |
|
|
137
148
|
| **qts.py** | QNAP [QTS](https://www.qnap.com/) NAS API with session authentication. | `get_auth_sid()` |
|
|
138
149
|
| **redfish.py** | [Redfish](https://www.dmtf.org/standards/redfish) BMC API for chassis, systems, storage, managers, and sensors, deriving Nagios states and perfdata. | `get_auth_header()`, `get_perfdata()`, `get_state()`, `get_systems()` |
|
|
139
150
|
| **rocket.py** | [Rocket.Chat](https://www.rocket.chat/) REST API for login, room/group history, statistics, and incoming webhooks. | `get_groups_history()`, `get_rooms_get()`, `get_stats()`, `get_token()`, `send2webhook()` |
|
|
@@ -145,21 +156,31 @@ These libraries are built with a clear set of priorities:
|
|
|
145
156
|
|
|
146
157
|
## Usage Example
|
|
147
158
|
|
|
148
|
-
A typical
|
|
159
|
+
A typical check-style tool using these libraries:
|
|
149
160
|
|
|
150
161
|
```python
|
|
162
|
+
import argparse
|
|
163
|
+
|
|
151
164
|
import lib.args
|
|
152
165
|
import lib.base
|
|
153
166
|
import lib.url
|
|
154
|
-
from lib.globals import STATE_CRIT, STATE_OK, STATE_UNKNOWN, STATE_WARN
|
|
155
167
|
|
|
156
168
|
|
|
157
169
|
def main():
|
|
158
|
-
|
|
159
|
-
parser = lib.args.
|
|
160
|
-
parser.add_argument(
|
|
161
|
-
|
|
162
|
-
|
|
170
|
+
parser = argparse.ArgumentParser(description='Checks the usage reported by an API.')
|
|
171
|
+
parser.add_argument('--url', required=True, help=lib.args.help('--url'))
|
|
172
|
+
parser.add_argument(
|
|
173
|
+
'--warning',
|
|
174
|
+
type=float,
|
|
175
|
+
default=80,
|
|
176
|
+
help=lib.args.help('--warning') + ' Default: %(default)s',
|
|
177
|
+
)
|
|
178
|
+
parser.add_argument(
|
|
179
|
+
'--critical',
|
|
180
|
+
type=float,
|
|
181
|
+
default=90,
|
|
182
|
+
help=lib.args.help('--critical') + ' Default: %(default)s',
|
|
183
|
+
)
|
|
163
184
|
args = parser.parse_args()
|
|
164
185
|
|
|
165
186
|
# Fetch data (coe = "Continue or Exit")
|
|
@@ -172,7 +193,7 @@ def main():
|
|
|
172
193
|
)
|
|
173
194
|
|
|
174
195
|
# Output and exit
|
|
175
|
-
lib.base.oao('Usage is {
|
|
196
|
+
lib.base.oao(f'Usage is {result["usage"]}%', state, perfdata)
|
|
176
197
|
|
|
177
198
|
|
|
178
199
|
if __name__ == '__main__':
|
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
import argparse
|
|
14
14
|
import os
|
|
15
15
|
import re
|
|
16
|
+
import sys
|
|
16
17
|
import textwrap
|
|
17
18
|
|
|
18
|
-
from . import base,
|
|
19
|
+
from . import base, human, txt
|
|
19
20
|
|
|
20
21
|
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
|
|
21
|
-
__version__ = '
|
|
22
|
+
__version__ = '2026102401'
|
|
22
23
|
|
|
23
24
|
# Base URL of the rendered online documentation.
|
|
24
25
|
DOCS_BASE_URL = 'https://linuxfabrik.github.io/monitoring-plugins'
|
|
@@ -289,8 +290,7 @@ HELP_TEXTS = {
|
|
|
289
290
|
'Send a short message. This can be useful when using an SMS relay, for example.'
|
|
290
291
|
),
|
|
291
292
|
'--stratum': (
|
|
292
|
-
'Warns if the
|
|
293
|
-
'to this value. '
|
|
293
|
+
'Warns if the stratum of this host is greater than or equal to this value. '
|
|
294
294
|
'Stratum 1 indicates a computer with a locally attached reference clock. '
|
|
295
295
|
'A computer that is synchronised to a stratum 1 computer is at stratum 2. '
|
|
296
296
|
'A computer that is synchronised to a stratum 2 computer is at stratum 3, '
|
|
@@ -325,14 +325,23 @@ HELP_TEXTS = {
|
|
|
325
325
|
'--warning-seconds': 'WARN threshold in seconds.',
|
|
326
326
|
'--warning-temperature': 'WARN threshold in degrees Celsius.',
|
|
327
327
|
'--warning-voltage': 'WARN threshold in volts.',
|
|
328
|
+
'--winrm-domain': (
|
|
329
|
+
'AD domain name for NTLM authentication. '
|
|
330
|
+
'When set, the username is sent as user@DOMAIN. '
|
|
331
|
+
'Not needed for Kerberos or local accounts.'
|
|
332
|
+
),
|
|
333
|
+
'--winrm-password': (
|
|
334
|
+
'WinRM account password. '
|
|
335
|
+
'Optional for Kerberos (uses the credential cache from kinit).'
|
|
336
|
+
),
|
|
337
|
+
'--winrm-transport': 'WinRM transport type.',
|
|
338
|
+
'--winrm-username': (
|
|
339
|
+
'WinRM account name. '
|
|
340
|
+
'Optional for Kerberos (uses the credential cache from kinit).'
|
|
341
|
+
),
|
|
328
342
|
}
|
|
329
343
|
|
|
330
344
|
|
|
331
|
-
# Predefined sets for checking units and methods
|
|
332
|
-
_UNITS = {'%', 'K', 'M', 'G', 'T', 'P'}
|
|
333
|
-
_METHODS = {'USED', 'FREE'}
|
|
334
|
-
|
|
335
|
-
|
|
336
345
|
class HelpFormatter(argparse.HelpFormatter):
|
|
337
346
|
"""Formats the help output like argparse does, but never splits long words.
|
|
338
347
|
|
|
@@ -568,6 +577,11 @@ def load_secret(path, param='--password-file'):
|
|
|
568
577
|
>>> load_secret('/etc/icinga2/secrets/storage')
|
|
569
578
|
'linuxfabrik'
|
|
570
579
|
"""
|
|
580
|
+
# Imported here, not at module level: this is the only consumer of lib.disk in this
|
|
581
|
+
# module, and loading it (with lib.shell and subprocess behind it) would slow down
|
|
582
|
+
# the startup of everything that only wants the argparse helpers.
|
|
583
|
+
from . import disk
|
|
584
|
+
|
|
571
585
|
success, content = disk.read_file(path)
|
|
572
586
|
if not success:
|
|
573
587
|
base.cu(f'Cannot read the file given in {param}: {content}')
|
|
@@ -578,13 +592,27 @@ def load_secret(path, param='--password-file'):
|
|
|
578
592
|
return secret
|
|
579
593
|
|
|
580
594
|
|
|
595
|
+
# `<number>[unit][method]`, blanks allowed between the parts. A unit may carry a
|
|
596
|
+
# trailing `B` or `iB` (`10GiB`, `10MB`): the unit is binary either way, and these
|
|
597
|
+
# were always accepted. The blanks after the unit belong to the unit's group: two
|
|
598
|
+
# `\s*` in a row with only an optional group between them split a run of blanks in
|
|
599
|
+
# quadratically many ways, and a threshold of `1`, 30000 blanks and `x` took 21 s to
|
|
600
|
+
# refuse (Python 3.14).
|
|
601
|
+
_NUMBER_UNIT_METHOD = re.compile(
|
|
602
|
+
r'(?P<number>\d+(?:\.\d+)?|\.\d+)\s*'
|
|
603
|
+
r'(?:(?P<unit>[%KMGTP])(?:I?B)?\s*)?'
|
|
604
|
+
r'(?P<method>USED|FREE)?',
|
|
605
|
+
re.IGNORECASE,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
|
|
581
609
|
def number_unit_method(arg, unit='%', method='USED'):
|
|
582
610
|
"""Parses a string in the format `<number>[unit][method]` for threshold arguments.
|
|
583
611
|
|
|
584
612
|
Parameters
|
|
585
613
|
----------
|
|
586
614
|
arg : str
|
|
587
|
-
The input string.
|
|
615
|
+
The input string. Blanks between number, unit and method are allowed.
|
|
588
616
|
unit : str, optional
|
|
589
617
|
Default unit. Defaults to `%`.
|
|
590
618
|
method : str, optional
|
|
@@ -593,9 +621,10 @@ def number_unit_method(arg, unit='%', method='USED'):
|
|
|
593
621
|
Returns
|
|
594
622
|
-------
|
|
595
623
|
tuple (str, str, str)
|
|
596
|
-
The number as it was written, the unit and the method, both upper case.
|
|
597
|
-
|
|
598
|
-
|
|
624
|
+
The number as it was written, the unit and the method, both upper case. An
|
|
625
|
+
argument that does not have this form, a negative number or `5.5.5G` among
|
|
626
|
+
them, yields an empty number, so that the caller's `float()` rejects it
|
|
627
|
+
instead of working with a threshold nobody gave.
|
|
599
628
|
|
|
600
629
|
Examples
|
|
601
630
|
--------
|
|
@@ -604,32 +633,120 @@ def number_unit_method(arg, unit='%', method='USED'):
|
|
|
604
633
|
|
|
605
634
|
>>> number_unit_method('9.5GFREE')
|
|
606
635
|
('9.5', 'G', 'FREE')
|
|
636
|
+
|
|
637
|
+
>>> number_unit_method('10 G')
|
|
638
|
+
('10', 'G', 'USED')
|
|
639
|
+
|
|
640
|
+
>>> number_unit_method('-5')
|
|
641
|
+
('', '%', 'USED')
|
|
607
642
|
"""
|
|
608
643
|
arg = arg.strip()
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
644
|
+
match = _NUMBER_UNIT_METHOD.fullmatch(arg)
|
|
645
|
+
if not match:
|
|
646
|
+
return '', unit.upper(), method.upper()
|
|
647
|
+
return (
|
|
648
|
+
match.group('number'),
|
|
649
|
+
(match.group('unit') or unit).upper(),
|
|
650
|
+
(match.group('method') or method).upper(),
|
|
651
|
+
)
|
|
617
652
|
|
|
618
|
-
if i < len(arg) and arg[i].upper() in _UNITS:
|
|
619
|
-
unit_part = arg[i]
|
|
620
|
-
i += 1
|
|
621
653
|
|
|
622
|
-
|
|
623
|
-
|
|
654
|
+
def _repair_ansi_argument(arg, codec):
|
|
655
|
+
"""
|
|
656
|
+
Undo the damage the ANSI process API does to a UTF-8 argument on Windows.
|
|
657
|
+
|
|
658
|
+
A program that starts another one through `CreateProcessA` hands it a byte string,
|
|
659
|
+
and Windows turns every byte of it into a character of the ANSI code page. When
|
|
660
|
+
those bytes were UTF-8, a "ü" (`C3 BC`) arrives as "ü". Encoding the argument
|
|
661
|
+
back into the code page recovers the original bytes. Windows maps a byte the code
|
|
662
|
+
page leaves undefined (`0x81`, `0x8D`, `0x8F`, `0x90`, `0x9D` in cp1252) to the code
|
|
663
|
+
point of the same value, which Python's codec does not know, so those are taken
|
|
664
|
+
over one to one.
|
|
665
|
+
|
|
666
|
+
The repair only applies when the recovered bytes are valid UTF-8. An argument that
|
|
667
|
+
arrived intact does not qualify: "Prüfung" becomes `50 72 FC ...` in cp1252, which
|
|
668
|
+
is not UTF-8, and characters outside the code page ("ő", "日本") cannot be encoded
|
|
669
|
+
at all. What remains ambiguous is text that already looks like mojibake: a literal
|
|
670
|
+
"ü" is indistinguishable from a damaged "ü" and is read as the latter.
|
|
671
|
+
|
|
672
|
+
The repair never produces a character below U+0080 that was not there before: only
|
|
673
|
+
characters that encode to a single byte are taken back, ASCII bytes map to
|
|
674
|
+
themselves, and a strict UTF-8 decoder turns bytes from `0x80` up into characters
|
|
675
|
+
from U+0080 up only, overlong encodings of ASCII included. So it cannot
|
|
676
|
+
introduce a `-`, a `/`, a quote or a `..` into an argument.
|
|
624
677
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
678
|
+
Parameters
|
|
679
|
+
----------
|
|
680
|
+
arg : str
|
|
681
|
+
One command-line argument.
|
|
682
|
+
codec : str
|
|
683
|
+
The Python codec of the ANSI code page, for example `'cp1252'`.
|
|
628
684
|
|
|
629
|
-
|
|
630
|
-
|
|
685
|
+
Returns
|
|
686
|
+
-------
|
|
687
|
+
str
|
|
688
|
+
The repaired argument, or `arg` unchanged.
|
|
631
689
|
|
|
632
|
-
|
|
633
|
-
|
|
690
|
+
Examples
|
|
691
|
+
--------
|
|
692
|
+
>>> _repair_ansi_argument('Pr\u00c3\u00bcfung', 'cp1252')
|
|
693
|
+
'Prüfung'
|
|
634
694
|
|
|
635
|
-
|
|
695
|
+
>>> _repair_ansi_argument('Prüfung', 'cp1252')
|
|
696
|
+
'Prüfung'
|
|
697
|
+
"""
|
|
698
|
+
raw = bytearray()
|
|
699
|
+
for char in arg:
|
|
700
|
+
try:
|
|
701
|
+
encoded = txt.to_bytes(char, encoding=codec, errors='strict')
|
|
702
|
+
except UnicodeEncodeError:
|
|
703
|
+
if ord(char) > 0xFF:
|
|
704
|
+
return arg
|
|
705
|
+
encoded = bytes([ord(char)])
|
|
706
|
+
# The damage maps one byte to one character. A character that takes two
|
|
707
|
+
# bytes (the double-byte code pages 932, 936, 949 and 950) was not produced
|
|
708
|
+
# by it, and its trail byte can be ASCII: cp932 encodes "―" as `81 5C`.
|
|
709
|
+
if len(encoded) != 1:
|
|
710
|
+
return arg
|
|
711
|
+
raw += encoded
|
|
712
|
+
try:
|
|
713
|
+
return txt.to_text(bytes(raw), encoding='utf-8', errors='strict')
|
|
714
|
+
except UnicodeDecodeError:
|
|
715
|
+
return arg
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _repair_windows_argv():
|
|
719
|
+
"""
|
|
720
|
+
Repair the command-line arguments the Icinga 2 agent for Windows hands over.
|
|
721
|
+
|
|
722
|
+
The agent keeps its strings in UTF-8 and starts a check through `CreateProcessA`
|
|
723
|
+
(`Process::Run()` in lib/base/process.cpp), so every argument with a character
|
|
724
|
+
outside of ASCII arrives in the ANSI code page: "Prüfung" as "Prüfung"
|
|
725
|
+
(Icinga/icinga2#8962). A caller that passes the arguments intact, such as a shell,
|
|
726
|
+
NSClient++ or any other program using the Unicode API, is left alone, see
|
|
727
|
+
`_repair_ansi_argument()`. Verified with Icinga 2 v2.16.5 on Windows Server 2025,
|
|
728
|
+
ANSI code page 1252.
|
|
729
|
+
|
|
730
|
+
Icinga for Windows repairs its plugins the same way, back to the ANSI bytes and on
|
|
731
|
+
to UTF-8 (`ConvertTo-IcingaUTF8Value` in icinga-powershell-framework), but skips only
|
|
732
|
+
values that contain a German umlaut. Measured on the same host, that turns an intact
|
|
733
|
+
"café" into "caf�", "ő" into "o" and "€" into "�". Requiring the result
|
|
734
|
+
to be valid UTF-8 repairs the same damaged values and leaves every intact one alone.
|
|
735
|
+
"""
|
|
736
|
+
try:
|
|
737
|
+
import ctypes # Windows-only API below
|
|
738
|
+
|
|
739
|
+
codec = f'cp{ctypes.windll.kernel32.GetACP()}'
|
|
740
|
+
txt.to_bytes('', encoding=codec, errors='strict')
|
|
741
|
+
except (AttributeError, LookupError, OSError):
|
|
742
|
+
return
|
|
743
|
+
sys.argv[:] = [_repair_ansi_argument(arg, codec) for arg in sys.argv]
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
# Done on import, before any consumer parses its arguments, for the same reason
|
|
747
|
+
# `base.py` reconfigures stdout on import: it covers every consumer without each of them
|
|
748
|
+
# having to ask for it. It touches nothing but arguments that decode as the damage the
|
|
749
|
+
# ANSI API does, so on Linux, with an intact caller or with plain ASCII it changes
|
|
750
|
+
# nothing.
|
|
751
|
+
if base.WINDOWS:
|
|
752
|
+
_repair_windows_argv()
|