utp_service/backend/app/plugins/builtin/ssh/disk_usage.py
2026-05-05 11:32:25 +03:00

106 lines
3.1 KiB
Python

"""
Collects disk usage stats over SSH using df.
Designed for dashboard healthcheck loop.
"""
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
_CMD = "df -P -k -x tmpfs -x devtmpfs"
class DiskUsageAction(BaseAction):
"""Collects disk usage (df) and returns normalized stats."""
name = "disk_usage"
display_name = "Disk Usage"
compatible_categories = ["main_server", "vm"]
params_schema = []
async def execute(self, device: Device, obj: Object, params: dict, emit: Emitter, db=None) -> ActionResult:
await emit(f"[{device.ip}] Checking disk usage...", "info")
result = await ssh_service.run_command(device, obj, _CMD, timeout=30, db=db)
if result.error:
await emit(f"[{device.ip}] disk usage error: {result.error}", "error")
return ActionResult(success=False, error=result.error, stdout=result.stdout, stderr=result.stderr)
if result.exit_code != 0:
return ActionResult(
success=False,
error=f"df returned non-zero exit code: {result.exit_code}",
stdout=result.stdout,
stderr=result.stderr,
exit_code=result.exit_code,
)
disks = _parse_df(result.stdout)
if not disks:
return ActionResult(
success=False,
error="No disk rows parsed from df output",
stdout=result.stdout,
stderr=result.stderr,
exit_code=result.exit_code,
)
top = max(disks, key=lambda d: d["pct"])
max_pct = top["pct"]
max_mount = top["mount"]
await emit(f"[{device.ip}] max disk usage: {max_pct}% ({max_mount})", "info")
return ActionResult(
success=True,
stdout=result.stdout,
stderr=result.stderr,
exit_code=result.exit_code,
data={
"disks": disks,
"max_pct": max_pct,
"max_mount": max_mount,
},
)
def _parse_df(output: str) -> list[dict]:
rows: list[dict] = []
for line in output.splitlines():
line = line.strip()
if not line or line.startswith("Filesystem"):
continue
parts = line.split()
if len(parts) < 6:
continue
filesystem = parts[0]
total_kb = _to_int(parts[1])
used_kb = _to_int(parts[2])
avail_kb = _to_int(parts[3])
pct_raw = parts[4].rstrip("%")
mount = parts[5]
pct = _to_int(pct_raw)
if pct is None:
continue
rows.append(
{
"filesystem": filesystem,
"total_kb": total_kb,
"used_kb": used_kb,
"avail_kb": avail_kb,
"pct": pct,
"mount": mount,
}
)
return rows
def _to_int(value: str):
try:
return int(value)
except Exception:
return None