plugin system

This commit is contained in:
dv 2026-04-07 18:29:58 +03:00
parent 31f54c49ad
commit 74fc294d04
2 changed files with 152 additions and 0 deletions

View file

@ -0,0 +1,150 @@
"""
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 "=== CPU_CORES ===" && nproc && \
echo "=== MEMORY ===" && free -m && \
echo "=== DISK ===" && df -h --output=target,size,used,avail,pcent -x tmpfs -x devtmpfs 2>/dev/null || df -h && \
echo "=== TOP_PROCS ===" && ps aux --sort=-%cpu --no-headers 2>/dev/null | head -5 && \
echo "=== TEMPS ===" && (cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | awk '{printf "%.1f°C\n", $1/1000}' || echo "n/a")
""".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

View file

@ -25,6 +25,7 @@ from app.plugins.builtin.common.ping import PingAction
from app.plugins.builtin.mikrotik.get_config import MikrotikGetConfigAction
from app.plugins.builtin.mikrotik.get_ntp import MikrotikGetNTPAction
from app.plugins.builtin.mikrotik.set_ntp import MikrotikSetNTPAction
from app.plugins.builtin.ssh.debian_system_info import DebianSystemInfoAction
from app.plugins.builtin.ssh.get_file import GetFileAction
from app.plugins.builtin.ssh.get_timedatectl import GetTimedatectlAction
from app.plugins.builtin.ssh.ssh_command import SSHCommandAction
@ -37,6 +38,7 @@ BUILTIN_ACTIONS: dict[str, BaseAction] = {
for a in [
PingAction(),
SSHCommandAction(),
DebianSystemInfoAction(),
GetTimedatectlAction(),
GetFileAction(),
MikrotikGetConfigAction(),