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
- **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)
+44 -10
View File
@@ -1,6 +1,7 @@
package setup
import (
"context"
crand "crypto/rand"
"crypto/sha256"
"embed"
@@ -683,6 +684,7 @@ func (s *Server) executeLocalRestore(drivePath, historyFile string) {
s.restoreSteps = []RestoreStep{
{Label: "Mentés beolvasása...", Status: "running"},
{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"},
}
s.restoreMu.Unlock()
@@ -715,8 +717,13 @@ func (s *Server) executeLocalRestore(drivePath, historyFile string) {
}
s.setRestoreStepDone(1)
// Step 3: Finalize
// Step 3: Mount drives from disk layout
s.setRestoreStepRunning(2)
s.mountDrivesFromBackup(&ib)
s.setRestoreStepDone(2)
// Step 4: Finalize
s.setRestoreStepRunning(3)
// Save retrieval password from state if available
retrievalPw := s.state.GetFormField("retrieval_password")
@@ -730,7 +737,7 @@ func (s *Server) executeLocalRestore(drivePath, historyFile string) {
// Queue DR event
s.queueDREvent("local", ib.Timestamp, len(ib.DeployedStacks))
s.setRestoreStepDone(2)
s.setRestoreStepDone(3)
s.restoreMu.Lock()
s.restoreRunning = false
@@ -751,6 +758,7 @@ func (s *Server) executeHubRestore() {
s.restoreError = ""
s.restoreSteps = []RestoreStep{
{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"},
}
s.restoreMu.Unlock()
@@ -767,16 +775,25 @@ func (s *Server) executeHubRestore() {
}
// Restore settings from infra backup if available
var restoredIB *report.InfraBackup
if ibJSON != "" {
var ib report.InfraBackup
if err := json.Unmarshal([]byte(ibJSON), &ib); err == nil {
s.restoreFromInfraBackup(&ib)
restoredIB = &ib
}
}
s.setRestoreStepDone(0)
// Step 2: Finalize
// Step 2: Mount drives from disk layout
s.setRestoreStepRunning(1)
if restoredIB != nil {
s.mountDrivesFromBackup(restoredIB)
}
s.setRestoreStepDone(1)
// Step 3: Finalize
s.setRestoreStepRunning(2)
// Save retrieval password
retrievalPw := s.state.GetFormField("retrieval_password")
@@ -790,16 +807,13 @@ func (s *Server) executeHubRestore() {
// Queue DR event
stackCount := 0
timestamp := ""
if ibJSON != "" {
var ib report.InfraBackup
if json.Unmarshal([]byte(ibJSON), &ib) == nil {
stackCount = len(ib.DeployedStacks)
timestamp = ib.Timestamp
}
if restoredIB != nil {
stackCount = len(restoredIB.DeployedStacks)
timestamp = restoredIB.Timestamp
}
s.queueDREvent("hub", timestamp, stackCount)
s.setRestoreStepDone(1)
s.setRestoreStepDone(2)
s.restoreMu.Lock()
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 {
configPath := "/opt/docker/felhom-controller/controller.yaml"
if err := atomicWriteFile(configPath, []byte(configYAML), 0600); err != nil {
@@ -33,6 +33,19 @@
<script>
(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() {
fetch('/setup/restore/status')
.then(function(r) { return r.json(); })
@@ -59,15 +72,15 @@
}
if (data.done) {
document.getElementById('done-msg').style.display = 'block';
setTimeout(function() { window.location.href = '/'; }, 5000);
setTimeout(waitForRestart, 3000);
return;
}
setTimeout(poll, 1500);
})
.catch(function() {
// Connection lost — controller may be restarting
// Connection lost — controller is restarting
document.getElementById('done-msg').style.display = 'block';
setTimeout(function() { window.location.href = '/'; }, 5000);
setTimeout(waitForRestart, 3000);
});
}
poll();