feat(docker): collect + persist /system/df image/disk usage (agent 1.6.0)
Backend for the image/disk panel (milestone 77 #942). Agent gains collect_disk_usage() — one /system/df call (gated on containers existing, so Docker-less hosts pay nothing), surfacing reclaimable bytes (image size held by unreferenced images), layers/containers/volumes/build-cache sizes, and the top 50 images by size. Emitted as sample["docker_disk"]; host_agent ingest tracks the newest sample's copy and hands it to the docker capability as a 5th arg. New current-state tables docker_disk_usage (one row/host) + docker_images (per-host image rows), docker_007 migration; ingest upserts the summary and replaces the image set per host (stale images pruned). Unit tests for the df parsing/reclaimable math + build_sample gating; integration test for persistence + image-set replacement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC
This commit is contained in:
@@ -151,7 +151,57 @@ async def _persist_swarm(session, host, swarm: dict) -> None:
|
||||
await session.execute(stale_nodes)
|
||||
|
||||
|
||||
async def persist_host_docker(session, host, snapshots, swarm=None) -> None:
|
||||
async def _persist_disk(session, host, disk: dict) -> None:
|
||||
"""Upsert this host's /system/df summary + replace its image storage rows.
|
||||
|
||||
Current-state (not time-series): the agent re-reports the full summary each
|
||||
interval, so we overwrite the single summary row and replace the image set
|
||||
(delete rows no longer present, upsert the rest), scoped to this host.
|
||||
"""
|
||||
from datetime import timezone
|
||||
from .models import DockerDiskUsage, DockerImage
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
summary = await session.get(DockerDiskUsage, host.id)
|
||||
if summary is None:
|
||||
summary = DockerDiskUsage(host_id=host.id)
|
||||
session.add(summary)
|
||||
summary.layers_size = int(disk.get("layers_size") or 0)
|
||||
summary.images_size = int(disk.get("images_size") or 0)
|
||||
summary.images_reclaimable = int(disk.get("images_reclaimable") or 0)
|
||||
summary.containers_size = int(disk.get("containers_size") or 0)
|
||||
summary.volumes_size = int(disk.get("volumes_size") or 0)
|
||||
summary.build_cache_size = int(disk.get("build_cache_size") or 0)
|
||||
summary.images_total = int(disk.get("images_total") or 0)
|
||||
summary.images_active = int(disk.get("images_active") or 0)
|
||||
summary.containers_count = int(disk.get("containers_count") or 0)
|
||||
summary.volumes_count = int(disk.get("volumes_count") or 0)
|
||||
summary.updated_at = now
|
||||
|
||||
images = disk.get("images") or []
|
||||
seen: set[str] = set()
|
||||
for im in images:
|
||||
iid = im.get("image_id")
|
||||
if not iid or iid in seen:
|
||||
continue
|
||||
seen.add(iid)
|
||||
row = await session.get(DockerImage, (host.id, iid))
|
||||
if row is None:
|
||||
row = DockerImage(host_id=host.id, image_id=iid)
|
||||
session.add(row)
|
||||
row.repo_tag = (im.get("repo_tag") or "<none>")[:512]
|
||||
row.size = int(im.get("size") or 0)
|
||||
row.shared_size = int(im.get("shared_size") or 0)
|
||||
row.containers = int(im.get("containers") or 0)
|
||||
row.updated_at = now
|
||||
stale = delete(DockerImage).where(DockerImage.host_id == host.id)
|
||||
if seen:
|
||||
stale = stale.where(DockerImage.image_id.notin_(seen))
|
||||
await session.execute(stale)
|
||||
|
||||
|
||||
async def persist_host_docker(session, host, snapshots, swarm=None, disk=None) -> None:
|
||||
"""Upsert containers + time-series + lifecycle events + swarm for one host.
|
||||
|
||||
`snapshots` is a list of (recorded_at: datetime, containers: list[dict]) —
|
||||
@@ -160,12 +210,16 @@ async def persist_host_docker(session, host, snapshots, swarm=None) -> None:
|
||||
DockerMetric rows; the newest snapshot drives current container state, the
|
||||
alert pipeline, and lifecycle-event derivation. `swarm` is the newest
|
||||
sample's swarm object (or None off managers) — persisted when present.
|
||||
`disk` is the newest sample's /system/df summary (or None on Docker-less
|
||||
hosts) — persisted when present.
|
||||
"""
|
||||
from steward.core.alerts import record_metric
|
||||
from .models import DockerContainer, DockerEvent, DockerMetric
|
||||
|
||||
if swarm is not None:
|
||||
await _persist_swarm(session, host, swarm)
|
||||
if disk is not None:
|
||||
await _persist_disk(session, host, disk)
|
||||
|
||||
if not snapshots:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user