""" 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, db=None) -> ActionResult: await emit(f"[{device.ip}] Collecting system info...", "info") result = await ssh_service.run_command(device, obj, _CMD, timeout=30, db=db) 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 ("Filesystem ...", "target ...", "Mounted on ...") if line.startswith(("Filesystem", "target", "Mounted")): 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