fix: mount drives after restore + poll-based redirect

Restore flow now calls MountDrivesFromLayout() after writing config,
which mounts drives by UUID and adds fstab entries. Previously drives
from the infra backup were never mounted, causing "Adattároló nem
elérhető" warnings.

Post-restore redirect now polls until the controller responds instead
of using a fixed 5-second timeout that was too short for container
restart.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 15:07:38 +01:00
parent c0cdd95e56
commit 80b756f0e4
3 changed files with 62 additions and 13 deletions
+2
View File
@@ -20,6 +20,8 @@
#### Fixed #### Fixed
- **Bind mount write**: `atomicWriteFile()` now falls back to direct write when rename fails (fixes "device or resource busy" on Docker bind-mounted `controller.yaml`) - **Bind mount write**: `atomicWriteFile()` now falls back to direct write when rename fails (fixes "device or resource busy" on Docker bind-mounted `controller.yaml`)
- **Drive mounting after restore**: Restore flow now calls `MountDrivesFromLayout()` to mount drives by UUID and add fstab entries — previously drives referenced in the infra backup were not mounted, causing "Adattároló nem elérhető" warnings
- **Post-restore redirect**: UI now polls until the controller is actually up instead of using a fixed 5-second timeout (which was too short for container restart)
### v0.31.6 — UI: Brand-consistent button & card styling (2026-02-25) ### v0.31.6 — UI: Brand-consistent button & card styling (2026-02-25)
+44 -10
View File
@@ -1,6 +1,7 @@
package setup package setup
import ( import (
"context"
crand "crypto/rand" crand "crypto/rand"
"crypto/sha256" "crypto/sha256"
"embed" "embed"
@@ -683,6 +684,7 @@ func (s *Server) executeLocalRestore(drivePath, historyFile string) {
s.restoreSteps = []RestoreStep{ s.restoreSteps = []RestoreStep{
{Label: "Mentés beolvasása...", Status: "running"}, {Label: "Mentés beolvasása...", Status: "running"},
{Label: "Konfiguráció visszaállítása...", Status: "pending"}, {Label: "Konfiguráció visszaállítása...", Status: "pending"},
{Label: "Meghajtók csatolása...", Status: "pending"},
{Label: "Beállítás befejezése...", Status: "pending"}, {Label: "Beállítás befejezése...", Status: "pending"},
} }
s.restoreMu.Unlock() s.restoreMu.Unlock()
@@ -715,8 +717,13 @@ func (s *Server) executeLocalRestore(drivePath, historyFile string) {
} }
s.setRestoreStepDone(1) s.setRestoreStepDone(1)
// Step 3: Finalize // Step 3: Mount drives from disk layout
s.setRestoreStepRunning(2) s.setRestoreStepRunning(2)
s.mountDrivesFromBackup(&ib)
s.setRestoreStepDone(2)
// Step 4: Finalize
s.setRestoreStepRunning(3)
// Save retrieval password from state if available // Save retrieval password from state if available
retrievalPw := s.state.GetFormField("retrieval_password") retrievalPw := s.state.GetFormField("retrieval_password")
@@ -730,7 +737,7 @@ func (s *Server) executeLocalRestore(drivePath, historyFile string) {
// Queue DR event // Queue DR event
s.queueDREvent("local", ib.Timestamp, len(ib.DeployedStacks)) s.queueDREvent("local", ib.Timestamp, len(ib.DeployedStacks))
s.setRestoreStepDone(2) s.setRestoreStepDone(3)
s.restoreMu.Lock() s.restoreMu.Lock()
s.restoreRunning = false s.restoreRunning = false
@@ -751,6 +758,7 @@ func (s *Server) executeHubRestore() {
s.restoreError = "" s.restoreError = ""
s.restoreSteps = []RestoreStep{ s.restoreSteps = []RestoreStep{
{Label: "Konfiguráció visszaállítása...", Status: "running"}, {Label: "Konfiguráció visszaállítása...", Status: "running"},
{Label: "Meghajtók csatolása...", Status: "pending"},
{Label: "Beállítás befejezése...", Status: "pending"}, {Label: "Beállítás befejezése...", Status: "pending"},
} }
s.restoreMu.Unlock() s.restoreMu.Unlock()
@@ -767,16 +775,25 @@ func (s *Server) executeHubRestore() {
} }
// Restore settings from infra backup if available // Restore settings from infra backup if available
var restoredIB *report.InfraBackup
if ibJSON != "" { if ibJSON != "" {
var ib report.InfraBackup var ib report.InfraBackup
if err := json.Unmarshal([]byte(ibJSON), &ib); err == nil { if err := json.Unmarshal([]byte(ibJSON), &ib); err == nil {
s.restoreFromInfraBackup(&ib) s.restoreFromInfraBackup(&ib)
restoredIB = &ib
} }
} }
s.setRestoreStepDone(0) s.setRestoreStepDone(0)
// Step 2: Finalize // Step 2: Mount drives from disk layout
s.setRestoreStepRunning(1) s.setRestoreStepRunning(1)
if restoredIB != nil {
s.mountDrivesFromBackup(restoredIB)
}
s.setRestoreStepDone(1)
// Step 3: Finalize
s.setRestoreStepRunning(2)
// Save retrieval password // Save retrieval password
retrievalPw := s.state.GetFormField("retrieval_password") retrievalPw := s.state.GetFormField("retrieval_password")
@@ -790,16 +807,13 @@ func (s *Server) executeHubRestore() {
// Queue DR event // Queue DR event
stackCount := 0 stackCount := 0
timestamp := "" timestamp := ""
if ibJSON != "" { if restoredIB != nil {
var ib report.InfraBackup stackCount = len(restoredIB.DeployedStacks)
if json.Unmarshal([]byte(ibJSON), &ib) == nil { timestamp = restoredIB.Timestamp
stackCount = len(ib.DeployedStacks)
timestamp = ib.Timestamp
}
} }
s.queueDREvent("hub", timestamp, stackCount) s.queueDREvent("hub", timestamp, stackCount)
s.setRestoreStepDone(1) s.setRestoreStepDone(2)
s.restoreMu.Lock() s.restoreMu.Lock()
s.restoreRunning = false s.restoreRunning = false
@@ -863,6 +877,26 @@ func (s *Server) restoreFromInfraBackup(ib *report.InfraBackup) {
} }
} }
// mountDrivesFromBackup mounts drives from the infra backup's disk layout.
// Best-effort: logs warnings on failure but does not block restore.
func (s *Server) mountDrivesFromBackup(ib *report.InfraBackup) {
if len(ib.DiskLayout.Mounts) == 0 {
s.logger.Printf("[INFO] Setup: no drives in disk layout to mount")
return
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
mounted, err := backup.MountDrivesFromLayout(ctx, ib.DiskLayout, s.logger)
if err != nil {
s.logger.Printf("[WARN] Setup: drive mounting error: %v", err)
}
if len(mounted) > 0 {
s.logger.Printf("[INFO] Setup: mounted %d drive(s): %v", len(mounted), mounted)
}
}
func (s *Server) writeFreshConfig(configYAML, retrievalPassword string) error { func (s *Server) writeFreshConfig(configYAML, retrievalPassword string) error {
configPath := "/opt/docker/felhom-controller/controller.yaml" configPath := "/opt/docker/felhom-controller/controller.yaml"
if err := atomicWriteFile(configPath, []byte(configYAML), 0600); err != nil { if err := atomicWriteFile(configPath, []byte(configYAML), 0600); err != nil {
@@ -33,6 +33,19 @@
<script> <script>
(function() { (function() {
function waitForRestart() {
fetch('/', {redirect: 'follow'})
.then(function(r) {
if (r.ok) {
window.location.href = '/';
} else {
setTimeout(waitForRestart, 2000);
}
})
.catch(function() {
setTimeout(waitForRestart, 2000);
});
}
function poll() { function poll() {
fetch('/setup/restore/status') fetch('/setup/restore/status')
.then(function(r) { return r.json(); }) .then(function(r) { return r.json(); })
@@ -59,15 +72,15 @@
} }
if (data.done) { if (data.done) {
document.getElementById('done-msg').style.display = 'block'; document.getElementById('done-msg').style.display = 'block';
setTimeout(function() { window.location.href = '/'; }, 5000); setTimeout(waitForRestart, 3000);
return; return;
} }
setTimeout(poll, 1500); setTimeout(poll, 1500);
}) })
.catch(function() { .catch(function() {
// Connection lost — controller may be restarting // Connection lost — controller is restarting
document.getElementById('done-msg').style.display = 'block'; document.getElementById('done-msg').style.display = 'block';
setTimeout(function() { window.location.href = '/'; }, 5000); setTimeout(waitForRestart, 3000);
}); });
} }
poll(); poll();