147 lines
5.3 KiB
Python
147 lines
5.3 KiB
Python
"""
|
|
Collects Debian/Linux system status over SSH:
|
|
disk usage, RAM, CPU load, uptime, OS version, top processes.
|
|
"""
|
|
|
|
from app.models.device import Device
|
|
from app.models.object import Object
|
|
from app.plugins.base import ActionResult, BaseAction, Emitter
|
|
from app.services import ssh as ssh_service
|
|
|
|
# One-liner that collects all metrics in a single SSH session
|
|
_CMD = r"""
|
|
echo "=== OS ===" && (cat /etc/os-release 2>/dev/null | grep -E "^PRETTY_NAME|^VERSION_ID" || echo "unknown") && \
|
|
echo "=== UPTIME ===" && uptime -p 2>/dev/null || uptime && \
|
|
echo "=== CPU_LOAD ===" && cat /proc/loadavg && \
|
|
echo "=== MEMORY ===" && free -m && \
|
|
echo "=== DISK ===" && df -h --output=target,size,used,avail,pcent -x tmpfs -x devtmpfs 2>/dev/null || df -h && \
|
|
""".strip()
|
|
|
|
|
|
class DebianSystemInfoAction(BaseAction):
|
|
"""Collects disk, RAM, CPU, uptime and process info from a Debian/Linux host via SSH."""
|
|
|
|
name = "debian_system_info"
|
|
display_name = "Debian: System Info"
|
|
compatible_categories = ["server", "raspberry", "embedded", "other"]
|
|
params_schema = []
|
|
|
|
async def execute(self, device: Device, obj: Object, params: dict, emit: Emitter) -> ActionResult:
|
|
await emit(f"[{device.ip}] Collecting system info...", "info")
|
|
|
|
result = await ssh_service.run_command(device, obj, _CMD, timeout=30)
|
|
|
|
if result.error:
|
|
await emit(f"[{device.ip}] Error: {result.error}", "error")
|
|
return ActionResult(success=False, error=result.error)
|
|
|
|
data = _parse(result.stdout)
|
|
|
|
# Emit summary
|
|
await emit(f"[{device.ip}] OS: {data.get('os', '?')}", "info")
|
|
await emit(f"[{device.ip}] Uptime: {data.get('uptime', '?')}", "info")
|
|
await emit(f"[{device.ip}] Load: {data.get('load_1m', '?')} / {data.get('load_5m', '?')} / {data.get('load_15m', '?')} (cores: {data.get('cpu_cores', '?')})", "info")
|
|
await emit(f"[{device.ip}] RAM: used {data.get('mem_used_mb', '?')} MB / {data.get('mem_total_mb', '?')} MB ({data.get('mem_percent', '?')}%)", "info")
|
|
for disk in data.get("disks", []):
|
|
await emit(f"[{device.ip}] Disk {disk['mount']}: {disk['used']} / {disk['size']} ({disk['pct']} used, {disk['avail']} free)", "info")
|
|
if data.get("temps"):
|
|
await emit(f"[{device.ip}] Temps: {', '.join(data['temps'])}", "info")
|
|
|
|
return ActionResult(
|
|
success=True,
|
|
stdout=result.stdout,
|
|
data=data,
|
|
)
|
|
|
|
|
|
def _parse(output: str) -> dict:
|
|
sections: dict[str, list[str]] = {}
|
|
current = None
|
|
for line in output.splitlines():
|
|
line = line.strip()
|
|
if line.startswith("===") and line.endswith("==="):
|
|
current = line.strip("= ").strip()
|
|
sections[current] = []
|
|
elif current is not None and line:
|
|
sections[current].append(line)
|
|
|
|
data: dict = {}
|
|
|
|
# OS
|
|
os_lines = sections.get("OS", [])
|
|
for line in os_lines:
|
|
if line.startswith("PRETTY_NAME="):
|
|
data["os"] = line.split("=", 1)[1].strip('"')
|
|
break
|
|
if "os" not in data and os_lines:
|
|
data["os"] = os_lines[0]
|
|
|
|
# Uptime
|
|
uptime_lines = sections.get("UPTIME", [])
|
|
data["uptime"] = uptime_lines[0] if uptime_lines else "?"
|
|
|
|
# CPU load
|
|
load_lines = sections.get("CPU_LOAD", [])
|
|
if load_lines:
|
|
parts = load_lines[0].split()
|
|
data["load_1m"] = parts[0] if len(parts) > 0 else "?"
|
|
data["load_5m"] = parts[1] if len(parts) > 1 else "?"
|
|
data["load_15m"] = parts[2] if len(parts) > 2 else "?"
|
|
|
|
# CPU cores
|
|
cores_lines = sections.get("CPU_CORES", [])
|
|
data["cpu_cores"] = cores_lines[0] if cores_lines else "?"
|
|
|
|
# Memory (free -m output)
|
|
# Line format: "Mem: total used free shared buff/cache available"
|
|
mem_lines = sections.get("MEMORY", [])
|
|
for line in mem_lines:
|
|
if line.startswith("Mem:"):
|
|
parts = line.split()
|
|
if len(parts) >= 3:
|
|
total = int(parts[1])
|
|
used = int(parts[2])
|
|
data["mem_total_mb"] = total
|
|
data["mem_used_mb"] = used
|
|
data["mem_free_mb"] = int(parts[3]) if len(parts) > 3 else total - used
|
|
data["mem_percent"] = round(used / total * 100, 1) if total else 0
|
|
break
|
|
|
|
# Disk
|
|
disk_lines = sections.get("DISK", [])
|
|
disks = []
|
|
for line in disk_lines:
|
|
# Skip header lines
|
|
if line.startswith("Filesystem") or line.startswith("target"):
|
|
continue
|
|
parts = line.split()
|
|
if len(parts) >= 5:
|
|
disks.append({
|
|
"mount": parts[0],
|
|
"size": parts[1],
|
|
"used": parts[2],
|
|
"avail": parts[3],
|
|
"pct": parts[4],
|
|
})
|
|
data["disks"] = disks
|
|
|
|
# Top processes
|
|
proc_lines = sections.get("TOP_PROCS", [])
|
|
procs = []
|
|
for line in proc_lines:
|
|
parts = line.split(None, 10)
|
|
if len(parts) >= 11:
|
|
procs.append({
|
|
"user": parts[0],
|
|
"pid": parts[1],
|
|
"cpu": parts[2],
|
|
"mem": parts[3],
|
|
"cmd": parts[10],
|
|
})
|
|
data["top_processes"] = procs
|
|
|
|
# Temperatures
|
|
temp_lines = sections.get("TEMPS", [])
|
|
data["temps"] = [t for t in temp_lines if t != "n/a"]
|
|
|
|
return data
|