Docker's volume list has no size, so add a "Compute sizes" button that runs
`docker system df` (via client.df()) and shows per-volume size in a new Size
column. The df walk is expensive (seconds), so results are cached ~60s and
loaded on demand instead of on every poll.
- volume_service.volume_sizes(force) with a 60s TTL cache; GET /api/volumes/sizes
+ agent /agent/volumes/sizes + proxy /api/agents/{id}/volumes/sizes.
- Frontend: volumesApi.sizes(force, agentId); Volumes page gained a Size column
and a Compute sizes button (per host) that triggers the lookup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
107 lines
3.2 KiB
Python
107 lines
3.2 KiB
Python
"""Volume management, NFS/SMB YAML generation, and host path browser."""
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
from sqlmodel import Session
|
|
|
|
from auth import get_current_user, require_admin
|
|
from database import get_session
|
|
from models.user import User
|
|
from services import audit_service, device_service, volume_service
|
|
|
|
router = APIRouter(tags=["volumes"])
|
|
|
|
|
|
def _ip(request: Request) -> str:
|
|
return request.client.host if request.client else "unknown"
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Volumes
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
@router.get("/api/volumes")
|
|
def list_volumes(_user: User = Depends(get_current_user)) -> list[dict]:
|
|
return volume_service.list_volumes()
|
|
|
|
|
|
@router.get("/api/volumes/orphaned")
|
|
def orphaned(_user: User = Depends(get_current_user)) -> list[dict]:
|
|
return volume_service.orphaned_volumes()
|
|
|
|
|
|
@router.get("/api/volumes/sizes")
|
|
def volume_sizes(
|
|
force: bool = Query(False),
|
|
_user: User = Depends(get_current_user),
|
|
) -> dict:
|
|
"""Volume sizes in bytes ({name: size|null}). Expensive; cached ~60s."""
|
|
return volume_service.volume_sizes(force=force)
|
|
|
|
|
|
@router.delete("/api/volumes/{name}")
|
|
def delete_volume(
|
|
name: str,
|
|
request: Request,
|
|
force: bool = Query(False),
|
|
session: Session = Depends(get_session),
|
|
user: User = Depends(require_admin),
|
|
) -> dict:
|
|
vols = {v["name"]: v for v in volume_service.list_volumes()}
|
|
if name in vols and vols[name]["in_use"] and not force:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail={
|
|
"error": "volume_in_use",
|
|
"detail": f"Volume '{name}' is used by: {', '.join(vols[name]['used_by'])}",
|
|
},
|
|
)
|
|
volume_service.remove_volume(name, force=force)
|
|
audit_service.record(
|
|
session, user=user.username, action="volume.delete", target=name, ip=_ip(request)
|
|
)
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/api/volumes/prune")
|
|
def prune(
|
|
request: Request,
|
|
session: Session = Depends(get_session),
|
|
user: User = Depends(require_admin),
|
|
) -> dict:
|
|
result = volume_service.prune_volumes()
|
|
audit_service.record(
|
|
session, user=user.username, action="volume.prune", target="*",
|
|
detail=str(result.get("VolumesDeleted")), ip=_ip(request),
|
|
)
|
|
return result
|
|
|
|
|
|
@router.post("/api/volumes/generate-yaml")
|
|
def generate_yaml(
|
|
spec: dict,
|
|
_user: User = Depends(get_current_user),
|
|
) -> dict:
|
|
try:
|
|
return {"yaml": volume_service.generate_yaml(spec)}
|
|
except (KeyError, ValueError) as exc:
|
|
raise HTTPException(status_code=400, detail=f"Invalid volume spec: {exc}") from exc
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Host path browser
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
@router.get("/api/host/paths")
|
|
def host_paths(
|
|
path: str = Query("/"),
|
|
show_hidden: bool = Query(False),
|
|
_user: User = Depends(get_current_user),
|
|
) -> dict:
|
|
try:
|
|
return device_service.browse(path, show_hidden)
|
|
except device_service.BrowseError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|