LinuxMonitor 1.5.5__tar.gz → 1.5.7__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.
- {linuxmonitor-1.5.5 → linuxmonitor-1.5.7/LinuxMonitor.egg-info}/PKG-INFO +44 -2
- {linuxmonitor-1.5.5/LinuxMonitor.egg-info → linuxmonitor-1.5.7}/PKG-INFO +44 -2
- {linuxmonitor-1.5.5 → linuxmonitor-1.5.7}/README.md +29 -0
- {linuxmonitor-1.5.5 → linuxmonitor-1.5.7}/linuxmonitor/__main__.py +6 -5
- {linuxmonitor-1.5.5 → linuxmonitor-1.5.7}/linuxmonitor/linuxmonitor.py +179 -48
- {linuxmonitor-1.5.5 → linuxmonitor-1.5.7}/setup.py +1 -1
- {linuxmonitor-1.5.5 → linuxmonitor-1.5.7}/LICENSE.md +0 -0
- {linuxmonitor-1.5.5 → linuxmonitor-1.5.7}/LinuxMonitor.egg-info/SOURCES.txt +0 -0
- {linuxmonitor-1.5.5 → linuxmonitor-1.5.7}/LinuxMonitor.egg-info/dependency_links.txt +0 -0
- {linuxmonitor-1.5.5 → linuxmonitor-1.5.7}/LinuxMonitor.egg-info/requires.txt +0 -0
- {linuxmonitor-1.5.5 → linuxmonitor-1.5.7}/LinuxMonitor.egg-info/top_level.txt +0 -0
- {linuxmonitor-1.5.5 → linuxmonitor-1.5.7}/linuxmonitor/__init__.py +0 -0
- {linuxmonitor-1.5.5 → linuxmonitor-1.5.7}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: LinuxMonitor
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.7
|
|
4
4
|
Summary: Get information and warning status of Linux server like service, port, ping, ssl certificate, disk/folder/cpu/ram/swap usage, ip connection, ... (Python and shell library, Linux ONLY)
|
|
5
5
|
Home-page: https://github.com/QuentinCG/Linux-Monitor-Python-Library
|
|
6
6
|
Author: Quentin Comte-Gaz
|
|
@@ -25,6 +25,19 @@ License-File: LICENSE.md
|
|
|
25
25
|
Requires-Dist: psutil
|
|
26
26
|
Requires-Dist: typing
|
|
27
27
|
Requires-Dist: asyncio
|
|
28
|
+
Dynamic: author
|
|
29
|
+
Dynamic: author-email
|
|
30
|
+
Dynamic: classifier
|
|
31
|
+
Dynamic: description
|
|
32
|
+
Dynamic: description-content-type
|
|
33
|
+
Dynamic: home-page
|
|
34
|
+
Dynamic: keywords
|
|
35
|
+
Dynamic: license
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
Dynamic: platform
|
|
38
|
+
Dynamic: requires-dist
|
|
39
|
+
Dynamic: requires-python
|
|
40
|
+
Dynamic: summary
|
|
28
41
|
|
|
29
42
|
# Linux Monitor (Python library)
|
|
30
43
|
[](https://pypi.org/project/LinuxMonitor/) [](https://github.com/QuentinCG/Linux-Monitor-Python-Library/blob/master/LICENSE.md) [](https://paypal.me/QuentinCG) [](https://pepy.tech/project/LinuxMonitor) [](https://pepy.tech/project/LinuxMonitor)
|
|
@@ -162,10 +175,39 @@ python3 -m linuxmonitor --list_commands --config_file config-example.json --node
|
|
|
162
175
|
# Execute a custom commands
|
|
163
176
|
python3 -m linuxmonitor --execute_command CUSTOM_COMMAND_HERE --config_file config-example.json --nodebug
|
|
164
177
|
|
|
178
|
+
# Execute a custom command with parameters (only for commands with "accept_parameters": true or a {arg1}/{args} placeholder)
|
|
179
|
+
python3 -m linuxmonitor --execute_command unban_ip --parameters "1.2.3.4" --config_file config-example.json --nodebug
|
|
180
|
+
|
|
165
181
|
# Execute all custom commands
|
|
166
182
|
python3 -m linuxmonitor --execute_all_commands --config_file config-example.json --nodebug
|
|
167
183
|
```
|
|
168
184
|
|
|
185
|
+
## Custom commands with parameters
|
|
186
|
+
|
|
187
|
+
A custom command (in the `commands` section of the config file) can receive parameters at execution time.
|
|
188
|
+
This is opt-in per command: set `"accept_parameters": true` and/or use a placeholder in the `command` string.
|
|
189
|
+
|
|
190
|
+
Placeholders inside the `command` string:
|
|
191
|
+
- `{arg1}`, `{arg2}`, ... : replaced by the Nth provided parameter
|
|
192
|
+
- `{args}` : replaced by all provided parameters
|
|
193
|
+
- no placeholder : all parameters are appended at the end of the command
|
|
194
|
+
|
|
195
|
+
Every parameter is shell-quoted before substitution, so it is always treated as a single literal
|
|
196
|
+
argument. This prevents command injection through the parameters.
|
|
197
|
+
|
|
198
|
+
Example config entry:
|
|
199
|
+
```json
|
|
200
|
+
"unban_ip": {
|
|
201
|
+
"display_name": "Unban an IP from Fail2Ban",
|
|
202
|
+
"command": "sudo fail2ban-client unban {arg1}",
|
|
203
|
+
"is_private": true,
|
|
204
|
+
"accept_parameters": true,
|
|
205
|
+
"show_content_if_success": true,
|
|
206
|
+
"show_content_if_issue": true,
|
|
207
|
+
"timeout_in_sec": 60
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
169
211
|
## How to use in python script
|
|
170
212
|
|
|
171
213
|
Example of python script using this library:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: LinuxMonitor
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.7
|
|
4
4
|
Summary: Get information and warning status of Linux server like service, port, ping, ssl certificate, disk/folder/cpu/ram/swap usage, ip connection, ... (Python and shell library, Linux ONLY)
|
|
5
5
|
Home-page: https://github.com/QuentinCG/Linux-Monitor-Python-Library
|
|
6
6
|
Author: Quentin Comte-Gaz
|
|
@@ -25,6 +25,19 @@ License-File: LICENSE.md
|
|
|
25
25
|
Requires-Dist: psutil
|
|
26
26
|
Requires-Dist: typing
|
|
27
27
|
Requires-Dist: asyncio
|
|
28
|
+
Dynamic: author
|
|
29
|
+
Dynamic: author-email
|
|
30
|
+
Dynamic: classifier
|
|
31
|
+
Dynamic: description
|
|
32
|
+
Dynamic: description-content-type
|
|
33
|
+
Dynamic: home-page
|
|
34
|
+
Dynamic: keywords
|
|
35
|
+
Dynamic: license
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
Dynamic: platform
|
|
38
|
+
Dynamic: requires-dist
|
|
39
|
+
Dynamic: requires-python
|
|
40
|
+
Dynamic: summary
|
|
28
41
|
|
|
29
42
|
# Linux Monitor (Python library)
|
|
30
43
|
[](https://pypi.org/project/LinuxMonitor/) [](https://github.com/QuentinCG/Linux-Monitor-Python-Library/blob/master/LICENSE.md) [](https://paypal.me/QuentinCG) [](https://pepy.tech/project/LinuxMonitor) [](https://pepy.tech/project/LinuxMonitor)
|
|
@@ -162,10 +175,39 @@ python3 -m linuxmonitor --list_commands --config_file config-example.json --node
|
|
|
162
175
|
# Execute a custom commands
|
|
163
176
|
python3 -m linuxmonitor --execute_command CUSTOM_COMMAND_HERE --config_file config-example.json --nodebug
|
|
164
177
|
|
|
178
|
+
# Execute a custom command with parameters (only for commands with "accept_parameters": true or a {arg1}/{args} placeholder)
|
|
179
|
+
python3 -m linuxmonitor --execute_command unban_ip --parameters "1.2.3.4" --config_file config-example.json --nodebug
|
|
180
|
+
|
|
165
181
|
# Execute all custom commands
|
|
166
182
|
python3 -m linuxmonitor --execute_all_commands --config_file config-example.json --nodebug
|
|
167
183
|
```
|
|
168
184
|
|
|
185
|
+
## Custom commands with parameters
|
|
186
|
+
|
|
187
|
+
A custom command (in the `commands` section of the config file) can receive parameters at execution time.
|
|
188
|
+
This is opt-in per command: set `"accept_parameters": true` and/or use a placeholder in the `command` string.
|
|
189
|
+
|
|
190
|
+
Placeholders inside the `command` string:
|
|
191
|
+
- `{arg1}`, `{arg2}`, ... : replaced by the Nth provided parameter
|
|
192
|
+
- `{args}` : replaced by all provided parameters
|
|
193
|
+
- no placeholder : all parameters are appended at the end of the command
|
|
194
|
+
|
|
195
|
+
Every parameter is shell-quoted before substitution, so it is always treated as a single literal
|
|
196
|
+
argument. This prevents command injection through the parameters.
|
|
197
|
+
|
|
198
|
+
Example config entry:
|
|
199
|
+
```json
|
|
200
|
+
"unban_ip": {
|
|
201
|
+
"display_name": "Unban an IP from Fail2Ban",
|
|
202
|
+
"command": "sudo fail2ban-client unban {arg1}",
|
|
203
|
+
"is_private": true,
|
|
204
|
+
"accept_parameters": true,
|
|
205
|
+
"show_content_if_success": true,
|
|
206
|
+
"show_content_if_issue": true,
|
|
207
|
+
"timeout_in_sec": 60
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
169
211
|
## How to use in python script
|
|
170
212
|
|
|
171
213
|
Example of python script using this library:
|
|
@@ -134,10 +134,39 @@ python3 -m linuxmonitor --list_commands --config_file config-example.json --node
|
|
|
134
134
|
# Execute a custom commands
|
|
135
135
|
python3 -m linuxmonitor --execute_command CUSTOM_COMMAND_HERE --config_file config-example.json --nodebug
|
|
136
136
|
|
|
137
|
+
# Execute a custom command with parameters (only for commands with "accept_parameters": true or a {arg1}/{args} placeholder)
|
|
138
|
+
python3 -m linuxmonitor --execute_command unban_ip --parameters "1.2.3.4" --config_file config-example.json --nodebug
|
|
139
|
+
|
|
137
140
|
# Execute all custom commands
|
|
138
141
|
python3 -m linuxmonitor --execute_all_commands --config_file config-example.json --nodebug
|
|
139
142
|
```
|
|
140
143
|
|
|
144
|
+
## Custom commands with parameters
|
|
145
|
+
|
|
146
|
+
A custom command (in the `commands` section of the config file) can receive parameters at execution time.
|
|
147
|
+
This is opt-in per command: set `"accept_parameters": true` and/or use a placeholder in the `command` string.
|
|
148
|
+
|
|
149
|
+
Placeholders inside the `command` string:
|
|
150
|
+
- `{arg1}`, `{arg2}`, ... : replaced by the Nth provided parameter
|
|
151
|
+
- `{args}` : replaced by all provided parameters
|
|
152
|
+
- no placeholder : all parameters are appended at the end of the command
|
|
153
|
+
|
|
154
|
+
Every parameter is shell-quoted before substitution, so it is always treated as a single literal
|
|
155
|
+
argument. This prevents command injection through the parameters.
|
|
156
|
+
|
|
157
|
+
Example config entry:
|
|
158
|
+
```json
|
|
159
|
+
"unban_ip": {
|
|
160
|
+
"display_name": "Unban an IP from Fail2Ban",
|
|
161
|
+
"command": "sudo fail2ban-client unban {arg1}",
|
|
162
|
+
"is_private": true,
|
|
163
|
+
"accept_parameters": true,
|
|
164
|
+
"show_content_if_success": true,
|
|
165
|
+
"show_content_if_issue": true,
|
|
166
|
+
"timeout_in_sec": 60
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
141
170
|
## How to use in python script
|
|
142
171
|
|
|
143
172
|
Example of python script using this library:
|
|
@@ -31,6 +31,7 @@ def main() -> None:
|
|
|
31
31
|
parser.add_argument('--list_commands', action='store_true', help='📋 List all available commands 📋')
|
|
32
32
|
parser.add_argument('execute_command', type=str, help='📋 Execute a command 📋')
|
|
33
33
|
parser.add_argument('execute_all_commands', type=str, help='📋 Execute all commands 📋')
|
|
34
|
+
parser.add_argument('--parameters', type=str, default='', help='Optional parameters passed to --execute_command (space separated, quoted if needed)')
|
|
34
35
|
|
|
35
36
|
parser.add_argument('--debug', action='store_true', help='Enable debug mode')
|
|
36
37
|
parser.add_argument('--nodebug', action='store_true', help='Disable all logs')
|
|
@@ -71,10 +72,10 @@ def main() -> None:
|
|
|
71
72
|
out_msg += msg
|
|
72
73
|
|
|
73
74
|
out_msg += "\n"
|
|
74
|
-
out_msg += monitoring.check_load_average(display_only_if_critical=False) + "\n"
|
|
75
|
-
out_msg += monitoring.check_cpu_usage(display_only_if_critical=False) + "\n"
|
|
76
|
-
out_msg += monitoring.check_ram_usage(display_only_if_critical=False) + "\n"
|
|
77
|
-
out_msg += monitoring.check_swap_usage(display_only_if_critical=False) + "\n"
|
|
75
|
+
out_msg += asyncio.run(monitoring.check_load_average(display_only_if_critical=False)) + "\n"
|
|
76
|
+
out_msg += asyncio.run(monitoring.check_cpu_usage(display_only_if_critical=False)) + "\n"
|
|
77
|
+
out_msg += asyncio.run(monitoring.check_ram_usage(display_only_if_critical=False)) + "\n"
|
|
78
|
+
out_msg += asyncio.run(monitoring.check_swap_usage(display_only_if_critical=False)) + "\n"
|
|
78
79
|
out_msg += monitoring.check_cpu_temperature(display_only_if_critical=False) + "\n"
|
|
79
80
|
out_msg += monitoring.get_network_info()
|
|
80
81
|
print(out_msg)
|
|
@@ -163,7 +164,7 @@ def main() -> None:
|
|
|
163
164
|
if args.execute_command is not None:
|
|
164
165
|
handled = True
|
|
165
166
|
print(f"Executing command: {args.execute_command}...")
|
|
166
|
-
out_msg: str = asyncio.run(monitoring.execute_command(is_private=True, command_name=args.execute_command))
|
|
167
|
+
out_msg: str = asyncio.run(monitoring.execute_command(is_private=True, command_name=args.execute_command, parameters=args.parameters))
|
|
167
168
|
print(out_msg)
|
|
168
169
|
|
|
169
170
|
if args.execute_all_commands is not None:
|
|
@@ -33,7 +33,7 @@ __email__ = "quentin@comte-gaz.com"
|
|
|
33
33
|
__license__ = "MIT License"
|
|
34
34
|
__copyright__ = "Copyright Quentin Comte-Gaz (2024)"
|
|
35
35
|
__python_version__ = "3.+"
|
|
36
|
-
__version__ = "1.5.
|
|
36
|
+
__version__ = "1.5.7 (2026/08/19)"
|
|
37
37
|
__status__ = "Usable for any Linux project"
|
|
38
38
|
|
|
39
39
|
import json
|
|
@@ -48,6 +48,7 @@ import socket
|
|
|
48
48
|
from datetime import datetime, timedelta
|
|
49
49
|
import platform
|
|
50
50
|
import re
|
|
51
|
+
import shlex
|
|
51
52
|
import logging
|
|
52
53
|
import asyncio
|
|
53
54
|
from http.client import responses
|
|
@@ -587,7 +588,7 @@ class LinuxMonitor:
|
|
|
587
588
|
logging.exception(msg=f"Error getting CPU name:\n{e}")
|
|
588
589
|
return ""
|
|
589
590
|
|
|
590
|
-
def check_cpu_usage(self, display_only_if_critical: bool=False) -> str:
|
|
591
|
+
async def check_cpu_usage(self, display_only_if_critical: bool=False) -> str:
|
|
591
592
|
"""
|
|
592
593
|
Check the CPU usage.
|
|
593
594
|
|
|
@@ -613,9 +614,9 @@ class LinuxMonitor:
|
|
|
613
614
|
out_msg = f"- 🚨 **Critical CPU usage**:\n- **{cpu_percent:.2f}%** used on {cpu_cores} core of {cpu_info:.2f}GHz ({cpu_name})\n⚠️ **Check what is using so much CPU power** ⚠️"
|
|
614
615
|
|
|
615
616
|
# If there is a critical CPU usage, we also display the top 10 processes consuming the most CPU
|
|
616
|
-
out_msg += "\n" +
|
|
617
|
+
out_msg += "\n" + await self.get_ordered_processes(get_non_consuming_processes=False, order_by_ram=False, max_processes=10)
|
|
617
618
|
|
|
618
|
-
logging.warning(msg=out_msg)
|
|
619
|
+
logging.warning(msg=out_msg)
|
|
619
620
|
elif not display_only_if_critical:
|
|
620
621
|
if cpu_percent >= self.warning_cpu_percent:
|
|
621
622
|
icon = "⚠️"
|
|
@@ -632,7 +633,7 @@ class LinuxMonitor:
|
|
|
632
633
|
|
|
633
634
|
return out_msg
|
|
634
635
|
|
|
635
|
-
def check_ram_usage(self, display_only_if_critical: bool=False) -> str:
|
|
636
|
+
async def check_ram_usage(self, display_only_if_critical: bool=False) -> str:
|
|
636
637
|
"""
|
|
637
638
|
Check the RAM usage.
|
|
638
639
|
|
|
@@ -656,9 +657,9 @@ class LinuxMonitor:
|
|
|
656
657
|
out_msg = f"- 🚨 **Critical RAM usage**:\n- Total: {total_ram:.2f}GB\n- Used: {used_ram:.2f}GB ({percent_ram:.2f}%)\n- Free: {free_ram:.2f}GB\n⚠️ **Check what is using so much RAM** ⚠️"
|
|
657
658
|
|
|
658
659
|
# If there is a critical RAM usage, we also display the top 10 processes consuming the most RAM
|
|
659
|
-
out_msg += "\n" +
|
|
660
|
+
out_msg += "\n" + await self.get_ordered_processes(get_non_consuming_processes=False, order_by_ram=True, max_processes=10)
|
|
660
661
|
|
|
661
|
-
logging.warning(msg=out_msg)
|
|
662
|
+
logging.warning(msg=out_msg)
|
|
662
663
|
elif not display_only_if_critical:
|
|
663
664
|
if percent_ram >= self.warning_ram_percent:
|
|
664
665
|
icon = "⚠️"
|
|
@@ -675,7 +676,7 @@ class LinuxMonitor:
|
|
|
675
676
|
|
|
676
677
|
return out_msg
|
|
677
678
|
|
|
678
|
-
def check_load_average(self, display_only_if_critical: bool=False) -> str:
|
|
679
|
+
async def check_load_average(self, display_only_if_critical: bool=False) -> str:
|
|
679
680
|
"""
|
|
680
681
|
Check the load average.
|
|
681
682
|
|
|
@@ -702,9 +703,9 @@ class LinuxMonitor:
|
|
|
702
703
|
out_msg = f"- 🚨 **High load average**: **{avg_load_avg:.2f}%**\n⚠️ **Check what is causing the high load average** ⚠️"
|
|
703
704
|
|
|
704
705
|
# If there is a critical load average, we also display the top 10 processes consuming the most CPU
|
|
705
|
-
out_msg += "\n" +
|
|
706
|
+
out_msg += "\n" + await self.get_ordered_processes(get_non_consuming_processes=False, order_by_ram=False, max_processes=10)
|
|
706
707
|
|
|
707
|
-
logging.warning(msg=out_msg)
|
|
708
|
+
logging.warning(msg=out_msg)
|
|
708
709
|
elif not display_only_if_critical:
|
|
709
710
|
if avg_load_avg >= self.warning_load_average_percent:
|
|
710
711
|
icon = "⚠️"
|
|
@@ -721,7 +722,7 @@ class LinuxMonitor:
|
|
|
721
722
|
|
|
722
723
|
return out_msg
|
|
723
724
|
|
|
724
|
-
def check_swap_usage(self, display_only_if_critical: bool=False) -> str:
|
|
725
|
+
async def check_swap_usage(self, display_only_if_critical: bool=False) -> str:
|
|
725
726
|
"""
|
|
726
727
|
Check the SWAP usage.
|
|
727
728
|
|
|
@@ -743,6 +744,10 @@ class LinuxMonitor:
|
|
|
743
744
|
percent_swap: float = swap.percent
|
|
744
745
|
if percent_swap >= self.critical_swap_percent:
|
|
745
746
|
out_msg = f"- 🚨 **Critical SWAP usage**\n- Total: {total_swap:.2f}GB\n- Used: {used_swap:.2f}GB ({percent_swap:.2f}%)\n- Free: {free_swap:.2f}GB\n⚠️ **Check what is using so much SWAP** ⚠️"
|
|
747
|
+
|
|
748
|
+
# If there is a critical SWAP/RAM usage, we also display the top 10 processes consuming the most RAM
|
|
749
|
+
out_msg += "\n" + await self.get_ordered_processes(get_non_consuming_processes=False, order_by_ram=True, max_processes=10)
|
|
750
|
+
|
|
746
751
|
logging.warning(msg=out_msg)
|
|
747
752
|
elif not display_only_if_critical:
|
|
748
753
|
if percent_swap >= self.warning_swap_percent:
|
|
@@ -2016,40 +2021,166 @@ class LinuxMonitor:
|
|
|
2016
2021
|
logging.exception(msg=out_msg)
|
|
2017
2022
|
return out_msg
|
|
2018
2023
|
|
|
2019
|
-
|
|
2024
|
+
def _apply_command_parameters(self, command_template: str, parameters: str) -> str:
|
|
2025
|
+
"""
|
|
2026
|
+
Substitute user-provided parameters into a command template in an injection-safe way.
|
|
2027
|
+
|
|
2028
|
+
Placeholders supported in the template:
|
|
2029
|
+
- {args} : replaced by all provided parameters (space separated)
|
|
2030
|
+
- {arg1}, {arg2} : replaced by the Nth provided parameter
|
|
2031
|
+
If the template contains no placeholder, all parameters are appended at the end.
|
|
2032
|
+
|
|
2033
|
+
Every parameter is shell-quoted (shlex.quote) so it is always treated as a single
|
|
2034
|
+
literal argument, which prevents command injection through the parameters.
|
|
2035
|
+
|
|
2036
|
+
:param command_template: The command as defined in the configuration file.
|
|
2037
|
+
:param parameters: The raw parameters string provided by the user.
|
|
2038
|
+
|
|
2039
|
+
:return: The final command ready to be executed.
|
|
2040
|
+
"""
|
|
2041
|
+
param_list: List[str] = shlex.split(parameters) if parameters else []
|
|
2042
|
+
quoted_params: List[str] = [shlex.quote(param) for param in param_list]
|
|
2043
|
+
|
|
2044
|
+
has_placeholder: bool = ("{args}" in command_template) or bool(re.search(pattern=r"\{arg\d+\}", string=command_template))
|
|
2045
|
+
|
|
2046
|
+
# No placeholder: append every quoted parameter at the end of the command
|
|
2047
|
+
if not has_placeholder:
|
|
2048
|
+
if quoted_params:
|
|
2049
|
+
return command_template + " " + " ".join(quoted_params)
|
|
2050
|
+
return command_template
|
|
2051
|
+
|
|
2052
|
+
final_command: str = command_template
|
|
2053
|
+
|
|
2054
|
+
# Replace positional placeholders {arg1}, {arg2}, ...
|
|
2055
|
+
for index, quoted_param in enumerate(iterable=quoted_params, start=1):
|
|
2056
|
+
final_command = final_command.replace(f"{{arg{index}}}", quoted_param)
|
|
2057
|
+
|
|
2058
|
+
# Replace the catch-all {args} placeholder with every quoted parameter
|
|
2059
|
+
final_command = final_command.replace("{args}", " ".join(quoted_params))
|
|
2060
|
+
|
|
2061
|
+
# Any positional placeholder left unprovided is replaced by an empty string
|
|
2062
|
+
final_command = re.sub(pattern=r"\{arg\d+\}", repl="", string=final_command)
|
|
2063
|
+
|
|
2064
|
+
return final_command
|
|
2065
|
+
|
|
2066
|
+
def _normalize_command_name(self, name: str) -> str:
|
|
2067
|
+
"""
|
|
2068
|
+
Normalize a command name for permissive matching: lowercase and ignore spaces, '-' and '_'.
|
|
2069
|
+
"""
|
|
2070
|
+
return re.sub(pattern=r"[\s\-_]+", repl="", string=name.strip().lower())
|
|
2071
|
+
|
|
2072
|
+
def _levenshtein_distance(self, a: str, b: str) -> int:
|
|
2073
|
+
"""
|
|
2074
|
+
Compute the Levenshtein (edit) distance between two strings (number of single-character
|
|
2075
|
+
insertions, deletions or substitutions needed to turn one into the other).
|
|
2076
|
+
"""
|
|
2077
|
+
if a == b:
|
|
2078
|
+
return 0
|
|
2079
|
+
if len(a) == 0:
|
|
2080
|
+
return len(b)
|
|
2081
|
+
if len(b) == 0:
|
|
2082
|
+
return len(a)
|
|
2083
|
+
|
|
2084
|
+
previous_row: List[int] = list(range(len(b) + 1))
|
|
2085
|
+
for i, char_a in enumerate(iterable=a, start=1):
|
|
2086
|
+
current_row: List[int] = [i]
|
|
2087
|
+
for j, char_b in enumerate(iterable=b, start=1):
|
|
2088
|
+
insert_cost: int = current_row[j - 1] + 1
|
|
2089
|
+
delete_cost: int = previous_row[j] + 1
|
|
2090
|
+
replace_cost: int = previous_row[j - 1] + (0 if char_a == char_b else 1)
|
|
2091
|
+
current_row.append(min(insert_cost, delete_cost, replace_cost))
|
|
2092
|
+
previous_row = current_row
|
|
2093
|
+
|
|
2094
|
+
return previous_row[-1]
|
|
2095
|
+
|
|
2096
|
+
def _find_matching_command_key(self, command_name: str) -> Optional[str]:
|
|
2097
|
+
"""
|
|
2098
|
+
Find the configured command key matching the requested name in a permissive way:
|
|
2099
|
+
exact match first, then case/'-'/'_'/space-insensitive match, then a small typo tolerance.
|
|
2100
|
+
Ambiguous matches (several equally good candidates) are rejected to avoid running the wrong command.
|
|
2101
|
+
|
|
2102
|
+
:param command_name: The command name requested by the user.
|
|
2103
|
+
|
|
2104
|
+
:return: The matching command key from the configuration, or None if no safe match is found.
|
|
2105
|
+
"""
|
|
2106
|
+
command_keys: List[str] = list(self.config['commands'].keys())
|
|
2107
|
+
|
|
2108
|
+
# 1. Exact match
|
|
2109
|
+
if command_name in command_keys:
|
|
2110
|
+
return command_name
|
|
2111
|
+
|
|
2112
|
+
# 2. Normalized match (case-insensitive, '-'/'_'/spaces ignored)
|
|
2113
|
+
target: str = self._normalize_command_name(name=command_name)
|
|
2114
|
+
normalized_matches: List[str] = [key for key in command_keys if self._normalize_command_name(name=key) == target]
|
|
2115
|
+
if len(normalized_matches) == 1:
|
|
2116
|
+
return normalized_matches[0]
|
|
2117
|
+
if len(normalized_matches) > 1:
|
|
2118
|
+
# Several commands normalize to the same name: too ambiguous to guess
|
|
2119
|
+
return None
|
|
2120
|
+
|
|
2121
|
+
# 3. Fuzzy match tolerating small typos (on the normalized names)
|
|
2122
|
+
threshold: int = 1 if len(target) <= 4 else 2
|
|
2123
|
+
best_distance: Optional[int] = None
|
|
2124
|
+
best_keys: List[str] = []
|
|
2125
|
+
for key in command_keys:
|
|
2126
|
+
distance: int = self._levenshtein_distance(a=self._normalize_command_name(name=key), b=target)
|
|
2127
|
+
if best_distance is None or distance < best_distance:
|
|
2128
|
+
best_distance = distance
|
|
2129
|
+
best_keys = [key]
|
|
2130
|
+
elif distance == best_distance:
|
|
2131
|
+
best_keys.append(key)
|
|
2132
|
+
|
|
2133
|
+
# Only accept a typo-based match if it is close enough AND unambiguous
|
|
2134
|
+
if best_distance is not None and best_distance <= threshold and len(best_keys) == 1:
|
|
2135
|
+
return best_keys[0]
|
|
2136
|
+
|
|
2137
|
+
return None
|
|
2138
|
+
|
|
2139
|
+
async def execute_command(self, is_private: bool, command_name: str, parameters: str = "") -> str:
|
|
2020
2140
|
try:
|
|
2021
2141
|
out_msg: str = ""
|
|
2022
|
-
display_name: str = command_name
|
|
2023
|
-
for command_config_key in self.config['commands'].keys():
|
|
2024
|
-
if command_config_key == command_name:
|
|
2025
|
-
display_name = self.config['commands'][command_config_key]['display_name']
|
|
2026
|
-
command: str = self.config['commands'][command_config_key]['command']
|
|
2027
|
-
is_private_cmd: bool = self.config['commands'][command_config_key]['is_private']
|
|
2028
|
-
|
|
2029
|
-
if is_private or is_private == is_private_cmd:
|
|
2030
|
-
show_content_if_success: bool = self.config['commands'][command_config_key].get('show_content_if_success', False)
|
|
2031
|
-
show_content_if_issue: bool = self.config['commands'][command_config_key].get('show_content_if_issue', False)
|
|
2032
|
-
is_content_json: bool = self.config['commands'][command_config_key].get('is_content_json', False)
|
|
2033
|
-
timeout_in_sec: int = self.config['commands'][command_config_key].get('timeout_in_sec', 10)
|
|
2034
|
-
|
|
2035
|
-
res: str = await self._execute_command(command=command, display_name=display_name, show_content_if_success=show_content_if_success,
|
|
2036
|
-
show_content_if_issue=show_content_if_issue, is_content_json=is_content_json,
|
|
2037
|
-
timeout_in_sec=timeout_in_sec, display_only_if_critical=False)
|
|
2038
|
-
if res != "":
|
|
2039
|
-
if out_msg != "":
|
|
2040
|
-
out_msg += "\n"
|
|
2041
|
-
out_msg += f"{res}"
|
|
2042
|
-
else:
|
|
2043
|
-
out_msg = f"⚠️ **Command {display_name} is not allowed**"
|
|
2044
|
-
logging.warning(msg=out_msg)
|
|
2045
2142
|
|
|
2046
|
-
|
|
2047
|
-
|
|
2143
|
+
command_config_key: Optional[str] = self._find_matching_command_key(command_name=command_name)
|
|
2144
|
+
if command_config_key is None:
|
|
2145
|
+
out_msg = f"⚠️ **Command {command_name} not found**"
|
|
2146
|
+
logging.warning(msg=out_msg)
|
|
2147
|
+
return out_msg
|
|
2048
2148
|
|
|
2049
|
-
|
|
2149
|
+
display_name: str = self.config['commands'][command_config_key]['display_name']
|
|
2150
|
+
command: str = self.config['commands'][command_config_key]['command']
|
|
2151
|
+
is_private_cmd: bool = self.config['commands'][command_config_key]['is_private']
|
|
2152
|
+
|
|
2153
|
+
if is_private or is_private == is_private_cmd:
|
|
2154
|
+
parameters = parameters.strip()
|
|
2155
|
+
# A command accepts parameters only if it exposes a placeholder or explicitly opts in
|
|
2156
|
+
accept_parameters: bool = self.config['commands'][command_config_key].get('accept_parameters', False) \
|
|
2157
|
+
or ("{args}" in command) or bool(re.search(pattern=r"\{arg\d+\}", string=command))
|
|
2158
|
+
|
|
2159
|
+
if parameters != "" and not accept_parameters:
|
|
2160
|
+
out_msg = f"⚠️ **Command {display_name} does not accept parameters**"
|
|
2161
|
+
logging.warning(msg=out_msg)
|
|
2162
|
+
else:
|
|
2163
|
+
final_command: str = self._apply_command_parameters(command_template=command, parameters=parameters)
|
|
2164
|
+
|
|
2165
|
+
show_content_if_success: bool = self.config['commands'][command_config_key].get('show_content_if_success', False)
|
|
2166
|
+
show_content_if_issue: bool = self.config['commands'][command_config_key].get('show_content_if_issue', False)
|
|
2167
|
+
is_content_json: bool = self.config['commands'][command_config_key].get('is_content_json', False)
|
|
2168
|
+
timeout_in_sec: int = self.config['commands'][command_config_key].get('timeout_in_sec', 10)
|
|
2169
|
+
|
|
2170
|
+
res: str = await self._execute_command(command=final_command, display_name=display_name, show_content_if_success=show_content_if_success,
|
|
2171
|
+
show_content_if_issue=show_content_if_issue, is_content_json=is_content_json,
|
|
2172
|
+
timeout_in_sec=timeout_in_sec, display_only_if_critical=False)
|
|
2173
|
+
if res != "":
|
|
2174
|
+
if out_msg != "":
|
|
2175
|
+
out_msg += "\n"
|
|
2176
|
+
out_msg += f"{res}"
|
|
2177
|
+
else:
|
|
2178
|
+
out_msg = f"⚠️ **Command {display_name} is not allowed**"
|
|
2179
|
+
logging.warning(msg=out_msg)
|
|
2180
|
+
|
|
2181
|
+
if out_msg != "":
|
|
2182
|
+
out_msg = f"# 📜 Command {display_name} 📜\n{out_msg}"
|
|
2050
2183
|
|
|
2051
|
-
out_msg = f"⚠️ **Command {display_name} not found**"
|
|
2052
|
-
logging.warning(msg=out_msg)
|
|
2053
2184
|
return out_msg
|
|
2054
2185
|
except Exception as e:
|
|
2055
2186
|
return f"⚠️ **Error executing command {command_name}**:\n```sh\n{e}\n```"
|
|
@@ -2150,7 +2281,7 @@ class LinuxMonitor:
|
|
|
2150
2281
|
try:
|
|
2151
2282
|
# Load average
|
|
2152
2283
|
if is_private:
|
|
2153
|
-
msg = self.check_load_average(display_only_if_critical=True)
|
|
2284
|
+
msg = await self.check_load_average(display_only_if_critical=True)
|
|
2154
2285
|
if msg != "":
|
|
2155
2286
|
logging.warning(msg=msg)
|
|
2156
2287
|
if datetime_last_load_average_error_displayed is None or ((datetime.now() - datetime_last_load_average_error_displayed).total_seconds() > self.max_duration_seconds_showing_same_error_again_in_scheduled_tasks):
|
|
@@ -2172,7 +2303,7 @@ class LinuxMonitor:
|
|
|
2172
2303
|
|
|
2173
2304
|
# CPU
|
|
2174
2305
|
if is_private:
|
|
2175
|
-
msg = self.check_cpu_usage(display_only_if_critical=True)
|
|
2306
|
+
msg = await self.check_cpu_usage(display_only_if_critical=True)
|
|
2176
2307
|
if msg != "":
|
|
2177
2308
|
logging.warning(msg=msg)
|
|
2178
2309
|
if datetime_last_cpu_usage_error_displayed is None or ((datetime.now() - datetime_last_cpu_usage_error_displayed).total_seconds() > self.max_duration_seconds_showing_same_error_again_in_scheduled_tasks):
|
|
@@ -2194,7 +2325,7 @@ class LinuxMonitor:
|
|
|
2194
2325
|
|
|
2195
2326
|
# RAM
|
|
2196
2327
|
if is_private:
|
|
2197
|
-
msg = self.check_ram_usage(display_only_if_critical=True)
|
|
2328
|
+
msg = await self.check_ram_usage(display_only_if_critical=True)
|
|
2198
2329
|
if msg != "":
|
|
2199
2330
|
logging.warning(msg=msg)
|
|
2200
2331
|
if datetime_last_ram_usage_error_displayed is None or ((datetime.now() - datetime_last_ram_usage_error_displayed).total_seconds() > self.max_duration_seconds_showing_same_error_again_in_scheduled_tasks):
|
|
@@ -2216,7 +2347,7 @@ class LinuxMonitor:
|
|
|
2216
2347
|
|
|
2217
2348
|
# Swap
|
|
2218
2349
|
if is_private:
|
|
2219
|
-
msg = self.check_swap_usage(display_only_if_critical=True)
|
|
2350
|
+
msg = await self.check_swap_usage(display_only_if_critical=True)
|
|
2220
2351
|
if msg != "":
|
|
2221
2352
|
logging.warning(msg=msg)
|
|
2222
2353
|
if datetime_last_swap_usage_error_displayed is None or ((datetime.now() - datetime_last_swap_usage_error_displayed).total_seconds() > self.max_duration_seconds_showing_same_error_again_in_scheduled_tasks):
|
|
@@ -2565,7 +2696,7 @@ class LinuxMonitor:
|
|
|
2565
2696
|
|
|
2566
2697
|
# Load average
|
|
2567
2698
|
if is_private:
|
|
2568
|
-
msg = self.check_load_average(display_only_if_critical=False)
|
|
2699
|
+
msg = await self.check_load_average(display_only_if_critical=False)
|
|
2569
2700
|
if msg != "":
|
|
2570
2701
|
if out_msg != "":
|
|
2571
2702
|
out_msg += "\n"
|
|
@@ -2573,7 +2704,7 @@ class LinuxMonitor:
|
|
|
2573
2704
|
|
|
2574
2705
|
# CPU
|
|
2575
2706
|
if is_private:
|
|
2576
|
-
msg = self.check_cpu_usage(display_only_if_critical=False)
|
|
2707
|
+
msg = await self.check_cpu_usage(display_only_if_critical=False)
|
|
2577
2708
|
if msg != "":
|
|
2578
2709
|
if out_msg != "":
|
|
2579
2710
|
out_msg += "\n"
|
|
@@ -2581,7 +2712,7 @@ class LinuxMonitor:
|
|
|
2581
2712
|
|
|
2582
2713
|
# RAM
|
|
2583
2714
|
if is_private:
|
|
2584
|
-
msg = self.check_ram_usage(display_only_if_critical=False)
|
|
2715
|
+
msg = await self.check_ram_usage(display_only_if_critical=False)
|
|
2585
2716
|
if msg != "":
|
|
2586
2717
|
if out_msg != "":
|
|
2587
2718
|
out_msg += "\n"
|
|
@@ -2589,7 +2720,7 @@ class LinuxMonitor:
|
|
|
2589
2720
|
|
|
2590
2721
|
# Swap
|
|
2591
2722
|
if is_private:
|
|
2592
|
-
msg = self.check_swap_usage(display_only_if_critical=False)
|
|
2723
|
+
msg = await self.check_swap_usage(display_only_if_critical=False)
|
|
2593
2724
|
if msg != "":
|
|
2594
2725
|
if out_msg != "":
|
|
2595
2726
|
out_msg += "\n"
|
|
@@ -6,7 +6,7 @@ with io.open(file='README.md', mode='r', encoding='utf-8') as readme_file:
|
|
|
6
6
|
|
|
7
7
|
setup(
|
|
8
8
|
name="LinuxMonitor",
|
|
9
|
-
version="1.5.
|
|
9
|
+
version="1.5.7",
|
|
10
10
|
description="Get information and warning status of Linux server like service, port, ping, ssl certificate, disk/folder/cpu/ram/swap usage, ip connection, ... (Python and shell library, Linux ONLY)",
|
|
11
11
|
long_description=readme,
|
|
12
12
|
long_description_content_type="text/markdown",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|