mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 02:48:38 -04:00
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>
This commit is contained in:
parent
697a8180c7
commit
dc2ab22ae0
6 changed files with 119 additions and 1 deletions
|
|
@ -19,6 +19,7 @@ from controller.lib.onboarding import (
|
|||
cancel_probe,
|
||||
commit_onboarding,
|
||||
get_onboarding_candidates,
|
||||
onboard_without_drive,
|
||||
probe_drive,
|
||||
)
|
||||
|
||||
|
|
@ -106,6 +107,25 @@ async def commit(request: OnboardingCommitRequest):
|
|||
)
|
||||
|
||||
|
||||
@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)."""
|
||||
|
|
|
|||
|
|
@ -165,6 +165,31 @@ def cancel_probe() -> dict:
|
|||
return {"success": True}
|
||||
|
||||
|
||||
def onboard_without_drive() -> dict:
|
||||
"""
|
||||
Set up WROLPi with the library on the internal disk, then start repair.
|
||||
|
||||
controller.yaml marks the primary drive as set up; without it the wizard
|
||||
would be shown again after repair.
|
||||
|
||||
Returns:
|
||||
dict with success, mounts, and repair_started
|
||||
"""
|
||||
config_dir = get_media_directory() / "config"
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
save_config()
|
||||
|
||||
repair_result = start_script("repair")
|
||||
if not repair_result.get("success"):
|
||||
logger.warning("Failed to start repair: %s", repair_result.get("error"))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"mounts": [],
|
||||
"repair_started": bool(repair_result.get("success")),
|
||||
}
|
||||
|
||||
|
||||
def commit_onboarding(
|
||||
device_path: str,
|
||||
fstype: str,
|
||||
|
|
|
|||
|
|
@ -2547,7 +2547,7 @@
|
|||
document.querySelector('#onboarding-progress p').textContent = 'Starting repair without external drive. Please wait...';
|
||||
|
||||
try {
|
||||
const resp = await fetch('api/scripts/repair/start', {
|
||||
const resp = await fetch('api/onboarding/without-drive', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', 'Accept': 'application/json'},
|
||||
signal: AbortSignal.timeout(60000)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from controller.lib.onboarding import (
|
|||
cancel_probe,
|
||||
commit_onboarding,
|
||||
get_onboarding_candidates,
|
||||
onboard_without_drive,
|
||||
probe_drive,
|
||||
)
|
||||
|
||||
|
|
@ -516,3 +517,37 @@ class TestCommitOnboardingShadowedData:
|
|||
result = commit_onboarding("/dev/sda1", "ext4", force_shadowed=True)
|
||||
|
||||
assert result["success"] is True
|
||||
|
||||
|
||||
class TestOnboardWithoutDrive:
|
||||
"""Tests for onboard_without_drive."""
|
||||
|
||||
def test_writes_marker_and_starts_repair(self, mock_docker_mode, tmp_path):
|
||||
"""The library stays on the internal disk, and the wizard does not return after repair."""
|
||||
media_dir = tmp_path / "wrolpi"
|
||||
media_dir.mkdir()
|
||||
config_path = media_dir / "config" / "controller.yaml"
|
||||
|
||||
with mock.patch("controller.lib.onboarding.get_media_directory", return_value=media_dir), \
|
||||
mock.patch("controller.lib.config.CONFIG_PATH_ON_DRIVE", config_path), \
|
||||
mock.patch("controller.lib.onboarding.start_script", return_value={"success": True}) as start:
|
||||
result = onboard_without_drive()
|
||||
|
||||
assert result == {"success": True, "mounts": [], "repair_started": True}
|
||||
assert config_path.is_file()
|
||||
start.assert_called_once_with("repair")
|
||||
|
||||
def test_repair_failure_is_reported(self, mock_docker_mode, tmp_path):
|
||||
media_dir = tmp_path / "wrolpi"
|
||||
media_dir.mkdir()
|
||||
config_path = media_dir / "config" / "controller.yaml"
|
||||
|
||||
with mock.patch("controller.lib.onboarding.get_media_directory", return_value=media_dir), \
|
||||
mock.patch("controller.lib.config.CONFIG_PATH_ON_DRIVE", config_path), \
|
||||
mock.patch("controller.lib.onboarding.start_script",
|
||||
return_value={"success": False, "error": "already running"}):
|
||||
result = onboard_without_drive()
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["repair_started"] is False
|
||||
assert config_path.is_file()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ class TestOnboardingDockerAndMounted:
|
|||
("post", "/api/onboarding/probe", {"device_path": "/dev/sda1", "fstype": "ext4"}),
|
||||
("post", "/api/onboarding/commit", {"device_path": "/dev/sda1", "fstype": "ext4"}),
|
||||
("post", "/api/onboarding/cancel", None),
|
||||
("post", "/api/onboarding/without-drive", None),
|
||||
])
|
||||
def test_endpoint_returns_501_in_docker(self, test_client_docker_mode, method, endpoint, payload):
|
||||
client_call = getattr(test_client_docker_mode, method)
|
||||
|
|
@ -24,6 +25,7 @@ class TestOnboardingDockerAndMounted:
|
|||
@pytest.mark.parametrize("method,endpoint,payload", [
|
||||
("get", "/api/onboarding/candidates", None),
|
||||
("post", "/api/onboarding/probe", {"device_path": "/dev/sda1", "fstype": "ext4"}),
|
||||
("post", "/api/onboarding/without-drive", None),
|
||||
])
|
||||
def test_endpoint_returns_409_when_drive_mounted(self, test_client, method, endpoint, payload):
|
||||
"""Onboarding endpoints reject when the primary drive is already mounted."""
|
||||
|
|
@ -196,3 +198,26 @@ class TestOnboardingCancelEndpoint:
|
|||
):
|
||||
response = test_client.post("/api/onboarding/cancel")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestOnboardingWithoutDrive:
|
||||
|
||||
def test_without_drive_success(self, test_client):
|
||||
with mock.patch("controller.api.onboarding.is_primary_drive_mounted", return_value=False), \
|
||||
mock.patch("controller.api.onboarding.onboard_without_drive",
|
||||
return_value={"success": True, "mounts": [], "repair_started": True}) as onboard:
|
||||
response = test_client.post("/api/onboarding/without-drive")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["success"] is True
|
||||
assert response.json()["repair_started"] is True
|
||||
onboard.assert_called_once_with()
|
||||
|
||||
def test_without_drive_failure(self, test_client):
|
||||
with mock.patch("controller.api.onboarding.is_primary_drive_mounted", return_value=False), \
|
||||
mock.patch("controller.api.onboarding.onboard_without_drive",
|
||||
return_value={"success": False, "error": "disk full"}):
|
||||
response = test_client.post("/api/onboarding/without-drive")
|
||||
|
||||
assert response.status_code == 500
|
||||
assert "disk full" in response.json()["detail"]
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@
|
|||
# drive, or is a plain directory on the root filesystem if the user
|
||||
# installed without one. The SQLite database lives inside
|
||||
# /media/wrolpi/config/ in every shape, so it travels with the drive.
|
||||
# Until the Controller onboarding wizard writes controller.yaml, this
|
||||
# script does nothing; the wizard's repair.sh finishes setup.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
|
@ -225,6 +227,17 @@ if [ ! -d /media/wrolpi ]; then
|
|||
fi
|
||||
chown wrolpi:wrolpi /media/wrolpi 2>/dev/null || true
|
||||
|
||||
# --- Installed: wait for the Controller onboarding wizard ---------------
|
||||
# Until the user picks a drive (or the internal disk) in the wizard, there
|
||||
# is no controller.yaml. Leave Caddy on the onboarding Caddyfile from
|
||||
# wrolpi-first-startup.service so every page leads to the wizard, and leave
|
||||
# certificates, the database, and services to the wizard's repair.sh.
|
||||
|
||||
if [ "$MODE" = "installed" ] && [ ! -e /media/wrolpi/config/controller.yaml ]; then
|
||||
log "primary drive is not set up; the Controller onboarding wizard will finish setup"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p /media/wrolpi/config
|
||||
chown wrolpi:wrolpi /media/wrolpi/config
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue