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.
@@ -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
+