wrolpi/controller/api/disks.py

188 lines
4.9 KiB
Python

"""
Disk management API endpoints for WROLPi Controller.
"""
from typing import Optional
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from controller.lib.config import is_docker_mode
from controller.lib.disks import (
get_block_devices,
get_mounts,
mount_drive,
unmount_drive,
)
from controller.lib.fstab import (
add_fstab_entry,
get_wrolpi_fstab_entries,
remove_fstab_entry,
)
from controller.lib.smart import (
get_all_smart_status,
is_smart_available,
)
router = APIRouter(prefix="/api/disks", tags=["disks"])
class MountRequest(BaseModel):
"""Request model for mounting a drive."""
device: str = Field(description="Device path (e.g., /dev/sda1)")
mount_point: str = Field(description="Where to mount (must be under /media)")
fstype: Optional[str] = Field(default=None, description="Filesystem type (auto-detected if not specified)")
options: str = Field(default="defaults", description="Mount options")
persist: bool = Field(default=False, description="Add to fstab for persistent mounting")
class UnmountRequest(BaseModel):
"""Request model for unmounting a drive."""
mount_point: str = Field(description="Mount point to unmount")
lazy: bool = Field(default=False, description="Use lazy unmount if busy")
class FstabRequest(BaseModel):
"""Request model for adding an fstab entry."""
device: str = Field(description="Device path")
mount_point: str = Field(description="Where to mount")
fstype: str = Field(description="Filesystem type")
options: str = Field(default="defaults,nofail,x-systemd.device-timeout=10s", description="Mount options")
def _check_native_mode():
"""Raise 501 if in Docker mode."""
if is_docker_mode():
raise HTTPException(
status_code=501,
detail="Disk management is not available in Docker mode. "
"Configure mounts on the host system."
)
@router.get("")
async def list_disks():
"""List all detected disks/partitions."""
_check_native_mode()
devices = get_block_devices()
return [
{
"name": d.name,
"path": d.path,
"size": d.size,
"fstype": d.fstype,
"mountpoint": d.mountpoint,
"label": d.label,
"uuid": d.uuid,
"model": d.model,
}
for d in devices
]
@router.get("/mounts")
async def list_mounts():
"""List current mounts under /media."""
_check_native_mode()
return get_mounts()
@router.post("/mount")
async def disk_mount(request: MountRequest):
"""Mount a partition."""
_check_native_mode()
result = mount_drive(
device=request.device,
mount_point=request.mount_point,
fstype=request.fstype,
options=request.options,
)
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Mount failed"))
# Add to fstab if requested
if request.persist:
fstab_result = add_fstab_entry(
device=request.device,
mount_point=request.mount_point,
fstype=request.fstype or "auto",
)
result["fstab"] = fstab_result
return result
@router.post("/unmount")
async def disk_unmount(request: UnmountRequest):
"""Unmount a partition."""
_check_native_mode()
result = unmount_drive(
mount_point=request.mount_point,
lazy=request.lazy,
)
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Unmount failed"))
return result
@router.get("/fstab")
async def list_fstab():
"""Get WROLPi-related fstab entries."""
_check_native_mode()
return get_wrolpi_fstab_entries()
@router.post("/fstab")
async def add_fstab(request: FstabRequest):
"""Add an fstab entry for persistent mounting."""
_check_native_mode()
result = add_fstab_entry(
device=request.device,
mount_point=request.mount_point,
fstype=request.fstype,
options=request.options,
)
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Failed"))
return result
@router.delete("/fstab/{mount_point:path}")
async def delete_fstab(mount_point: str):
"""Remove an fstab entry."""
_check_native_mode()
# URL decode the mount point
mount_point = f"/{mount_point}"
result = remove_fstab_entry(mount_point)
if not result.get("success"):
raise HTTPException(status_code=500, detail=result.get("error", "Failed"))
return result
# --- SMART ---
@router.get("/smart")
async def list_smart():
"""Get SMART status for all drives."""
_check_native_mode()
if not is_smart_available():
return {"available": False, "reason": "pySMART not installed or not supported"}
return {
"available": True,
"drives": get_all_smart_status(),
}