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
+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();