wrolpi/controller/api/onboarding.py
lrnselfreliance dc2ab22ae0
Debian installs show the Controller drive wizard (#826)
wrolpi-bootstrap.sh created controller.yaml on installed systems, which
hid the onboarding wizard.  Installed systems now wait for the wizard,
whose repair.sh finishes setup.

"Run repair without external drive" now writes controller.yaml before
starting repair, so the wizard does not return after repair.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:50:20 -06:00

138 lines
4.4 KiB
Python

"""
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,
ShadowedDataInfo,
)
from controller.lib.config import is_docker_mode, is_primary_drive_mounted
from controller.lib.onboarding import (
cancel_probe,
commit_onboarding,
get_onboarding_candidates,
onboard_without_drive,
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_shadowed=request.force_shadowed,
)
# Soft-block: existing data at the mount target would be hidden. Return
# a structured response (200 OK with success=False) so the UI can prompt
# the user to override.
if not result.get("success") and result.get("needs_force") == "shadowed":
shadowed = result.get("shadowed_data") or {}
return OnboardingCommitResponse(
success=False,
error=result.get("error"),
mounts=[],
repair_started=False,
needs_force="shadowed",
shadowed_data=ShadowedDataInfo(**shadowed) if shadowed else None,
)
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("/without-drive", response_model=OnboardingCommitResponse)
async def without_drive():
"""Keep the library on the internal disk, and start repair."""
_check_preconditions()
try:
result = onboard_without_drive()
except Exception as e:
result = {"success": False, "error": str(e)}
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