106 lines
3.1 KiB
Python
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
|