""" Pool management endpoints """ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from typing import List import re from services.zfs_runner import zfs_runner from services import auth as auth_service from models import Pool, PoolStatus, PoolHealth router = APIRouter(prefix="/api/pools", tags=["pools"]) security = HTTPBearer() def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): """Verify JWT token and return username""" username = auth_service.verify_token(credentials.credentials) if not username: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token" ) return username @router.get("/", response_model=List[Pool]) async def list_pools(): """ Get list of all ZFS pools (public) """ try: pools = zfs_runner.list_pools() # Convert to Pydantic models return [ Pool( name=p["name"], size=p["size"], alloc=p["alloc"], free=p["free"], fragmentation=p["fragmentation"], capacity=p["capacity"], health=PoolHealth(p["health"]) ) for p in pools ] except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/{pool_name}", response_model=PoolStatus) async def get_pool_status(pool_name: str): """ Get detailed status of specific pool (public) """ try: status_data = zfs_runner.get_pool_status(pool_name) if not status_data: raise HTTPException(status_code=404, detail=f"Pool {pool_name} not found") # Map state to health enum health = PoolHealth.ONLINE if "state" in status_data and status_data["state"]: try: health = PoolHealth(status_data["state"]) except ValueError: health = PoolHealth.ONLINE return PoolStatus( name=pool_name, state=status_data.get("state"), health=health, scan=status_data.get("scan"), errors=status_data.get("errors"), vdevs=status_data.get("vdevs", []) ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/{pool_name}/scrub") async def scrub_pool(pool_name: str, current_user: str = Depends(get_current_user)): """ Start or resume scrub on pool """ try: result = zfs_runner.scrub_pool(pool_name) if result.get("status") == "error": raise HTTPException(status_code=400, detail=result.get("message")) return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/{pool_name}/clear") async def clear_pool_errors(pool_name: str, current_user: str = Depends(get_current_user)): """ Clear error counters on pool """ try: stdout, stderr, rc = zfs_runner.run_command(["zpool", "clear", pool_name]) if rc != 0: raise HTTPException(status_code=400, detail=stderr.strip() or "Failed to clear errors") return {"status": "success", "message": f"Errors cleared on {pool_name}"} except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/{pool_name}/resilver") async def resilver_pool(pool_name: str, current_user: str = Depends(get_current_user)): """ Start resilver on pool """ try: stdout, stderr, rc = zfs_runner.run_command(["zpool", "resilver", pool_name]) if rc != 0: raise HTTPException(status_code=400, detail=stderr.strip() or "Failed to start resilver") return {"status": "success", "message": f"Resilver started on {pool_name}"} except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/{pool_name}/disk/offline") async def disk_offline(pool_name: str, disk: str, current_user: str = Depends(get_current_user)): try: stdout, stderr, rc = zfs_runner.run_command(["zpool", "offline", pool_name, disk]) if rc != 0: raise HTTPException(status_code=400, detail=stderr.strip() or "Failed to offline disk") return {"status": "success", "message": f"{disk} offlined"} except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/{pool_name}/disk/online") async def disk_online(pool_name: str, disk: str, current_user: str = Depends(get_current_user)): try: stdout, stderr, rc = zfs_runner.run_command(["zpool", "online", pool_name, disk]) if rc != 0: raise HTTPException(status_code=400, detail=stderr.strip() or "Failed to online disk") return {"status": "success", "message": f"{disk} onlined"} except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/{pool_name}/disk/detach") async def disk_detach(pool_name: str, disk: str, current_user: str = Depends(get_current_user)): try: stdout, stderr, rc = zfs_runner.run_command(["zpool", "detach", pool_name, disk]) if rc != 0: raise HTTPException(status_code=400, detail=stderr.strip() or "Failed to detach disk") return {"status": "success", "message": f"{disk} detached"} except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/disks/{disk}/smart") async def get_disk_smart(disk: str, current_user: str = Depends(get_current_user)): """ Get SMART data for a specific disk (e.g. sda, sdb, nvme0n1). Requires smartmontools installed on the system. """ # Basic validation to prevent path traversal if not re.match(r'^[a-zA-Z0-9]+$', disk): raise HTTPException(status_code=400, detail="Invalid disk name") try: data = zfs_runner.get_smart_info(disk) return data except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/clear-cache") async def clear_cache(current_user: str = Depends(get_current_user)): """ Clear ZFS command cache (for testing/debugging) """ zfs_runner.clear_cache() return {"status": "success", "message": "Cache cleared"}