linux-admin-mcp-server 0.1.0__py3-none-any.whl
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.
- linux_admin_mcp_server-0.1.0.dist-info/METADATA +109 -0
- linux_admin_mcp_server-0.1.0.dist-info/RECORD +11 -0
- linux_admin_mcp_server-0.1.0.dist-info/WHEEL +4 -0
- linux_admin_mcp_server-0.1.0.dist-info/entry_points.txt +2 -0
- src/__init__.py +0 -0
- src/server.py +213 -0
- src/tools/__init__.py +0 -0
- src/tools/network.py +139 -0
- src/tools/processes.py +151 -0
- src/tools/services.py +148 -0
- src/tools/system.py +173 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: linux-admin-mcp-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for Linux system administration — manage services, processes, disk, network and logs via AI agents
|
|
5
|
+
Project-URL: Homepage, https://github.com/AiAgentKarl/linux-admin-mcp-server
|
|
6
|
+
Project-URL: Repository, https://github.com/AiAgentKarl/linux-admin-mcp-server
|
|
7
|
+
Author-email: AiAgentKarl <coach1916@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: admin,claude,devops,linux,mcp,server,sysadmin,systemd
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: System Administrators
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Topic :: System :: Systems Administration
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: fastmcp>=0.1.0
|
|
19
|
+
Requires-Dist: mcp[cli]>=1.0.0
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# linux-admin-mcp-server
|
|
23
|
+
|
|
24
|
+
**Linux system administration via AI agents** — MCP server for managing services, processes, disk, network and logs on Linux systems.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **14 tools** for complete Linux system management
|
|
29
|
+
- **System info**: CPU, RAM, Uptime, Kernel, OS-Release
|
|
30
|
+
- **Disk & Memory**: Usage statistics with warnings for high utilization
|
|
31
|
+
- **Health Check**: Automated system health assessment (OK/WARNING)
|
|
32
|
+
- **Systemd Services**: List, status, start/stop/restart, enable/disable, logs
|
|
33
|
+
- **Process Management**: List processes sorted by CPU/memory, search by name
|
|
34
|
+
- **Network**: Interfaces, open ports, active connections, DNS lookup
|
|
35
|
+
- **Safety**: Only predefined commands, no arbitrary shell execution
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install linux-admin-mcp-server
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage with Claude Desktop
|
|
44
|
+
|
|
45
|
+
Add to your `claude_desktop_config.json`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"linux-admin": {
|
|
51
|
+
"command": "linux-admin-mcp-server"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
> **Note:** Run Claude Desktop on your Linux machine, or use this server on a Linux system where you want AI-assisted administration.
|
|
58
|
+
|
|
59
|
+
## Available Tools
|
|
60
|
+
|
|
61
|
+
| Tool | Description | Sudo Required |
|
|
62
|
+
|------|-------------|---------------|
|
|
63
|
+
| `system_info` | Hostname, kernel, CPU, RAM, uptime | No |
|
|
64
|
+
| `disk_usage` | Disk usage for all mounted filesystems | No |
|
|
65
|
+
| `memory_usage` | RAM and swap usage details | No |
|
|
66
|
+
| `health_check` | Full system health assessment | No |
|
|
67
|
+
| `services_list` | List systemd services (all/running/failed) | No |
|
|
68
|
+
| `service_status` | Detailed status of a specific service | No |
|
|
69
|
+
| `service_manage` | Start/stop/restart/enable/disable a service | Yes |
|
|
70
|
+
| `service_logs` | Last N log lines via journalctl | No |
|
|
71
|
+
| `processes_list` | Running processes sorted by CPU/memory | No |
|
|
72
|
+
| `process_find` | Find processes by name | No |
|
|
73
|
+
| `top_consumers` | Top 5 CPU and memory consumers | No |
|
|
74
|
+
| `network_interfaces` | Network interfaces and IP addresses | No |
|
|
75
|
+
| `open_ports` | Listening ports (LISTEN state) | No |
|
|
76
|
+
| `active_connections` | Established network connections | No |
|
|
77
|
+
| `dns_lookup` | Resolve hostname to IP address | No |
|
|
78
|
+
|
|
79
|
+
## Example Prompts
|
|
80
|
+
|
|
81
|
+
- "Show me the system health status"
|
|
82
|
+
- "What services are currently failing?"
|
|
83
|
+
- "Restart the nginx service"
|
|
84
|
+
- "Show the last 100 lines of the postgres logs"
|
|
85
|
+
- "Which processes are consuming the most memory?"
|
|
86
|
+
- "What ports is this server listening on?"
|
|
87
|
+
- "What's the disk usage on this machine?"
|
|
88
|
+
|
|
89
|
+
## Requirements
|
|
90
|
+
|
|
91
|
+
- Linux operating system (Ubuntu, Debian, CentOS, Fedora, etc.)
|
|
92
|
+
- Python 3.10+
|
|
93
|
+
- systemd (for service management tools)
|
|
94
|
+
- `ss` or `netstat` (for network tools)
|
|
95
|
+
- sudo access (only for service start/stop/enable/disable)
|
|
96
|
+
|
|
97
|
+
## Compared to ssh-mcp-server
|
|
98
|
+
|
|
99
|
+
| Feature | linux-admin-mcp-server | ssh-mcp-server |
|
|
100
|
+
|---------|------------------------|----------------|
|
|
101
|
+
| Use case | Local Linux admin | Remote via SSH |
|
|
102
|
+
| SSH required | No | Yes |
|
|
103
|
+
| systemd support | Full (14 tools) | Basic |
|
|
104
|
+
| Health check | Yes | No |
|
|
105
|
+
| Network tools | 4 tools | Limited |
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT License — AiAgentKarl 2026
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
src/server.py,sha256=0HDiZrlGQOE4_hYx1IPdfGO1Cnabto_vi3XsRtAStXc,5758
|
|
3
|
+
src/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
src/tools/network.py,sha256=PhlkQVvp38W8IhvIBwkLnnmB2NRGqkIRiC-OyTB_Big,4563
|
|
5
|
+
src/tools/processes.py,sha256=KIxtPa0D_YIWeHvbHssnfRTTaHRvP1MSoPtPpWXWqjg,4407
|
|
6
|
+
src/tools/services.py,sha256=Dzs2EUBjJGukI1Sc3f0zDTnrLiS6RNMqV96m-Y5Sk8k,5234
|
|
7
|
+
src/tools/system.py,sha256=8d2g9IjdznKadahWBxFnuR275t7-46ShuzNSiQoBXKQ,5439
|
|
8
|
+
linux_admin_mcp_server-0.1.0.dist-info/METADATA,sha256=vwC6rL3KleDX8wawZVNd9q-rPXayt5dAdWbOD-ckDtk,4007
|
|
9
|
+
linux_admin_mcp_server-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
10
|
+
linux_admin_mcp_server-0.1.0.dist-info/entry_points.txt,sha256=Jr1cvgaAgdEG0vrchsSDEWZ1dh3zdHge7cVWGm92BkQ,59
|
|
11
|
+
linux_admin_mcp_server-0.1.0.dist-info/RECORD,,
|
src/__init__.py
ADDED
|
File without changes
|
src/server.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Linux Admin MCP Server — AI-Zugriff auf Linux-Systemverwaltung"""
|
|
2
|
+
import sys
|
|
3
|
+
from fastmcp import FastMCP
|
|
4
|
+
|
|
5
|
+
from src.tools.system import (
|
|
6
|
+
get_system_info,
|
|
7
|
+
get_disk_usage,
|
|
8
|
+
get_memory_usage,
|
|
9
|
+
run_health_check,
|
|
10
|
+
)
|
|
11
|
+
from src.tools.services import (
|
|
12
|
+
list_services,
|
|
13
|
+
get_service_status,
|
|
14
|
+
manage_service,
|
|
15
|
+
get_service_logs,
|
|
16
|
+
)
|
|
17
|
+
from src.tools.processes import (
|
|
18
|
+
list_processes,
|
|
19
|
+
find_process,
|
|
20
|
+
get_top_consumers,
|
|
21
|
+
)
|
|
22
|
+
from src.tools.network import (
|
|
23
|
+
get_network_interfaces,
|
|
24
|
+
get_open_ports,
|
|
25
|
+
get_active_connections,
|
|
26
|
+
check_dns_resolution,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# FastMCP Server initialisieren
|
|
30
|
+
mcp = FastMCP(
|
|
31
|
+
"linux-admin-mcp-server",
|
|
32
|
+
instructions=(
|
|
33
|
+
"Linux system administration MCP server. Provides tools for managing "
|
|
34
|
+
"systemd services, monitoring processes, checking disk/memory usage, "
|
|
35
|
+
"inspecting network configuration, and running system health checks. "
|
|
36
|
+
"Requires root/sudo for service management actions (start/stop/enable/disable). "
|
|
37
|
+
"All other tools work as a regular user."
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# --- System-Tools ---
|
|
43
|
+
|
|
44
|
+
@mcp.tool()
|
|
45
|
+
def system_info() -> dict:
|
|
46
|
+
"""
|
|
47
|
+
Gibt allgemeine System-Informationen zurück.
|
|
48
|
+
Enthält: Hostname, Kernel-Version, CPU-Modell, RAM, Uptime, Architektur, OS-Release.
|
|
49
|
+
Kein sudo erforderlich.
|
|
50
|
+
"""
|
|
51
|
+
return get_system_info()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@mcp.tool()
|
|
55
|
+
def disk_usage() -> dict:
|
|
56
|
+
"""
|
|
57
|
+
Zeigt Festplatten-Belegung aller gemounteten Laufwerke.
|
|
58
|
+
Enthält: Filesystem, Größe, Belegung, verfügbar, Auslastungsprozent, Mountpoint.
|
|
59
|
+
Kein sudo erforderlich.
|
|
60
|
+
"""
|
|
61
|
+
return get_disk_usage()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@mcp.tool()
|
|
65
|
+
def memory_usage() -> dict:
|
|
66
|
+
"""
|
|
67
|
+
Zeigt detaillierte RAM- und Swap-Nutzung.
|
|
68
|
+
Enthält: Gesamt, verwendet, frei, verfügbar für RAM und Swap.
|
|
69
|
+
Kein sudo erforderlich.
|
|
70
|
+
"""
|
|
71
|
+
return get_memory_usage()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@mcp.tool()
|
|
75
|
+
def health_check() -> dict:
|
|
76
|
+
"""
|
|
77
|
+
Führt einen vollständigen System-Health-Check durch.
|
|
78
|
+
Prüft: Disk-Belegung über 80%, CPU-Load, RAM-Verfügbarkeit, Uptime.
|
|
79
|
+
Gibt status='OK' oder 'WARNING' mit einer Liste der gefundenen Probleme zurück.
|
|
80
|
+
Kein sudo erforderlich.
|
|
81
|
+
"""
|
|
82
|
+
return run_health_check()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# --- Service-Tools ---
|
|
86
|
+
|
|
87
|
+
@mcp.tool()
|
|
88
|
+
def services_list(filter_state: str = "all") -> dict:
|
|
89
|
+
"""
|
|
90
|
+
Listet systemd-Services auf.
|
|
91
|
+
filter_state: 'all' (Standard), 'running', 'failed', 'inactive'
|
|
92
|
+
Gibt max. 50 Services zurück mit Name, Status, Beschreibung.
|
|
93
|
+
Kein sudo erforderlich.
|
|
94
|
+
"""
|
|
95
|
+
return list_services(filter_state=filter_state)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@mcp.tool()
|
|
99
|
+
def service_status(service_name: str) -> dict:
|
|
100
|
+
"""
|
|
101
|
+
Gibt den detaillierten Status eines systemd-Services zurück.
|
|
102
|
+
service_name: Name des Services (z.B. 'nginx', 'postgresql', 'sshd')
|
|
103
|
+
Enthält: aktiv/inaktiv, enabled/disabled, letzten Log-Output.
|
|
104
|
+
Kein sudo erforderlich.
|
|
105
|
+
"""
|
|
106
|
+
return get_service_status(service_name=service_name)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@mcp.tool()
|
|
110
|
+
def service_manage(service_name: str, action: str) -> dict:
|
|
111
|
+
"""
|
|
112
|
+
Verwaltet einen systemd-Service.
|
|
113
|
+
service_name: Name des Services (z.B. 'nginx', 'postgresql')
|
|
114
|
+
action: 'start', 'stop', 'restart', 'reload', 'enable', 'disable'
|
|
115
|
+
HINWEIS: Erfordert sudo-Rechte. Server muss als root laufen oder sudoers-Eintrag haben.
|
|
116
|
+
"""
|
|
117
|
+
return manage_service(service_name=service_name, action=action)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@mcp.tool()
|
|
121
|
+
def service_logs(service_name: str, lines: int = 50) -> dict:
|
|
122
|
+
"""
|
|
123
|
+
Gibt die letzten Log-Zeilen eines Services über journalctl zurück.
|
|
124
|
+
service_name: Name des Services
|
|
125
|
+
lines: Anzahl der Log-Zeilen (Standard: 50, max: 500)
|
|
126
|
+
Kein sudo erforderlich.
|
|
127
|
+
"""
|
|
128
|
+
return get_service_logs(service_name=service_name, lines=lines)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# --- Prozess-Tools ---
|
|
132
|
+
|
|
133
|
+
@mcp.tool()
|
|
134
|
+
def processes_list(sort_by: str = "cpu", limit: int = 20) -> dict:
|
|
135
|
+
"""
|
|
136
|
+
Listet laufende Prozesse auf.
|
|
137
|
+
sort_by: 'cpu' (Standard), 'memory', 'pid'
|
|
138
|
+
limit: Anzahl der Prozesse (Standard: 20, max: 100)
|
|
139
|
+
Kein sudo erforderlich.
|
|
140
|
+
"""
|
|
141
|
+
return list_processes(sort_by=sort_by, limit=limit)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@mcp.tool()
|
|
145
|
+
def process_find(name: str) -> dict:
|
|
146
|
+
"""
|
|
147
|
+
Sucht nach laufenden Prozessen anhand eines Namens oder Befehlsteils.
|
|
148
|
+
name: Suchbegriff (z.B. 'nginx', 'python', 'postgres')
|
|
149
|
+
Gibt PID und vollständigen Befehl zurück.
|
|
150
|
+
Kein sudo erforderlich.
|
|
151
|
+
"""
|
|
152
|
+
return find_process(name=name)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@mcp.tool()
|
|
156
|
+
def top_consumers() -> dict:
|
|
157
|
+
"""
|
|
158
|
+
Zeigt die Top-5 CPU- und Memory-Verbraucher.
|
|
159
|
+
Nützlich für schnelle Performance-Diagnose.
|
|
160
|
+
Kein sudo erforderlich.
|
|
161
|
+
"""
|
|
162
|
+
return get_top_consumers()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# --- Netzwerk-Tools ---
|
|
166
|
+
|
|
167
|
+
@mcp.tool()
|
|
168
|
+
def network_interfaces() -> dict:
|
|
169
|
+
"""
|
|
170
|
+
Listet alle Netzwerk-Interfaces mit IP-Adressen auf.
|
|
171
|
+
Enthält: Interface-Name, IPv4/IPv6-Adressen, Flags.
|
|
172
|
+
Kein sudo erforderlich.
|
|
173
|
+
"""
|
|
174
|
+
return get_network_interfaces()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@mcp.tool()
|
|
178
|
+
def open_ports() -> dict:
|
|
179
|
+
"""
|
|
180
|
+
Listet alle Ports auf, die auf eingehende Verbindungen warten (LISTEN).
|
|
181
|
+
Nützlich um zu sehen welche Services aktiv Verbindungen annehmen.
|
|
182
|
+
Kein sudo erforderlich (ohne Prozess-Info).
|
|
183
|
+
"""
|
|
184
|
+
return get_open_ports()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@mcp.tool()
|
|
188
|
+
def active_connections() -> dict:
|
|
189
|
+
"""
|
|
190
|
+
Zeigt aktive Netzwerkverbindungen (ESTABLISHED).
|
|
191
|
+
Gibt lokale und Remote-Adressen für bis zu 50 Verbindungen zurück.
|
|
192
|
+
Kein sudo erforderlich.
|
|
193
|
+
"""
|
|
194
|
+
return get_active_connections()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@mcp.tool()
|
|
198
|
+
def dns_lookup(hostname: str) -> dict:
|
|
199
|
+
"""
|
|
200
|
+
Löst einen Hostnamen per DNS auf und gibt die IP-Adresse zurück.
|
|
201
|
+
hostname: Zu auflösender Hostname (z.B. 'google.com', 'api.example.com')
|
|
202
|
+
Kein sudo erforderlich.
|
|
203
|
+
"""
|
|
204
|
+
return check_dns_resolution(hostname=hostname)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def main():
|
|
208
|
+
"""Server-Einstiegspunkt."""
|
|
209
|
+
mcp.run(transport="stdio")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
main()
|
src/tools/__init__.py
ADDED
|
File without changes
|
src/tools/network.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Netzwerk-Tools: Interfaces, Verbindungen, Ports, DNS"""
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _run(cmd: list[str], timeout: int = 10) -> tuple[int, str, str]:
|
|
7
|
+
"""Führt einen Befehl sicher aus."""
|
|
8
|
+
try:
|
|
9
|
+
result = subprocess.run(
|
|
10
|
+
cmd,
|
|
11
|
+
capture_output=True,
|
|
12
|
+
text=True,
|
|
13
|
+
timeout=timeout,
|
|
14
|
+
check=False,
|
|
15
|
+
)
|
|
16
|
+
return result.returncode, result.stdout.strip(), result.stderr.strip()
|
|
17
|
+
except subprocess.TimeoutExpired:
|
|
18
|
+
return 1, "", "Timeout"
|
|
19
|
+
except FileNotFoundError:
|
|
20
|
+
return 1, "", f"Befehl nicht gefunden: {cmd[0]}"
|
|
21
|
+
except Exception as e:
|
|
22
|
+
return 1, "", str(e)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_network_interfaces() -> dict[str, Any]:
|
|
26
|
+
"""Gibt alle Netzwerk-Interfaces mit IP-Adressen zurück."""
|
|
27
|
+
rc, stdout, stderr = _run(["ip", "addr", "show"])
|
|
28
|
+
if rc != 0:
|
|
29
|
+
# Fallback auf ifconfig
|
|
30
|
+
rc, stdout, stderr = _run(["ifconfig", "-a"])
|
|
31
|
+
|
|
32
|
+
interfaces = []
|
|
33
|
+
current_iface = None
|
|
34
|
+
|
|
35
|
+
for line in stdout.split("\n"):
|
|
36
|
+
# Neue Interface-Zeile (beginnt mit Zahl und Doppelpunkt)
|
|
37
|
+
if line and line[0].isdigit() and ": " in line:
|
|
38
|
+
parts = line.split(": ")
|
|
39
|
+
if len(parts) >= 2:
|
|
40
|
+
current_iface = {
|
|
41
|
+
"name": parts[1].split("@")[0].split(" ")[0],
|
|
42
|
+
"flags": line.split("<")[1].split(">")[0] if "<" in line else "",
|
|
43
|
+
"addresses": [],
|
|
44
|
+
}
|
|
45
|
+
interfaces.append(current_iface)
|
|
46
|
+
elif current_iface and "inet " in line:
|
|
47
|
+
parts = line.strip().split()
|
|
48
|
+
if len(parts) >= 2:
|
|
49
|
+
current_iface["addresses"].append({
|
|
50
|
+
"type": "ipv4",
|
|
51
|
+
"address": parts[1],
|
|
52
|
+
})
|
|
53
|
+
elif current_iface and "inet6 " in line:
|
|
54
|
+
parts = line.strip().split()
|
|
55
|
+
if len(parts) >= 2:
|
|
56
|
+
current_iface["addresses"].append({
|
|
57
|
+
"type": "ipv6",
|
|
58
|
+
"address": parts[1],
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
"interfaces": interfaces,
|
|
63
|
+
"count": len(interfaces),
|
|
64
|
+
"raw": stdout[:2000], # Roh-Ausgabe kürzen
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_open_ports() -> dict[str, Any]:
|
|
69
|
+
"""Listet alle offenen Ports (LISTEN) auf."""
|
|
70
|
+
# ss bevorzugen (moderner), Fallback auf netstat
|
|
71
|
+
rc, stdout, stderr = _run(["ss", "-tlnp"])
|
|
72
|
+
if rc != 0:
|
|
73
|
+
rc, stdout, stderr = _run(["netstat", "-tlnp"])
|
|
74
|
+
|
|
75
|
+
ports = []
|
|
76
|
+
for line in stdout.split("\n"):
|
|
77
|
+
if "LISTEN" in line or (rc != 0 and "tcp" in line.lower()):
|
|
78
|
+
parts = line.split()
|
|
79
|
+
if len(parts) >= 4:
|
|
80
|
+
local_addr = parts[3] if rc == 0 else parts[3]
|
|
81
|
+
ports.append({
|
|
82
|
+
"local_address": local_addr,
|
|
83
|
+
"raw": line.strip(),
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
"listening_ports": ports,
|
|
88
|
+
"count": len(ports),
|
|
89
|
+
"raw": stdout[:3000],
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_active_connections() -> dict[str, Any]:
|
|
94
|
+
"""Zeigt aktive Netzwerkverbindungen (ESTABLISHED)."""
|
|
95
|
+
rc, stdout, stderr = _run(["ss", "-tnp", "state", "established"])
|
|
96
|
+
if rc != 0:
|
|
97
|
+
rc, stdout, stderr = _run(["netstat", "-tnp"])
|
|
98
|
+
|
|
99
|
+
connections = []
|
|
100
|
+
for line in stdout.split("\n")[1:]: # Header überspringen
|
|
101
|
+
if line.strip():
|
|
102
|
+
parts = line.split()
|
|
103
|
+
if len(parts) >= 5:
|
|
104
|
+
connections.append({
|
|
105
|
+
"local": parts[3] if len(parts) > 3 else "",
|
|
106
|
+
"remote": parts[4] if len(parts) > 4 else "",
|
|
107
|
+
"raw": line.strip(),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
"connections": connections[:50],
|
|
112
|
+
"count": len(connections),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def check_dns_resolution(hostname: str) -> dict[str, Any]:
|
|
117
|
+
"""Löst einen Hostnamen per DNS auf."""
|
|
118
|
+
# Sicherheitscheck
|
|
119
|
+
safe_chars = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._")
|
|
120
|
+
if not all(c in safe_chars for c in hostname):
|
|
121
|
+
return {"error": f"Ungültiger Hostname: {hostname}"}
|
|
122
|
+
|
|
123
|
+
rc, stdout, stderr = _run(["getent", "hosts", hostname])
|
|
124
|
+
if rc == 0 and stdout:
|
|
125
|
+
parts = stdout.split()
|
|
126
|
+
return {
|
|
127
|
+
"hostname": hostname,
|
|
128
|
+
"resolved": True,
|
|
129
|
+
"ip": parts[0] if parts else "unbekannt",
|
|
130
|
+
"aliases": parts[1:],
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Fallback: nslookup
|
|
134
|
+
rc2, stdout2, _ = _run(["nslookup", hostname])
|
|
135
|
+
return {
|
|
136
|
+
"hostname": hostname,
|
|
137
|
+
"resolved": rc2 == 0,
|
|
138
|
+
"output": stdout2[:500] if stdout2 else stderr[:200],
|
|
139
|
+
}
|
src/tools/processes.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Prozess-Management-Tools: Laufende Prozesse, Top-Verbraucher, Suche"""
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _run(cmd: list[str], timeout: int = 10) -> tuple[int, str, str]:
|
|
7
|
+
"""Führt einen Befehl sicher aus."""
|
|
8
|
+
try:
|
|
9
|
+
result = subprocess.run(
|
|
10
|
+
cmd,
|
|
11
|
+
capture_output=True,
|
|
12
|
+
text=True,
|
|
13
|
+
timeout=timeout,
|
|
14
|
+
check=False,
|
|
15
|
+
)
|
|
16
|
+
return result.returncode, result.stdout.strip(), result.stderr.strip()
|
|
17
|
+
except subprocess.TimeoutExpired:
|
|
18
|
+
return 1, "", "Timeout"
|
|
19
|
+
except FileNotFoundError:
|
|
20
|
+
return 1, "", f"Befehl nicht gefunden: {cmd[0]}"
|
|
21
|
+
except Exception as e:
|
|
22
|
+
return 1, "", str(e)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def list_processes(sort_by: str = "cpu", limit: int = 20) -> dict[str, Any]:
|
|
26
|
+
"""
|
|
27
|
+
Listet laufende Prozesse auf.
|
|
28
|
+
sort_by: 'cpu', 'memory', 'pid'
|
|
29
|
+
limit: Anzahl der zurückgegebenen Prozesse (max 100)
|
|
30
|
+
"""
|
|
31
|
+
limit = min(max(1, limit), 100)
|
|
32
|
+
|
|
33
|
+
# Sortier-Option festlegen
|
|
34
|
+
sort_map = {
|
|
35
|
+
"cpu": "-k3",
|
|
36
|
+
"memory": "-k4",
|
|
37
|
+
"pid": "-k1",
|
|
38
|
+
}
|
|
39
|
+
sort_flag = sort_map.get(sort_by, "-k3")
|
|
40
|
+
|
|
41
|
+
rc, stdout, stderr = _run([
|
|
42
|
+
"ps", "aux", "--no-headers"
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
if rc != 0:
|
|
46
|
+
return {"error": stderr, "processes": []}
|
|
47
|
+
|
|
48
|
+
processes = []
|
|
49
|
+
lines = stdout.split("\n")
|
|
50
|
+
|
|
51
|
+
# Nach CPU oder Memory sortieren
|
|
52
|
+
def sort_key(line):
|
|
53
|
+
parts = line.split(None, 10)
|
|
54
|
+
if len(parts) < 4:
|
|
55
|
+
return 0.0
|
|
56
|
+
try:
|
|
57
|
+
if sort_by == "memory":
|
|
58
|
+
return float(parts[3])
|
|
59
|
+
elif sort_by == "pid":
|
|
60
|
+
return int(parts[1])
|
|
61
|
+
else: # cpu
|
|
62
|
+
return float(parts[2])
|
|
63
|
+
except ValueError:
|
|
64
|
+
return 0.0
|
|
65
|
+
|
|
66
|
+
lines.sort(key=sort_key, reverse=(sort_by != "pid"))
|
|
67
|
+
|
|
68
|
+
for line in lines[:limit]:
|
|
69
|
+
parts = line.split(None, 10)
|
|
70
|
+
if len(parts) >= 11:
|
|
71
|
+
processes.append({
|
|
72
|
+
"user": parts[0],
|
|
73
|
+
"pid": parts[1],
|
|
74
|
+
"cpu_pct": parts[2],
|
|
75
|
+
"mem_pct": parts[3],
|
|
76
|
+
"vsz": parts[4],
|
|
77
|
+
"rss": parts[5],
|
|
78
|
+
"stat": parts[7],
|
|
79
|
+
"start": parts[8],
|
|
80
|
+
"time": parts[9],
|
|
81
|
+
"command": parts[10][:100], # Befehl kürzen
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
"sort_by": sort_by,
|
|
86
|
+
"count": len(processes),
|
|
87
|
+
"processes": processes,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def find_process(name: str) -> dict[str, Any]:
|
|
92
|
+
"""Sucht nach Prozessen mit einem bestimmten Namen."""
|
|
93
|
+
# Sicherheitscheck: Name darf keine Shell-Sonderzeichen enthalten
|
|
94
|
+
safe_chars = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_. /")
|
|
95
|
+
if not all(c in safe_chars for c in name):
|
|
96
|
+
return {"error": f"Ungültiger Prozessname: {name}"}
|
|
97
|
+
|
|
98
|
+
rc, stdout, stderr = _run(["pgrep", "-a", "-f", name])
|
|
99
|
+
|
|
100
|
+
processes = []
|
|
101
|
+
if rc == 0 and stdout:
|
|
102
|
+
for line in stdout.split("\n"):
|
|
103
|
+
parts = line.split(None, 1)
|
|
104
|
+
if len(parts) == 2:
|
|
105
|
+
processes.append({"pid": parts[0], "command": parts[1]})
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"search": name,
|
|
109
|
+
"found": len(processes),
|
|
110
|
+
"processes": processes,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_top_consumers() -> dict[str, Any]:
|
|
115
|
+
"""Gibt die Top-5 CPU- und Memory-Verbraucher zurück."""
|
|
116
|
+
rc, stdout, _ = _run(["ps", "aux", "--no-headers"])
|
|
117
|
+
if rc != 0:
|
|
118
|
+
return {"error": "ps nicht verfügbar"}
|
|
119
|
+
|
|
120
|
+
lines = [l for l in stdout.split("\n") if l.strip()]
|
|
121
|
+
|
|
122
|
+
def parse_line(line: str) -> dict:
|
|
123
|
+
parts = line.split(None, 10)
|
|
124
|
+
if len(parts) < 11:
|
|
125
|
+
return {}
|
|
126
|
+
return {
|
|
127
|
+
"user": parts[0],
|
|
128
|
+
"pid": parts[1],
|
|
129
|
+
"cpu_pct": parts[2],
|
|
130
|
+
"mem_pct": parts[3],
|
|
131
|
+
"command": parts[10][:80],
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# CPU-Verbraucher
|
|
135
|
+
try:
|
|
136
|
+
cpu_sorted = sorted(lines, key=lambda l: float(l.split(None, 3)[2]), reverse=True)
|
|
137
|
+
top_cpu = [parse_line(l) for l in cpu_sorted[:5] if parse_line(l)]
|
|
138
|
+
except (IndexError, ValueError):
|
|
139
|
+
top_cpu = []
|
|
140
|
+
|
|
141
|
+
# Memory-Verbraucher
|
|
142
|
+
try:
|
|
143
|
+
mem_sorted = sorted(lines, key=lambda l: float(l.split(None, 4)[3]), reverse=True)
|
|
144
|
+
top_mem = [parse_line(l) for l in mem_sorted[:5] if parse_line(l)]
|
|
145
|
+
except (IndexError, ValueError):
|
|
146
|
+
top_mem = []
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"top_cpu": top_cpu,
|
|
150
|
+
"top_memory": top_mem,
|
|
151
|
+
}
|
src/tools/services.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Systemd-Service-Management-Tools: Status, Start, Stop, Restart"""
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _run(cmd: list[str], timeout: int = 15) -> tuple[int, str, str]:
|
|
7
|
+
"""Führt einen Befehl aus und gibt (returncode, stdout, stderr) zurück."""
|
|
8
|
+
try:
|
|
9
|
+
result = subprocess.run(
|
|
10
|
+
cmd,
|
|
11
|
+
capture_output=True,
|
|
12
|
+
text=True,
|
|
13
|
+
timeout=timeout,
|
|
14
|
+
check=False,
|
|
15
|
+
)
|
|
16
|
+
return result.returncode, result.stdout.strip(), result.stderr.strip()
|
|
17
|
+
except subprocess.TimeoutExpired:
|
|
18
|
+
return 1, "", "Timeout: Befehl hat zu lange gedauert"
|
|
19
|
+
except FileNotFoundError:
|
|
20
|
+
return 1, "", f"Befehl nicht gefunden: {cmd[0]}"
|
|
21
|
+
except Exception as e:
|
|
22
|
+
return 1, "", str(e)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def list_services(filter_state: str = "all") -> dict[str, Any]:
|
|
26
|
+
"""
|
|
27
|
+
Listet systemd-Services auf.
|
|
28
|
+
filter_state: 'all', 'running', 'failed', 'inactive'
|
|
29
|
+
"""
|
|
30
|
+
cmd = ["systemctl", "list-units", "--type=service", "--no-pager", "--plain"]
|
|
31
|
+
if filter_state == "running":
|
|
32
|
+
cmd.extend(["--state=running"])
|
|
33
|
+
elif filter_state == "failed":
|
|
34
|
+
cmd.extend(["--state=failed"])
|
|
35
|
+
elif filter_state == "inactive":
|
|
36
|
+
cmd.extend(["--state=inactive"])
|
|
37
|
+
|
|
38
|
+
rc, stdout, stderr = _run(cmd)
|
|
39
|
+
if rc != 0:
|
|
40
|
+
return {"error": stderr or "systemctl nicht verfügbar", "services": []}
|
|
41
|
+
|
|
42
|
+
services = []
|
|
43
|
+
for line in stdout.split("\n"):
|
|
44
|
+
parts = line.split(None, 4)
|
|
45
|
+
if len(parts) >= 4 and parts[0].endswith(".service"):
|
|
46
|
+
services.append({
|
|
47
|
+
"name": parts[0],
|
|
48
|
+
"load": parts[1],
|
|
49
|
+
"active": parts[2],
|
|
50
|
+
"sub": parts[3],
|
|
51
|
+
"description": parts[4].strip() if len(parts) > 4 else "",
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
"filter": filter_state,
|
|
56
|
+
"count": len(services),
|
|
57
|
+
"services": services[:50], # Max 50 zurückgeben
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_service_status(service_name: str) -> dict[str, Any]:
|
|
62
|
+
"""Gibt detaillierten Status eines systemd-Services zurück."""
|
|
63
|
+
# Sicherheitscheck: Service-Name darf keine Shell-Sonderzeichen enthalten
|
|
64
|
+
safe_chars = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.")
|
|
65
|
+
if not all(c in safe_chars for c in service_name):
|
|
66
|
+
return {"error": f"Ungültiger Service-Name: {service_name}"}
|
|
67
|
+
|
|
68
|
+
# .service anhängen falls nicht vorhanden
|
|
69
|
+
if not service_name.endswith(".service"):
|
|
70
|
+
service_name = service_name + ".service"
|
|
71
|
+
|
|
72
|
+
rc, stdout, stderr = _run(["systemctl", "status", service_name, "--no-pager", "-l"])
|
|
73
|
+
|
|
74
|
+
# Is-active und Is-enabled abfragen
|
|
75
|
+
_, active_out, _ = _run(["systemctl", "is-active", service_name])
|
|
76
|
+
_, enabled_out, _ = _run(["systemctl", "is-enabled", service_name])
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
"service": service_name,
|
|
80
|
+
"active": active_out,
|
|
81
|
+
"enabled": enabled_out,
|
|
82
|
+
"status_output": stdout if stdout else stderr,
|
|
83
|
+
"exit_code": rc,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def manage_service(service_name: str, action: str) -> dict[str, Any]:
|
|
88
|
+
"""
|
|
89
|
+
Verwaltet einen systemd-Service.
|
|
90
|
+
action: 'start', 'stop', 'restart', 'reload', 'enable', 'disable'
|
|
91
|
+
|
|
92
|
+
HINWEIS: Benötigt sudo-Rechte für start/stop/restart/enable/disable.
|
|
93
|
+
"""
|
|
94
|
+
# Sicherheitscheck
|
|
95
|
+
safe_chars = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.")
|
|
96
|
+
if not all(c in safe_chars for c in service_name):
|
|
97
|
+
return {"error": f"Ungültiger Service-Name: {service_name}"}
|
|
98
|
+
|
|
99
|
+
allowed_actions = {"start", "stop", "restart", "reload", "enable", "disable"}
|
|
100
|
+
if action not in allowed_actions:
|
|
101
|
+
return {"error": f"Ungültige Aktion. Erlaubt: {', '.join(allowed_actions)}"}
|
|
102
|
+
|
|
103
|
+
if not service_name.endswith(".service"):
|
|
104
|
+
service_name = service_name + ".service"
|
|
105
|
+
|
|
106
|
+
rc, stdout, stderr = _run(["systemctl", action, service_name])
|
|
107
|
+
|
|
108
|
+
if rc == 0:
|
|
109
|
+
# Nach Statusänderung den neuen Status abrufen
|
|
110
|
+
_, active_out, _ = _run(["systemctl", "is-active", service_name])
|
|
111
|
+
return {
|
|
112
|
+
"success": True,
|
|
113
|
+
"service": service_name,
|
|
114
|
+
"action": action,
|
|
115
|
+
"new_state": active_out,
|
|
116
|
+
"message": f"Service {service_name} erfolgreich: {action}",
|
|
117
|
+
}
|
|
118
|
+
else:
|
|
119
|
+
return {
|
|
120
|
+
"success": False,
|
|
121
|
+
"service": service_name,
|
|
122
|
+
"action": action,
|
|
123
|
+
"error": stderr or stdout,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_service_logs(service_name: str, lines: int = 50) -> dict[str, Any]:
|
|
128
|
+
"""Gibt die letzten Log-Zeilen eines Services über journalctl zurück."""
|
|
129
|
+
safe_chars = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.")
|
|
130
|
+
if not all(c in safe_chars for c in service_name):
|
|
131
|
+
return {"error": f"Ungültiger Service-Name: {service_name}"}
|
|
132
|
+
|
|
133
|
+
if not service_name.endswith(".service"):
|
|
134
|
+
service_name = service_name + ".service"
|
|
135
|
+
|
|
136
|
+
lines = min(max(1, lines), 500) # Zwischen 1 und 500
|
|
137
|
+
|
|
138
|
+
rc, stdout, stderr = _run(
|
|
139
|
+
["journalctl", "-u", service_name, "-n", str(lines), "--no-pager", "--output=short"],
|
|
140
|
+
timeout=20,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
"service": service_name,
|
|
145
|
+
"lines_requested": lines,
|
|
146
|
+
"logs": stdout if stdout else stderr,
|
|
147
|
+
"success": rc == 0,
|
|
148
|
+
}
|
src/tools/system.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""System-Informations-Tools: CPU, Speicher, Disk, OS-Details"""
|
|
2
|
+
import subprocess
|
|
3
|
+
import platform
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _run(cmd: list[str], timeout: int = 10) -> str:
|
|
9
|
+
"""Führt einen Befehl sicher aus und gibt stdout zurück."""
|
|
10
|
+
try:
|
|
11
|
+
result = subprocess.run(
|
|
12
|
+
cmd,
|
|
13
|
+
capture_output=True,
|
|
14
|
+
text=True,
|
|
15
|
+
timeout=timeout,
|
|
16
|
+
check=False,
|
|
17
|
+
)
|
|
18
|
+
return result.stdout.strip() if result.returncode == 0 else result.stderr.strip()
|
|
19
|
+
except subprocess.TimeoutExpired:
|
|
20
|
+
return "Timeout: Befehl hat zu lange gedauert"
|
|
21
|
+
except FileNotFoundError:
|
|
22
|
+
return f"Befehl nicht gefunden: {cmd[0]}"
|
|
23
|
+
except Exception as e:
|
|
24
|
+
return f"Fehler: {str(e)}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_system_info() -> dict[str, Any]:
|
|
28
|
+
"""Gibt allgemeine System-Informationen zurück: OS, Kernel, Uptime, Hostname."""
|
|
29
|
+
hostname = _run(["hostname"])
|
|
30
|
+
kernel = _run(["uname", "-r"])
|
|
31
|
+
os_release = _run(["cat", "/etc/os-release"])
|
|
32
|
+
uptime = _run(["uptime", "-p"])
|
|
33
|
+
cpu_count = _run(["nproc"])
|
|
34
|
+
arch = _run(["uname", "-m"])
|
|
35
|
+
|
|
36
|
+
# CPU-Modell aus /proc/cpuinfo
|
|
37
|
+
cpu_model = "unbekannt"
|
|
38
|
+
try:
|
|
39
|
+
with open("/proc/cpuinfo", "r") as f:
|
|
40
|
+
for line in f:
|
|
41
|
+
if "model name" in line:
|
|
42
|
+
cpu_model = line.split(":")[1].strip()
|
|
43
|
+
break
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
# Arbeitsspeicher aus /proc/meminfo
|
|
48
|
+
mem_total = "unbekannt"
|
|
49
|
+
mem_available = "unbekannt"
|
|
50
|
+
try:
|
|
51
|
+
with open("/proc/meminfo", "r") as f:
|
|
52
|
+
for line in f:
|
|
53
|
+
if line.startswith("MemTotal:"):
|
|
54
|
+
kb = int(line.split()[1])
|
|
55
|
+
mem_total = f"{kb // 1024} MB ({kb // 1048576} GB)"
|
|
56
|
+
elif line.startswith("MemAvailable:"):
|
|
57
|
+
kb = int(line.split()[1])
|
|
58
|
+
mem_available = f"{kb // 1024} MB ({kb // 1048576} GB)"
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
"hostname": hostname,
|
|
64
|
+
"kernel": kernel,
|
|
65
|
+
"arch": arch,
|
|
66
|
+
"uptime": uptime,
|
|
67
|
+
"cpu_count": cpu_count,
|
|
68
|
+
"cpu_model": cpu_model,
|
|
69
|
+
"mem_total": mem_total,
|
|
70
|
+
"mem_available": mem_available,
|
|
71
|
+
"os_release": os_release,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_disk_usage() -> dict[str, Any]:
|
|
76
|
+
"""Gibt Festplatten-Belegung aller gemounteten Laufwerke zurück."""
|
|
77
|
+
output = _run(["df", "-h", "--output=source,size,used,avail,pcent,target"])
|
|
78
|
+
lines = output.strip().split("\n")
|
|
79
|
+
if not lines:
|
|
80
|
+
return {"error": "Keine Disk-Daten verfügbar"}
|
|
81
|
+
|
|
82
|
+
header = lines[0].split()
|
|
83
|
+
disks = []
|
|
84
|
+
for line in lines[1:]:
|
|
85
|
+
parts = line.split()
|
|
86
|
+
if len(parts) >= 6:
|
|
87
|
+
disks.append({
|
|
88
|
+
"filesystem": parts[0],
|
|
89
|
+
"size": parts[1],
|
|
90
|
+
"used": parts[2],
|
|
91
|
+
"available": parts[3],
|
|
92
|
+
"use_pct": parts[4],
|
|
93
|
+
"mountpoint": parts[5],
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
return {"disks": disks, "raw": output}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_memory_usage() -> dict[str, Any]:
|
|
100
|
+
"""Gibt detaillierte Speicher-Nutzung zurück (RAM + Swap)."""
|
|
101
|
+
output = _run(["free", "-h"])
|
|
102
|
+
mem_info = {}
|
|
103
|
+
for line in output.split("\n"):
|
|
104
|
+
if line.startswith("Mem:"):
|
|
105
|
+
parts = line.split()
|
|
106
|
+
mem_info["ram_total"] = parts[1]
|
|
107
|
+
mem_info["ram_used"] = parts[2]
|
|
108
|
+
mem_info["ram_free"] = parts[3]
|
|
109
|
+
if len(parts) > 6:
|
|
110
|
+
mem_info["ram_available"] = parts[6]
|
|
111
|
+
elif line.startswith("Swap:"):
|
|
112
|
+
parts = line.split()
|
|
113
|
+
mem_info["swap_total"] = parts[1]
|
|
114
|
+
mem_info["swap_used"] = parts[2]
|
|
115
|
+
mem_info["swap_free"] = parts[3]
|
|
116
|
+
return {"memory": mem_info, "raw": output}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def run_health_check() -> dict[str, Any]:
|
|
120
|
+
"""Führt einen vollständigen System-Health-Check durch."""
|
|
121
|
+
checks = {}
|
|
122
|
+
|
|
123
|
+
# Disk-Belegung über 80%?
|
|
124
|
+
df_out = _run(["df", "-h", "--output=pcent,target"])
|
|
125
|
+
high_disk = []
|
|
126
|
+
for line in df_out.split("\n")[1:]:
|
|
127
|
+
parts = line.strip().split()
|
|
128
|
+
if len(parts) == 2:
|
|
129
|
+
pct_str = parts[0].rstrip("%")
|
|
130
|
+
try:
|
|
131
|
+
if int(pct_str) >= 80:
|
|
132
|
+
high_disk.append({"mountpoint": parts[1], "usage": parts[0]})
|
|
133
|
+
except ValueError:
|
|
134
|
+
pass
|
|
135
|
+
checks["disk_warnings"] = high_disk
|
|
136
|
+
|
|
137
|
+
# Load Average
|
|
138
|
+
load_out = _run(["cat", "/proc/loadavg"])
|
|
139
|
+
parts = load_out.split()
|
|
140
|
+
if len(parts) >= 3:
|
|
141
|
+
checks["load_average"] = {
|
|
142
|
+
"1min": parts[0],
|
|
143
|
+
"5min": parts[1],
|
|
144
|
+
"15min": parts[2],
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Speicher verfügbar
|
|
148
|
+
mem_out = get_memory_usage()
|
|
149
|
+
checks["memory"] = mem_out.get("memory", {})
|
|
150
|
+
|
|
151
|
+
# Uptime
|
|
152
|
+
checks["uptime"] = _run(["uptime", "-p"])
|
|
153
|
+
|
|
154
|
+
# Hostname und Kernel
|
|
155
|
+
checks["hostname"] = _run(["hostname"])
|
|
156
|
+
checks["kernel"] = _run(["uname", "-r"])
|
|
157
|
+
|
|
158
|
+
# Status-Bewertung
|
|
159
|
+
issues = []
|
|
160
|
+
if high_disk:
|
|
161
|
+
issues.append(f"{len(high_disk)} Laufwerk(e) über 80% Belegung")
|
|
162
|
+
try:
|
|
163
|
+
load_1m = float(checks.get("load_average", {}).get("1min", "0"))
|
|
164
|
+
cpu_count = int(_run(["nproc"]) or "1")
|
|
165
|
+
if load_1m > cpu_count * 0.8:
|
|
166
|
+
issues.append(f"Hohe CPU-Last: {load_1m} (bei {cpu_count} Kernen)")
|
|
167
|
+
except (ValueError, TypeError):
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
checks["status"] = "WARNING" if issues else "OK"
|
|
171
|
+
checks["issues"] = issues
|
|
172
|
+
|
|
173
|
+
return checks
|