feat: drive migration & Tier 2 restic deprecation (v0.18.0)
Phase 1: Deprecate restic as Tier 2 method (rsync only), auto-migrate on startup Phase 2: Enhanced per-app migration with backup awareness, DB dump copy, auto-cleanup Phase 3: Full drive migration with decommissioned state, rollback support, wizard UI Phase 4: Hub report includes decommissioned drive state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import (
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
catalogsync "gitea.dooplex.hu/admin/felhom-controller/internal/sync"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/storage"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/web"
|
||||
)
|
||||
|
||||
@@ -545,6 +546,48 @@ func main() {
|
||||
webServer := web.NewServer(cfg, stackMgr, cpuCollector, backupMgr, crossDriveRunner, sched, sett, alertMgr, notifier, updater, logger, Version)
|
||||
webServer.SetStorageWatchdog(storageWatchdog)
|
||||
|
||||
// --- Initialize drive migrator ---
|
||||
driveMigrator := &storage.DriveMigrator{
|
||||
Sett: sett,
|
||||
StackProvider: &driveMigrateStackAdapter{mgr: stackMgr},
|
||||
Logger: logger,
|
||||
}
|
||||
// Only set BackupTrigger if backup is enabled (avoid non-nil interface with nil concrete value)
|
||||
if backupMgr != nil {
|
||||
driveMigrator.BackupTrigger = backupMgr
|
||||
}
|
||||
driveMigrator.AlertRefresh = func() {
|
||||
healthReport := monitor.RunHealthCheck(cfg, cpuCollector, sett.GetStoragePaths())
|
||||
updateAvailable := false
|
||||
latestVersion := ""
|
||||
if updater != nil {
|
||||
status := updater.GetStatus()
|
||||
if status.LastCheck != nil {
|
||||
updateAvailable = status.LastCheck.UpdateAvailable
|
||||
latestVersion = status.LastCheck.LatestVersion
|
||||
}
|
||||
}
|
||||
alertMgr.Refresh(healthReport, cfg, backupMgr, updateAvailable, latestVersion, sett.GetStoragePaths())
|
||||
}
|
||||
if hubPusher != nil {
|
||||
driveMigrator.PushHubReport = func() {
|
||||
r := report.BuildReport(cfg, stackMgr, backupMgr, cpuCollector, metricsStore, Version, sett.GetStoragePaths())
|
||||
hubPusher.Push(r)
|
||||
}
|
||||
driveMigrator.PushInfraBackup = func() {
|
||||
pushInfraBackup(cfg, sett, stackProv, hubPusher, logger)
|
||||
}
|
||||
}
|
||||
driveMigrator.SyncFBMounts = func() {
|
||||
webServer.SyncFileBrowserMounts()
|
||||
}
|
||||
webServer.SetDriveMigrator(driveMigrator)
|
||||
|
||||
// Wire migration-active check into backup manager
|
||||
if backupMgr != nil {
|
||||
backupMgr.MigrationActiveCheck = driveMigrator.IsActive
|
||||
}
|
||||
|
||||
// Phase 3: Set DR restore mode if a restore plan was built
|
||||
if restorePlan != nil && len(restorePlan.Apps) > 0 {
|
||||
webServer.SetRestoreState(restorePlan)
|
||||
@@ -741,6 +784,65 @@ func (a *watchdogStackAdapter) StartStack(name string) error {
|
||||
return a.mgr.StartStack(name)
|
||||
}
|
||||
|
||||
// driveMigrateStackAdapter implements storage.StackProviderForMigration using stacks.Manager.
|
||||
type driveMigrateStackAdapter struct {
|
||||
mgr *stacks.Manager
|
||||
}
|
||||
|
||||
func (a *driveMigrateStackAdapter) ListDeployedStacks() []storage.StackSummaryForMigration {
|
||||
var result []storage.StackSummaryForMigration
|
||||
for _, s := range a.mgr.GetStacks() {
|
||||
if !s.Deployed {
|
||||
continue
|
||||
}
|
||||
result = append(result, storage.StackSummaryForMigration{
|
||||
Name: s.Name,
|
||||
DisplayName: s.Meta.DisplayName,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (a *driveMigrateStackAdapter) GetStackHDDPath(name string) string {
|
||||
s, ok := a.mgr.GetStack(name)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
stackDir := filepath.Dir(s.ComposePath)
|
||||
appCfg := stacks.LoadAppConfig(stackDir)
|
||||
if appCfg != nil && appCfg.Env["HDD_PATH"] != "" {
|
||||
return filepath.Clean(appCfg.Env["HDD_PATH"])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *driveMigrateStackAdapter) StopStack(name string) error {
|
||||
return a.mgr.StopStack(name)
|
||||
}
|
||||
|
||||
func (a *driveMigrateStackAdapter) StartStack(name string) error {
|
||||
return a.mgr.StartStack(name)
|
||||
}
|
||||
|
||||
func (a *driveMigrateStackAdapter) UpdateStackHDDPath(name, newPath string) error {
|
||||
s, ok := a.mgr.GetStack(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("stack not found: %s", name)
|
||||
}
|
||||
stackDir := filepath.Dir(s.ComposePath)
|
||||
appCfg := stacks.LoadAppConfig(stackDir)
|
||||
if appCfg == nil {
|
||||
return fmt.Errorf("app.yaml not found for stack: %s", name)
|
||||
}
|
||||
appCfg.Env["HDD_PATH"] = newPath
|
||||
return stacks.SaveAppConfig(stackDir, appCfg)
|
||||
}
|
||||
|
||||
func (a *driveMigrateStackAdapter) StackExists(name string) bool {
|
||||
_, ok := a.mgr.GetStack(name)
|
||||
return ok
|
||||
}
|
||||
|
||||
// pushInfraBackup builds and sends the infrastructure snapshot to the Hub.
|
||||
func pushInfraBackup(cfg *config.Config, sett *settings.Settings,
|
||||
stackProv *stackAdapter, pusher *report.Pusher, logger *log.Logger) {
|
||||
@@ -793,13 +895,6 @@ func restorePasswordsFromHub(ib *report.InfraBackup, cfg *config.Config,
|
||||
}
|
||||
}
|
||||
|
||||
if ib.CrossDrivePassword != "" {
|
||||
if err := sett.SetCrossDriveResticPassword(ib.CrossDrivePassword); err == nil {
|
||||
logger.Println("[INFO] Cross-drive restic password restored from Hub")
|
||||
} else {
|
||||
logger.Printf("[WARN] Failed to set cross-drive password: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// restoreSettingsFromHub restores settings.json from a Hub infra backup.
|
||||
|
||||
Reference in New Issue
Block a user