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:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user