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.
Files changed (72) hide show
  1. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/PKG-INFO +39 -19
  2. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/README.md +37 -16
  3. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/args.py +151 -34
  4. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/base.py +142 -32
  5. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/container.py +96 -3
  6. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/db_mysql.py +130 -23
  7. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/db_sqlite.py +44 -63
  8. linuxfabrik_lib-8.3.0/disk.py +2508 -0
  9. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/dmidecode.py +59 -15
  10. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/endoflifedate.py +2115 -326
  11. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/feedparser.py +74 -46
  12. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/huawei_dorado.py +30 -4
  13. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/huawei_pacific.py +29 -14
  14. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/human.py +55 -20
  15. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/icinga.py +30 -6
  16. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/infomaniak.py +9 -2
  17. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/jitsi.py +4 -5
  18. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/keycloak.py +24 -4
  19. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/kvm.py +122 -3
  20. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/lftest.py +195 -99
  21. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/linuxfabrik_lib.egg-info/PKG-INFO +39 -19
  22. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/linuxfabrik_lib.egg-info/SOURCES.txt +6 -0
  23. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/linuxfabrik_lib.egg-info/requires.txt +0 -1
  24. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/logsource.py +482 -87
  25. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/lvm.py +71 -60
  26. linuxfabrik_lib-8.3.0/lynis.py +226 -0
  27. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/mail.py +33 -18
  28. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/net.py +312 -51
  29. linuxfabrik_lib-8.3.0/nextcloud.py +380 -0
  30. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/openstack.py +18 -11
  31. linuxfabrik_lib-8.3.0/powershell.py +87 -0
  32. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/pyproject.toml +2 -3
  33. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/redfish.py +412 -269
  34. linuxfabrik_lib-8.3.0/restic.py +239 -0
  35. linuxfabrik_lib-8.3.0/rhelappstreams.py +1689 -0
  36. linuxfabrik_lib-8.3.0/shell.py +669 -0
  37. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/smb.py +79 -24
  38. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/ssh.py +72 -14
  39. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/task.py +136 -53
  40. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/time.py +88 -7
  41. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/txt.py +143 -34
  42. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/uptimerobot.py +147 -52
  43. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/url.py +613 -232
  44. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/user.py +42 -11
  45. linuxfabrik_lib-8.3.0/version.py +741 -0
  46. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/winrm.py +322 -128
  47. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/wordpress.py +146 -91
  48. linuxfabrik_lib-8.2.0/disk.py +0 -1469
  49. linuxfabrik_lib-8.2.0/nextcloud.py +0 -142
  50. linuxfabrik_lib-8.2.0/powershell.py +0 -82
  51. linuxfabrik_lib-8.2.0/shell.py +0 -373
  52. linuxfabrik_lib-8.2.0/version.py +0 -357
  53. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/LICENSE +0 -0
  54. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/__init__.py +0 -0
  55. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/bexio.py +0 -0
  56. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/cache.py +0 -0
  57. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/distro.py +0 -0
  58. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/globals.py +0 -0
  59. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/grassfish.py +0 -0
  60. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/librenms.py +0 -0
  61. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/linuxfabrik_lib.egg-info/dependency_links.txt +0 -0
  62. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/linuxfabrik_lib.egg-info/top_level.txt +0 -0
  63. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/logmatch.py +0 -0
  64. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/nodebb.py +0 -0
  65. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/openmetrics.py +0 -0
  66. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/psi.py +0 -0
  67. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/psutil.py +0 -0
  68. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/qts.py +0 -0
  69. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/rocket.py +0 -0
  70. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/setup.cfg +0 -0
  71. {linuxfabrik_lib-8.2.0 → linuxfabrik_lib-8.3.0}/veeam.py +0 -0
  72. {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.2.0
4
- Summary: Python libraries used in various Linuxfabrik projects, including the 'Linuxfabrik Monitoring Plugins' project.
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+ modules for Linuxfabrik projects: DB access, SQLite KVS caching, WinRM, SMB, shell execution, 15+ API integrations (Icinga2, Veeam, Nextcloud, ...). Available on PyPI.
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>&#8226;</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 providing 40 modules with 300+ functions for system administration, monitoring, and infrastructure automation. These libraries are used across several Linuxfabrik projects -- most prominently the [Linuxfabrik Monitoring Plugins](https://github.com/Linuxfabrik/monitoring-plugins) (Nagios/Icinga check plugins), but also in [ChecklistFabrik](https://github.com/Linuxfabrik/checklistfabrik) and other tools.
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. This is especially true for the most prominent use case -- monitoring plugins -- which are short-lived, linear processes with no complex state to manage over time, where unnecessary abstraction layers would add overhead without tangible benefit.
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 -- the library never exits on its own. The `base.coe()` ("Continue or Exit") helper makes this pattern concise.
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 plugins work out of the box without extensive configuration.
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 library for plugin development. 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()` |
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 plugin runs. | `get()`, `set()` |
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 monitoring plugin using these libraries:
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
- # Parse arguments with custom threshold types
230
- parser = lib.args.ArgumentParser()
231
- parser.add_argument('--url', required=True)
232
- parser.add_argument('--warning', type=lib.args.float_or_none, default=80)
233
- parser.add_argument('--critical', type=lib.args.float_or_none, default=90)
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 {}%'.format(result['usage']), state, perfdata)
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+ modules for Linuxfabrik projects: DB access, SQLite KVS caching, WinRM, SMB, shell execution, 15+ API integrations (Icinga2, Veeam, Nextcloud, ...). Available on PyPI.
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>&#8226;</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 providing 40 modules with 300+ functions for system administration, monitoring, and infrastructure automation. These libraries are used across several Linuxfabrik projects -- most prominently the [Linuxfabrik Monitoring Plugins](https://github.com/Linuxfabrik/monitoring-plugins) (Nagios/Icinga check plugins), but also in [ChecklistFabrik](https://github.com/Linuxfabrik/checklistfabrik) and other tools.
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. This is especially true for the most prominent use case -- monitoring plugins -- which are short-lived, linear processes with no complex state to manage over time, where unnecessary abstraction layers would add overhead without tangible benefit.
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 -- the library never exits on its own. The `base.coe()` ("Continue or Exit") helper makes this pattern concise.
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 plugins work out of the box without extensive configuration.
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 library for plugin development. 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()` |
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 plugin runs. | `get()`, `set()` |
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 monitoring plugin using these libraries:
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
- # Parse arguments with custom threshold types
159
- parser = lib.args.ArgumentParser()
160
- parser.add_argument('--url', required=True)
161
- parser.add_argument('--warning', type=lib.args.float_or_none, default=80)
162
- parser.add_argument('--critical', type=lib.args.float_or_none, default=90)
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 {}%'.format(result['usage']), state, perfdata)
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, disk, human
19
+ from . import base, human, txt
19
20
 
20
21
  __author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
21
- __version__ = '2026092101'
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 determined stratum of the time server is greater than or equal '
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
- An argument carrying no digits yields `('0.0', unit, method)`, and a unit
598
- or method the parser does not know falls back to the default.
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
- number_part = []
610
- unit_part = ''
611
- method_part = ''
612
-
613
- i = 0
614
- while i < len(arg) and (arg[i].isdigit() or arg[i] == '.'):
615
- number_part.append(arg[i])
616
- i += 1
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
- if i < len(arg):
623
- method_part = arg[i:].upper()
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
- number = ''.join(number_part)
626
- if not number:
627
- return '0.0', unit.upper(), method.upper()
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
- if unit_part:
630
- unit = unit_part
685
+ Returns
686
+ -------
687
+ str
688
+ The repaired argument, or `arg` unchanged.
631
689
 
632
- if method_part in _METHODS:
633
- method = method_part
690
+ Examples
691
+ --------
692
+ >>> _repair_ansi_argument('Pr\u00c3\u00bcfung', 'cp1252')
693
+ 'Prüfung'
634
694
 
635
- return number, unit.upper(), method.upper()
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()