202fdfaaeb
- VdevTree rendert jetzt alle Ebenen (pool → mirror → sda/sdb) - Disk-⋮-Menü: Clear Disk Errors, Offline, Online, Detach - Backend: neue Endpoints /disk/offline, /disk/online, /disk/detach Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
174 lines
5.9 KiB
Python
174 lines
5.9 KiB
Python
"""
|
|
Pool management endpoints
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
from typing import List
|
|
|
|
from services.zfs_runner import zfs_runner
|
|
from services.auth import 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.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"}
|