linux-mcp-server 0.1.0.dev0__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_mcp_server/__init__.py +4 -0
- linux_mcp_server/__main__.py +30 -0
- linux_mcp_server/audit.py +285 -0
- linux_mcp_server/logging_config.py +144 -0
- linux_mcp_server/server.py +328 -0
- linux_mcp_server/tools/__init__.py +2 -0
- linux_mcp_server/tools/logs.py +225 -0
- linux_mcp_server/tools/network.py +281 -0
- linux_mcp_server/tools/processes.py +257 -0
- linux_mcp_server/tools/services.py +147 -0
- linux_mcp_server/tools/ssh_executor.py +324 -0
- linux_mcp_server/tools/storage.py +358 -0
- linux_mcp_server/tools/system_info.py +507 -0
- linux_mcp_server/tools/utils.py +27 -0
- linux_mcp_server/tools/validation.py +58 -0
- linux_mcp_server-0.1.0.dev0.dist-info/METADATA +331 -0
- linux_mcp_server-0.1.0.dev0.dist-info/RECORD +20 -0
- linux_mcp_server-0.1.0.dev0.dist-info/WHEEL +4 -0
- linux_mcp_server-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- linux_mcp_server-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Network diagnostic tools."""
|
|
2
|
+
|
|
3
|
+
import psutil
|
|
4
|
+
import socket
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .ssh_executor import execute_command
|
|
8
|
+
from .utils import format_bytes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def get_network_interfaces(host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Get network interface information.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
host: Optional remote host to connect to
|
|
17
|
+
username: Optional SSH username (required if host is provided)
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Formatted string with network interface information
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
if host:
|
|
24
|
+
# Remote execution - use ip command
|
|
25
|
+
info = []
|
|
26
|
+
info.append("=== Network Interfaces ===\n")
|
|
27
|
+
|
|
28
|
+
# Get interface info
|
|
29
|
+
returncode, stdout, _ = await execute_command(
|
|
30
|
+
["ip", "-brief", "address"],
|
|
31
|
+
host=host,
|
|
32
|
+
username=username
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if returncode == 0 and stdout:
|
|
36
|
+
info.append(stdout)
|
|
37
|
+
|
|
38
|
+
# Get detailed interface info
|
|
39
|
+
returncode, stdout, _ = await execute_command(
|
|
40
|
+
["ip", "address"],
|
|
41
|
+
host=host,
|
|
42
|
+
username=username
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if returncode == 0 and stdout:
|
|
46
|
+
info.append("\n=== Detailed Interface Information ===")
|
|
47
|
+
info.append(stdout)
|
|
48
|
+
|
|
49
|
+
# Get network statistics using netstat or ss
|
|
50
|
+
returncode, stdout, _ = await execute_command(
|
|
51
|
+
["cat", "/proc/net/dev"],
|
|
52
|
+
host=host,
|
|
53
|
+
username=username
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if returncode == 0 and stdout:
|
|
57
|
+
info.append("\n=== Network I/O Statistics ===")
|
|
58
|
+
info.append(stdout)
|
|
59
|
+
|
|
60
|
+
return "\n".join(info)
|
|
61
|
+
else:
|
|
62
|
+
# Local execution - use psutil
|
|
63
|
+
info = []
|
|
64
|
+
info.append("=== Network Interfaces ===\n")
|
|
65
|
+
|
|
66
|
+
# Get network interface addresses
|
|
67
|
+
net_if_addrs = psutil.net_if_addrs()
|
|
68
|
+
net_if_stats = psutil.net_if_stats()
|
|
69
|
+
|
|
70
|
+
for interface, addrs in sorted(net_if_addrs.items()):
|
|
71
|
+
info.append(f"\n{interface}:")
|
|
72
|
+
|
|
73
|
+
# Get interface stats
|
|
74
|
+
if interface in net_if_stats:
|
|
75
|
+
stats = net_if_stats[interface]
|
|
76
|
+
status = "UP" if stats.isup else "DOWN"
|
|
77
|
+
info.append(f" Status: {status}")
|
|
78
|
+
info.append(f" Speed: {stats.speed} Mbps")
|
|
79
|
+
info.append(f" MTU: {stats.mtu}")
|
|
80
|
+
|
|
81
|
+
# Get addresses
|
|
82
|
+
for addr in addrs:
|
|
83
|
+
if addr.family == socket.AF_INET:
|
|
84
|
+
info.append(f" IPv4 Address: {addr.address}")
|
|
85
|
+
if addr.netmask:
|
|
86
|
+
info.append(f" Netmask: {addr.netmask}")
|
|
87
|
+
if addr.broadcast:
|
|
88
|
+
info.append(f" Broadcast: {addr.broadcast}")
|
|
89
|
+
elif addr.family == socket.AF_INET6:
|
|
90
|
+
info.append(f" IPv6 Address: {addr.address}")
|
|
91
|
+
if addr.netmask:
|
|
92
|
+
info.append(f" Netmask: {addr.netmask}")
|
|
93
|
+
elif addr.family == psutil.AF_LINK:
|
|
94
|
+
info.append(f" MAC Address: {addr.address}")
|
|
95
|
+
|
|
96
|
+
# Network I/O statistics
|
|
97
|
+
net_io = psutil.net_io_counters()
|
|
98
|
+
info.append("\n\n=== Network I/O Statistics (total) ===")
|
|
99
|
+
info.append(f"Bytes Sent: {format_bytes(net_io.bytes_sent)}")
|
|
100
|
+
info.append(f"Bytes Received: {format_bytes(net_io.bytes_recv)}")
|
|
101
|
+
info.append(f"Packets Sent: {net_io.packets_sent}")
|
|
102
|
+
info.append(f"Packets Received: {net_io.packets_recv}")
|
|
103
|
+
info.append(f"Errors In: {net_io.errin}")
|
|
104
|
+
info.append(f"Errors Out: {net_io.errout}")
|
|
105
|
+
info.append(f"Drops In: {net_io.dropin}")
|
|
106
|
+
info.append(f"Drops Out: {net_io.dropout}")
|
|
107
|
+
|
|
108
|
+
return "\n".join(info)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
return f"Error getting network interface information: {str(e)}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async def get_network_connections(host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Get active network connections.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
host: Optional remote host to connect to
|
|
119
|
+
username: Optional SSH username (required if host is provided)
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Formatted string with active network connections
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
if host:
|
|
126
|
+
# Remote execution - use ss or netstat command
|
|
127
|
+
# Try ss first (modern tool)
|
|
128
|
+
returncode, stdout, _ = await execute_command(
|
|
129
|
+
["ss", "-tunap"],
|
|
130
|
+
host=host,
|
|
131
|
+
username=username
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if returncode == 0 and stdout:
|
|
135
|
+
info = []
|
|
136
|
+
info.append("=== Active Network Connections ===\n")
|
|
137
|
+
info.append(stdout)
|
|
138
|
+
|
|
139
|
+
# Count connections
|
|
140
|
+
lines = stdout.strip().split('\n')
|
|
141
|
+
info.append(f"\n\nTotal connections: {len(lines) - 1}") # -1 for header
|
|
142
|
+
|
|
143
|
+
return "\n".join(info)
|
|
144
|
+
else:
|
|
145
|
+
# Fallback to netstat
|
|
146
|
+
returncode, stdout, _ = await execute_command(
|
|
147
|
+
["netstat", "-tunap"],
|
|
148
|
+
host=host,
|
|
149
|
+
username=username
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if returncode == 0 and stdout:
|
|
153
|
+
info = []
|
|
154
|
+
info.append("=== Active Network Connections ===\n")
|
|
155
|
+
info.append(stdout)
|
|
156
|
+
return "\n".join(info)
|
|
157
|
+
else:
|
|
158
|
+
return "Error: Neither ss nor netstat command available on remote host"
|
|
159
|
+
else:
|
|
160
|
+
# Local execution - use psutil
|
|
161
|
+
info = []
|
|
162
|
+
info.append("=== Active Network Connections ===\n")
|
|
163
|
+
info.append(f"{'Proto':<8} {'Local Address':<30} {'Remote Address':<30} {'Status':<15} {'PID/Program'}")
|
|
164
|
+
info.append("-" * 110)
|
|
165
|
+
|
|
166
|
+
# Get all network connections
|
|
167
|
+
connections = psutil.net_connections(kind='inet')
|
|
168
|
+
|
|
169
|
+
for conn in connections:
|
|
170
|
+
proto = "TCP" if conn.type == socket.SOCK_STREAM else "UDP"
|
|
171
|
+
|
|
172
|
+
local_addr = f"{conn.laddr.ip}:{conn.laddr.port}" if conn.laddr else "N/A"
|
|
173
|
+
remote_addr = f"{conn.raddr.ip}:{conn.raddr.port}" if conn.raddr else "N/A"
|
|
174
|
+
status = conn.status if conn.status else "N/A"
|
|
175
|
+
|
|
176
|
+
# Try to get process info
|
|
177
|
+
pid_info = str(conn.pid) if conn.pid else "N/A"
|
|
178
|
+
if conn.pid:
|
|
179
|
+
try:
|
|
180
|
+
proc = psutil.Process(conn.pid)
|
|
181
|
+
pid_info = f"{conn.pid}/{proc.name()}"
|
|
182
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
info.append(
|
|
186
|
+
f"{proto:<8} {local_addr:<30} {remote_addr:<30} {status:<15} {pid_info}"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
info.append(f"\n\nTotal connections: {len(connections)}")
|
|
190
|
+
|
|
191
|
+
return "\n".join(info)
|
|
192
|
+
except psutil.AccessDenied:
|
|
193
|
+
return "Permission denied. This tool requires elevated privileges to view all network connections."
|
|
194
|
+
except Exception as e:
|
|
195
|
+
return f"Error getting network connections: {str(e)}"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
async def get_listening_ports(host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
199
|
+
"""
|
|
200
|
+
Get listening ports.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
host: Optional remote host to connect to
|
|
204
|
+
username: Optional SSH username (required if host is provided)
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Formatted string with listening ports
|
|
208
|
+
"""
|
|
209
|
+
try:
|
|
210
|
+
if host:
|
|
211
|
+
# Remote execution - use ss or netstat command
|
|
212
|
+
# Try ss first (modern tool)
|
|
213
|
+
returncode, stdout, _ = await execute_command(
|
|
214
|
+
["ss", "-tulnp"],
|
|
215
|
+
host=host,
|
|
216
|
+
username=username
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if returncode == 0 and stdout:
|
|
220
|
+
info = []
|
|
221
|
+
info.append("=== Listening Ports ===\n")
|
|
222
|
+
info.append(stdout)
|
|
223
|
+
|
|
224
|
+
# Count listening ports
|
|
225
|
+
lines = stdout.strip().split('\n')
|
|
226
|
+
info.append(f"\n\nTotal listening ports: {len(lines) - 1}") # -1 for header
|
|
227
|
+
|
|
228
|
+
return "\n".join(info)
|
|
229
|
+
else:
|
|
230
|
+
# Fallback to netstat
|
|
231
|
+
returncode, stdout, _ = await execute_command(
|
|
232
|
+
["netstat", "-tulnp"],
|
|
233
|
+
host=host,
|
|
234
|
+
username=username
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if returncode == 0 and stdout:
|
|
238
|
+
info = []
|
|
239
|
+
info.append("=== Listening Ports ===\n")
|
|
240
|
+
info.append(stdout)
|
|
241
|
+
return "\n".join(info)
|
|
242
|
+
else:
|
|
243
|
+
return "Error: Neither ss nor netstat command available on remote host"
|
|
244
|
+
else:
|
|
245
|
+
# Local execution - use psutil
|
|
246
|
+
info = []
|
|
247
|
+
info.append("=== Listening Ports ===\n")
|
|
248
|
+
info.append(f"{'Proto':<8} {'Local Address':<30} {'Status':<15} {'PID/Program'}")
|
|
249
|
+
info.append("-" * 80)
|
|
250
|
+
|
|
251
|
+
# Get connections in LISTEN state
|
|
252
|
+
connections = psutil.net_connections(kind='inet')
|
|
253
|
+
listening = [c for c in connections if c.status == 'LISTEN' or c.type == socket.SOCK_DGRAM]
|
|
254
|
+
|
|
255
|
+
for conn in listening:
|
|
256
|
+
proto = "TCP" if conn.type == socket.SOCK_STREAM else "UDP"
|
|
257
|
+
|
|
258
|
+
local_addr = f"{conn.laddr.ip}:{conn.laddr.port}" if conn.laddr else "N/A"
|
|
259
|
+
status = conn.status if conn.status else "LISTENING"
|
|
260
|
+
|
|
261
|
+
# Try to get process info
|
|
262
|
+
pid_info = str(conn.pid) if conn.pid else "N/A"
|
|
263
|
+
if conn.pid:
|
|
264
|
+
try:
|
|
265
|
+
proc = psutil.Process(conn.pid)
|
|
266
|
+
pid_info = f"{conn.pid}/{proc.name()}"
|
|
267
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
info.append(
|
|
271
|
+
f"{proto:<8} {local_addr:<30} {status:<15} {pid_info}"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
info.append(f"\n\nTotal listening ports: {len(listening)}")
|
|
275
|
+
|
|
276
|
+
return "\n".join(info)
|
|
277
|
+
except psutil.AccessDenied:
|
|
278
|
+
return "Permission denied. This tool requires elevated privileges to view all listening ports."
|
|
279
|
+
except Exception as e:
|
|
280
|
+
return f"Error getting listening ports: {str(e)}"
|
|
281
|
+
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Process management tools."""
|
|
2
|
+
|
|
3
|
+
import psutil
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .validation import validate_pid
|
|
8
|
+
from .ssh_executor import execute_command
|
|
9
|
+
from .utils import format_bytes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def list_processes(host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
13
|
+
"""
|
|
14
|
+
List running processes.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
host: Optional remote host to connect to
|
|
18
|
+
username: Optional SSH username (required if host is provided)
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Formatted string with process list
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
if host:
|
|
25
|
+
# Remote execution - use ps command
|
|
26
|
+
returncode, stdout, _ = await execute_command(
|
|
27
|
+
["ps", "aux", "--sort=-%cpu"],
|
|
28
|
+
host=host,
|
|
29
|
+
username=username
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if returncode == 0 and stdout:
|
|
33
|
+
info = []
|
|
34
|
+
info.append("=== Running Processes ===\n")
|
|
35
|
+
|
|
36
|
+
lines = stdout.strip().split('\n')
|
|
37
|
+
# Take header and top 100 processes
|
|
38
|
+
if len(lines) > 101:
|
|
39
|
+
info.append('\n'.join(lines[:101]))
|
|
40
|
+
info.append(f"\n\nTotal processes: {len(lines) - 1}")
|
|
41
|
+
info.append(f"Showing: Top 100 by CPU usage")
|
|
42
|
+
else:
|
|
43
|
+
info.append(stdout)
|
|
44
|
+
info.append(f"\n\nTotal processes: {len(lines) - 1}")
|
|
45
|
+
|
|
46
|
+
return "\n".join(info)
|
|
47
|
+
else:
|
|
48
|
+
return "Error executing ps command on remote host"
|
|
49
|
+
else:
|
|
50
|
+
# Local execution - use psutil
|
|
51
|
+
info = []
|
|
52
|
+
info.append("=== Running Processes ===\n")
|
|
53
|
+
info.append(f"{'PID':<8} {'User':<12} {'CPU%':<8} {'Memory%':<10} {'Status':<12} {'Name':<30} {'Command'}")
|
|
54
|
+
info.append("-" * 120)
|
|
55
|
+
|
|
56
|
+
# Get all processes
|
|
57
|
+
processes = []
|
|
58
|
+
for proc in psutil.process_iter(['pid', 'name', 'username', 'cpu_percent', 'memory_percent', 'status', 'cmdline']):
|
|
59
|
+
try:
|
|
60
|
+
processes.append(proc.info)
|
|
61
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
# Sort by CPU usage (descending)
|
|
65
|
+
processes.sort(key=lambda x: x.get('cpu_percent', 0) or 0, reverse=True)
|
|
66
|
+
|
|
67
|
+
# Show top processes (limit to reasonable number)
|
|
68
|
+
for proc_info in processes[:100]: # Show top 100 processes
|
|
69
|
+
pid = proc_info.get('pid', 'N/A')
|
|
70
|
+
username_val = proc_info.get('username', 'N/A')
|
|
71
|
+
if username_val and len(username_val) > 12:
|
|
72
|
+
username_val = username_val[:9] + '...'
|
|
73
|
+
|
|
74
|
+
cpu = proc_info.get('cpu_percent', 0) or 0
|
|
75
|
+
mem = proc_info.get('memory_percent', 0) or 0
|
|
76
|
+
status = proc_info.get('status', 'N/A')
|
|
77
|
+
name = proc_info.get('name', 'N/A')
|
|
78
|
+
if name and len(name) > 30:
|
|
79
|
+
name = name[:27] + '...'
|
|
80
|
+
|
|
81
|
+
cmdline = proc_info.get('cmdline', [])
|
|
82
|
+
if cmdline:
|
|
83
|
+
cmd = ' '.join(cmdline)
|
|
84
|
+
if len(cmd) > 40:
|
|
85
|
+
cmd = cmd[:37] + '...'
|
|
86
|
+
else:
|
|
87
|
+
cmd = name
|
|
88
|
+
|
|
89
|
+
info.append(
|
|
90
|
+
f"{pid:<8} {username_val:<12} {cpu:<8.1f} {mem:<10.1f} {status:<12} {name:<30} {cmd}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Add summary
|
|
94
|
+
total_processes = len(list(psutil.process_iter()))
|
|
95
|
+
info.append(f"\n\nTotal processes: {total_processes}")
|
|
96
|
+
info.append(f"Showing: Top 100 by CPU usage")
|
|
97
|
+
|
|
98
|
+
return "\n".join(info)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
return f"Error listing processes: {str(e)}"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def get_process_info(pid: int, host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
104
|
+
"""
|
|
105
|
+
Get information about a specific process.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
pid: Process ID
|
|
109
|
+
host: Optional remote host to connect to
|
|
110
|
+
username: Optional SSH username (required if host is provided)
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Formatted string with process information
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
# Validate PID (accepts floats from LLMs)
|
|
117
|
+
pid, error = validate_pid(pid)
|
|
118
|
+
if error:
|
|
119
|
+
return error
|
|
120
|
+
|
|
121
|
+
if host:
|
|
122
|
+
# Remote execution - use ps command
|
|
123
|
+
returncode, stdout, _ = await execute_command(
|
|
124
|
+
["ps", "-p", str(pid), "-o", "pid,user,stat,pcpu,pmem,vsz,rss,etime,comm,args"],
|
|
125
|
+
host=host,
|
|
126
|
+
username=username
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if returncode != 0:
|
|
130
|
+
return f"Process with PID {pid} does not exist on remote host."
|
|
131
|
+
|
|
132
|
+
if stdout:
|
|
133
|
+
info = []
|
|
134
|
+
info.append(f"=== Process Information for PID {pid} ===\n")
|
|
135
|
+
info.append(stdout)
|
|
136
|
+
|
|
137
|
+
# Try to get more details with /proc
|
|
138
|
+
returncode, stdout, _ = await execute_command(
|
|
139
|
+
["cat", f"/proc/{pid}/status"],
|
|
140
|
+
host=host,
|
|
141
|
+
username=username
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if returncode == 0 and stdout:
|
|
145
|
+
info.append("\n=== Detailed Status (/proc) ===")
|
|
146
|
+
# Filter to show most relevant fields
|
|
147
|
+
relevant_fields = ['Name:', 'State:', 'Tgid:', 'Pid:', 'PPid:', 'Threads:', 'VmPeak:', 'VmSize:', 'VmRSS:']
|
|
148
|
+
for line in stdout.split('\n'):
|
|
149
|
+
if any(field in line for field in relevant_fields):
|
|
150
|
+
info.append(line)
|
|
151
|
+
|
|
152
|
+
return "\n".join(info)
|
|
153
|
+
else:
|
|
154
|
+
return f"Process with PID {pid} does not exist on remote host."
|
|
155
|
+
else:
|
|
156
|
+
# Local execution - use psutil
|
|
157
|
+
# Check if process exists
|
|
158
|
+
if not psutil.pid_exists(pid):
|
|
159
|
+
return f"Process with PID {pid} does not exist."
|
|
160
|
+
|
|
161
|
+
proc = psutil.Process(pid)
|
|
162
|
+
info = []
|
|
163
|
+
|
|
164
|
+
info.append(f"=== Process Information for PID {pid} ===\n")
|
|
165
|
+
|
|
166
|
+
# Basic info
|
|
167
|
+
try:
|
|
168
|
+
info.append(f"Name: {proc.name()}")
|
|
169
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
info.append(f"Executable: {proc.exe()}")
|
|
174
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
175
|
+
info.append("Executable: [Access Denied]")
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
info.append(f"Command Line: {' '.join(proc.cmdline())}")
|
|
179
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
info.append(f"Status: {proc.status()}")
|
|
184
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
info.append(f"User: {proc.username()}")
|
|
189
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
# Process IDs
|
|
193
|
+
try:
|
|
194
|
+
info.append(f"\nPID: {proc.pid}")
|
|
195
|
+
info.append(f"Parent PID: {proc.ppid()}")
|
|
196
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
# Resource usage
|
|
200
|
+
try:
|
|
201
|
+
info.append(f"\n=== Resource Usage ===")
|
|
202
|
+
cpu_percent = proc.cpu_percent(interval=0.1)
|
|
203
|
+
info.append(f"CPU Percent: {cpu_percent}%")
|
|
204
|
+
|
|
205
|
+
mem_info = proc.memory_info()
|
|
206
|
+
info.append(f"Memory RSS: {format_bytes(mem_info.rss)}")
|
|
207
|
+
info.append(f"Memory VMS: {format_bytes(mem_info.vms)}")
|
|
208
|
+
info.append(f"Memory Percent: {proc.memory_percent():.2f}%")
|
|
209
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
210
|
+
info.append("Resource usage: [Access Denied]")
|
|
211
|
+
|
|
212
|
+
# Timing
|
|
213
|
+
try:
|
|
214
|
+
create_time = datetime.fromtimestamp(proc.create_time())
|
|
215
|
+
info.append(f"\n=== Timing ===")
|
|
216
|
+
info.append(f"Created: {create_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
217
|
+
|
|
218
|
+
cpu_times = proc.cpu_times()
|
|
219
|
+
info.append(f"CPU Time (user): {cpu_times.user:.2f}s")
|
|
220
|
+
info.append(f"CPU Time (system): {cpu_times.system:.2f}s")
|
|
221
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
# Threads
|
|
225
|
+
try:
|
|
226
|
+
num_threads = proc.num_threads()
|
|
227
|
+
info.append(f"\nThreads: {num_threads}")
|
|
228
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
# File descriptors
|
|
232
|
+
try:
|
|
233
|
+
num_fds = proc.num_fds()
|
|
234
|
+
info.append(f"Open File Descriptors: {num_fds}")
|
|
235
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, AttributeError):
|
|
236
|
+
pass # Not available on all systems
|
|
237
|
+
|
|
238
|
+
# Connections
|
|
239
|
+
try:
|
|
240
|
+
connections = proc.connections()
|
|
241
|
+
if connections:
|
|
242
|
+
info.append(f"\n=== Network Connections ({len(connections)}) ===")
|
|
243
|
+
for i, conn in enumerate(connections[:10]): # Show first 10
|
|
244
|
+
info.append(f" {conn.type.name}: {conn.laddr} -> {conn.raddr if conn.raddr else 'N/A'} [{conn.status}]")
|
|
245
|
+
if len(connections) > 10:
|
|
246
|
+
info.append(f" ... and {len(connections) - 10} more")
|
|
247
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
return "\n".join(info)
|
|
251
|
+
except psutil.NoSuchProcess:
|
|
252
|
+
return f"Process with PID {pid} does not exist."
|
|
253
|
+
except psutil.AccessDenied:
|
|
254
|
+
return f"Access denied to process with PID {pid}. Try running with elevated privileges."
|
|
255
|
+
except Exception as e:
|
|
256
|
+
return f"Error getting process information: {str(e)}"
|
|
257
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Service management tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .validation import validate_line_count
|
|
6
|
+
from .ssh_executor import execute_command
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def list_services(host: Optional[str] = None, username: Optional[str] = None) -> str:
|
|
10
|
+
"""
|
|
11
|
+
List all systemd services.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
host: Optional remote host to connect to
|
|
15
|
+
username: Optional SSH username (required if host is provided)
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Formatted string with service list
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
# Run systemctl to list all services
|
|
22
|
+
returncode, stdout, stderr = await execute_command(
|
|
23
|
+
["systemctl", "list-units", "--type=service", "--all", "--no-pager"],
|
|
24
|
+
host=host,
|
|
25
|
+
username=username
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if returncode != 0:
|
|
29
|
+
return f"Error listing services: {stderr}"
|
|
30
|
+
|
|
31
|
+
# Format the output
|
|
32
|
+
result = ["=== System Services ===\n"]
|
|
33
|
+
result.append(stdout)
|
|
34
|
+
|
|
35
|
+
# Get summary
|
|
36
|
+
returncode_summary, stdout_summary, _ = await execute_command(
|
|
37
|
+
["systemctl", "list-units", "--type=service", "--state=running", "--no-pager"],
|
|
38
|
+
host=host,
|
|
39
|
+
username=username
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if returncode_summary == 0:
|
|
43
|
+
running_count = len([l for l in stdout_summary.split('\n') if '.service' in l])
|
|
44
|
+
result.append(f"\n\nSummary: {running_count} services currently running")
|
|
45
|
+
|
|
46
|
+
return "\n".join(result)
|
|
47
|
+
except FileNotFoundError:
|
|
48
|
+
return "Error: systemctl command not found. This tool requires systemd."
|
|
49
|
+
except Exception as e:
|
|
50
|
+
return f"Error listing services: {str(e)}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def get_service_status(
|
|
54
|
+
service_name: str,
|
|
55
|
+
host: Optional[str] = None,
|
|
56
|
+
username: Optional[str] = None
|
|
57
|
+
) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Get status of a specific service.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
service_name: Name of the service
|
|
63
|
+
host: Optional remote host to connect to
|
|
64
|
+
username: Optional SSH username (required if host is provided)
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Formatted string with service status
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
# Ensure service name has .service suffix if not present
|
|
71
|
+
if not service_name.endswith('.service') and '.' not in service_name:
|
|
72
|
+
service_name = f"{service_name}.service"
|
|
73
|
+
|
|
74
|
+
# Run systemctl status
|
|
75
|
+
returncode, stdout, stderr = await execute_command(
|
|
76
|
+
["systemctl", "status", service_name, "--no-pager", "--full"],
|
|
77
|
+
host=host,
|
|
78
|
+
username=username
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Note: systemctl status returns non-zero for inactive services, but that's expected
|
|
82
|
+
if not stdout and stderr:
|
|
83
|
+
# Service not found
|
|
84
|
+
if "not found" in stderr.lower() or "could not be found" in stderr.lower():
|
|
85
|
+
return f"Service '{service_name}' not found on this system."
|
|
86
|
+
return f"Error getting service status: {stderr}"
|
|
87
|
+
|
|
88
|
+
result = [f"=== Status of {service_name} ===\n"]
|
|
89
|
+
result.append(stdout)
|
|
90
|
+
|
|
91
|
+
return "\n".join(result)
|
|
92
|
+
except FileNotFoundError:
|
|
93
|
+
return "Error: systemctl command not found. This tool requires systemd."
|
|
94
|
+
except Exception as e:
|
|
95
|
+
return f"Error getting service status: {str(e)}"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def get_service_logs(
|
|
99
|
+
service_name: str,
|
|
100
|
+
lines: int = 50,
|
|
101
|
+
host: Optional[str] = None,
|
|
102
|
+
username: Optional[str] = None
|
|
103
|
+
) -> str:
|
|
104
|
+
"""
|
|
105
|
+
Get logs for a specific service.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
service_name: Name of the service
|
|
109
|
+
lines: Number of log lines to retrieve (default: 50)
|
|
110
|
+
host: Optional remote host to connect to
|
|
111
|
+
username: Optional SSH username (required if host is provided)
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Formatted string with service logs
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
# Validate lines parameter (accepts floats from LLMs)
|
|
118
|
+
lines, _ = validate_line_count(lines, default=50)
|
|
119
|
+
|
|
120
|
+
# Ensure service name has .service suffix if not present
|
|
121
|
+
if not service_name.endswith('.service') and '.' not in service_name:
|
|
122
|
+
service_name = f"{service_name}.service"
|
|
123
|
+
|
|
124
|
+
# Run journalctl for the service
|
|
125
|
+
returncode, stdout, stderr = await execute_command(
|
|
126
|
+
["journalctl", "-u", service_name, "-n", str(lines), "--no-pager"],
|
|
127
|
+
host=host,
|
|
128
|
+
username=username
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if returncode != 0:
|
|
132
|
+
if "not found" in stderr.lower() or "no entries" in stderr.lower():
|
|
133
|
+
return f"No logs found for service '{service_name}'. The service may not exist or has no log entries."
|
|
134
|
+
return f"Error getting service logs: {stderr}"
|
|
135
|
+
|
|
136
|
+
if not stdout or stdout.strip() == "":
|
|
137
|
+
return f"No log entries found for service '{service_name}'."
|
|
138
|
+
|
|
139
|
+
result = [f"=== Last {lines} log entries for {service_name} ===\n"]
|
|
140
|
+
result.append(stdout)
|
|
141
|
+
|
|
142
|
+
return "\n".join(result)
|
|
143
|
+
except FileNotFoundError:
|
|
144
|
+
return "Error: journalctl command not found. This tool requires systemd."
|
|
145
|
+
except Exception as e:
|
|
146
|
+
return f"Error getting service logs: {str(e)}"
|
|
147
|
+
|