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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ linux-admin-mcp-server = src.server:main
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