mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Add controller onboarding flow for primary drive setup
Introduce an onboarding system in the Controller that guides users through selecting and mounting their primary media drive. This is the fallback UI flow used when the main React app is unavailable. Key changes: - Add onboarding API endpoints and schemas (controller/api/onboarding.py) - Add onboarding logic to detect drives, handle already-mounted drives (needed for RPi which auto-mounts), and save primary drive selection to the controller config when no mounts are configured - Extend the Controller's index.html template with the onboarding UI - Skip root cert creation until onboarding is complete - Update first_startup.sh to support the onboarding/repair flow - Add comprehensive tests for onboarding logic and API
This commit is contained in:
parent
f146b332a8
commit
07f601e77f
15 changed files with 1464 additions and 66 deletions
103
controller/api/onboarding.py
Normal file
103
controller/api/onboarding.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""
|
||||
Onboarding API endpoints for WROLPi Controller.
|
||||
|
||||
Guides first-time setup: select drive, probe for config, mount, repair.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from controller.api.schemas import (
|
||||
OnboardingCandidateResponse,
|
||||
OnboardingCommitRequest,
|
||||
OnboardingCommitResponse,
|
||||
OnboardingProbeRequest,
|
||||
OnboardingProbeResponse,
|
||||
)
|
||||
from controller.lib.config import is_docker_mode, is_primary_drive_mounted
|
||||
from controller.lib.onboarding import (
|
||||
cancel_probe,
|
||||
commit_onboarding,
|
||||
get_onboarding_candidates,
|
||||
probe_drive,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/onboarding", tags=["onboarding"])
|
||||
|
||||
|
||||
def _check_preconditions():
|
||||
"""Check that onboarding is possible."""
|
||||
if is_docker_mode():
|
||||
raise HTTPException(
|
||||
status_code=501,
|
||||
detail="Onboarding is not available in Docker mode. "
|
||||
"Configure mounts on the host system.",
|
||||
)
|
||||
if is_primary_drive_mounted():
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Primary drive is already mounted. Onboarding is not needed.",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/candidates", response_model=list[OnboardingCandidateResponse])
|
||||
async def list_candidates():
|
||||
"""List available drives for onboarding."""
|
||||
_check_preconditions()
|
||||
candidates = get_onboarding_candidates()
|
||||
return [OnboardingCandidateResponse(**c) for c in candidates]
|
||||
|
||||
|
||||
@router.post("/probe", response_model=OnboardingProbeResponse)
|
||||
async def probe(request: OnboardingProbeRequest):
|
||||
"""Temp-mount a drive and check for WROLPi configuration."""
|
||||
_check_preconditions()
|
||||
|
||||
result = probe_drive(request.device_path, request.fstype)
|
||||
if not result.get("success"):
|
||||
raise HTTPException(status_code=500, detail=result.get("error", "Probe failed"))
|
||||
|
||||
return OnboardingProbeResponse(
|
||||
config_found=result["config_found"],
|
||||
mounts=result["mounts"],
|
||||
device_path=result["device_path"],
|
||||
fstype=result["fstype"],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/commit", response_model=OnboardingCommitResponse)
|
||||
async def commit(request: OnboardingCommitRequest):
|
||||
"""Execute full onboarding: mount drives, add fstab entries, start repair."""
|
||||
if is_docker_mode():
|
||||
raise HTTPException(
|
||||
status_code=501,
|
||||
detail="Onboarding is not available in Docker mode.",
|
||||
)
|
||||
# Note: we don't check is_primary_drive_mounted here because commit
|
||||
# will mount it. The probe step already validated preconditions.
|
||||
|
||||
result = commit_onboarding(
|
||||
device_path=request.device_path,
|
||||
fstype=request.fstype,
|
||||
force=request.force,
|
||||
)
|
||||
|
||||
if not result.get("success"):
|
||||
raise HTTPException(status_code=500, detail=result.get("error", "Onboarding failed"))
|
||||
|
||||
return OnboardingCommitResponse(
|
||||
success=True,
|
||||
mounts=result.get("mounts", []),
|
||||
repair_started=result.get("repair_started", False),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/cancel")
|
||||
async def cancel():
|
||||
"""Cancel onboarding probe (unmount temp mount)."""
|
||||
if is_docker_mode():
|
||||
raise HTTPException(
|
||||
status_code=501,
|
||||
detail="Onboarding is not available in Docker mode.",
|
||||
)
|
||||
result = cancel_probe()
|
||||
return result
|
||||
|
|
@ -362,3 +362,53 @@ class ReadyResponse(BaseModel):
|
|||
|
||||
api: bool = Field(description="Whether the main WROLPi API is responding")
|
||||
app: bool = Field(description="Whether the React app is responding")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Onboarding
|
||||
# ============================================================================
|
||||
|
||||
class OnboardingCandidateResponse(BaseModel):
|
||||
"""A drive that could be selected as the primary WROLPi drive."""
|
||||
|
||||
path: str = Field(description="Device path (e.g., /dev/sda1)")
|
||||
name: str = Field(description="Device name (e.g., sda1)")
|
||||
size: str = Field(description="Human-readable size (e.g., 1.8T)")
|
||||
fstype: Optional[str] = Field(default=None, description="Filesystem type")
|
||||
label: Optional[str] = Field(default=None, description="Filesystem label")
|
||||
uuid: Optional[str] = Field(default=None, description="Filesystem UUID")
|
||||
model: Optional[str] = Field(default=None, description="Drive model")
|
||||
mountpoint: Optional[str] = Field(default=None, description="Current mount point if auto-mounted (e.g., /media/pi/MyDrive)")
|
||||
|
||||
|
||||
class OnboardingProbeRequest(BaseModel):
|
||||
"""Request to probe a drive for WROLPi configuration."""
|
||||
|
||||
device_path: str = Field(description="Device path to probe (e.g., /dev/sda1)")
|
||||
fstype: str = Field(description="Filesystem type (e.g., ext4)")
|
||||
|
||||
|
||||
class OnboardingProbeResponse(BaseModel):
|
||||
"""Result of probing a drive for WROLPi configuration."""
|
||||
|
||||
config_found: bool = Field(description="Whether a controller.yaml was found on the drive")
|
||||
mounts: list = Field(default_factory=list, description="Mount entries from config if found")
|
||||
device_path: str = Field(description="Device path that was probed")
|
||||
fstype: str = Field(description="Filesystem type")
|
||||
|
||||
|
||||
class OnboardingCommitRequest(BaseModel):
|
||||
"""Request to execute onboarding setup."""
|
||||
|
||||
device_path: str = Field(description="Device path to use as primary drive")
|
||||
fstype: str = Field(description="Filesystem type")
|
||||
force: bool = Field(default=False, description="Proceed even if no config found on drive")
|
||||
|
||||
|
||||
class OnboardingCommitResponse(BaseModel):
|
||||
"""Result of onboarding setup."""
|
||||
|
||||
success: bool = Field(description="Whether onboarding completed successfully")
|
||||
error: Optional[str] = Field(default=None, description="Error message if failed")
|
||||
mounts: list[str] = Field(default_factory=list, description="Mount points that were mounted")
|
||||
repair_started: bool = Field(default=False, description="Whether repair script was started")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue