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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: LinuxMonitor
3
- Version: 1.5.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
  [![PyPI version](https://badge.fury.io/py/LinuxMonitor.svg)](https://pypi.org/project/LinuxMonitor/) [![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://github.com/QuentinCG/Linux-Monitor-Python-Library/blob/master/LICENSE.md) [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/QuentinCG) [![Downloads](https://static.pepy.tech/badge/LinuxMonitor)](https://pepy.tech/project/LinuxMonitor) [![Downloads](https://static.pepy.tech/badge/LinuxMonitor/month)](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
1
+ Metadata-Version: 2.4
2
2
  Name: LinuxMonitor
3
- Version: 1.5.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
  [![PyPI version](https://badge.fury.io/py/LinuxMonitor.svg)](https://pypi.org/project/LinuxMonitor/) [![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://github.com/QuentinCG/Linux-Monitor-Python-Library/blob/master/LICENSE.md) [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/QuentinCG) [![Downloads](https://static.pepy.tech/badge/LinuxMonitor)](https://pepy.tech/project/LinuxMonitor) [![Downloads](https://static.pepy.tech/badge/LinuxMonitor/month)](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.5 (2024/10/27)"
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" + asyncio.run(self.get_ordered_processes(get_non_consuming_processes=False, order_by_ram=False, max_processes=10)) # type: ignore
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) # type: ignore
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" + asyncio.run(self.get_ordered_processes(get_non_consuming_processes=False, order_by_ram=True, max_processes=10)) # type: ignore
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) # type: ignore
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" + asyncio.run(self.get_ordered_processes(get_non_consuming_processes=False, order_by_ram=False, max_processes=10)) # type: ignore
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) # type: ignore
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
- async def execute_command(self, is_private: bool, command_name: str) -> str:
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
- if out_msg != "":
2047
- out_msg = f"# 📜 Command {display_name} 📜\n{out_msg}"
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
- return out_msg
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.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